Wuko
Components

Input

Single-line text field. Supports labels, hints, error states, leading and trailing icons, and any native input type.

Installation

terminal
npx shadcn@latest add @wuko/input

Usage

components/example.tsx
import { Input } from "@/components/ui/input";

export function Example() {
  return <Input label="Email" placeholder="you@example.com" />;
}

Sizes

Three sizes mirror Button's sm / md / lg, so inputs and buttons line up cleanly when composed in the same row.

tsx
<Input size="sm" placeholder="Small" />
<Input size="md" placeholder="Medium" />
<Input size="lg" placeholder="Large" />

With icons

Pass any ReactNode to leftIcon or rightIcon. Left icons are decorative. Pointer events are disabled so clicks fall through to the input. Right icons accept pointer events to support interactive controls (see Password below).

tsx
import { Mail, Search } from "lucide-react";

<Input
  leftIcon={<Mail className="size-4" />}
  placeholder="you@example.com"
/>
<Input
  leftIcon={<Search className="size-4" />}
  placeholder="Search docs..."
/>

Hints & errors

hint renders supportive text below the field. error replaces it with an error message, swaps the border to --wuko-danger-fg, and sets aria-invalid="true". Visual and screen-reader signals are coupled. The styling is driven from the ARIA attribute, so you cannot show the error state without also announcing it.

Must be 3–20 characters.

Enter a valid email address.

tsx
<Input label="Username" defaultValue="wuko" hint="Must be 3–20 characters." />
<Input
  label="Email"
  defaultValue="not-an-email"
  error="Enter a valid email address."
/>

Password with toggle

Compose rightIcon with a clickable visibility toggle. This is the design intent behind the asymmetric icon slots. The right slot accepts pointer events so an embedded button can receive clicks.

components/password-field.tsx
"use client";

import { useState } from "react";
import { Eye, EyeOff } from "lucide-react";

import { Input } from "@/components/ui/input";

export function PasswordField() {
  const [show, setShow] = useState(false);
  return (
    <Input
      label="Password"
      type={show ? "text" : "password"}
      defaultValue="kitsune-9-tails"
      rightIcon={
        <button
          type="button"
          onClick={() => setShow((s) => !s)}
          className="hover:text-wuko-heading"
          aria-label={show ? "Hide password" : "Show password"}
        >
          {show ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
        </button>
      }
    />
  );
}

Disabled

tsx
<Input label="Read-only" disabled defaultValue="wuko@wuko.dev" />

Props

PropTypeDefaultDescription
labelstringVisible field label, linked to the input via htmlFor.
hintstringHelper text below the field. Linked to the input via aria-describedby.
errorstringReplaces hint with an error message, swaps the border to the destructive foreground token, and sets aria-invalid.
size"sm" | "md" | "lg""md"Controls height, font size, and horizontal padding.
leftIconReactNodeDecorative icon inside the field on the left. Pointer events disabled so clicks fall through to the input.
rightIconReactNodeElement inside the field on the right. Pointer events allowed. Supports interactive controls like a password reveal toggle.
typestring"text"Standard HTML input type.
disabledbooleanfalseDisables interaction and dims the field.
idstringOptional. Defaults to a stable id generated via React.useId().
...restInputHTMLAttributes<HTMLInputElement>All native input attributes are forwarded to the element.

Accessibility

  • Renders a native <input> element. Tab focus, typing, and form submission work without extra wiring.
  • Each input gets a stable id via React.useId(). The label is linked via htmlFor; clicking the label focuses the input. Pass id to override.
  • error sets aria-invalid="true"; the visual error styling is driven from that ARIA attribute, so the screen-reader and visual signals are structurally coupled.
  • The hint or error message gets a unique id (suffixed -msg). The input's aria-describedby points at it. If you pass your own aria-describedby, Wuko merges yours with the message id rather than overwriting.
  • Focus uses focus-visible:border-wuko-accent plus a soft focus-visible:ring-wuko-accent/30ring inside the visual frame, different convention from Button's external focus-visible:outline, because input fields conventionally show focus on the editable surface itself.
  • disabled uses the native attribute, removing the input from the tab order and blocking interaction. Style adjusts via disabled:opacity-50 and disabled:cursor-not-allowed.
  • Left icons set pointer-events-noneso they don't swallow clicks meant for the input. Right icons leave pointer events enabled, so an embedded button (e.g., password toggle) receives clicks. When using an interactive right-icon control, give the button a clear aria-label.