Aksara UI

Input

A single-line native text input, and TextInput, the field with a label, description, message and counter.

import { Input, TextInput } from '@aksornproject/aksara-ui'

Labelled field

TextInput renders a <label> linked to the input, then description, the input, message and counter, in that order. The label gets a generated id unless you pass id. required sets native required and draws a star after the label. The native input owns value, defaultValue, onChange and form submission, and ref is the HTMLInputElement.

<div className="w-full max-w-spacing-320">
  <TextInput
    label="Full name"
    description="As it appears on your student card"
    name="fullName"
    placeholder="Somchai Jaidee"
    required
  />
</div>

Status and message

status takes 'error', 'warning' or 'success' and colours the border. message renders under the input in the status colour, with its glyph. 'error' also sets aria-invalid, unless you pass your own. destructive is a shorthand for status="error". Prefer status, which wins when you pass both.

<div className="flex w-full max-w-spacing-320 flex-col gap-spacing-16">
  <TextInput
    label="Email"
    defaultValue="somchai@school"
    status="error"
    message="Enter a full email address."
  />
  <TextInput
    label="Username"
    defaultValue="somchai"
    status="warning"
    message="This name is already common in your class."
  />
  <TextInput
    label="Classroom code"
    defaultValue="M4-2"
    status="success"
    message="Classroom found."
  />
</div>

Adornments

startAdornment and endAdornment render content in spans before and after the input. An adornment takes the placeholder colour while the input is empty and the primary text colour once it has a value. endAdornment is at most 80px wide. startAdornmentClassName and endAdornmentClassName reach the spans.

บาท
<div className="flex w-full max-w-spacing-320 flex-col gap-spacing-16">
  <TextInput
    label="Search lessons"
    type="search"
    placeholder="Search by title"
    startAdornment={<IconInterfaceEssentialSearchLoupe size={20} />}
  />
  <TextInput
    label="Price"
    type="number"
    placeholder="0"
    endAdornment="บาท"
  />
</div>

Clear action and counter

clearAction renders in a 36px square slot before endAdornment. It is hidden while the input is disabled, read-only or loading. The component never clears the value itself, so the app owns the button's visibility, its accessible name, the value change and where focus goes. counter is app-owned text in a right-aligned row. It does not count anything, and maxLength passes through to the input unchanged.

<div className="w-full max-w-spacing-320">
  <TextInput
    ref={ref}
    label="Course name"
    value={value}
    onChange={(event) => setValue(event.target.value)}
    maxLength={100}
    counter={`${value.length} / 100`}
    clearAction={
      value && (
        <Button
          type="button"
          size="xs"
          iconOnly
          variant="tertiary-accent"
          aria-label="Clear course name"
          leadingIcon={<IconInterfaceEssentialDeleteDisabled />}
          onClick={() => {
            setValue('')
            ref.current?.focus()
          }}
        />
      )
    }
  />
</div>

States

disabled keeps native disabled behaviour, so the value is left out of form data. readOnly uses a grey surface and keeps the text at full strength. loading keeps the input mounted with its value, sets readOnly and aria-busy, and covers the label and value with skeletons. Adornments, clearAction and message hide while loading. description and counter stay.

<div className="flex w-full max-w-spacing-320 flex-col gap-spacing-16">
  <TextInput label="Disabled" defaultValue="Term 1, 2026" disabled />
  <TextInput label="Read-only" defaultValue="STU-104238" readOnly />
  <TextInput
    label="Loading"
    defaultValue="Somchai Jaidee"
    description="Fetching your profile"
    loading
  />
</div>

Input

Input is the control without the field around it. Native props, events and ref go to the <input>. className and wrapperRef target the styled div around it, and containerShadowClassName targets the outer div. There are no size variants. size is the native attribute. Name a bare Input with aria-label or an InputLabel.

/ 100
<Input
  aria-label="Score"
  type="number"
  inputMode="numeric"
  placeholder="0"
  endAdornment="/ 100"
  containerShadowClassName="w-spacing-160"
/>

Accessibility

  • aria-describedby lists your own ids first, then the description, the message and the counter.
  • The required star is aria-hidden. The input carries native required.
  • No live region is added. Announce validation messages yourself if they need it.
  • The focus ring draws when the input has keyboard focus. Hover and focus also switch the border to the accent colour, and a status border wins over both.
  • Selectors must match attribute values. The input always has data-disabled and data-invalid as "true" or "false".

API

TextInput

TextInput takes every Input prop as well.

Prop

Type

Input

Prop

Type

Edit on GitHub

On this page