# Contact Page

> A whole contact page, built from our website components: a floating navbar, a page title over the ways to reach you, a form in one soft card, a FAQ, and a footer.

- Name: `contact-page`
- Status: beta
- Categories: contact, form, marketing, page
- Files: the install writes `contact-page.tsx`, and any part files named `contact-page-*.tsx`, to your `components` alias, `@/components` by default. Edit them freely. They are yours.
- Other registry items: `@arbokui/navbar`, `@arbokui/headline`, `@arbokui/faq`, `@arbokui/footer`, `@arbokui/button`, `@arbokui/field-frame`, `@arbokui/input`, `@arbokui/textarea`, `@arbokui/select`, `@arbokui/glide-highlight`. The install adds them.
- Gallery: https://arbokui.com/templates/contact-page
- Registry item: https://arbokui.com/r/contact-page.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @arbokui/theme
   ```

3. Add the template. It also adds every component it uses:

   ```bash
   npx shadcn@latest add @arbokui/contact-page
   ```

## Use it for

- The contact page of a product site. Render it as the whole page, from your app's page file.
- Write every word in the props: one object for the navbar, the header, the ways to reach you, the form, the FAQ, and the footer.
- Keep the form short: a name, an email, a topic, and a message. Every field you add loses people.
- Send the message in \`form.onSubmit\`, like a fetch to your own route. Throw when it fails, and the form says so and keeps what they wrote.
- From a server page, pass a Server Action as \`form.onSubmit\`, since a server page cannot pass a plain function. Or render the page from a client file.
- Use \`channels\` for the places people can go instead, like a sales email, support, or the docs. Give each one a \`label\`, like the address, which reads as the link.
- Use the FAQ for what people ask before they write, like when you answer.
- Pass your router's link once in \`renderLink\`. The navbar, the ways to reach you, and the footer all use it.
- Set \`embedded\` when you show the page inside another page, like a gallery, so it has no second main.

## Avoid it for

- A form with no \`onSubmit\`. It shows the sent note, but the message goes nowhere.
- A promise you can't keep in \`note\`, like an answer within an hour.
- Asking for a phone number when email will do.

## Props

- `navbar`: `NavbarProps`. The Navbar's props. It floats as a pill once the page scrolls, and folds into a menu below 42rem.
- `header`: `HeadlineProps`. The headline at the top, left-aligned over the ways to reach you. It renders as the page's one h1.
- `channels`: `{ title: string; description?: ReactNode; href: string; label?: string }[]`. Ways to reach you besides the form, one row each, with a highlight that glides from row to row. Each title must be unique, since it is the key.
- `channelsLabel`: `string`. Default `"Other ways to reach us"`. Names the ways to reach you for screen readers.
- `form`: `ContactFormProps`. Required. The form's \`fields\` (each with a unique \`name\`, a \`label\`, and a \`type\` of text, email, tel, textarea, or select), its \`title\`, \`submitLabel\`, \`note\`, \`onSubmit\`, the \`sent\` note, and the \`failed\` line.
- `faq`: `{ id?: string; headline?: HeadlineProps; faq: FaqProps }`. Questions people ask before they write, under the form. Its id defaults to "faq".
- `footer`: `FooterProps`. The Footer's props.
- `renderLink`: `(props: NavbarLinkProps) => ReactNode`. Default `a plain <a>`. Draws every link in the navbar, the ways to reach you, and the footer, unless a part sets its own.
- `embedded`: `boolean`. Default `false`. Renders the content in a div, not a main, for a preview inside another page.
- `className`: `string`. Extra classes on the root, which holds the container query.

## Accessibility

- The content sits in a main landmark, between the navbar's nav and the footer, so skip links and screen readers find it.
- The header renders the page's one h1. The form's title and the FAQ's headline are h2s.
- Every field has a visible label, and its hint is linked to it, so screen readers read it.
- Send checks every field, gives each one that is wrong a red edge, and moves focus to the first. One line under the button says what to fix, and each wrong field is linked to it. It is under the button, so it never pushes the button. Typing in a field clears its edge.
- While it sends, the button is busy and takes no second press. A failure, or no answer within 20 seconds, is announced under the button, and the form keeps what was typed.
- Once sent, focus moves to the sent note, which is announced and scrolls clear of the floating navbar. For a moment after, a second tap goes nowhere, so a double tap on Send cannot land on what slid under it. Send another clears every field and moves focus to the first.
- The form and the sent note slide and fade in place of each other. With reduced motion they only fade, and with motion off they swap at once.
- Each part keeps its own accessibility, listed on its page.

## Examples

### A short contact page

```tsx
<ContactPage
  navbar={{ brand: "Workspace", links: [{ href: "/contact", label: "Contact", current: true }] }}
  header={{ title: "Talk to us.", muted: "We answer every message." }}
  channels={[{ title: "Support", description: "For help with your account.", href: "mailto:help@example.com", label: "help@example.com" }]}
  form={{
    fields: [
      { name: "name", label: "Name", required: true, autoComplete: "name", half: true },
      { name: "email", label: "Email", type: "email", required: true, autoComplete: "email", half: true },
      { name: "message", label: "Message", type: "textarea", required: true },
    ],
    note: "We answer within one working day.",
    onSubmit: async (values) => {
      const response = await fetch("/api/contact", { method: "POST", body: JSON.stringify(values) });
      if (!response.ok) throw new Error("Not sent");
    },
  }}
  footer={{
    brand: "Workspace",
    columns: [{ title: "Company", links: [{ href: "/contact", label: "Contact" }] }],
    legal: "© 2026 Workspace",
  }}
/>
```
