Input

A hand-drawn Base UI input styled with shadcn conventions.

Input
A hand-drawn Base UI input across its types and states.
Default
Types
With value
Invalid
Disabled

Installation

npx shadcn@latest add https://sketchcn.chuwii.com/r/input.json

Styles

Input leans on these rules from sketch.css. The shadcn CLI installs the file with sketch-provider; copying the component by hand means copying them too.

[data-sketch-outline] path {
  stroke-dasharray: var(--sketch-dash, none);
  animation: var(--sketch-dash-animation, none);
}

@keyframes sketch-dash-boil {
  to {
    stroke-dashoffset: -10;
  }
}

Read the whole file for the drawing variables, paper textures and reduced-motion rules.

Usage

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

export function SearchField() {
  return <Input placeholder="Sketch something" />;
}

Props

Input adds no props of its own. Every prop is forwarded to the Base UI Input primitive, which documents them.

States

  • Focus draws the outline in the ring colour.
  • aria-invalid draws it in the destructive colour.
  • disabled drops the opacity and blocks pointer events.

Examples

Labelled field with a hint

We only use this to send you sketches.
import { Input } from "@/components/ui/input";

export function EmailField() {
  const id = useId();

  return (
    <div className="flex w-full flex-col gap-1.5">
      <label htmlFor={id} className="text-sm">
        Email
      </label>
      <Input id={id} type="email" placeholder="[email protected]" />
      <span className="text-muted-foreground text-xs">
        We only use this to send you sketches.
      </span>
    </div>
  );
}
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";

export function SearchBar({ onSearch }: { onSearch: (term: string) => void }) {
  const [term, setTerm] = useState("");

  return (
    <div className="flex w-full items-center gap-2">
      <Input
        value={term}
        onChange={(event) => setTerm(event.target.value)}
        placeholder="Search sketches"
      />
      <Button size="icon" aria-label="Search" onClick={() => onSearch(term)}>
        <Search />
      </Button>
    </div>
  );
}

Live validation

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

export function UsernameField() {
  const [username, setUsername] = useState("");
  const isTooShort = username.length > 0 && username.length < 3;

  return (
    <div className="flex w-full flex-col gap-1.5">
      <Input
        value={username}
        aria-invalid={isTooShort}
        onChange={(event) => setUsername(event.target.value)}
        placeholder="Username"
      />
      {isTooShort && (
        <span className="text-destructive text-xs">
          Usernames need at least 3 characters.
        </span>
      )}
    </div>
  );
}

Animation

The file-selector outline boils while the field is hovered, using the sketch-dash-boil keyframe from sketch.css.

// on the svg drawn over the file-selector button
"group-hover/input:[--sketch-dash:6_4] group-hover/input:[--sketch-dash-animation:sketch-dash-boil_1s_steps(2)_infinite]"
<Input type="file" className="[--sketch-dash-animation:none]" />