Inputs and fields
Inputs collect text, choices and codes, and Field gives each one its label, hint and error. Use them whenever people type or pick a value: Input, Select, DatePicker, PasswordInput, Textarea or DigitInput, inside a Field.
Beta until the default border contrast (C7) is resolved, and design confirms the digit gap and text area resize handle (C13). Everything else on this page is implemented and tested.
Preview
Pick a control and change the controls to try every supported combination. The preview renders the real components and the code updates with it.
Students see this on the lesson card.
import { Input, Field } from '@nuton/ui'
<Field label="Lesson title" hint="Students see this on the lesson card.">
<Input placeholder="Introduction to Design Principles" />
</Field>Installation
Availability
@nuton/ui and @nuton/tokens are private workspace packages. They aren't published to a registry yet (D4: registry deferred; D12: distribution waits on the provenance review). Inside the Nuton monorepo, add them as workspace dependencies. The commands below apply once the packages are published.
1. Add the packages
pnpm add @nuton/ui @nuton/tokens @remixicon/reactnpm install @nuton/ui @nuton/tokens @remixicon/reactyarn add @nuton/ui @nuton/tokens @remixicon/reactPeer dependencies: react and react-dom 18 or later. @remixicon/react supplies the icons you pass to leadingIcon, trailingIcon and hintIcon (Remix Icon is Nuton's icon set, D1).
2. Load tokens and component styles once
// app/layout.tsx (or your app's entry file)
import '@nuton/tokens/tokens.css' // --nu-* design tokens
import '@nuton/ui/styles.css' // component styles (uses the tokens)3. Point the font tokens at your loaded fonts
Labels, values and hints use --nu-font-body (Inter). DigitInput boxes use Title/H5, which reads --nu-font-header(Fraunces). Load both fonts and map the tokens to them. All controls are native elements (D5), so there's no provider to add: Field shares its ids and state with the control inside it.
import { Fraunces, Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
const fraunces = Fraunces({ subsets: ['latin'], variable: '--font-fraunces' })
<body
className={`${inter.variable} ${fraunces.variable}`}
style={{
'--nu-font-body': 'var(--font-inter), var(--nu-font-fallback-body)',
'--nu-font-header': 'var(--font-fraunces), var(--nu-font-fallback-header)',
}}
>Usage
import { Field, Input } from '@nuton/ui'
export function SpaceName({ error }: { error?: string }) {
return (
<Field label="Space name" hint="For example, Biology 101." error={error} required>
<Input name="spaceName" autoComplete="off" />
</Field>
)
}- Put every control inside a
Field. It renders a real<label for>and links the hint and error witharia-describedby, so you don't wire ids yourself. - Pass
errorto the Field, notinvalidto the control. The control picks uparia-invalidand the red border from it. requiredanddisabledon the Field reach the control too. A prop set on the control itself wins.- Set
type,inputModeandautoCompleteso phones show the right keyboard and browsers can fill the value. - Placeholders are examples (“Biology 101”), never the label or an instruction.
Variants and sizes
Five controls share one box style and one Field. Input covers most Text box 2 types through its slots; Select, PasswordInput, Textarea and DigitInput cover the rest.
Input
Two sizes for the box appearance. Use one size per form.
<Field label="Large, 48px">
<Input placeholder="name@school.edu" type="email" />
</Field>
<Field label="Small, 36px">
<Input size="sm" placeholder="name@school.edu" type="email" />
</Field>| Size | Height | Radius | Inline padding | Leading icon | Trailing icon | Icon gap | Type |
|---|---|---|---|---|---|---|---|
lg (default) | 48 | 16 | 16 | 24 | 20 | 8 | Paragraph/Small (14/24) |
sm | 36 | 8 | 12 | 20 | 20 | 8 | |
appearance="inline" | 36 | 8 | 12 | 20 | 20 | 8 |
Heights are measured in the browser and match Figma. Insets were rebalanced at the owner's request (P1); see Spacing below.
Input slots
Slots cover the Figma types without separate components. leadingIcon and trailingIcon are decorative; leadingAddon is a separate segment with a divider; trailingAction holds one labelled button.
<Field label="Search lessons">
<Input type="search" leadingIcon={<RiSearchLine />} placeholder="Photosynthesis" />
</Field>
<Field label="Phone number">
<Input
type="tel"
autoComplete="tel-national"
placeholder="020 7946 0958"
leadingAddon={
<select aria-label="Country code" defaultValue="+44">
<option>+44</option>
<option>+234</option>
<option>+1</option>
</select>
}
/>
</Field>
<Field label="Lesson length" sublabel="(Minutes)">
<Input inputMode="numeric" placeholder="45" trailingIcon={<RiTimeLine />} />
</Field>Brand badges and flags (C15)
Figma's Card type shows a card-brand badge and Phone Number shows a flag. No brand or flag assets ship in @nuton/ui(provenance and trademark). Your app passes them through trailingIcon or leadingAddon.
Inline input
appearance="inline" is borderless until hover or focus, for editing text in place such as a lesson title. It's always 36px; size is ignored. Clear the title with the button, then type a new one.
const [title, setTitle] = useState('Introduction to Design Principles')
<Input
appearance="inline"
aria-label="Lesson title"
placeholder="Untitled lesson"
leadingIcon={<RiBookOpenLine />}
value={title}
onChange={(e) => setTitle(e.target.value)}
trailingAction={title ? (
<button type="button" className="nu-input__reveal" aria-label="Clear lesson title" onClick={() => setTitle('')}>
<RiCloseLine aria-hidden="true" />
</button>
) : null}
/>Select
A native <select> (D5) styled as a text box, with the same sizes. placeholder adds a hidden first option that shows until people choose.
<Field label="Country">
<Select placeholder="Choose a country" leadingIcon={<RiGlobalLine />}>
<option value="ng">Nigeria</option>
<option value="gb">United Kingdom</option>
</Select>
</Field>
<Field label="Subject">
<Select size="sm" defaultValue="biology">
<option value="biology">Biology</option>
<option value="chemistry">Chemistry</option>
<option value="physics">Physics</option>
</Select>
</Field>The chevron is 20px and sits at the input padding (16 / 12). Its colour follows Figma: Soft-400 while empty, Sub-600 on hover and focus, Strong-950 in error, Disabled-500 when disabled. The legacy TextBox Country type is a Select with a flag leadingIcon (C14).
Date picker
DatePicker is the Text box 2 Date type made real: people either type the date or choose it from a calendar in a popover. Typing follows the order of the locale (dd/mm/yyyy by default, mm/dd/yyyy for en-US); a half-typed date waits, and a date that does not exist or is outside min and max turns the box invalid when people leave it. DateRangePicker chooses a start and an end day from one calendar, and Calendar is the month grid on its own.
Type it as dd/mm/yyyy or choose it from the calendar.
No date chosen.
<Field label="Date of birth" hint="Type it as dd/mm/yyyy or choose it from the calendar.">
<DatePicker value={value} onValueChange={setValue} min={new Date(1920, 0, 1)} max={new Date()} name="birth-date" />
</Field>Weekdays from today onwards.
No date chosen.
<Field label="Exam date" hint="Weekdays from today onwards.">
<DatePicker
value={value}
onValueChange={setValue}
min={new Date()}
isDateDisabled={(d) => d.getDay() === 0 || d.getDay() === 6}
captionLayout="label"
size="sm"
/>
</Field>No dates chosen.
<Field label="Study period">
<DateRangePicker value={range} onValueChange={setRange} />
</Field>November 2026
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
No day chosen.
<Calendar value={value} onValueChange={setValue} aria-label="Pick a revision day" />The Figma calendar could not be read (C131)
Dates are local days with no time zone. Post them as yyyy-mm-dd (the name prop does this). Weeks start on Monday unless weekStartsOn says otherwise. The calendar shows one month; a range that spans months is chosen by moving between them.
Password input
An Input with a show and hide button. Select the eye button to reveal the password.
Use at least 8 characters.
<Field label="Create a password" hint="Use at least 8 characters.">
<PasswordInput autoComplete="new-password" defaultValue="correct-horse" />
</Field>Textarea
Multi-line text for questions, answers and notes. showCounter with maxLength adds the Figma counter. Type to see it update.
Your tutor sees your question.
<Field label="Ask about this lesson" hint="Your tutor sees your question.">
<Textarea placeholder="Explain human-centred design with an example." maxLength={200} showCounter />
</Field>Radius 12, padding 12 on every side (P1), Stroke/Soft-200 border and the regular x-small shadow. Four rows by default; the native resize handle replaces Figma's resize icon (C13). The counter uses Subheading/2X Small (11/12).
Digit input
One box per character for one-time codes. Four boxes by default; alphanumeric accepts letters too.
We sent a 4-digit code to your email.
<Field label="Verification code" hint="We sent a 4-digit code to your email.">
<DigitInput />
</Field>
<Field label="Recovery code">
<DigitInput length={6} alphanumeric />
</Field>Boxes are 80 × 80 with radius 10, set in Title/H5 (Fraunces 24/32). They shrink to fit narrow screens. The 12px gap between boxes isn't specified in Figma, which has a single-box component; design to confirm (C13).
Field
Field adds the label row (label, required asterisk, sublabel), the hint or error and an optional hint icon.
For example, Biology 101.
Enter an email address, like name@school.edu.
<Field label="Space name" required hint="For example, Biology 101.">
<Input />
</Field>
<Field label="Phone number" sublabel="(Optional)">
<Input type="tel" autoComplete="tel" />
</Field>
<Field label="Email address" error="Enter an email address, like name@school.edu.">
<Input type="email" defaultValue="learner@school" leadingIcon={<RiMailLine />} />
</Field>Matches Figma Label (4446:59279) and Hint Texts (4446:59288). Label: Label/Small (14/20) in a 24px row, 1px between label, asterisk and sublabel; the sublabel is Paragraph/Small (14/24) in Sub-600. Hint and error: Paragraph/X Small (12/16) after a 16px information-fill icon, which is shown by default (Soft-400, Error/Base on errors, Disabled-300 when disabled); pass hintIcon={null} to hide it. Label, control and hint are 4px apart.
Text box 2: every type, size and state
The full Figma Text box 2 set: seven types, two sizes and the static states. Hover and focus are interactive: point at or tab into any control. Choose a type to see it.
Large (48)
This hint helps people fill in the field.
This hint helps people fill in the field.
Check this value and try again.
This hint helps people fill in the field.
Small (36)
This hint helps people fill in the field.
This hint helps people fill in the field.
Check this value and try again.
This hint helps people fill in the field.
Large (48)
This hint helps people fill in the field.
This hint helps people fill in the field.
Check this value and try again.
This hint helps people fill in the field.
Small (36)
This hint helps people fill in the field.
This hint helps people fill in the field.
Check this value and try again.
This hint helps people fill in the field.
Large (48)
This hint helps people fill in the field.
This hint helps people fill in the field.
Check this value and try again.
This hint helps people fill in the field.
Small (36)
This hint helps people fill in the field.
This hint helps people fill in the field.
Check this value and try again.
This hint helps people fill in the field.
Large (48)
This hint helps people fill in the field.
This hint helps people fill in the field.
Check this value and try again.
This hint helps people fill in the field.
Small (36)
This hint helps people fill in the field.
This hint helps people fill in the field.
Check this value and try again.
This hint helps people fill in the field.
Large (48)
This hint helps people fill in the field.
This hint helps people fill in the field.
Check this value and try again.
This hint helps people fill in the field.
Small (36)
This hint helps people fill in the field.
This hint helps people fill in the field.
Check this value and try again.
This hint helps people fill in the field.
Large (48)
This hint helps people fill in the field.
This hint helps people fill in the field.
Check this value and try again.
This hint helps people fill in the field.
Small (36)
This hint helps people fill in the field.
This hint helps people fill in the field.
Check this value and try again.
This hint helps people fill in the field.
Large (48)
This hint helps people fill in the field.
This hint helps people fill in the field.
Check this value and try again.
This hint helps people fill in the field.
Small (36)
This hint helps people fill in the field.
This hint helps people fill in the field.
Check this value and try again.
This hint helps people fill in the field.
Spacing (P1)
Padding was rebalanced at the owner's request (P1, a product decision that deviates from Figma). Heights are unchanged (48 / 36, inline 36). The Figma correction is listed as FC-08 in docs/design-system/component-notes.md.
| Part | Figma | Code |
|---|---|---|
| Inline padding, Large / Small | 20 / 20 | 16 / 12 |
| Leading icon, Small | 24 | 20 |
| Icon ↔ text gap | 8 | 8 (unchanged) |
| Inline appearance padding | 10 | 12 |
| Select chevron inset | 20 | Follows the input padding (16 / 12) |
| Text area padding | 10 / 10 / 10 / 12 | 12 |
| Label ↔ control ↔ hint gap | 4 | 4 (back to Figma at the owner's request; P1 had 6) |
| Label row / sublabel line box | 24 | 24 (back to Figma) |
States and interactions
Figma defines six states for every text box: Placeholder, Hover, Focus, Filled, Error and Disabled. Hover and focus are live here; interact to see them.
@media (hover: hover)).aria-invalid. The red border stays on hover, so the error cue never disappears under the pointer. In a Field, set by error, with the message below.readOnly is passed through to the native element but has no distinct style, and Figma defines none.
Inline input, text area and digit input
The same static states for the other Figma sets. Hover and focus are live.
This hint helps people fill in the field.
This hint helps people fill in the field.
Check this value and try again.
This hint helps people fill in the field.
We sent a 4-digit code to your email.
That code has expired. Request a new one.
Gaps: contrast in three states (C6, C7, C8)
Error text and the required asterisk use Red 700, 5.26:1 (C6; Figma's State/Error/Base is 4.15:1). The default box border Stroke/Sub-300 is 1.53:1, below the 3:1 a lone boundary needs (C7). A disabled field's hint now uses Disabled-500, 4.23:1 (C8, resolved). The border follows Figma until design decides.
State tokens
The states are built from these tokens. Only the focus shadow is a component property (--nu-input-focus-shadow); the rest are applied directly, so change them through the tokens, not per control.
| State | Box input and Select | Inline input | Text area and digit input |
|---|---|---|---|
| Default | Bg/White, Stroke/Sub-300 | Transparent, no border | Bg/White, Stroke/Soft-200, regular x-small shadow |
| Hover | Bg/Weak-50, Stroke/Strong-950 | Bg/Weak-50, no border | Bg/Weak-50, no border, no shadow |
| Focus | Bg/White, Stroke/Strong-950, buttons/important-focus | Bg/White, Stroke/Strong-950, buttons/important-focus | Bg/White, Stroke/Strong-950, buttons/important-focus |
| Error | State/Error/Base border; focus shadow buttons/error-focus; icons Strong-950 | Bg/White, State/Error/Base border, regular x-small shadow | State/Error/Base border; focus shadow buttons/error-focus |
| Disabled | Bg/Weak-50, no border, Text/Disabled-500 | Transparent, Text/Disabled-500 | Bg/Weak-50, no border, Text/Disabled-500 |
Composition examples
Sign in
Submit the empty form to see errors. Each error is linked to its field, and focus moves to the first field that needs fixing.
Create a space
Input, Select and Textarea in one form. Mark the required field with required and the optional ones with a sublabel.
Verify an email
onComplete fires when every box is filled. Type any six digits to verify, or 000000 to see the expired-code error. Pasting a whole code into any box fills the rest.
Search lessons
A search input outside a form can use aria-label instead of a Field. The clear button appears once there's a query, and the result count is announced.
For a long list people search as they type, such as choosing a student or a space, use Combobox on the Dropdown and select page instead of Select.
API reference
Source: Field, Input, Select, PasswordInput, Textarea and DigitInput in packages/ui/src/components/, plus lib/useFieldControl.ts, which merges a control's own props with its Field.
Field props
| Prop | Type | Default | Description |
|---|---|---|---|
labelrequired | ReactNode | — | Visible label, rendered as <label for>. Every field needs one. |
childrenrequired | ReactNode | — | The control: Input, Select, PasswordInput, Textarea or DigitInput. |
hint | ReactNode | — | Helper text under the control, linked with aria-describedby. |
error | ReactNode | — | Error message. When set, the control gets aria-invalid and the message is linked. |
required | boolean | false | Shows the asterisk (hidden from screen readers) and passes native required to the control. |
sublabel | ReactNode | — | Secondary text after the label, such as “(Optional)”. No default: pass a translated string. |
hintIcon | ReactNode | <RiInformationFill /> | Decorative icon before the hint and error text (Figma Hint Texts). Rendered aria-hidden. Pass null to hide it. |
disabled | boolean | false | Disables the control and greys the label and hint. |
controlId | string | auto | Id for the control instead of a generated one. Use it to focus the control from your code. |
ref | Ref<HTMLDivElement> | — | Forwarded to the wrapper div. |
…rest | Omit<HTMLAttributes<HTMLDivElement>, 'children'> | — | Any other attribute for the wrapper div (className, data-*…). |
useField() returns the surrounding Field's ids and state (or null), for building a custom control that wires up the same way.
Input props
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'sm' | 'lg' | 'lg' | Height 36 / 48px. Ignored when appearance is inline. |
appearance | 'box' | 'inline' | 'box' | Box is Figma Text box 2; inline is Figma Inline Input (borderless until hover or focus, 36px). |
invalid | boolean | from Field | Marks the value invalid. Inside a Field, comes from error; an explicit value wins. |
leadingIcon | ReactNode | — | Decorative icon before the text. Rendered aria-hidden. |
trailingIcon | ReactNode | — | Decorative icon after the text, such as a card-brand badge your app supplies. Rendered aria-hidden. |
leadingAddon | ReactNode | — | A segment before the text with a divider, such as a country-code <select>. Label it yourself. |
trailingAction | ReactNode | — | An interactive element after the text, such as a clear button. Must be focusable and labelled. |
controlClassName | string | — | Class for the outer box (layout). |
className | string | — | Also applied to the outer box, not the <input>. |
id | string | from Field | Id of the native input. Inside a Field, the Field sets it. |
disabled | boolean | from Field | Native disabled. Inside a Field, inherited; an explicit value wins. |
required | boolean | from Field | Native required. Inside a Field, inherited; an explicit value wins. |
aria-describedby | string | — | Merged with the Field’s hint and error ids. |
ref | Ref<HTMLInputElement> | — | Forwarded to the native input. |
…rest | Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> | — | Any other native input attribute (type, value, onChange, autoComplete, inputMode…), applied to the <input>. |
Select props
| Prop | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | — | The <option> elements. |
size | 'sm' | 'lg' | 'lg' | Height 36 / 48px. |
placeholder | string | — | Text shown when nothing is selected. Rendered as a disabled, hidden first option. |
leadingIcon | ReactNode | — | Decorative icon before the value, such as a flag your app supplies. Rendered aria-hidden. |
invalid | boolean | from Field | Marks the value invalid. Inside a Field, comes from error. |
value | string | number | readonly string[] | — | Controlled value (native). |
defaultValue | string | number | readonly string[] | '' with placeholder | Uncontrolled initial value. With a placeholder and no value, the placeholder option is selected. |
className | string | — | Applied to the outer box, not the <select>. |
id | string | from Field | Id of the native select. |
disabled | boolean | from Field | Native disabled. |
required | boolean | from Field | Native required. |
aria-describedby | string | — | Merged with the Field’s hint and error ids. |
ref | Ref<HTMLSelectElement> | — | Forwarded to the native select. |
…rest | Omit<SelectHTMLAttributes<HTMLSelectElement>, 'size'> | — | Any other native select attribute (name, onChange, autoComplete…). |
DatePicker props
| Prop | Type | Default | Description |
|---|---|---|---|
value | Date | null | — | Controlled date (a local day). null is empty. |
defaultValue | Date | null | null | Uncontrolled initial date. |
onValueChange | (value: Date | null) => void | — | Called with a complete, allowed date, or null when the box is cleared. Half-typed text does not call it. |
min | Date | — | Earliest day that can be typed or chosen. |
max | Date | — | Latest day that can be typed or chosen. |
isDateDisabled | (date: Date) => boolean | — | Return true for days that cannot be chosen, such as weekends or fully booked days. |
locale | string | 'en-GB' | BCP 47 locale: the typed order and separator, and the month and day names. |
weekStartsOn | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 1 | First day of the week: 0 is Sunday. |
captionLayout | 'label' | 'dropdown' | 'dropdown' | dropdown: month and year dropdowns between the arrows, quick for a birth date. label: only the month and year. |
placeholder | string | the locale format | Shown when empty, for example dd/mm/yyyy. |
size | 'sm' | 'lg' | 'lg' | Height 36 / 48px, as Input. |
invalid | boolean | from Field | Marks the value invalid. A typed date that is not real or not allowed also marks it, once people leave the box. |
name | string | — | Posts the date as yyyy-mm-dd in a hidden input. |
labels | Partial<DatePickerLabels> | DATE_PICKER_LABELS | Button, popover, month buttons, month and year dropdowns and the invalid hint, for translation. |
id, disabled, required, className, aria-label, aria-describedby | as Input | — | Come from the Field when inside one. |
ref | Ref<HTMLInputElement> | — | Forwarded to the text box. |
DateRangePicker props
| Prop | Type | Default | Description |
|---|---|---|---|
value | DateRange | — | Controlled range, { from: Date | null, to: Date | null }. |
defaultValue | DateRange | — | Uncontrolled initial range. |
onValueChange | (value: DateRange) => void | — | Called after each click. The range is complete when both ends are set; the popover then closes. |
placeholder | string | 'Select dates' | Shown when nothing is chosen. |
min, max, isDateDisabled, locale, weekStartsOn, size, invalid, name, labels | as DatePicker | — | Same meaning. name posts name-from and name-to. |
ref | Ref<HTMLButtonElement> | — | Forwarded to the button that opens the calendar. |
Calendar props
| Prop | Type | Default | Description |
|---|---|---|---|
mode | 'single' | 'range' | 'single' | One day, or a start and an end day. |
value / defaultValue | Date | null (single), DateRange (range) | — | Controlled or initial selection. |
onValueChange | (value) => void | — | Called when a day is chosen. In range mode, first with only from, then with both ends (earlier day first). |
month / defaultMonth / onMonthChange | Date / Date / (month: Date) => void | — | The month on show. Default is the selected month, else today. |
min, max, isDateDisabled | Date, Date, (date) => boolean | — | Days outside min and max, and days the function rules out, are disabled. Month arrows stop at min and max. |
locale, weekStartsOn, captionLayout, labels | as DatePicker | — | Month and day names, week start, dropdowns or plain label, and translated button names. |
showToday | boolean | — | Adds a Today button under the grid that moves to today and chooses it when it is allowed. |
autoFocus | boolean | — | Moves focus to the active day on mount, for a calendar that opens in a popover. |
aria-label | string | — | Names the grid. Without it the grid is named by the month shown. |
ref | Ref<{ focusDay(): void }> | — | Handle to move focus to the active day. |
PasswordInput props
| Prop | Type | Default | Description |
|---|---|---|---|
showLabel | string | 'Show password' | Accessible name of the reveal button while the password is hidden. Pass a translated string. |
hideLabel | string | 'Hide password' | Accessible name of the reveal button while the password is visible. |
autoComplete | string | 'current-password' | Use 'new-password' on sign-up and change-password forms. |
ref | Ref<HTMLInputElement> | — | Forwarded to the native input. |
…rest | Omit<InputProps, 'type' | 'trailingAction'> | — | Every Input prop except type (managed by the toggle) and trailingAction (the reveal button). |
Textarea props
| Prop | Type | Default | Description |
|---|---|---|---|
showCounter | boolean | false | Shows the character counter (“12/200”). Needs maxLength; without it nothing renders. |
maxLength | number | — | Native maximum length. Also the counter’s maximum. |
formatCounter | (count: number, max: number) => string | `${count}/${max}` | Formats the counter text. Pass a localised formatter if needed. |
rows | number | 4 | Visible rows (native). |
invalid | boolean | from Field | Marks the value invalid. Inside a Field, comes from error. |
value | string | number | readonly string[] | — | Controlled value. The counter follows it. |
defaultValue | string | number | readonly string[] | — | Uncontrolled initial value. The counter follows typing. |
onChange | ChangeEventHandler<HTMLTextAreaElement> | — | Native change handler. Called after the counter updates. |
className | string | — | Applied to the outer box, not the <textarea>. |
id | string | from Field | Id of the native textarea. |
disabled | boolean | from Field | Native disabled. Also turns off resizing. |
required | boolean | from Field | Native required. |
aria-describedby | string | — | Merged with the Field’s hint and error ids and the counter id. |
ref | Ref<HTMLTextAreaElement> | — | Forwarded to the native textarea. |
…rest | TextareaHTMLAttributes<HTMLTextAreaElement> | — | Any other native textarea attribute (name, placeholder, onBlur…). |
DigitInput props
| Prop | Type | Default | Description |
|---|---|---|---|
length | number | 4 | Number of boxes. |
value | string | — | Controlled value. |
defaultValue | string | '' | Uncontrolled initial value. |
onChange | (value: string) => void | — | Called with the whole code after every change. |
onComplete | (value: string) => void | — | Called when every box is filled. |
alphanumeric | boolean | false | Accept letters as well as digits. Switches the keyboard from numeric to text. |
disabled | boolean | from Field | Disables every box. |
invalid | boolean | from Field | Error border on every box and aria-invalid. |
aria-label | string | — | Name for the group. Required unless the DigitInput is inside a Field, which names it. |
digitLabel | (index: number, length: number) => string | "Digit {n} of {N}" | Accessible name of each box. Pass a translated formatter. |
name | string | — | Adds a hidden input with this name, so the code is submitted with a form. |
id | string | from Field | Id of the group. |
className | string | — | Class for the group. |
ref | Ref<HTMLDivElement> | — | Forwarded to the group div. |
DigitInput doesn't accept other native attributes: only the props above are applied.
Styling hooks
Field renders .nu-field with data-disabled and data-invalid. Input and Select render .nu-input (Select adds .nu-select) with data-size, data-appearance, data-invalid, data-disabled and data-has-addon. Textarea renders .nu-textarea; DigitInput renders .nu-digit-input. Use props rather than styling these attributes.
Supported override points: --nu-input-height-sm, --nu-input-height-lg, --nu-input-height-inline, --nu-input-pad-inline, --nu-input-gap, --nu-input-icon-leading, --nu-input-focus-shadow and --nu-digit-size. A button in trailingAction can use .nu-input__reveal for the same 24 × 24 hit area as the password toggle.
Accessibility
Keyboard
| Key | Behaviour |
|---|---|
| Tab | Moves to the control. Disabled controls are skipped. In PasswordInput, the next Tab reaches the reveal button. Each DigitInput box is a tab stop. |
| Enter / Space | Toggles the password reveal button, or a trailing action button. |
| ← / → | In DigitInput, move to the previous or next box. |
| Backspace | In DigitInput, clears the box, or moves back and clears the previous one when the box is empty. |
| Ctrl+V / ⌘V | In DigitInput, pastes a whole code from the current box onwards. |
| Alt+↓ | In DatePicker, opens the calendar from the text box. |
| ← / → / ↑ / ↓ | In the calendar, move one day or one week. Enter or Space chooses the day; Esc closes the popover and returns to the box. |
| Home / End | In the calendar, go to the first or last day of the week. |
| Page Up / Page Down | In the calendar, change month. With Shift, change year. |
| Esc | Closes the calendar popover and returns focus to the calendar button. |
Input, Select and Textarea keep their native keys. In DatePicker the box keeps its native typing keys, and the calendar button is the next Tab stop.
Screen readers
Each control is named by the Field's <label for>, which includes the sublabel. Required is announced from native required; the asterisk is hidden. Hint, error and the Textarea counter are linked with aria-describedby, and errors set aria-invalid. Errors aren't a live region: they're read when focus reaches the field. DigitInput is a group named by the Field label, with each box labelled “Digit n of N”. The reveal button has aria-pressed and aria-controls. Icons are hidden. In DatePicker, the calendar button says it opens a dialog and whether it is open; the calendar is a grid whose days are named in full (“Wednesday, 25 November 2026”), with the selected day marked by aria-selected and today by aria-current; a typed date that is not real is announced with an example.
Focus
Focus shows a Strong-950 border with the focus shadow (error-focus when invalid); forced-colours mode adds a 2px Highlight outline. Focusing a digit box selects its content, and typing moves to the next box. Field never moves focus on its own.
Contrast and colour
Placeholders and the counter use Sub-600 (6.69:1) instead of Figma's Soft-400 (2.49:1) (D7, C1, C12). Error text is Red 700, 5.26:1 (C6). The default border Sub-300 is 1.53:1 (C7); hover and focus borders (Strong-950) pass. A disabled hint is 1.52:1 (C8). Errors are never colour-only: the message says what's wrong.
Touch and pointer
Controls are 48 or 36px tall and full width. Reveal and clear buttons have a 24 × 24 hit area (WCAG 2.5.8). Digit boxes are 80 × 80 and shrink on narrow screens; they bring up the numeric keyboard and support one-time-code autofill. Hover styles apply only on hover-capable pointers.
You must: put every control in a Field, or give it an aria-label; label any leadingAddon or trailingAction control; move focus to the first invalid field when a form fails; pass translated showLabel, hideLabel, digitLabel and formatCounter text; give DigitInput a name when it's part of a form.
Figma mapping
| Figma set | Variants | Code |
|---|---|---|
| Text box 2 (5:878 / 5:1320) | Type: Basic · Select · Phone Number · Password · Search · Card · Date. State: Placeholder · Hover · Focus · Filled · Disabled · Error. Size: Small · Large (84 variants in total) | Input (+ leadingIcon, trailingIcon, leadingAddon), Select, PasswordInput; size: sm · lg; hover and focus are interactive; invalid, disabled |
| Inline Input (5:1255) | 6 states; Left Icon; Button | Input appearance="inline" + leadingIcon, trailingAction |
| Text Area (5:1282) | 6 states; Label; Hint; Counter | Textarea + showCounter / maxLength inside Field |
| Digit Input (5:1242) | 6 states | DigitInput (a group of N boxes) |
| Date & Range Picker [1.1] (My Design system library) | Date and Range | DatePicker, DateRangePicker, Calendar. Not readable from this file, so built from tokens (C131). |
| Label (5:896) | Required · Sublabel · Normal / Disabled | Field label, required, sublabel, disabled |
| Label (4446:59279) · Hint Texts (4446:59288; earlier 5:905) | Default · Error · Disabled; Left Icon | Field hint, error, hintIcon |
| TextBox (5:915, legacy) | Basic · Select · Country · Date · Card · Phone (broken properties in Figma) | Deprecated: superseded by Text box 2 (C14). Country = Select with a flag leadingIcon |
Guidelines and related components
Use inputs and fields for
- Short text people type: names, emails, search terms (Input).
- Choosing one option from a short, known list (Select).
- Longer answers, prompts and notes (Textarea), and one-time codes (DigitInput).
- A day or a span of days: birth dates, deadlines, study periods (DatePicker, DateRangePicker).
Don't use them for
- Searching a long list as you type. Use Combobox on the Dropdown and select page.
- Switching between two or three views. Use Button group.
- Showing a value people can't change. Show it as text rather than a disabled field.
Do and don't
Enter an email address, like name@school.edu.
Invalid input.
Common mistakes
- Setting
showCounterwithoutmaxLength: no counter renders. - Passing
invalidto the control anderrorto the Field with different values. The control's own prop wins, so the border and message disagree. - Leaving out
autoCompleteandinputMode, so phones show the wrong keyboard and browsers can't fill the value. - Using a DigitInput in a form without
name: the code isn't submitted. - Putting a control in
trailingActionwithout an accessible name.