Exclusivo

Contact <mv-contact>

Sección de contacto editorial: un formulario junto a canales de contacto numerados (correo, teléfono, oficina, horario con estado abierto o cerrado en vivo), un formulario corto centrado, o una cuadrícula de oficinas con la hora local sobre un formulario compacto; se envía de forma nativa sin JavaScript, envía en segundo plano con estados de envío, éxito y error anunciados cuando <mv-contact> está presente, y descarta el spam detectado por un honeypot.

CategoríaBloques
TipoBloque (sección lista para usar) (<mv-contact>)
Estadoestable
También instalabutton, checkbox, field, input, select, textarea
Keywordsexclusive, contact, contact form, form, support, sales, offices, locations, honeypot, landing

When to use

  • A marketing site needs a contact page with a form next to email, phone, office address and opening hours
  • A support or sales team wants a short centered request form with an email fallback
  • A company with offices in several cities shoul d list them wit h local time above one routed for min
  • The form must keep working without JavaScript and report sending, success and errors to screen readers

Avoid when

  • Visitors only leave an email address to join a newsletter or a waitlist → use CTA instead
  • The form signs people in, creates an account or resets a password → use Auth instead
  • The page mostly answers common questions and a contact link is enough → use FAQ instead

Instalación

node scripts/add.mjs contact --out ./src/marvelous

Agente de IA con el servidor MCP de Marvelous UI: install_components({ slugs: ["contact"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Archivos copiados (dependencias incluidas): tokens/tokens.css, core/base.css, components/button/button.css, components/checkbox/checkbox.css, components/field/field.css, components/input/input.css, components/select/select.css, core/dom.js, core/element.js, components/textarea/textarea.css, components/textarea/char-count.js, components/contact/contact.css, components/contact/contact.js.

Uso

Marcado de referencia: parte de él y personalízalo con atributos, data-* y variables CSS:

<mv-tabs id="contact-demo" value="split" data-size="sm" style="gap:0;background:var(--mv-bg)">
  <style>#contact-demo > [role="tablist"] { justify-self: center; margin: .75rem auto 0; }</style>
  <div role="tablist" aria-label="Block variant">
    <button role="tab" data-value="split">Split</button>
    <button role="tab" data-value="centered">Centered</button>
    <button role="tab" data-value="offices">Offices</button>
  </div>

  <!-- ═════════ Split (default) ═════════ -->
  <div role="tabpanel" data-value="split">
    <mv-contact>
      <section class="mv-contact" aria-labelledby="ct-s-title">
        <div class="mv-contact-inner">
          <div class="mv-contact-aside">
            <header class="mv-contact-header">
              <p class="mv-contact-eyebrow">Contact</p>
              <h2 class="mv-contact-title" id="ct-s-title">Talk to the people <em>who build it.</em></h2>
              <p class="mv-contact-lead">Questions about pricing, a rollout for 400 technicians or a feature you need? A product specialist answers within one business day.</p>
            </header>
            <ul class="mv-contact-channels" aria-label="Other ways to reach us">
              <li>
                <a class="mv-contact-channel" href="mailto:[email protected]">
                  <small>Sales</small>
                  <strong>[email protected]</strong>
                  <span>Quotes, volume pricing and procurement.</span>
                </a>
              </li>
              <li>
                <a class="mv-contact-channel" href="tel:+14155550142">
                  <small>Phone</small>
                  <strong>+1 (415) 555-0142</strong>
                  <span>Ask for Priya or Mateo on the sales desk.</span>
                </a>
              </li>
              <li>
                <a class="mv-contact-channel" href="https://www.openstreetmap.org/search?query=548%20Market%20Street%20San%20Francisco" target="_blank" rel="noopener">
                  <small>Office</small>
                  <strong>548 Market Street, Suite 300</strong>
                  <span>San Francisco, CA 94104 · Open in maps</span>
                </a>
              </li>
              <li>
                <div class="mv-contact-channel">
                  <small>Hours</small>
                  <strong>Mon to Fri, 8 am to 6 pm</strong>
                  <span>Pacific Time (San Francisco)</span>
                  <span class="mv-contact-clock" data-timezone="America/Los_Angeles" data-hours="08:00-18:00"></span>
                </div>
              </li>
            </ul>
          </div>

          <form class="mv-contact-form" id="ct-s-form" action="/api/contact" method="post" aria-labelledby="ct-s-form-title">
            <div class="mv-contact-form-head">
              <h3 class="mv-contact-form-title" id="ct-s-form-title">Send a message</h3>
              <p class="mv-contact-form-meta">Median reply · 3 h 20 min</p>
            </div>
            <div class="mv-field-group">
              <div class="mv-field-row">
                <div class="mv-field">
                  <label class="mv-label" for="ct-s-name" data-required>Full name</label>
                  <input class="mv-input" id="ct-s-name" name="name" autocomplete="name" required aria-describedby="ct-s-name-err">
                  <p class="mv-field-error" id="ct-s-name-err">Enter your name so we know who to reply to.</p>
                </div>
                <div class="mv-field">
                  <label class="mv-label" for="ct-s-email" data-required>Work email</label>
                  <input class="mv-input" id="ct-s-email" name="email" type="email" autocomplete="email" placeholder="[email protected]" required aria-describedby="ct-s-email-err">
                  <p class="mv-field-error" id="ct-s-email-err">Enter a full address, like [email protected].</p>
                </div>
              </div>
              <div class="mv-field-row">
                <div class="mv-field">
                  <label class="mv-label" for="ct-s-company" data-optional="Optional">Company</label>
                  <input class="mv-input" id="ct-s-company" name="company" autocomplete="organization">
                </div>
                <div class="mv-field">
                  <label class="mv-label" for="ct-s-topic">Topic</label>
                  <select class="mv-select" id="ct-s-topic" name="topic">
                    <option value="sales">Pricing and plans</option>
                    <option value="demo">Book a product demo</option>
                    <option value="partners">Partnerships</option>
                    <option value="press">Press and media</option>
                    <option value="other">Something else</option>
                  </select>
                </div>
              </div>
              <div class="mv-field">
                <div class="mv-field-header">
                  <label class="mv-label" for="ct-s-message" data-required>Message</label>
                  <mv-char-count for="ct-s-message"></mv-char-count>
                </div>
                <textarea class="mv-textarea" id="ct-s-message" name="message" minlength="20" maxlength="2000" required placeholder="Team size, the tools you use today, what you’d like to change…" aria-describedby="ct-s-message-err"></textarea>
                <p class="mv-field-error" id="ct-s-message-err">Tell us a little more: at least 20 characters.</p>
              </div>
              <div class="mv-field">
                <label class="mv-choice">
                  <input class="mv-checkbox" type="checkbox" name="consent" required aria-describedby="ct-s-consent-err">
                  <span>I agree to the <a href="#contact-demo">privacy policy</a> and to being contacted about this request.</span>
                </label>
                <p class="mv-field-error" id="ct-s-consent-err">Please accept so we can reply to you.</p>
              </div>
            </div>
            <div class="mv-contact-trap" aria-hidden="true">
              <label for="ct-s-website">Leave this field empty</label>
              <input id="ct-s-website" name="website" tabindex="-1" autocomplete="off">
            </div>
            <div class="mv-contact-actions">
              <button class="mv-button" type="submit">Send message</button>
              <p class="mv-contact-fine">No newsletter, no sales sequence. One person reads it and replies.</p>
            </div>
            <p class="mv-contact-status" role="status" aria-live="polite"></p>
          </form>
        </div>
      </section>
    </mv-contact>
  </div>

  <!-- ═════════ Centered ═════════ -->
  <div role="tabpanel" data-value="centered">
    <mv-contact>
      <section class="mv-contact" data-variant="centered" aria-labelledby="ct-c-title">
        <div class="mv-contact-inner">
          <header class="mv-contact-header">
            <p class="mv-contact-eyebrow">Support</p>
            <h2 class="mv-contact-title" id="ct-c-title">How can we help?</h2>
            <p class="mv-contact-lead">Describe what you’re trying to do. A support engineer in your time zone picks it up, usually within two hours.</p>
          </header>
          <form class="mv-contact-form" id="ct-c-form" action="/api/support" method="post" aria-label="Support request">
            <div class="mv-field-group">
              <div class="mv-field-row">
                <div class="mv-field">
                  <label class="mv-label" for="ct-c-name" data-required>Name</label>
                  <input class="mv-input" id="ct-c-name" name="name" autocomplete="name" required aria-describedby="ct-c-name-err">
                  <p class="mv-field-error" id="ct-c-name-err">Enter your name.</p>
                </div>
                <div class="mv-field">
                  <label class="mv-label" for="ct-c-email" data-required>Email</label>
                  <input class="mv-input" id="ct-c-email" name="email" type="email" autocomplete="email" placeholder="[email protected]" required aria-describedby="ct-c-email-err">
                  <p class="mv-field-error" id="ct-c-email-err">Enter a full address, like [email protected].</p>
                </div>
              </div>
              <div class="mv-field">
                <label class="mv-label" for="ct-c-message" data-required>What’s going on?</label>
                <textarea class="mv-textarea" id="ct-c-message" name="message" minlength="10" maxlength="2000" required placeholder="Steps to reproduce, the page you were on, what you expected…" aria-describedby="ct-c-message-err" style="--mv-textarea-min-rows:3"></textarea>
                <p class="mv-field-error" id="ct-c-message-err">Add a few words: at least 10 characters.</p>
              </div>
            </div>
            <div class="mv-contact-trap" aria-hidden="true">
              <label for="ct-c-website">Leave this field empty</label>
              <input id="ct-c-website" name="website" tabindex="-1" autocomplete="off">
            </div>
            <div class="mv-contact-actions">
              <button class="mv-button" type="submit">Send request</button>
              <p class="mv-contact-fine">By sending, you agree to our <a href="#contact-demo">privacy policy</a>.</p>
            </div>
            <p class="mv-contact-status" role="status" aria-live="polite"></p>
          </form>
          <p class="mv-contact-alt">
            <span>Prefer email? <a href="mailto:[email protected]">[email protected]</a></span>
            <span><a href="#contact-demo">Status page</a></span>
          </p>
        </div>
      </section>
    </mv-contact>
  </div>

  <!-- ═════════ Offices ═════════ -->
  <div role="tabpanel" data-value="offices">
    <mv-contact>
      <section class="mv-contact" data-variant="offices" aria-labelledby="ct-o-title">
        <div class="mv-contact-inner">
          <header class="mv-contact-header">
            <p class="mv-contact-eyebrow">Offices</p>
            <h2 class="mv-contact-title" id="ct-o-title">Four offices, <em>one team.</em></h2>
            <p class="mv-contact-lead">Visit us, call the nearest desk, or write below and the right office replies in your language.</p>
          </header>
          <ul class="mv-contact-offices" aria-label="Our offices">
            <li class="mv-contact-office">
              <p class="mv-contact-office-label">Headquarters</p>
              <h3 class="mv-contact-office-city">San Francisco</h3>
              <address>548 Market Street, Suite 300<br>San Francisco, CA 94104<br>United States</address>
              <ul class="mv-contact-office-links">
                <li><a href="tel:+14155550142">+1 (415) 555-0142</a></li>
                <li><a href="https://www.openstreetmap.org/search?query=548%20Market%20Street%20San%20Francisco" target="_blank" rel="noopener">Directions</a></li>
              </ul>
              <span class="mv-contact-clock" data-timezone="America/Los_Angeles" data-hours="08:00-18:00"></span>
            </li>
            <li class="mv-contact-office">
              <p class="mv-contact-office-label">Europe</p>
              <h3 class="mv-contact-office-city">Lisbon</h3>
              <address>Rua do Alecrim 47, 3rd floor<br>1200-018 Lisbon<br>Portugal</address>
              <ul class="mv-contact-office-links">
                <li><a href="tel:+351215550192">+351 21 555 0192</a></li>
                <li><a href="https://www.openstreetmap.org/search?query=Rua%20do%20Alecrim%2047%20Lisboa" target="_blank" rel="noopener">Directions</a></li>
              </ul>
              <span class="mv-contact-clock" data-timezone="Europe/Lisbon" data-hours="09:00-18:00"></span>
            </li>
            <li class="mv-contact-office">
              <p class="mv-contact-office-label">Asia Pacific</p>
              <h3 class="mv-contact-office-city">Singapore</h3>
              <address>71 Robinson Road, #14-01<br>Singapore 068895</address>
              <ul class="mv-contact-office-links">
                <li><a href="tel:+6565550147">+65 6555 0147</a></li>
                <li><a href="https://www.openstreetmap.org/search?query=71%20Robinson%20Road%20Singapore" target="_blank" rel="noopener">Directions</a></li>
              </ul>
              <span class="mv-contact-clock" data-timezone="Asia/Singapore" data-hours="09:00-18:00"></span>
            </li>
            <li class="mv-contact-office">
              <p class="mv-contact-office-label">Latin America</p>
              <h3 class="mv-contact-office-city">São Paulo</h3>
              <address>Av. Paulista 1842, 12th floor<br>São Paulo, SP 01310-200<br>Brazil</address>
              <ul class="mv-contact-office-links">
                <li><a href="tel:+551155550133">+55 11 5555 0133</a></li>
                <li><a href="https://www.openstreetmap.org/search?query=Avenida%20Paulista%201842%20Sao%20Paulo" target="_blank" rel="noopener">Directions</a></li>
              </ul>
              <span class="mv-contact-clock" data-timezone="America/Sao_Paulo" data-hours="09:00-18:00"></span>
            </li>
          </ul>

          <form class="mv-contact-form" id="ct-o-form" action="/api/contact" method="post" aria-labelledby="ct-o-form-title">
            <div class="mv-contact-form-intro">
              <h3 id="ct-o-form-title">Write to the nearest team</h3>
              <p>Pick an office and we’ll route your message there. Replies come from a real person, in English, Portuguese or Mandarin.</p>
            </div>
            <div class="mv-field-group">
              <div class="mv-field-row">
                <div class="mv-field">
                  <label class="mv-label" for="ct-o-name" data-required>Name</label>
                  <input class="mv-input" id="ct-o-name" name="name" autocomplete="name" required aria-describedby="ct-o-name-err">
                  <p class="mv-field-error" id="ct-o-name-err">Enter your name.</p>
                </div>
                <div class="mv-field">
                  <label class="mv-label" for="ct-o-email" data-required>Email</label>
                  <input class="mv-input" id="ct-o-email" name="email" type="email" autocomplete="email" placeholder="[email protected]" required aria-describedby="ct-o-email-err">
                  <p class="mv-field-error" id="ct-o-email-err">Enter a full address.</p>
                </div>
                <div class="mv-field">
                  <label class="mv-label" for="ct-o-office">Office</label>
                  <select class="mv-select" id="ct-o-office" name="office">
                    <option value="sfo">San Francisco</option>
                    <option value="lis">Lisbon</option>
                    <option value="sin">Singapore</option>
                    <option value="sao">São Paulo</option>
                  </select>
                </div>
              </div>
              <div class="mv-field">
                <label class="mv-label" for="ct-o-message" data-required>Message</label>
                <textarea class="mv-textarea" id="ct-o-message" name="message" minlength="10" maxlength="2000" required placeholder="What can we help you with?" aria-describedby="ct-o-message-err" style="--mv-textarea-min-rows:3"></textarea>
                <p class="mv-field-error" id="ct-o-message-err">Add a few words: at least 10 characters.</p>
              </div>
            </div>
            <div class="mv-contact-trap" aria-hidden="true">
              <label for="ct-o-website">Leave this field empty</label>
              <input id="ct-o-website" name="website" tabindex="-1" autocomplete="off">
            </div>
            <div class="mv-contact-actions">
              <div class="mv-field" style="flex:1 1 16rem">
                <label class="mv-choice">
                  <input class="mv-checkbox" type="checkbox" name="consent" required aria-describedby="ct-o-consent-err">
                  <span>I agree to the <a href="#contact-demo">privacy policy</a>.</span>
                </label>
                <p class="mv-field-error" id="ct-o-consent-err">Please accept so we can reply.</p>
              </div>
              <button class="mv-button" type="submit">Send message</button>
            </div>
            <p class="mv-contact-status" role="status" aria-live="polite"></p>
          </form>
        </div>
      </section>
    </mv-contact>
  </div>

  <script type="module">
    // Demo only: nothing is sent. An address at example.com shows the error state.
    const demo = document.querySelector("#contact-demo");
    demo?.addEventListener("mv-submit", (e) => {
      e.preventDefault();
      const contact = e.target;
      const failing = /@example\.com$/i.test(e.detail.data.email ?? "");
      setTimeout(() => failing
        ? contact.fail("Our mail server didn’t answer. Your message is still here: try again in a minute.")
        : contact.done(`Thanks, ${String(e.detail.data.name).split(" ")[0]}. Your message is on its way, and we’ll reply by email.`), 1400);
    });
  </script>
</mv-tabs>

API

Attributes

NameTipoDefaultDescription
data-variantsplit | centered | officessplitOn .mv-contact. split (default, or no attribute): .mv-contact-aside (header + channels) beside the form from 56rem; centered: header, a form up to 36rem wide and .mv-contact-alt on one axis; offices: header with title and lead side by side from 48rem, .mv-contact-offices grid (1, 2, then --mv-contact-offices-columns), then a compact form with its intro on the left.
data-timezoneIANA time zoneOn .mv-contact-clock: shows the local time there (Intl, nearest lang), refreshed every minute. Without JavaScript the element stays empty and hidden, or keeps any fallback text you put in it.
data-hoursHH:MM-HH:MMOn .mv-contact-clock: opening hours in that time zone; adds an Open now / Closed state and toggles data-open.
data-daysstring1-5On .mv-contact-clock: open weekdays as a range or list (0 or 7 = Sunday), e.g. "1-6" or "1,3,5".
action / method / enctypenativeOn .mv-contact-form: where the form posts. Without JavaScript it submits normally; with <mv-contact> the same request is sent with fetch (urlencoded unless enctype is multipart/form-data, Accept: application/json). No action: nothing is sent and the form resolves as sent, so handle mv-submit.
data-statesending | sent | errorSet on the form by <mv-contact> while sending and after the answer, for your own styling.
aria-busytrueSet on the form and its submit button while sending: the button shows its spinner, fields dim and ignore the pointer.

Properties

NameTipoDescription
stringsobjectOverride any default text: sending, success, error, offline, open, closed, time ({time}). Clock times follow the nearest lang (Intl). Can be set before the element is defined.
pendingHTMLFormElement | nullForm currently being sent (read-only).

Methods

NameDescription
done(message?)Resolves the pending send: resets the form and announces message (or strings.success).
fail(message?)Rejects the pending send: keeps what was typed and announces message (or strings.error).
reset(form?)Clears the status of every form, or of one form.

Events

NameDescription
mv-submitA valid form is about to be sent. detail: { data, formData, form }. Cancelable: preventDefault() and send it yourself, then call done() or fail(). Not fired when the honeypot is filled.
mv-successThe message was sent. detail: { form }.
mv-errorSending failed. detail: { form, message }.

Content structure

NameDescription
.mv-contact-header-eyebrow (accent square), -title (<em> dims a phrase), -lead.
ul.mv-contact-channelsSplit variant: li > a.mv-contact-channel (mailto:, tel:, map link) or div.mv-contact-channel with <small> label, <strong> value, <span> note, optional .mv-contact-clock. Rows are numbered 01, 02… in a mono gutter; links get an arrow (↗ for http links).
form.mv-contact-formPanel form: optional .mv-contact-form-head (-title, -meta) or .mv-contact-form-intro (offices), a .mv-field-group of field, input, select, textarea and checkbox components, .mv-contact-trap, .mv-contact-actions (button + .mv-contact-fine), .mv-contact-status.
.mv-contact-trapHoneypot: a label and an input (tabindex="-1", autocomplete="off") moved off screen and aria-hidden. A filled trap is never sent by <mv-contact>; check it on the server too.
.mv-contact-statusrole="status" aria-live="polite", hidden when empty; data-tone="pending | success | error" styles it.
ul.mv-contact-offices > li.mv-contact-officeOffices variant: .mv-contact-office-label (numbered), h3.mv-contact-office-city, <address>, ul.mv-contact-office-links, .mv-contact-clock.
.mv-contact-altCentered variant: fallback line under the form; <span> items are separated by a middle dot.

CSS classes

NameDescription
mv-contactRoot section, mv-contact query container.
mv-contact-innerWidth, padding and numbering scope.
mv-contact-clockLive local time; -state (Open now / Closed with a dot) and -time are generated.

CSS variables

NameDefaultDescription
--mv-contact-max72remMax width.
--mv-contact-bgtransparentSection background.
--mv-contact-panel-bgvar(--mv-surface)Form panel background.
--mv-contact-offices-columns4Office columns from 60rem.

Accessibility

The section is labeled by its h2 and the form by its own heading or aria-label. Fields use native validation: errors appear under each field after interaction (:user-invalid) and are tied with aria-describedby; required fields carry required, the asterisk is decorative. Sending, success and error messages are spelled out in a polite live region, the submit button gets aria-busy and keeps focus. The honeypot is off screen, aria-hidden and out of the tab order. Channels are real mailto:, tel: and map links with visible text; external links open with rel="noopener". Row numbers are CSS-generated with empty alternative text. Focus rings stay visible in forced colors, and the pending pulse stops with reduced motion.

Esta página se tradujo con IA. Informar de un problema de traducción