Westy Design Systemv0.2.7
OverviewComponentsExamplesStorybook

Foundations

  • Typography
  • Colors
  • Spacing
  • Radius
  • Shadows
  • Icons

Components

  • Accordion
  • Alert
  • Alert Dialog
  • Aspect Ratio
  • Avatar
  • Badge
  • Breadcrumb
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Data Table
  • Date Picker
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • Field
  • Hover Card
  • Input
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Label
  • Menubar
  • Native Select
  • Navigation Menu
  • Pagination
  • Popover
  • Progress
  • Questionnaire
  • Radio Group
  • Resizable
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Skeleton
  • Slider
  • Sonner
  • Spinner
  • Switch
  • Table
  • Tabs
  • Textarea
  • Toggle
  • Toggle Group
  • Tooltip

Chat

  • Attachment
  • Bubble
  • Marker
  • Message
  • Message Scroller

Components

Field

Combine labels, controls, and help text to compose accessible form fields and grouped inputs.

Playground

We never share your email with anyone.

—orientation
—data-invalid
—data-disabled
Usage
Field wraps a single control with its FieldLabel, FieldDescription and FieldError. Stack related fields inside a FieldGroup. The description can sit before or after the control — spacing adjusts automatically. Field works with any control: Input, Textarea, Select, Slider, Checkbox, Radio Group or Switch.

Choose a unique username for your account.

Must be at least 8 characters long.

import {
  Field,
  FieldDescription,
  FieldGroup,
  FieldLabel,
} from "@workspace/ui/components/field"
import { Input } from "@workspace/ui/components/input"

<FieldGroup>
  <Field>
    <FieldLabel htmlFor="username">Username</FieldLabel>
    <Input id="username" placeholder="westy" />
    <FieldDescription>
      Choose a unique username for your account.
    </FieldDescription>
  </Field>
  <Field>
    <FieldLabel htmlFor="password">Password</FieldLabel>
    <FieldDescription>Must be at least 8 characters long.</FieldDescription>
    <Input id="password" type="password" placeholder="••••••••" />
  </Field>
</FieldGroup>
Horizontal
orientation="horizontal" places the label beside the control — the natural fit for Switch, Checkbox and Radio. When the field also has a description, wrap label and description in a FieldContent so they stay aligned as one column.

Folders are synced and accessible from other devices.

<Field orientation="horizontal">
  <FieldLabel htmlFor="2fa">Multi-factor authentication</FieldLabel>
  <Switch id="2fa" />
</Field>
<Field orientation="horizontal">
  <Checkbox id="sync" defaultChecked />
  <FieldContent>
    <FieldLabel htmlFor="sync">Sync Desktop & Documents folders</FieldLabel>
    <FieldDescription>
      Folders are synced and accessible from other devices.
    </FieldDescription>
  </FieldContent>
</Field>
Responsive
orientation="responsive" stacks vertically in narrow containers and switches to a horizontal row when the surrounding FieldGroup is at least 28rem wide. FieldGroup is a container-query root (@container/field-group), so the breakpoint follows the container — not the viewport. Resize the window to see it switch.

Provide your full name for identification.

<FieldGroup>
  <Field orientation="responsive">
    <FieldContent>
      <FieldLabel htmlFor="name">Name</FieldLabel>
      <FieldDescription>
        Provide your full name for identification.
      </FieldDescription>
    </FieldContent>
    <Input id="name" placeholder="Evil Rabbit" required />
  </Field>
  <Field orientation="responsive">
    <Button type="submit">Submit</Button>
    <Button type="button" variant="outline">
      Cancel
    </Button>
  </Field>
</FieldGroup>
Fieldset
FieldSet renders a semantic <fieldset> named by a FieldLegend, grouping related fields for keyboard and assistive tech users. Compose multi-column rows with a plain grid — each cell is its own Field.
Address Information

We need your address to deliver your order.

<FieldSet>
  <FieldLegend>Address Information</FieldLegend>
  <FieldDescription>
    We need your address to deliver your order.
  </FieldDescription>
  <FieldGroup>
    <Field>
      <FieldLabel htmlFor="street">Street Address</FieldLabel>
      <Input id="street" placeholder="123 Main St" />
    </Field>
    <div className="grid grid-cols-2 gap-4">
      <Field>
        <FieldLabel htmlFor="city">City</FieldLabel>
        <Input id="city" placeholder="New York" />
      </Field>
      <Field>
        <FieldLabel htmlFor="zip">Postal Code</FieldLabel>
        <Input id="zip" placeholder="90502" />
      </Field>
    </div>
  </FieldGroup>
</FieldSet>
Radio
A RadioGroup of horizontal Fields makes a choice list — the variant="label" legend drops to label size so nested groups don't shout over the fields. The same pattern works with Checkbox for multi-select lists.
Subscription Plan

Yearly and lifetime plans offer significant savings.

<FieldSet>
  <FieldLegend variant="label">Subscription Plan</FieldLegend>
  <FieldDescription>
    Yearly and lifetime plans offer significant savings.
  </FieldDescription>
  <RadioGroup defaultValue="monthly">
    <Field orientation="horizontal">
      <RadioGroupItem value="monthly" id="plan-monthly" />
      <FieldLabel htmlFor="plan-monthly" className="font-normal">
        Monthly ($9.99/month)
      </FieldLabel>
    </Field>
    <Field orientation="horizontal">
      <RadioGroupItem value="yearly" id="plan-yearly" />
      <FieldLabel htmlFor="plan-yearly" className="font-normal">
        Yearly ($99.99/year)
      </FieldLabel>
    </Field>
    <Field orientation="horizontal">
      <RadioGroupItem value="lifetime" id="plan-lifetime" />
      <FieldLabel htmlFor="plan-lifetime" className="font-normal">
        Lifetime ($299.99)
      </FieldLabel>
    </Field>
  </RadioGroup>
</FieldSet>
Choice Card
Wrap a Field inside a FieldLabel to turn the whole block into a selectable card — clicking anywhere toggles the control, and the checked state tints the border and background with the primary token. Works with RadioGroupItem, Checkbox and Switch. Use FieldTitle instead of a nested label to avoid duplicate label semantics.
Compute Environment

Select the compute environment for your cluster.

<RadioGroup defaultValue="kubernetes">
  <FieldLabel htmlFor="kubernetes">
    <Field orientation="horizontal">
      <FieldContent>
        <FieldTitle>Kubernetes</FieldTitle>
        <FieldDescription>
          Run GPU workloads on a K8s cluster.
        </FieldDescription>
      </FieldContent>
      <RadioGroupItem value="kubernetes" id="kubernetes" />
    </Field>
  </FieldLabel>
  <FieldLabel htmlFor="vm">
    <Field orientation="horizontal">
      <FieldContent>
        <FieldTitle>Virtual Machine</FieldTitle>
        <FieldDescription>
          Access a cluster to run GPU workloads.
        </FieldDescription>
      </FieldContent>
      <RadioGroupItem value="vm" id="vm" />
    </Field>
  </FieldLabel>
</RadioGroup>
Slider
Controls that carry their own accessible name — like a Slider with aria-label — pair with FieldTitle instead of FieldLabel, since there is no input element to target with htmlFor.
Price Range

Set your budget range ($200 – $800).

<Field>
  <FieldTitle>Price Range</FieldTitle>
  <FieldDescription>
    Set your budget range ($200 – $800).
  </FieldDescription>
  <Slider
    defaultValue={[200, 800]}
    max={1000}
    step={10}
    className="mt-2"
    aria-label="Price Range"
  />
</Field>
Field Group & Separator
FieldSeparator divides sections inside a FieldGroup. Pass children to float a line of text over the rule — the classic or continue with divider.
Or continue with
<FieldGroup>
  <Field>
    <FieldLabel htmlFor="email">Email</FieldLabel>
    <Input id="email" type="email" placeholder="hello@westy.com" />
  </Field>
  <Field>
    <Button type="submit">Sign in</Button>
  </Field>
  <FieldSeparator>Or continue with</FieldSeparator>
  <Field>
    <Button variant="outline" type="button">
      Continue with Google
    </Button>
  </Field>
</FieldGroup>
Validation
Set data-invalid on the Field to switch the whole block into the destructive state, and aria-invalid on the control for assistive technologies. FieldError renders with role="alert" — it also accepts an errors array (from react-hook-form or any Standard Schema validator) and renders a list automatically when there is more than one message.
Enter a valid email address.
<Field data-invalid>
  <FieldLabel htmlFor="email">Email</FieldLabel>
  <Input id="email" type="email" aria-invalid defaultValue="hello@" />
  <FieldError>Enter a valid email address.</FieldError>
</Field>

// With react-hook-form
<FieldError errors={errors.username ? [errors.username] : []} />
RTL
The Field family is plain markup with logical properties (text-start, ms-*), so a dir="rtl" ancestor is all it needs — labels, descriptions and error lists mirror without extra classes. Radix-based controls inside the field still need their own dir via the DirectionProvider.

أدخل رقم البطاقة المكون من 16 رقمًا

<div dir="rtl">
  <FieldGroup>
    <Field>
      <FieldLabel htmlFor="card">رقم البطاقة</FieldLabel>
      <Input id="card" placeholder="1234 5678 9012 3456" />
      <FieldDescription>أدخل رقم البطاقة المكون من 16 رقمًا</FieldDescription>
    </Field>
    <Field orientation="horizontal">
      <Checkbox id="shipping" defaultChecked />
      <FieldLabel htmlFor="shipping" className="font-normal">
        نفس عنوان الشحن
      </FieldLabel>
    </Field>
  </FieldGroup>
</div>
Accessibility
Field renders role="group" so nested controls inherit labeling; FieldSet and FieldLegend keep related controls grouped semantically. Always pair FieldLabel's htmlFor with the control's id, and mirror data-invalid on the Field with aria-invalid on the control. FieldError announces itself via role="alert". Use FieldSeparator sparingly so screen readers encounter clear section boundaries.
<Field data-invalid>
  <FieldLabel htmlFor="email">Email</FieldLabel>
  <Input id="email" type="email" aria-invalid />
  <FieldError>Enter a valid email address.</FieldError>
</Field>
API Reference
Ten parts compose the family: FieldSet + FieldLegend for semantic grouping, FieldGroup for stacking, Field for a single control, FieldContent + FieldTitle for side-by-side layouts, plus FieldLabel, FieldDescription, FieldError and FieldSeparator. All accept their native element props.
ComponentPropDefault
Fieldorientation?: "vertical" | "horizontal" | "responsive""vertical"
Fielddata-invalid?: boolean—
FieldLegendvariant?: "legend" | "label""legend"
FieldLabelasChild?: booleanfalse
FieldErrorerrors?: Array<{ message?: string } | undefined>—
FieldSeparatorchildren?: ReactNode—
<FieldSet data-slot="field-set">
<FieldLegend data-slot="field-legend" data-variant="legend|label" />
<FieldGroup data-slot="field-group" />
<Field data-slot="field" role="group"
  data-orientation="vertical|horizontal|responsive" data-invalid />
<FieldContent data-slot="field-content" />
<FieldLabel data-slot="field-label" />
<FieldTitle data-slot="field-label" />
<FieldDescription data-slot="field-description" />
<FieldSeparator data-slot="field-separator" data-content />
<FieldError data-slot="field-error" role="alert" />

Tokens used

9 design tokens consumed by the Field component.

  • --destructive

    Destructive

    data-[invalid=true]:text-destructivetext-destructive
  • --primary

    Primary

    has-data-checked:border-primary/30has-data-checked:bg-primary/5dark:has-data-checked:bg-primary/10[&>a:hover]:text-primary
  • --muted-foreground

    Muted Foreground

    text-muted-foreground
  • --background

    Background

    bg-background
  • --border

    Border

    has-[>[data-slot=field]]:borderbg-border
  • --text-sm

    Text Small (14px)

    text-smdata-[variant=label]:text-sm
  • --text-base

    Text Base (16px)

    data-[variant=legend]:text-base
  • --radius-md

    Radius 0.8× (8px)

    has-[>[data-slot=field]]:rounded-md
  • --spacing

    Spacing unit (4px)

    gap-1gap-3gap-6gap-7p-3mb-3h-5