Sophic Interface Systems
Components

Field

A labeled control with optional help text and errors.

Choose a unique username for your account.

States

Rest, filled, error, success, and disabled. Fill encodes availability; edge encodes interaction. Hover only strengthens the edge — never the fill. Focus, error, and success share 1px edge + 3px halo geometry. Disabled is the only state that changes fill.

Looks good.

You can change this later.

Examples

Textarea

Select

Controls lose their own border inside a field — the card carries the frame.

Checkbox

Horizontal orientation puts the control beside the label. Wrap label and description in FieldContent.

A short monthly email. No marketing.

Radio Group

Each option is its own horizontal field inside a FieldSet.

Statement frequency

Bundled with performance notes.

Choice card

Wrap the field in its own FieldLabel to make the whole card clickable.

Validation error

Mark the wrapper with data-invalid and the control with aria-invalid. Help text and the error stack below the card — description first, then the message.

In a dialog

The same Field card inside a dialog — rest and invalid.

Locked

data-locked uses the same muted fill as disabled, but the field can be unlocked. Put the value in FieldLockedValue with onUnlock (default label Change). The unlock control keeps cursor-pointer; the card does not use not-allowed.

<Field data-locked>
  <FieldTitle>Email</FieldTitle>
  <FieldLockedValue onUnlock={() => goTo("email")}>
    [email protected]
  </FieldLockedValue>
</Field>

Disabled

Disable the control (or set data-disabled on the Field). Same muted fill as locked, but permanently non-interactive — cursor: not-allowed, and any unlock control is hidden. Not an opacity fade.

Success

Mark the wrapper with data-success and put the message in FieldDescription below the card (same slot as help; error replaces it when both would show).

Field group

Stack related fields with FieldGroup. Each field keeps its own card.

We only use your email to send account notifications.

Experimental condensed

variant="experimental-condensed" tightens padding around the card and the control metrics inside it.

Default

Choose a unique username for your account.

experimental-condensed

Choose a unique username for your account.

Playground

<Field className="max-w-sm"> <FieldLabel htmlFor="username">Username</FieldLabel> <Input id="username" placeholder="sophic" /> <FieldDescription>Choose a unique username for your account.</FieldDescription></Field>

Usage

import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
  FieldLockedValue,
} from "@sophic/portal-ui/components/ui/field";
<Field>
  <FieldLabel htmlFor="username">Username</FieldLabel>
  <Input id="username" />
  <FieldDescription>Choose a unique username.</FieldDescription>
  <FieldError errors={[fieldState.error]} />
</Field>

Help text and errors written as direct children of Field render below the card — not inside the input container. Nested helpers (for example inside FieldContent on a checkbox row) stay with their row.

The module also exports FieldGroup, FieldSet, FieldLegend, FieldContent, FieldTitle, FieldLockedValue, and FieldSeparator for composing larger form sections. This is the required wrapper for form controls in app code.

See Field Composition for how controls align inside a Field and the live showcase.

Props

Every subcomponent also accepts its native element's attributes — <div> for Field, <fieldset> for FieldSet, <legend> for FieldLegend, and so on.

Field

Prop

Type

Default

FieldLegend

Prop

Type

Default

FieldError

Renders nothing when there are no errors, a single message for one error, and a list for several.

Prop

Type

Default

FieldLockedValue

Use inside Field data-locked. Omit onUnlock (or put the Field in data-disabled) when the value must stay permanent.

Prop

Type

Default

The rest

FieldGroup, FieldSet, FieldContent, FieldTitle, and FieldDescription have no props of their own. FieldLabel forwards to Label and takes htmlFor. FieldSeparator renders its children as text inside the divider line.

Tokens

Field reads --field-* from the Base System (with fallbacks so apps without those tokens keep the previous look). Nested controls switch to --field-control-* inside the card.

Changelog

  • 3 October 2026 — Dark rest fill is a 4% white lift over the parent; disabled sinks; locked keeps rest fill with muted value text. Full notes
  • 3 October 2026 — Control edges use --control-border, a step above layout --outer-border. Full notes
  • 3 October 2026 — Padding is --spacing × --density; height is the line-box plus padding. Full notes
  • 3 October 2026 — Choice radios and checkboxes sit on the label line with a 12px gap. Full notes
  • 3 October 2026 — Choice cards zero nested Field chrome so one surface owns fill, hover, and focus. Full notes
  • 3 October 2026 — Dark checked choice cards use fill and the control, not a white ring. Full notes
  • 1 October 2026 — Fill vs edge: hover is edge-only; focus / error / success share 1px + 3px halo; disabled is the only permanent fill change. Locked (data-locked + FieldLockedValue) shares that fill but can unlock. Full notes
  • 14 September 2026 — Rest, hover, focus, filled, and error match the Base System input states (brand focus rim, muted label, error below). Full notes
  • 18 August 2026 — Styles live in a CSS module; look and tokens unchanged. Full notes
  • 29 July 2026 — Field hairline tracks Card’s --card-ring (radius stays 16px). Full notes

On this page