Input
A text field in a drawn box, or on the line you write on. Focus draws one more pass in full ink; invalid redraws it in red pen.
Built on Base UI InputThat code has already been used.
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export default function InputDemo() {
return (
<div className="grid w-full max-w-md gap-6">
<div className="grid gap-2">
<Label htmlFor="input-email">Email</Label>
<Input id="input-email" type="email" placeholder="you@example.com" seed="input-email" />
</div>
<div className="grid gap-2">
<Label htmlFor="input-name">Name, written on the line</Label>
<Input id="input-name" variant="line" placeholder="Oussama" seed="input-name" />
</div>
<div className="grid gap-2">
<Label htmlFor="input-code">Invite code</Label>
<Input id="input-code" aria-invalid defaultValue="NOPE-123" seed="input-code" />
<p className="text-sm text-destructive">That code has already been used.</p>
</div>
<div className="grid grid-cols-2 gap-4">
<Input placeholder="Rounded" radius={10} seed="input-rounded" aria-label="Rounded" />
<Input placeholder="Disabled" disabled seed="input-disabled" aria-label="Disabled" />
</div>
</div>
);
}Installation
pnpm dlx shadcn@latest add @ballpoint/ inputOr copy the source
"use client";
import { Input as InputPrimitive } from "@base-ui/react/input";
import { cn } from "@/lib/utils";
import { usePen, type Pen } from "@/hooks/use-ink-box";
import { InkOutline } from "@/lib/ink-outline";
type InputProps = InputPrimitive.Props &
Pen & {
/** "box" is drawn around the field; "line" is the line you write on. */
variant?: "box" | "line";
seed?: string | number;
};
/**
* A text field in a drawn box. The drawing sits in a wrapper around the
* <input>, so `className` styles that box (width, margins, type size);
* every other prop goes to the input. Focus draws one more pass in full ink;
* aria-invalid redraws it in red pen.
*/
function Input({
className,
variant = "box",
seed,
roughness,
passes,
radius,
corners,
draw,
weight,
speed,
...props
}: Omit<InputProps, "fill" | "shadow">) {
const pen = usePen({ roughness, passes, radius, corners, draw, weight, speed });
return (
<span
data-slot="input"
data-variant={variant}
className={cn("group/input ink-within relative inline-flex h-10 w-full min-w-0 items-center", className)}
>
<InkOutline
pen={pen}
seed={seed}
estimate={[240, 40]}
shape={variant === "line" ? "line" : "box"}
focusPass
className={cn(
"text-ink-3 transition-colors duration-(--dur-hover)",
"group-hover/input:text-ink group-focus-within/input:text-ink",
"group-has-[[aria-invalid=true]]/input:text-destructive group-has-[[data-invalid]]/input:text-destructive",
"group-has-disabled/input:opacity-50",
)}
/>
<InputPrimitive
data-slot="input-control"
className={cn(
"relative h-full w-full min-w-0 bg-transparent text-base text-foreground outline-none placeholder:text-ink-3",
"disabled:cursor-not-allowed disabled:opacity-50",
"file:mr-3 file:border-0 file:bg-transparent file:text-base file:text-ink",
variant === "line" ? "px-1" : "px-3",
)}
{...props}
/>
</span>
);
}
export { Input };Usage
import { Input } from "@/components/ui/input"
<Input type="email" placeholder="you@example.com" />API reference
- variant
"box" | "line"default"box" - Drawn around the field, or the line you write on.
- className
string - Styles the drawn box (width, margins, type size). Every other prop goes to the <input>.
- aria-invalid
boolean - Redraws the box in red pen.
- seed
string | number - Pins the drawing.
Every other prop goes to Base UI's Input.
Pen settings
The drawing also takes draw, roughness, passes, radius, corners, weight and speed, as props or from the nearest InkProvider. What each one does.