# Contact Form

> Headline and contact details beside a short native form with name, email, company and message. No JavaScript.

- **Slug:** `contact-form`
- **Kind:** section · **Category:** Conversion
- **Source:** original to Viewership UI
- **Docs page:** https://viewership.ai/design/components/contact-form
- **All files in one bundle:** https://viewership.ai/design/components/contact-form.bundle.txt
- **Structured JSON:** https://viewership.ai/design/components/contact-form.json

## Install

1. Import the foundation tokens once (download: https://viewership.ai/design/raw/foundation.css.txt):

```astro
---
// src/layouts/Layout.astro
import "../styles/foundation.css";
---
```

2. Copy this file into `src/components/viewership/contact-form/`:

   - `ContactForm.astro` (raw: https://viewership.ai/design/raw/contact-form/ContactForm.astro.txt)

3. Copy its registry dependencies, each into its own sibling folder under `src/components/viewership/`:

   - `button`: `Button.astro` (docs: https://viewership.ai/design/components/button.md)

npm packages: none.

## Usage

```astro
---
import ContactForm from "../components/viewership/contact-form/ContactForm.astro";
---

<ContactForm />
```

## Example

The gallery demo, verbatim. Use the import path from Usage instead of `./`.

```astro
---
import ContactForm from "./ContactForm.astro";
---

<ContactForm
  title="Tell us about"
  accent="your trip."
  body="Group bookings, repairs or a question about gear. A real person reads every message."
  details={[
    { label: "Email", value: "hello@northwind.test", href: "mailto:hello@northwind.test" },
    { label: "Replies", value: "Within one working day" },
  ]}
  action="#"
  note="We never share your details."
/>
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` | `string` |  | Headline. |
| `accent` | `string` |  | Closing words of the headline, drawn in the theme's accent style. |
| `body` | `string` |  | Supporting paragraph. |
| `details` | `{ label: string; value: string; href?: string }[]` |  | Contact lines under the copy, such as email or reply time. |
| `action` | `string` |  | Form handler URL. |
| `method` | `"post" \| "get"` | `"post"` | Form method. |
| `company` | `boolean` | `true` | Show the optional company field. |
| `messagePlaceholder` | `string` |  | Placeholder for the message box. |
| `submitLabel` | `string` | `"Send message"` | Submit button text. |
| `note` | `string` |  | Small line beside the submit button. |
| `as` | `"h1" \| "h2"` | `"h2"` | Heading element. |

## Source

### `src/components/viewership/contact-form/ContactForm.astro`

```astro
---
/**
 * ContactForm: headline and a few ways to reach you on one side, a short
 * native form on the other. Stacks when narrow. Uses the browser's own
 * validation (required, type="email") and needs no JavaScript; point
 * `action` at your form handler.
 *
 * @example
 * <ContactForm title="Tell us about" accent="your trip." action="/api/contact" details={[{ label: "Email", value: "hello@northwind.test", href: "mailto:hello@northwind.test" }]} />
 */
import Button from "../button/Button.astro";

interface Detail {
  label: string;
  value: string;
  href?: string;
}
interface Props {
  title: string;
  accent?: string;
  body?: string;
  /** Contact lines under the copy: email, phone, hours, response time. */
  details?: Detail[];
  /** Form handler URL. */
  action?: string;
  method?: "post" | "get";
  /** Show the optional company field. */
  company?: boolean;
  messagePlaceholder?: string;
  submitLabel?: string;
  /** Small line beside the submit button. */
  note?: string;
  as?: "h1" | "h2";
  class?: string;
}

const {
  title,
  accent,
  body,
  details = [],
  action,
  method = "post",
  company = true,
  messagePlaceholder = "A few lines about what you need",
  submitLabel = "Send message",
  note,
  as: Heading = "h2",
  class: className,
} = Astro.props;

const id = `vs-cf-${Math.random().toString(36).slice(2, 8)}`;
---

<section class:list={["vs-contact-form", className]}>
  <div class="vs-contact-form__inner">
    <div class="vs-contact-form__copy">
      <Heading class="vs-display vs-contact-form__title">{title}{accent && <> <em>{accent}</em></>}</Heading>
      {body && <p class="vs-contact-form__body">{body}</p>}
      {
        details.length > 0 && (
          <dl class="vs-contact-form__details">
            {details.map((d) => (
              <div>
                <dt>{d.label}</dt>
                <dd>{d.href ? <a href={d.href}>{d.value}</a> : d.value}</dd>
              </div>
            ))}
          </dl>
        )
      }
    </div>

    <form class="vs-contact-form__card" action={action} method={method}>
      <div class="vs-contact-form__row">
        <div class="vs-contact-form__field">
          <label for={`${id}-name`}>Name</label>
          <input id={`${id}-name`} name="name" type="text" autocomplete="name" required />
        </div>
        <div class="vs-contact-form__field">
          <label for={`${id}-email`}>Work email</label>
          <input id={`${id}-email`} name="email" type="email" autocomplete="email" required />
        </div>
      </div>
      {
        company && (
          <div class="vs-contact-form__field">
            <label for={`${id}-company`}>
              Company <span>Optional</span>
            </label>
            <input id={`${id}-company`} name="company" type="text" autocomplete="organization" />
          </div>
        )
      }
      <div class="vs-contact-form__field">
        <label for={`${id}-message`}>Message</label>
        <textarea id={`${id}-message`} name="message" rows="4" placeholder={messagePlaceholder} required></textarea>
      </div>
      <div class="vs-contact-form__submit">
        <Button type="submit" variant="primary">{submitLabel}</Button>
        {note && <p>{note}</p>}
      </div>
    </form>
  </div>
</section>

<style>
  .vs-contact-form {
    container-type: inline-size;
    background: var(--vs-page);
    color: var(--vs-ink);
    font-family: var(--vs-font-sans);
    font-stretch: var(--vs-font-stretch);
  }
  .vs-contact-form__inner {
    display: grid;
    gap: clamp(32px, 6cqi, 72px);
    max-width: var(--vs-container);
    margin-inline: auto;
    padding: var(--vs-section-y) var(--vs-gutter);
  }
  .vs-contact-form__title {
    font-size: clamp(2.25rem, 6cqi, 4rem);
  }
  .vs-contact-form__body {
    max-width: 28rem;
    margin: 1.25rem 0 0;
    font-size: 1.0625rem;
    line-height: 1.55;
    color: var(--vs-ink-2);
  }
  .vs-contact-form__details {
    display: grid;
    gap: 18px;
    margin: 2.25rem 0 0;
  }
  .vs-contact-form__details dt {
    font-size: 0.8125rem;
    color: var(--vs-ink-3);
  }
  .vs-contact-form__details dd {
    margin: 4px 0 0;
    font-size: 1.0625rem;
  }
  .vs-contact-form__details a {
    color: inherit;
    text-decoration: underline;
    text-decoration-color: var(--vs-line-strong);
    text-underline-offset: 4px;
    transition: text-decoration-color var(--vs-duration-fast) ease-out;
  }
  .vs-contact-form__details a:hover {
    text-decoration-color: var(--vs-accent);
  }

  .vs-contact-form__card {
    display: grid;
    gap: 18px;
    align-self: start;
    padding: clamp(20px, 4cqi, 32px);
    border-radius: calc(var(--vs-radius-window) + 6px);
    background: var(--vs-surface);
    border: var(--vs-border-w) solid var(--vs-elev-ring);
    box-shadow: var(--vs-pop-shadow, var(--vs-shadow-md));
  }
  .vs-contact-form__row {
    display: grid;
    gap: 18px;
  }
  .vs-contact-form__field {
    display: grid;
    gap: 8px;
  }
  .vs-contact-form__field label {
    display: flex;
    justify-content: space-between;
    font-size: 0.875rem;
    font-weight: 500;
  }
  .vs-contact-form__field label span {
    font-weight: 400;
    color: var(--vs-ink-3);
  }
  .vs-contact-form input,
  .vs-contact-form textarea {
    width: 100%;
    box-sizing: border-box;
    padding: 12px 14px;
    border: var(--vs-border-w) solid var(--vs-line);
    border-radius: var(--vs-radius-control);
    background: var(--vs-inset);
    color: var(--vs-ink);
    font: inherit;
    font-size: 1rem;
    transition:
      border-color var(--vs-duration-fast) ease-out,
      background-color var(--vs-duration-fast) ease-out;
  }
  .vs-contact-form textarea {
    min-height: 120px;
    resize: vertical;
    line-height: 1.5;
  }
  .vs-contact-form input::placeholder,
  .vs-contact-form textarea::placeholder {
    color: var(--vs-ink-3);
  }
  .vs-contact-form input:hover,
  .vs-contact-form textarea:hover {
    border-color: var(--vs-line-strong);
  }
  .vs-contact-form input:focus-visible,
  .vs-contact-form textarea:focus-visible {
    outline: none;
    background: var(--vs-surface);
    box-shadow: var(--vs-focus-ring);
  }
  .vs-contact-form :is(input, textarea):user-invalid {
    border-color: var(--vs-red);
  }
  .vs-contact-form__submit {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 12px 16px;
    margin-top: 4px;
  }
  .vs-contact-form__submit p {
    margin: 0;
    font-size: 0.8125rem;
    color: var(--vs-ink-3);
  }

  @container (min-width: 560px) {
    .vs-contact-form__row {
      grid-template-columns: 1fr 1fr;
    }
  }
  @container (min-width: 860px) {
    .vs-contact-form__inner {
      grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
    }
  }
</style>
```
