Text Field
A text field takes one line of input, such as a name, an email address, a search term or a number.A text field takes one line of text. Slots on either side hold an icon, a unit or a small button. For text that runs over several lines, use a Text Area. To pick from a fixed list, use a Select or a Combobox.
Playground
Usage
import { Flex, Text, TextField } from '@kushagradhawan/kookie-ui';
export function EmailField({ error }: { error?: string }) {
return (
<Flex direction="column" gap="1">
<Text as="label" htmlFor="email" size="2" weight="medium">
Email address
</Text>
<TextField.Root
id="email"
type="email"
autoComplete="email"
error={Boolean(error)}
errorMessage={error}
/>
</Flex>
);
}Anatomy
<TextField.Root>
<TextField.Slot />
<TextField.Slot side="right" />
</TextField.Root>
<TextField.Group>
<TextField.Root />
<TextField.Root />
</TextField.Group>- Root: The input and the box around it. Props on Root go to the
inputelement. - Slot: Content before or after the input, such as an icon, a unit or a button.
- Group: Two or more fields as one control, such as X, Y and Z.
Best practices
Give every field a visible label. Put the label above the field and connect it with htmlFor or aria-labelledby. A placeholder disappears as soon as someone types, so it can't stand in for the label.
Size the field to the answer. A postcode field and an address field shouldn't be the same width. The width hints at how much to type.
Say what is wrong and how to fix it. Set error and pass an errorMessage such as "Enter an email address like name@example.com". "Invalid input" leaves people guessing.
Set the right type and autoComplete. type="email" or type="tel" brings up the matching keyboard on phones, and autoComplete lets the browser fill in what it already knows.
Use a Text Area for long answers. A text field scrolls sideways when the text runs past its edge, so people can't see what they wrote. Messages, descriptions and addresses belong in a Text Area.
Variant
variant sets the look. All of them share one height, so they can sit in the same form.
| Variant | Use it for |
|---|---|
raised | Most forms. This is the default |
surface | Fields on a plain background that need a clear edge |
soft | Fields inside a card or a panel that already has a border |
outline | Dense forms where a fill would feel heavy |
ghost | Inline editing, where the field should look like text until focused |
Size
size picks one of four heights: 28, 32, 40 and 48 pixels. Size 2 is the default. A text field is as tall as a Button or a Select of the same size, so a search field and its button line up. size accepts a responsive object, such as { initial: '3', md: '2' }.
Examples
Search
Put the icon in a left slot. Clicking a slot focuses the input, so the whole box acts as one target.
<TextField.Root type="search" placeholder="Search projects" aria-label="Search projects">
<TextField.Slot>
<HugeiconsIcon icon={Search01Icon} strokeWidth={1.75} />
</TextField.Slot>
</TextField.Root>Units
A right slot holds the unit, so people type only the number.
<TextField.Root type="number" defaultValue="16" aria-label="Font size">
<TextField.Slot side="right">
<Text size="2" emphasis="medium">px</Text>
</TextField.Slot>
</TextField.Root>Dragging to change a number
scrub on a slot changes a number when someone drags across the slot, like the property fields in a design tool. onScrub receives the change, and scrubMin and scrubMax keep the result in range when you pass the current value as scrubValue. Hold Shift for steps 10 times larger and Alt for steps 10 times smaller.
const [width, setWidth] = React.useState(320);
<TextField.Root
type="number"
value={width}
onChange={(event) => setWidth(Number(event.target.value) || 0)}
aria-label="Width"
>
<TextField.Slot
scrub
scrubValue={width}
scrubMin={0}
scrubMax={2000}
onScrub={(delta) => setWidth((value) => value + delta)}
>
<Text size="2" emphasis="medium">W</Text>
</TextField.Slot>
</TextField.Root>Three values in one control
TextField.Group draws one edge around its fields and a hairline between them. Its variant, size, radius, color and disabled pass to every field. ghost keeps only the hairlines, for a dense panel.
<TextField.Group aria-label="Position">
<TextField.Root defaultValue="0" aria-label="X">
<TextField.Slot><Text size="2" color="red">X</Text></TextField.Slot>
</TextField.Root>
<TextField.Root defaultValue="50" aria-label="Y">
<TextField.Slot><Text size="2" color="grass">Y</Text></TextField.Slot>
</TextField.Root>
<TextField.Root defaultValue="0" aria-label="Z">
<TextField.Slot><Text size="2" color="blue">Z</Text></TextField.Slot>
</TextField.Root>
</TextField.Group>Validation
error marks the field as invalid and turns it red. errorMessage shows below the field and is read with it. Show the error after someone leaves the field or submits the form, not while they are still typing.
Props
Root
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'raised' | 'surface' | 'soft' | 'outline' | 'ghost' | 'raised' | The look |
size | '1' | '2' | '3' | '4', responsive | '2' | The height step |
color | AccentColor | Theme accent | The colour of the focus ring |
radius | 'none' | 'small' | 'medium' | 'large' | 'full' | Theme radius | The corner |
type | 'text' | 'email' | 'password' | 'search' | 'tel' | 'url' | 'number' | 'date' | 'time' | 'datetime-local' | 'month' | 'week' | 'hidden' | 'text' | The input type |
value | string | number | The value, when you control it | |
defaultValue | string | number | The starting value, when the field controls it | |
error | boolean | false | Marks the field as invalid |
errorMessage | string | Text shown below the field and linked to it | |
required | boolean | false | Must be filled before the form submits |
Root also accepts every attribute of the HTML input element, such as placeholder, disabled, readOnly, autoComplete and onChange. Spell check follows the browser unless you set spellCheck.
Group
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'raised' | 'surface' | 'soft' | 'outline' | 'ghost' | 'raised' | The look of every field. ghost is the hairlines alone |
size | '1' | '2' | '3' | '4', responsive | '2' | The height step |
color | AccentColor | Theme accent | The colour of the focus ring |
radius | 'none' | 'small' | 'medium' | 'large' | 'full' | Theme radius | The outer corners |
disabled | boolean | false | Disables every field |
A field in the group can still take its own color, disabled and error.
Slot
| Prop | Type | Default | Description |
|---|---|---|---|
side | 'left' | 'right' | Which side of the input. Without it, the first slot goes left and later ones go right | |
color | AccentColor | The colour of the slot's content | |
gap | Space scale, responsive | The space between items in the slot | |
px, pl, pr | Space scale, responsive | The slot's side padding | |
scrub | boolean | false | Changes a number when someone drags across the slot |
scrubValue | number | The current value. Needed for scrubMin and scrubMax | |
scrubStep | number | 1 | How much one step changes the value |
scrubSensitivity | number | 1 | Pixels of dragging per step. Higher is slower |
scrubMin | number | The lowest value | |
scrubMax | number | The highest value | |
scrubShiftMultiplier | number | 10 | Step multiplier while Shift is held |
scrubAltMultiplier | number | 0.1 | Step multiplier while Alt is held |
onScrub | (delta: number, isChanging: boolean) => void | Called with each change. isChanging is false once the drag ends |
Slot also accepts every attribute of the HTML div element.
Accessibility
| Key | Action |
|---|---|
| Tab | Moves focus into the field |
| Typing | Edits the value |
- Label: The field renders no label of its own. Connect a visible label with
htmlForandid, or passaria-labelwhen the purpose is clear from what is around it, as in a search bar. - Errors:
errorsetsaria-invalid. TheerrorMessageelement is linked witharia-describedbyand hasrole="alert", so a screen reader reads it when it appears. - Slots: Clicking a slot moves focus to the input, with the cursor at the start for a left slot and at the end for a right slot. A button, link, select, label or other focusable control inside a slot keeps its own focus. A slot of text is linked to the input with
aria-describedby, so a screen reader reads a unit like "USD" with the field. A slot that holds a control, such as a show-password button, is not. - Scrubbing: Dragging works with a pointer only. Keyboard users type the number in the input, so keep the input editable.
- Focus: The focus ring wraps the whole box, slots included.
- High contrast: In forced colors mode the field draws a real border, since the browser drops its shadows.