Experience System
Components

Form

Combine TanStack Form and Zod with Field, inputs, and related primitives from the experience system.

Bug Report
Help us improve by reporting bugs you encounter.

Include steps to reproduce, expected behavior, and what actually happened.

0/100 characters

Installation

Primitives such as Field, Input, and Card come from @by/experience-system. Add the package with your package manager:

pnpm add @by/experience-system

TanStack Form and Zod are not bundled with @by/experience-system. Install them in the application that owns the form:

pnpm add @tanstack/react-form zod

In this monorepo, depend on the workspace package (for example via workspace:* or your catalog) so imports resolve to packages/experience-system.

Composition

Use TanStack Form for state and validation, and Experience System Field parts for layout, labels, and errors:

useForm (TanStack Form)
└── form
    └── HTML form (onSubmit → preventDefault, handleSubmit)
        └── form.Field (per value or mode="array")
            └── Field (experience system)
                ├── FieldLabel
                ├── (control: Input, Textarea, Select, Checkbox, …)
                ├── FieldDescription (optional)
                └── FieldError (optional)

form.Field uses a render function so each control reads field.state, field.handleChange, and field.handleBlur. Pair FieldError with field.state.meta.errors after submit or touch, following TanStack Form and the shadcn/ui TanStack Form guide.

Usage

Add 'use client' in the Next.js App Router when you use hooks. Wire validators (for example onSubmit) to a Zod schema, set data-invalid on Field and aria-invalid on the control when field.state.meta.isTouched && !field.state.meta.isValid, and use noValidate on the <form> if you rely on schema messages instead of native browser validation.

import {
  Button,
  Field,
  FieldError,
  FieldGroup,
  FieldLabel,
  Input,
} from '@by/experience-system';
import { useForm } from '@tanstack/react-form';
import * as z from 'zod';

const schema = z.object({
  name: z.string().min(1, 'Required.'),
});

export function Example() {
  const form = useForm({
    defaultValues: { name: '' },
    validators: { onSubmit: schema },
    onSubmit: async ({ value }) => {
      console.log(value);
    },
  });

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        void form.handleSubmit();
      }}
      noValidate
    >
      <FieldGroup>
        <form.Field name="name">
          {(field) => {
            const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid;
            return (
              <Field data-invalid={isInvalid}>
                <FieldLabel htmlFor={field.name}>Name</FieldLabel>
                <Input
                  id={field.name}
                  name={field.name}
                  value={field.state.value}
                  onBlur={field.handleBlur}
                  onChange={(e) => field.handleChange(e.target.value)}
                  aria-invalid={isInvalid}
                />
                {isInvalid ? <FieldError errors={field.state.meta.errors} /> : null}
              </Field>
            );
          }}
        </form.Field>
      </FieldGroup>
      <Button type="submit">Submit</Button>
    </form>
  );
}

Mount Sonner once (for example in the root layout) if you use toast from @by/experience-system for submit feedback, or follow the isolated preview pattern in the Sonner article.

Examples

Input

Username with length and pattern rules.

Profile Settings
Update your profile information below.

This is your public display name. Must be between 3 and 10 characters. Must only contain letters, numbers, and underscores.

Textarea

This example uses variant="inset" (the default recessed surface). When Textarea sits next to Input, Select, combobox, or InputGroup, use variant="flat" for surface parity—see Textarea — Surface variant.

Longer text with a minimum length.

Personalization
Customize your experience by telling us more about yourself.

Tell us more about yourself. This will be used to help personalize your experience.

Select

Controlled Select with SelectTrigger, SelectValue, and SelectContent.

Language Preferences
Select your preferred spoken language.

For best results, select the language you speak.

Checkbox

form.Field with mode="array" and checkboxes that push or remove values.

Notifications
Manage your notification preferences.
Responses

Get notified for requests that take time, like research or image generation.

Radio group

RadioGroup with Radio options and descriptions.

Subscription Plan
See pricing and features for each plan.
Plan

You can upgrade or downgrade your plan at any time.

Switch

Horizontal Field with a boolean Switch and validation.

Security Settings
Manage your account security preferences.

Enable multi-factor authentication to secure your account.

API Reference

This page is a cookbook, not a new export from @by/experience-system. Behavior and types for useForm, form.Field, validators, and field state come from TanStack Form. Schema validation in the examples uses Zod.

Layout, validation affordances (data-invalid), FieldError (errors prop), and controls are the same components documented elsewhere on this site. Use the API Reference sections on those pages for props, data attributes, and accessibility hooks:

TopicExperience system article
Field, FieldGroup, FieldLabel, FieldError, …Field
InputInput
TextareaTextarea
SelectSelect
CheckboxCheckbox
RadioGroup, RadioRadio group
SwitchSwitch
Card, ButtonCard, Button
toast, SonnerSonner

Accessibility

Set aria-invalid on the focused control when the field is touched and invalid, keep FieldLabel associated via htmlFor / id, and render FieldError with field.state.meta.errors so assistive technologies receive role="alert" messages from the Experience System FieldError. TanStack Form manages focus and submission flow; see TanStack Form — React and the Field accessibility section for baseline patterns.

Source in the repo for Field: packages/experience-system/src/components/Field/Field.tsx. Agent-oriented contracts: packages/experience-system/src/components/Field/Field.instructions.md.