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 input element.
  • 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.

VariantUse it for
raisedMost forms. This is the default
surfaceFields on a plain background that need a clear edge
softFields inside a card or a panel that already has a border
outlineDense forms where a fill would feel heavy
ghostInline 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

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

PropTypeDefaultDescription
variant'raised' | 'surface' | 'soft' | 'outline' | 'ghost''raised'The look
size'1' | '2' | '3' | '4', responsive'2'The height step
colorAccentColorTheme accentThe colour of the focus ring
radius'none' | 'small' | 'medium' | 'large' | 'full'Theme radiusThe corner
type'text' | 'email' | 'password' | 'search' | 'tel' | 'url' | 'number' | 'date' | 'time' | 'datetime-local' | 'month' | 'week' | 'hidden''text'The input type
valuestring | numberThe value, when you control it
defaultValuestring | numberThe starting value, when the field controls it
errorbooleanfalseMarks the field as invalid
errorMessagestringText shown below the field and linked to it
requiredbooleanfalseMust 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

PropTypeDefaultDescription
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
colorAccentColorTheme accentThe colour of the focus ring
radius'none' | 'small' | 'medium' | 'large' | 'full'Theme radiusThe outer corners
disabledbooleanfalseDisables every field

A field in the group can still take its own color, disabled and error.

Slot

PropTypeDefaultDescription
side'left' | 'right'Which side of the input. Without it, the first slot goes left and later ones go right
colorAccentColorThe colour of the slot's content
gapSpace scale, responsiveThe space between items in the slot
px, pl, prSpace scale, responsiveThe slot's side padding
scrubbooleanfalseChanges a number when someone drags across the slot
scrubValuenumberThe current value. Needed for scrubMin and scrubMax
scrubStepnumber1How much one step changes the value
scrubSensitivitynumber1Pixels of dragging per step. Higher is slower
scrubMinnumberThe lowest value
scrubMaxnumberThe highest value
scrubShiftMultipliernumber10Step multiplier while Shift is held
scrubAltMultipliernumber0.1Step multiplier while Alt is held
onScrub(delta: number, isChanging: boolean) => voidCalled with each change. isChanging is false once the drag ends

Slot also accepts every attribute of the HTML div element.

Accessibility

KeyAction
TabMoves focus into the field
TypingEdits the value
  • Label: The field renders no label of its own. Connect a visible label with htmlFor and id, or pass aria-label when the purpose is clear from what is around it, as in a search bar.
  • Errors: error sets aria-invalid. The errorMessage element is linked with aria-describedby and has role="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.