gridland
Components

TextInput

A single-line text input with label, prompt, and validation

A single-line text input field with label, prompt prefix, placeholder, character count, and validation states.

TextInput
?

Run demo

Terminal
bunx @gridland/demo text-input
Terminal
curl -fsSL https://raw.githubusercontent.com/thoughtfulllc/gridland/main/scripts/run-demo.sh | bash -s text-input

Installation

Terminal
bunx create-gridland add text-input

Usage

import { TextInput } from "@/components/ui/text-input"
const [name, setName] = useState("")

<TextInput
  focusId="username"
  autoFocus
  label="Username"
  value={name}
  onChange={setName}
  placeholder="enter your name"
  prompt="> "
/>

TextInput registers with the focus system via focusId. Wrap your app in GridlandProvider (which mounts <FocusProvider selectable> implicitly) for keyboard routing. Tab to focus the field, Enter to select (start editing), then type. Escape exits edit mode.

Examples

Form

Multiple inputs composed into a form with keyboard navigation.

TextInput
?

With onSubmit

Use onSubmit to handle Enter key presses.

With onSubmit
const [email, setEmail] = useState("")

<TextInput
  focusId="email"
  autoFocus
  label="Email"
  value={email}
  onChange={setEmail}
  onSubmit={(v) => console.log("Submitted:", v)}
  placeholder="user@example.com"
  prompt="> "
/>

Required

Show a required indicator (*) next to the label.

Required
const [name, setName] = useState("")

<TextInput
  focusId="name"
  autoFocus
  label="Name"
  value={name}
  onChange={setName}
  placeholder="enter your name"
  prompt="> "
  required
/>

Description

Display helper text inline next to the label.

With description
const [email, setEmail] = useState("")

<TextInput
  focusId="email"
  autoFocus
  label="Email"
  value={email}
  onChange={setEmail}
  placeholder="user@example.com"
  prompt="> "
  description="We'll never share your email"
/>

Error

Pass an error string to show a validation message. It replaces the description.

With error
const [email, setEmail] = useState("")

<TextInput
  focusId="email"
  autoFocus
  label="Email"
  value={email}
  onChange={setEmail}
  placeholder="user@example.com"
  prompt="> "
  error="This field is required"
/>

Max Length

Limit input length and show a character counter when the user starts typing.

Max length
const [bio, setBio] = useState("")

<TextInput
  focusId="bio"
  autoFocus
  label="Bio"
  value={bio}
  onChange={setBio}
  placeholder="tell us about yourself"
  prompt="> "
  maxLength={100}
/>

Disabled

A disabled input ignores focus and keystrokes.

Disabled
<TextInput
  focusId="api-key"
  label="API Key"
  value="sk-1234...abcd"
  placeholder="sk-..."
  prompt="> "
  disabled
/>

Form with keyboard navigation

TextInput registers with the focus system via focusId, so composing multiple inputs into a form is just a matter of giving each one its own id. Tab and Shift-Tab cycle between them automatically — no external keyboard wiring needed.

Form
const FIELDS = [
  { id: "username", label: "Username" },
  { id: "email", label: "Email" },
  { id: "password", label: "Password" },
]

function MyForm() {
  const [values, setValues] = useState(FIELDS.map(() => ""))

  return (
    <>
      {FIELDS.map((field, i) => (
        <TextInput
          key={field.id}
          focusId={field.id}
          autoFocus={i === 0}
          label={field.label}
          value={values[i]}
          onChange={(v) => setValues((prev) => prev.map((old, j) => j === i ? v : old))}
          prompt="> "
        />
      ))}
    </>
  )
}

Controls

  • Tab / Shift-Tab: Move between fields
  • Enter: Start editing the focused field (and submit on re-press)
  • Escape: Exit edit mode

API Reference

TextInput

PropTypeDefaultDescription
labelstring-Field label shown above the input
descriptionstring-Helper text shown inline next to the label
errorstring-Error message — overrides description when set
requiredbooleanfalseShow required indicator (*) on the label
disabledbooleanfalseDisable the input
valuestringrequiredCurrent value of the input
onChange(value: string) => void-Called on every keystroke
onSubmit(value: string) => void-Called when Enter is pressed
placeholderstring-Hint text shown when the input is empty
promptstring-Prompt string before the input (e.g. "> ")
focusIdstringauto-generatedStable id for the focus system
autoFocusbooleanfalseFocus this input on mount
maxLengthnumber-Maximum characters allowed (shows counter when typing)