Field
A label, control, hint and error laid out together, the same way every time.
In Dense data
See it live
npm run storybookWhen to use it
In Dense data: Lays out label, control and error the same way everywhere.
- Use it when: Every form control that needs a label, a hint or an error.
- Not when: A control that is not part of a form, such as a search box in a toolbar.
Instead:
- Label: The label on its own.
Variants
What each variant is for in any design system, then what Dense data decided and why. Each sample is the live story with that variant set.
verticalorientationUsed in Dense dataWhat it is for
The label above the control.
In Dense data
Forms in dialogs and sheets: the label above.
horizontalorientationUsed in Dense dataWhat it is for
The label beside the control.
In Dense data
Settings and filter panels: switches and short controls with the label beside.
responsiveorientationNot used in Dense dataWhat it is for
The label above on narrow screens and beside on wide ones.
In Dense data
A desk with a mouse and keyboard: there is no narrow screen to adapt to.
How Dense data uses it
Why it works this way
Field looks the way it does because of 4 answers about this product. Each card is one answer, in three steps: the answer, what it decided for Field, and what would move if it changed.
tasks.q11The answer
People scan rows and compare values.
2So Field
- ✓uses vertical: Forms in dialogs and sheets: the label above.
Item: muted3If it changed
and 20 answers that follow from it: People keep their place, Acting on the wrong record, Core and data, and 17 more.
content.q21The answer
Experts want many records on one screen.
2So Field
- ✓uses horizontal: Settings and filter panels: switches and short controls with the label beside.
Button: default Button: compact Button: large Input group: block-start Input group: block-end Sidebar: lg Toggle: default Toggle: sm Toggle: lg3If it changed
and 14 answers that follow from it: Quiet, Only the steps the screens need, Small, and 11 more.
spacing.count1The answer
A second, denser size serves tables and toolbars.
2So Field
- ✓uses horizontal: Settings and filter panels: switches and short controls with the label beside.
Sidebar: sm Item: default Item: sm3If it changed
No other answer follows from it.
scenarios.q11The answer
Experts work at a desk with a mouse and keyboard.
2So Field
- –does not use responsive: A desk with a mouse and keyboard: there is no narrow screen to adapt to.
3If it changed
and 2 answers that follow from it: Compact, for a pointer, A use scenario needs it.
Words
- Lead with the object, then the state: "Alert 2041 contained."
- Use numbers, not words for numbers.
- One verb per button. No "please", no exclamation marks.
- Use: Investigate, Filter, Assign, Escalated, Resolved. Avoid: Oops, Awesome, Please, Simply.
The labels components use, from the voice tokens:
content.action.commit: Savecontent.action.cancel: Cancelcontent.action.destroy: Deletecontent.action.retry: Retrycontent.action.empty-action: Change filters
See Voice and tone for the whole voice.
Accessibility and keyboard
Test before you ship:
- The label, hint and error are linked to the control, so they are read with it. undefined
Use it in code
npx open-ds add field
The component reads only component keys and semantic tokens, so it follows the brand and mode on <html>.
Tokens it reads
Field reads 30 tokens of its own, each an alias of a semantic token. Each row is one part: its own token, the semantic it aliases, and the value it ends at. Change a semantic and every component that reads it follows; override one of Field's tokens to change Field alone.
Dense data decides what changes when you switch theme. Here a switch changes colors only, so the marked rows differ between light and dark; everything else holds steady.
| Part | Its token and the semantic it aliases | Value |
|---|---|---|
| Set | ||
| Gap | component.field.set.gapspace.gap.sectionvar(--field-set-gap) | space.24:1.5rem |
| Gap 2 | component.field.set.gap-2component-spacing-map.x3var(--field-set-gap-2) | space.12:0.75rem |
| Legend | ||
| Weight | component.field.legend.font-weighttype.label.font-weightvar(--field-legend-font-weight) | font.weight.500:500 |
| Text size | component.field.legend.font-sizetype.body.font-sizevar(--field-legend-font-size) | font.size.14:0.875rem |
| Group | ||
| Gap | component.field.group.gapcomponent-spacing-map.x8var(--field-group-gap) | space.32:2rem |
| Gap 2 | component.field.group.gap-2component-spacing-map.x3var(--field-group-gap-2) | space.12:0.75rem |
| Gap 3 | component.field.group.gap-3space.gap.groupvar(--field-group-gap-3) | space.16:1rem |
| Field | ||
| Gap | component.field.gapcomponent-spacing-map.x3var(--field-gap) | space.12:0.75rem |
| Label invalid Changes with theme when invalid | component.field.label-invalidcolor.status.dangervar(--field-label-invalid) | Lightred.light.600:#C42B2BDarkred.dark.600:#F28B8B |
| Border checked Changes with theme when on | component.field.border-checkedcolor.action.primaryvar(--field-border-checked) | Lightviolet.light.600:#4B3FD9Darkviolet.dark.600:#A9A3F6 |
| Background checked Changes with theme when on | component.field.background-checkedcolor.action.primaryvar(--field-background-checked) | Lightviolet.light.600:#4B3FD9Darkviolet.dark.600:#A9A3F6 |
| Fill Changes with theme | component.field.backgroundcolor.surface.canvasvar(--field-background) | Lightslate.light.50:#F7F8FADarkslate.dark.0:#0D1117 |
| Padding, left and right | component.field.padding-xcomponent-spacing-map.x2var(--field-padding-x) | space.8:0.5rem |
| Gap 2 | component.field.gap-2space.gap.tightvar(--field-gap-2) | space.4:0.25rem |
| Content | ||
| Gap | component.field.content.gapcomponent-spacing-map.x2var(--field-content-gap) | space.8:0.5rem |
| Text | ||
| Gap | component.field.label.gapspace.gap.defaultvar(--field-label-gap) | space.8:0.5rem |
| Disabled opacity disabled when disabled | component.field.label.disabled-opacity-disabledopacity.disabledvar(--field-label-disabled-opacity-disabled) | opacity.40:0.4 |
| Corner | component.field.label.radiusradius.controlvar(--field-label-radius) | radius.2:0.125rem |
| Padding | component.field.label.paddingcomponent-spacing-map.x4var(--field-label-padding) | space.16:1rem |
| Text size | component.field.label.font-sizetype.body.font-sizevar(--field-label-font-size) | font.size.14:0.875rem |
| Weight | component.field.label.font-weighttype.label.font-weightvar(--field-label-font-weight) | font.weight.500:500 |
| Color Changes with theme | component.field.labelcolor.text.mutedvar(--field-label) | Lightslate.light.600:#535D6DDarkslate.dark.400:#969FAD |
| Description | ||
| Text size | component.field.description.font-sizetype.body.font-sizevar(--field-description-font-size) | font.size.14:0.875rem |
| Weight | component.field.description.font-weighttype.body.font-weightvar(--field-description-font-weight) | font.weight.400:400 |
| Text Changes with theme | component.field.description.labelcolor.text.mutedvar(--field-description-label) | Lightslate.light.600:#535D6DDarkslate.dark.400:#969FAD |
| Label 2 Changes with theme | component.field.description.label-2color.action.primaryvar(--field-description-label-2) | Lightviolet.light.600:#4B3FD9Darkviolet.dark.600:#A9A3F6 |
| Separator | ||
| Text size | component.field.separator.font-sizetype.body.font-sizevar(--field-separator-font-size) | font.size.14:0.875rem |
| Error | ||
| Text size | component.field.error.font-sizetype.body.font-sizevar(--field-error-font-size) | font.size.14:0.875rem |
| Weight | component.field.error.font-weighttype.body.font-weightvar(--field-error-font-weight) | font.weight.400:400 |
| Text Changes with theme | component.field.error.labelcolor.status.dangervar(--field-error-label) | Lightred.light.600:#C42B2BDarkred.dark.600:#F28B8B |
| From the system | ||
| Edge Changes with theme | color.border.default straight from the system, through --border | Lightslate.light.200 · #DDE2EADarkslate.dark.100 · #262D38 |