# Store forms (/themes/store-forms)

Store forms are **published definitions** that belong to the store. A theme renders them through
[`@poltura/theme-sdk/forms`](/themes/sdk/reference/forms). The definition decides the field
types, steps, conditions and success message. The theme decides only how the form looks.

## Render a form [#render-a-form]

```tsx
import { StoreForm } from '@poltura/theme-sdk/forms';

export function Contact() {
  return <StoreForm handle='contact' />;
}
```

`<StoreForm handle='contact' />` loads the published form and renders its visible fields,
steps, captcha, send button and success message. Its props shape the presentation without
changing the definition:

| Prop              | What it does                                                                     |
| ----------------- | -------------------------------------------------------------------------------- |
| `layout`          | `stacked`, or `compact` for two fields per row on wide screens                   |
| `appearance`      | `default`, `muted`, `outline`, `inverse`, in the theme's colours and fonts       |
| `chrome`          | `full` draws the title, description and card; `fields` draws only the fields     |
| `copy`            | The theme's own wording for the title, button, success message and each field's label and placeholder |
| `fieldVisibility` | Fields to hide or show by field id, over the merchant's settings                 |
| `emailHints`      | Deliverability hints under email fields; a domain that takes no mail blocks send |
| `onAccepted`      | Called with the submission id once the store accepts it, and `preview` (true in the Customizer) |

Pass children for a custom layout. `FormField` draws one field with its label, control, help and
error (nothing while a condition hides it), and `SubmitButton` is the send button, which moves to
the next step on multi-step forms:

```tsx
import { FormField, StoreForm, SubmitButton } from '@poltura/theme-sdk/forms';

export function Quote() {
  return (
    <StoreForm handle='quote' layout='compact' appearance='muted'>
      <FormField id='name' />
      <FormField id='email' />
      <FormField id='attachments' />
      <SubmitButton>Request a quote</SubmitButton>
    </StoreForm>
  );
}
```

`FormField` also takes a render function for custom markup. It keeps validation, errors and the
`aria-invalid` and `aria-describedby` wiring. `FieldControl` is the platform control to place
inside it:

```tsx
import { FieldControl, FormField } from '@poltura/theme-sdk/forms';

export function FramedMessage() {
  return (
    <FormField id='message'>
      {(state) => (
        <div className='rounded border p-3'>
          <FieldControl
            field={state.field}
            value={state.value}
            error={state.error}
            onChange={state.setValue}
          />
        </div>
      )}
    </FormField>
  );
}
```

For markup that is entirely your own, two hooks read the state inside a `StoreForm`.
`useStoreForm()` returns the whole form (status, values, errors, current step, `submit`, `next`,
`back`), and `useStoreFormField('email')` returns one field with its value, error, visibility and
setter.

## Newsletter [#newsletter]

`NewsletterForm` and `NewsletterTrigger` from `@poltura/theme-sdk/forms` render the newsletter
sign-up. `useNewsletter()` reads whether the store takes sign-ups, whether they must confirm by
email, and the wording to ask with. In a footer, the `newsletter` block (or
`<NewsletterForm layout='footer' />` in a section of your own) draws the footer column. Every
newsletter form uses the store's published newsletter form and follows the store-wide newsletter
setting, so the consent wording and validation are the same everywhere.

## Contact wrapper [#contact-wrapper]

`ContactForm` from `@poltura/theme-sdk/forms` renders `<StoreForm handle='contact' />`, the
store's published contact form, with wording props (`formTitle`, `nameLabel`, `showName`,
`showSubject`, `submitLabel` and more) for the theme's own copy. The platform `contact-form` block
passes its settings through to it.

The store must have a published form with the handle `contact`. The storefront never creates
one.

## Customizer [#customizer]

Merchants add a **Form** section (`store-form`) on any template and choose a published form by
its name. The form owns its fields, translations and delivery. The section owns its presentation.
One form can be used in several places.

| Setting               | What it does                                                       |
| --------------------- | ------------------------------------------------------------------ |
| `handle`              | The published form, chosen by name; its stable id in code          |
| `layout`              | `stacked` or `compact`                                             |
| `appearance`          | `default`, `muted`, `outline`, `inverse`, in the theme's colours   |
| `width`               | `compact`, `standard`, `wide`                                      |
| `placement`           | `inline`, `popup`, or right-side `drawer`                          |
| `trigger`             | `click` by default; `automatic` opens it after a delay             |
| `button_label`        | Text on the button that opens the popup or panel                   |
| `popup_delay_seconds` | Delay before it opens automatically; at least five seconds         |
| `popup_frequency`     | `once`, `session`, or every 30 days                                |

Automatic opening is off in the Customizer and on checkout pages. Use the preview button to test
the popup there. Buttons that open the form still work. Popups and panels scroll inside the
viewport, keep keyboard focus inside, close with Escape or the close button, and return focus to
the button that opened them. Closing and reopening the same form keeps what the shopper typed.
Navigating away or opening a different form clears it. Nothing typed is saved in browser
storage.

### Attach a form to a button [#attach-a-form-to-a-button]

In the Customizer, click a link or button on the canvas to select the section or block it
belongs to, or select it in the section tree. Its link setting offers **Open link** and **Open
form**. Choose a published form, a popup or right side panel, and a width. **Preview form** tries
it without publishing the theme, and publishing the theme puts it live. Hold Alt (Option on a Mac)
while you click the canvas to follow the link instead of selecting it. Form controls on the
canvas stay usable.

Every `url` and `link` setting and every link list entry offers the same choice. A button written
in code is only editable in the Customizer if it reads a link setting. Do not attach form actions
to checkout or add-to-cart buttons.

```tsx
import { FormTrigger, useFormActions } from '@poltura/theme-sdk/forms';
import { Link } from '@poltura/theme-sdk/routing';
import type { LinkValue } from '@poltura/theme-sdk/elements';

export function AskButton() {
  return (
    <FormTrigger handle='contact' display='drawer' width='standard'>
      Ask a question
    </FormTrigger>
  );
}

export function SettingLink({ link }: { link: LinkValue }) {
  return <Link href={link} className='underline' />;
}

export function QuoteButton() {
  const { openForm } = useFormActions();
  return (
    <button
      type='button'
      onClick={(event) => openForm({ handle: 'quote', display: 'drawer' }, event.currentTarget)}
    >
      Request a quote
    </button>
  );
}
```

`Link` from `@poltura/theme-sdk/routing` renders a link setting's value as it is. When the
merchant chose **Open form**, the stored href is a validated `#poltura-form?…` action, and
following it opens the form over the page without navigating. Ordinary links navigate as usual.
The platform supplies the form provider, so a theme does not mount one.

### Initial rendering [#initial-rendering]

The server loads the contact and newsletter forms ahead of time, and every form that a visible
`store-form` section or a link setting anywhere in the theme names (up to 32 different forms).
The first markup then contains the form instead of "Loading form". `FormTrigger` also starts
loading its form when it gets focus or the pointer moves over it. A handle that only code names
loads on demand. Call `useFormActions().preloadForm(handle)` to start loading it early, or expose
it through a link setting so the server loads it. Only published forms load this way, never a
draft.

## Files [#files]

`FormField` and `FieldControl` handle file fields for you. A theme that draws its own file
control calls `useStoreForm().uploadFiles(fieldId, files)` and reads the picked files from
`useStoreForm().files`. Uploads go through the storefront, and the submitted value holds upload
ids, never file URLs.

In the Customizer preview, picked files stay in the browser and are **not uploaded**. A preview
submission is sent as a preview, carries no files and is not stored.

## Where definitions come from [#where-definitions-come-from]

Theme code never defines a form's fields. Merchants create and edit forms on the admin's
**Forms** page, and the storefront reads only what they publish.

## Captcha [#captcha]

Each published form has its own captcha setting, on unless the merchant turned it off under
**Forms** > **Appearance** in the admin and published the form. The server enforces the published
setting whatever the browser sends. `StoreForm` draws the captcha itself.

## Next [#next]

- [Theme assets and media](/themes/theme-assets): ship images and fonts with the theme.
- [forms reference](/themes/sdk/reference/forms): every export of `@poltura/theme-sdk/forms`.
