Skip to content
LofiStack Gallery
Type: inputWeek 01

Signal Input

A labelled text input built for async validation — username checks, coupon codes, invite links. A thin signal bar along the bottom edge shows focus, sweeps while loading, then fills with the result. It also supports a leading icon, a clear button, a character counter, three sizes, and works controlled or uncontrolled.

View on GitHub
0/20

3–20 characters. Try “lofi” to see an error.

Usage

usage.tsx
import { SignalInput } from "@/components/gallery/signal-input";

const [value, setValue] = useState("");
const status = useUsernameCheck(value); // "idle" | "loading" | "success" | "error"

<SignalInput
  label="Username"
  hint="3–20 characters. Letters, numbers and dashes."
  placeholder="lofi-builder"
  value={value}
  onChange={(e) => setValue(e.target.value)}
  status={status}
  statusMessage={status === "error" ? "That name is taken" : "Nice — it's available"}
  leadingIcon={<AtIcon />}
  maxLength={20}
  showCount
  clearable
  onClear={() => setValue("")}
/>

States

Default · Hover · Focus (accent ring + bar) · Loading (sweeping bar + spinner) · Success · Error (shake + alert color) · Disabled · Read-only · Counter at limit

We never share it.

Default

Checking code…

Loading

20% off applied.

Success

That name is reserved.

Error

Contact an admin to rename.

Disabled
12/12

Counter at limit

Props

  • label*

    string

    Visible label, linked to the input via htmlFor.

  • hint

    string

    Helper text shown while idle or loading.

  • status= "idle"

    "idle" | "loading" | "success" | "error"

    Drives the signal bar, trailing icon, border and message color.

  • statusMessage

    string

    Shown for success / error. Falls back to hint.

  • size= "md"

    "sm" | "md" | "lg"

    Field height and font size.

  • leadingIcon

    ReactNode

    Decorative icon before the text. Picks up the accent color on focus.

  • clearable= false

    boolean

    Show a clear button when the field has a value.

  • onClear

    () => void

    Called after clearing. Reset value here when controlled.

  • showCount= false

    boolean

    Live character counter. Requires maxLength.

  • ref

    Ref<HTMLInputElement>

    Forwarded to the native input (React 19 ref-as-prop).

  • ...rest

    InputHTMLAttributes

    Every native input attribute — type, name, placeholder, disabled, required, etc.

Accessibility

  • Always-visible <label> bound with htmlFor / id.
  • aria-invalid on error and aria-busy while loading.
  • Hint and status messages are linked via aria-describedby and announced through a polite live region.
  • The clear button is a real <button> with an aria-label and a visible focus ring.
  • Shake animation is disabled under prefers-reduced-motion.
  • Status colors meet WCAG AA contrast on light and dark surfaces.

Build prompt

The final prompt used to build this component
Build a reusable React 19 + TypeScript + Tailwind v4 input component called SignalInput, designed for async validation (e.g. checking if a username is available).

Core idea: a 2px "signal bar" runs along the bottom edge of the field. It grows from the left on focus (indigo), shows an indeterminate sweeping segment while status="loading", fills emerald on success and rose on error. On error the field also does a short shake (disabled under prefers-reduced-motion).

Props (typed interface extending native input attributes, minus size/prefix): label (required, always visible), hint, status ("idle" | "loading" | "success" | "error"), statusMessage, size ("sm" | "md" | "lg"), leadingIcon (ReactNode), clearable + onClear, showCount (uses maxLength), className, ref (React 19 ref-as-prop, merged with an internal ref so the clear button can refocus the input).

Requirements:
- Works controlled (value + onChange) and uncontrolled (defaultValue).
- Trailing area: clear button (only when there is a value and the field is not disabled/read-only) + status icon (spinner / check / alert).
- States: hover, focus-within ring, loading, success, error, disabled (muted, not-allowed cursor), read-only, counter turns amber at the limit.
- Accessibility: label htmlFor/id via useId, aria-invalid, aria-busy, aria-describedby pointing to a polite live-region message, clear button with aria-label and focus-visible ring, AA contrast in light and dark mode.
- Self-contained: no dependencies, keyframes shipped with the component via a React 19 <style href precedence> tag.