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-react
import { Form } from "@wizeworks/silicaui-react";

Props

PropTypeRequiredDescription
focusOnErrorFormFocusOnErrornoHow 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-behaviors
import { 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.