Skip to content

Input

Preview

We'll never share it.

<div class="${fieldClass()} max-w-sm w-full">
  <label class=${labelClass()} for="email">Email</label>
  <input class=${inputClass()} id="email" name="email" type="email" placeholder="[email protected]" aria-describedby="email-hint">
  <p class=${hintClass()} id="email-hint">We'll never share it.</p>
</div>

Installation

webjs ui add input

API Reference

Parts

Name Description
inputClass() Apply to native <input>.

Source: components/ui/input.ts

/**
 * Input: styled native `<input>`. Tier-1 class helper. Works with every
 * input type (text, email, password, number, search, tel, url, date,
 * time, file, color, …). Form submission, autocomplete, browser
 * validation, and password managers all work because it IS the native
 * input.
 *
 * shadcn parity:
 *   Input  → inputClass()
 *
 * Pair with `<label class=${labelClass()} for="...">` and a hint paragraph
 * (`<p class=${hintClass()} id="...-hint">`). Wrap all three in
 * `<div class=${fieldClass()}>` for the canonical field rhythm.
 *
 * Design tokens used: --input, --background, --primary, --primary-foreground,
 * --muted-foreground, --foreground, --ring, --destructive.
 *
 * A11y (required for accessible output):
 *   LABEL IT. A `<label class=${labelClass()} for="<the input's id">` is the
 *   whole requirement, and the `for` / `id` pair is what links them. Without it
 *   the input has no accessible name and a screen reader announces only "edit
 *   text". A `placeholder` is NOT a label: it disappears the moment the user
 *   types, and some engines never expose it as a name at all.
 *   Every `aria-describedby` must point at an element that EXISTS on the page.
 *   A dangling id silently contributes nothing, so the hint or error text is
 *   never announced; the example below therefore renders the `#email-hint` node
 *   it references rather than assuming it.
 *   On a validation failure set `aria-invalid="true"` (the class styles the
 *   error ring off it) and point `aria-describedby` at the error text, so the
 *   reason is announced along with the field rather than only shown in red.
 *   Set the right `type` and `autocomplete`. Both are accessibility features:
 *   `type` picks the mobile keyboard and the native validation, and
 *   `autocomplete` is what lets a password manager or an autofill user avoid
 *   retyping. `type="search"` / `type="email"` also announce as such.
 *   A required field wants the native `required` attribute, not just an
 *   asterisk in the label, so the state reaches assistive tech.
 *
 * @example
 * ```html
 * <div class=${fieldClass()}>
 *   <label class=${labelClass()} for="email">Email</label>
 *   <input class=${inputClass()} type="email" name="email" id="email" required
 *          autocomplete="email" aria-describedby="email-hint">
 *   <p class=${hintClass()} id="email-hint">We never share it.</p>
 * </div>
 *
 * <!-- Failed validation: invalid state plus the reason, both announced. -->
 * <div class=${fieldClass()}>
 *   <label class=${labelClass()} for="email-2">Email</label>
 *   <input class=${inputClass()} type="email" name="email" id="email-2"
 *          aria-invalid="true" aria-describedby="email-2-error" value="nope">
 *   <p class=${errorClass()} id="email-2-error">Enter a valid email address.</p>
 * </div>
 * ```
 */
import { cn } from '../lib/utils.ts';

const INPUT_BASE =
  'h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30';

const INPUT_FOCUS =
  'focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50';

const INPUT_INVALID =
  'aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40';

/** Compose Tailwind classes for a native `<input>`. */
export function inputClass(): string {
  return cn(INPUT_BASE, INPUT_FOCUS, INPUT_INVALID);
}