Form
A <form> that coordinates its Fields' validation.
focusOnError
Submit with an invalid email to see the client-side path. For the async path, submit a valid address and keep typing your password during the 1.5s round trip — the rejection must not take the caret.
Sign in
Using Form
Silica ships one design system through three paths. They share a class vocabulary but are not interchangeable — pick the one that matches what you are building.
React
Typed components over those same classes, with behavior and accessibility from Base UI.
npm i @wizeworks/silicaui-reactimport { Form } from "@wizeworks/silicaui-react";Props
| Prop | Type | Required | Description |
|---|---|---|---|
| focusOnError | FormFocusOnError | no | How an invalid submit (or a late errors update) treats focus. Defaults to true. |
Source of the demo above
The exact file that renders the examples above, so it can never drift from them. It imports a few of this site’s own layout helpers (../lib/…), so read it for the component calls rather than copying it whole.
Show source
import { useState } from "react";
import {
Button,
Field,
FieldControl,
FieldError,
FieldLabel,
Form,
PasswordInput,
ToggleGroup,
ToggleGroupItem,
} from "@wizeworks/silicaui-react";
import type { FormFocusOnError } from "@wizeworks/silicaui-react";
import { Section, Stack } from "../lib/Section";
type Errors = Record<string, string>;
/**
* `Form` runs each Field's validation on submit and moves focus to the first
* invalid control. `focusOnError` decides how far that goes — the sign-in shape
* below is where it matters, because the rejection arrives from a server long
* after the user has moved on to the next field.
*/
export function FormDemo() {
const [focusOnError, setFocusOnError] = useState<FormFocusOnError>(true);
const [errors, setErrors] = useState<Errors>({});
const [pending, setPending] = useState(false);
function onSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
setErrors({});
setPending(true);
// Stands in for a sign-in round trip. Type into the password field while
// it runs: the caret has to stay put when the rejection lands.
window.setTimeout(() => {
setPending(false);
setErrors({ email: "That address isn't registered." });
}, 1500);
}
return (
<>
<Section title="focusOnError">
<Stack className="max-w-sm">
<ToggleGroup
size="sm"
value={[String(focusOnError)]}
onValueChange={(v) => {
const next = v[0];
if (next == null) return; // clicking the active item can't clear it
setFocusOnError(
next === "false" ? false : next === "scroll" ? "scroll" : true,
);
}}
>
<ToggleGroupItem value="true">true</ToggleGroupItem>
<ToggleGroupItem value="scroll">scroll</ToggleGroupItem>
<ToggleGroupItem value="false">false</ToggleGroupItem>
</ToggleGroup>
<p className="text-sm">
Submit with an invalid email to see the client-side path. For the
async path, submit a <em>valid</em> address and keep typing your
password during the 1.5s round trip — the rejection must not take
the caret.
</p>
</Stack>
</Section>
<Section title="Sign in">
<Form
errors={errors}
focusOnError={focusOnError}
onSubmit={onSubmit}
className="grid max-w-sm gap-4"
>
<Field name="email">
<FieldLabel required>Email</FieldLabel>
<FieldControl
type="email"
required
placeholder="you@example.com"
data-testid="demo-email"
/>
<FieldError />
</Field>
<Field
name="password"
validate={(v) =>
String(v ?? "").length >= 8 ? null : "Use at least 8 characters."
}
>
<FieldLabel required>Password</FieldLabel>
<FieldControl
required
render={<PasswordInput data-testid="demo-password" />}
/>
<FieldError />
</Field>
<Button type="submit" color="primary" loading={pending}>
{pending ? "Signing in…" : "Sign in"}
</Button>
</Form>
</Section>
</>
);
}Vanilla — node tree to HTML
Form — a <form> that ALWAYS lowers with the form behavior marker so a published form is functional (validate + submit) with zero author wiring. Build a node tree, project it with toHtml(), and let the zero-dependency runtime hydrate it. This is the path for generated or user-authored documents — a site builder, CMS output, a static export.
npm i @wizeworks/silicaui-html @wizeworks/silicaui-behaviorsimport { atom, toHtml } from "@wizeworks/silicaui-html";
const node = atom("Form", undefined, { /* props */ }, [/* children */]);
const markup = toHtml(node);Styling on this path is the class string in the second argument — the same classes as the Plain HTML section above. There is no color prop here; write "… …-primary" rather than passing a colour as a prop.
- Takes children
- yes
- Hydrated by
form
Load @wizeworks/silicaui-behaviors on the page or this markup renders correctly and does nothing.
The full API for Form, on every path, is available through the Silica MCP server.