Docs

Form

The whole form from a schema: fields, submit, inline errors and the success state, with zero markup.

Form takes a schema and a client and renders a finished form. With no children it renders every page itself; with children you compose the layout from the other components.

Preview

Usage

import { Form } from '@trymirio/mirio';
import { mirio } from '@/mirio/client';
import { waitlist } from '@/mirio/forms/waitlist';

export function Waitlist() {
  return <Form schema={waitlist} client={mirio} />;
}

One page renders the fields and a Submit button. Several pages render MultiStep. After a successful submit the form is replaced by Success, unless settings.success is a redirect or schedule, in which case the browser navigates.

Props

Prop Type Default Description
schema FormSchema required The schema from defineForm.
client MirioClient nearest MirioProvider From createClient (mirio/client.ts). Throws a descriptive error when neither is present.
onSuccess (result: SubmissionResponse) => void — Called with { id, status, success?, flags? }.
onError (error: MirioError) => void — Called with { code, message, hint?, fields?, status }.
initialValues Record<string, unknown> — Programmatic prefill, keyed by field key. Wins over prefill.query and defaultValue.
children ReactNode auto-render Replaces the automatic layout. Use Field, Submit, MultiStep etc. inside.
showSuccess boolean true Swap the form for Success after submit.
requiredIndicator ReactNode | null nothing What labels append to required fields. See Label.
optionalIndicator ReactNode | null muted "Optional" What labels append to optional fields; null for nothing.
theme MirioTheme from MirioProvider primary, primaryForeground, background, foreground, muted, mutedForeground, border, ring, error, inputBackground, radius ('none' | 'sm' | 'md' | 'lg' | 'full' or a CSS length), font, colorScheme ('light' | 'dark' | 'auto'). Inline --mirio-* variables on the root; merged over the provider's per key. See Theming.
size 'sm' | 'md' | 'lg' provider's, else 'md' Token bundle for control height, text size and spacing (data-mirio-size).
layout { columns?: 1 | 2 | 3; full?: string[] } one column Grid for the auto-rendered fields and Page. Long text, choices, consent and full keys span all columns; one column below 640px.
classNames Partial<Record<Slot, string>> — Classes per part: root, form, page, pageHeader, field, label, help, input, option, segment, rating, consent, error, submit, button, progress, success. Merged after the provider's and before each component's own className.
className string flex flex-col gap-mirio Merged onto the inner <form> (after classNames.form).
rootClassName string font-mirio text-mirio-fg Merged onto the outer [data-mirio-root] wrapper (after classNames.root).
…<form> attributes Everything else (id, aria-label, …) is spread on the <form>. aria-label defaults to schema.name unless you pass aria-label or aria-labelledby.

Rendered structure:

<div data-mirio-root data-mirio="form" data-slug="waitlist" data-status="idle|validating|submitting|success|error"
     data-mirio-size="md" [data-mirio-color-scheme="dark"] [style="--mirio-primary: …"]>
  <form novalidate aria-label="Waitlist">
    <div data-mirio="honeypot">…</div>            <!-- visually hidden, no CSS needed -->
    …fields…
    <div data-mirio="turnstile"></div>             <!-- only when a Turnstile site key is set -->
    <div role="alert" data-mirio="form-error" data-code="…"><p>message</p><p>hint</p></div>  <!-- only on a form-level error -->
  </form>
</div>

Form-level errors (network, rate_limited, project_unclaimed, agents_blocked, …) render the server's message and hint in data-mirio="form-error". Field-level server errors (validation_failed with fields) go to the matching FieldError instead and focus moves to the first one.

Variants and states

Composed layout: children replace the automatic rendering.

import { Form, Field, Submit } from '@trymirio/mirio';

<Form schema={contact} client={mirio} className="gap-5">
  <div className="grid grid-cols-2 gap-4">
    <Field name="first_name" />
    <Field name="last_name" />
  </div>
  <Field name="email" />
  <Field name="message" />
  <Submit fullWidth={false} className="self-end" />
</Form>

Rethemed with the theme prop, same markup:

<Form schema={waitlist} client={mirio} theme={{ primary: '#2563eb', ring: '#2563eb', radius: 'full', border: '#bfdbfe', muted: '#eff6ff', mutedForeground: '#1d4ed8' }} />

Dark, following the site: under a .dark (or [data-theme="dark"]) ancestor the built-in dark values apply. theme={{ colorScheme: 'dark' }} forces them on one form.

<div className="dark">
  <Form schema={waitlist} client={mirio} />
</div>

Two columns and a larger size:

<Form schema={demo} client={mirio} size="lg" layout={{ columns: 2, full: ['company_url'] }} />

Status is exposed as data-status on the wrapper, so [data-mirio-root][data-status="submitting"] can style the whole form while a request is in flight.

Customization

  • Variables the wrapper reads: --mirio-font, --mirio-fg, --mirio-text, --mirio-space (the gap between fields via gap-mirio), --mirio-color-scheme. Every child reads its own tokens; theme, size and colorScheme only change variables. See Styling and Theming.
  • className is merged with tailwind-merge, so className="gap-2" replaces the default gap-mirio; className="grid grid-cols-2" replaces flex flex-col.
  • CSS hooks: [data-mirio-root] (scope), [data-mirio="form"], [data-mirio="form-error"], [data-status="…"], [data-slug="…"], [data-mirio-size="…"], [data-mirio-color-scheme="…"].
  • To change the structure itself, compose children (Field, Submit, your own JSX) or build on the headless hooks.

Accessibility

  • Renders a real <form> with novalidate: validation messages are Mirio's (inline, below each control), not the browser's bubbles. aria-label is the form name unless you label it yourself.
  • Enter in a single-line input submits a single-page form, or advances to the next page in a multi-step form.
  • On a failed submit, focus moves to the first invalid control (jumping to its page in a multi-step form). On a page change, focus moves to the page heading, or the first field when the page has no title.
  • Form-level errors use role="alert".
  • The honeypot is aria-hidden, tabindex="-1" and off-screen; assistive technology never meets it.

Field, MultiStep, Success, MirioProvider, Headless, Schema, Styling, Theming