Input
PrimitivesFreeWrapper-carried focus ring, invalid/valid feedback and prefix/suffix adornment slots.
Install
npx shadcn@latest add https://labs.duku.design/r/input.jsonAsk your agent to use this
Connect DUKU Labs to MCP, then hand your coding agent this prompt:
Use the DUKU Labs MCP server to install the Input component (get_component "input", then its install command). Adapt it to my existing design tokens, preserve all accessibility and reduced-motion behavior, and keep every interaction state working.
Interaction
| Try | What happens |
|---|---|
| Focus the input | Border and ring fade in via CSS transition — no JS |
| Toggle valid | Emerald border + a check draws into the suffix slot |
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| invalid | boolean | false | Destructive border + ring, aria-invalid. |
| valid | boolean | false | Emerald border + check-draw suffix icon. |
| prefixEl / suffixEl | ReactNode | — | Adornment slots. |
Source — registry/default/ui/input.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { useReducedMotion } from "@/registry/default/lib/use-reduced-motion";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
invalid?: boolean;
valid?: boolean;
prefixEl?: React.ReactNode;
suffixEl?: React.ReactNode;
wrapperClassName?: string;
ref?: React.Ref<HTMLInputElement>;
}
function ValidCheck() {
const reduced = useReducedMotion();
const [drawn, setDrawn] = React.useState(false);
React.useEffect(() => {
if (reduced) {
setDrawn(true);
return;
}
const t = setTimeout(() => setDrawn(true), 50);
return () => clearTimeout(t);
}, [reduced]);
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2.5}
strokeLinecap="round"
strokeLinejoin="round"
className="size-4 text-emerald-600 dark:text-emerald-400"
aria-hidden="true"
>
<path
d="M5 13l4 4L19 7"
pathLength={1}
strokeDasharray={1}
strokeDashoffset={drawn ? 0 : 1}
className="transition-[stroke-dashoffset] duration-300 ease-out"
/>
</svg>
);
}
export function Input({
className,
wrapperClassName,
invalid = false,
valid = false,
prefixEl,
suffixEl,
disabled,
ref,
...rest
}: InputProps) {
return (
<div
data-slot="control"
data-invalid={invalid || undefined}
data-valid={valid || undefined}
className={cn(
"relative flex h-10 items-center rounded-lg border border-input bg-background",
"transition-[color,border-color,box-shadow] duration-200",
"focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/25",
invalid &&
"border-destructive focus-within:border-destructive focus-within:ring-destructive/25",
valid && "border-emerald-600 dark:border-emerald-400",
disabled && "opacity-50 pointer-events-none",
wrapperClassName
)}
>
{prefixEl ? (
<span
data-slot="prefix"
className="flex items-center pl-3 text-muted-foreground [&>svg]:size-4"
>
{prefixEl}
</span>
) : null}
<input
ref={ref}
data-slot="input"
className={cn(
"h-full flex-1 bg-transparent px-3 text-sm text-foreground outline-none",
"placeholder:text-muted-foreground",
"autofill:shadow-[inset_0_0_0_1000px_var(--background)]",
"disabled:cursor-not-allowed",
className
)}
aria-invalid={invalid || undefined}
disabled={disabled}
{...rest}
/>
{valid || suffixEl ? (
<span
data-slot="suffix"
className="flex items-center gap-2 pr-3 text-muted-foreground [&>svg]:size-4"
>
{suffixEl}
{valid ? <ValidCheck /> : null}
</span>
) : null}
</div>
);
}