# PresetUI — zero-dependency React + Tailwind components # https://presetui.com # # Do not install these components from npm. There is no package. # Write the sources below into the project as files — a common home # is components/ui/ — and they become the project's own code. # # Plain JavaScript, no TypeScript. Requirements: React 19+ (ref is a # plain prop, no forwardRef) and Tailwind CSS v4 (the theme uses the # @theme directive). # # React is the only thing a component may import, and only Tabs and # Dialog do. Never add clsx, tailwind-merge, or a headless UI library # when writing these files. # # The theme below has two tiers. The core tokens are required. The # optional ones (success, warning) are only needed by the Alert and # Badge variants of the same name — drop them and drop those variants. # # Page-level templates and more palettes live at # https://presetui.com/docs/templates and https://presetui.com/docs/themes. # # Every component reads only the seven tokens defined in the theme. # To restyle the whole kit, change the token values, not the components. ## Setup — add this to the global stylesheet, after @import "tailwindcss"; /* PresetUI theme — copy this into your global stylesheet. Every PresetUI component reads these tokens and nothing else. To rebrand the kit, change the values here. */ @theme { /* Core — required. Every component reads these seven. */ --color-surface: #ffffff; --color-fg: #0a0a0a; --color-muted: #737373; --color-border: #e5e5e5; --color-accent: #0a0a0a; --color-accent-fg: #ffffff; --color-danger: #dc2626; /* Optional — each powers exactly the variants listed beside it. Drop a line you don't need and only that variant stops working: --color-success → Alert and Badge, variant="success" --color-warning → Alert and Badge, variant="warning" Your own tokens work the same way: define --color-brand here, then use bg-brand or text-brand inside a component. */ --color-success: #16a34a; --color-warning: #d97706; } .dark { --color-surface: #0a0a0a; --color-fg: #fafafa; --color-muted: #a3a3a3; --color-border: #262626; --color-accent: #fafafa; --color-accent-fg: #0a0a0a; --color-danger: #ef4444; --color-success: #22c55e; --color-warning: #f59e0b; } ## Accordion — https://presetui.com/r/accordion.txt — save as components/ui/accordion.js # Collapsible sections built on native details elements. /** * Collapsible sections built on native `
`, so they open and close * with no JavaScript at all — including for keyboard users and before your * bundle has loaded. * * Pass the same `name` to make them exclusive (opening one closes the others); * leave it off and any number can be open at once. * * @param {object} props * @param {{ value: string, title: React.ReactNode, content: React.ReactNode }[]} props.items * @param {string} [props.name] Set it to make the group exclusive. * @param {string} [props.defaultOpen] `value` of the item that starts open. * @param {string} [props.className] */ export function Accordion({ items, name, defaultOpen, className = "" }) { return (
{items.map((item) => (
{item.title}
{item.content}
))}
) } ## Alert — https://presetui.com/r/alert.txt — save as components/ui/alert.js # Short message set apart from the surrounding text. /** * Short message set apart from the surrounding text. * * The `success` and `warning` variants need the optional `--color-success` * and `--color-warning` tokens. Without them the background renders empty. * * @param {object} props * @param {"default" | "accent" | "success" | "warning" | "danger"} [props.variant="default"] * @param {React.ReactNode} [props.title] * @param {string} [props.className] */ export function Alert({ variant = "default", title, className = "", children, ...props }) { const variants = { default: "border-border bg-border/20", accent: "border-accent/30 bg-accent/5", success: "border-success/40 bg-success/10", warning: "border-warning/40 bg-warning/10", danger: "border-danger/40 bg-danger/10", } return (
{title ?

{title}

: null}
{children}
) } ## Avatar — https://presetui.com/r/avatar.txt — save as components/ui/avatar.js # Circular user image with a text fallback behind it. /** * Circular user image with a text fallback behind it. The fallback sits * underneath rather than swapping in on error, so no JavaScript is involved: * if the image loads it covers the initials, and if it doesn't they show. * * @param {object} props * @param {string} [props.src] * @param {string} [props.alt=""] * @param {React.ReactNode} [props.fallback] Usually one or two initials. * @param {"sm" | "md" | "lg"} [props.size="md"] * @param {string} [props.className] */ export function Avatar({ src, alt = "", fallback, size = "md", className = "", ...props }) { const sizes = { sm: "size-7 text-[0.6875rem]", md: "size-9 text-xs", lg: "size-12 text-sm", } return ( {fallback} {src ? ( {alt} ) : null} ) } ## Badge — https://presetui.com/r/badge.txt — save as components/ui/badge.js # Compact label for status and metadata. /** * Compact label for status and metadata. Renders a ``, so it sits * inline with text. * * The `success` and `warning` variants need the optional `--color-success` * and `--color-warning` tokens. Without them the background renders empty. * * @param {object} props * @param {"default" | "outline" | "accent" | "success" | "warning" | "danger"} [props.variant="default"] * @param {"sm" | "md"} [props.size="md"] * @param {string} [props.className] */ export function Badge({ variant = "default", size = "md", className = "", ...props }) { const base = "inline-flex items-center rounded-full font-medium whitespace-nowrap transition-colors" const variants = { default: "bg-border/60 text-fg", outline: "border border-border text-fg", accent: "bg-accent text-accent-fg", success: "bg-success text-white", warning: "bg-warning text-white", danger: "bg-danger text-white", } const sizes = { sm: "px-2 py-0.5 text-[0.6875rem]", md: "px-2.5 py-0.5 text-xs", } return ( ) } ## Breadcrumb — https://presetui.com/r/breadcrumb.txt — save as components/ui/breadcrumb.js # Trail of links back up the hierarchy. /** * Trail of links back up the hierarchy. The last item is rendered as the * current page rather than a link, and marked `aria-current`. * * @param {object} props * @param {{ label: React.ReactNode, href?: string }[]} props.items * @param {string} [props.className] */ export function Breadcrumb({ items, className = "", ...props }) { return ( ) } ## Button — https://presetui.com/r/button.txt — save as components/ui/button.js # Clickable action element with four variants and three sizes. /** * Clickable action element. Renders a real `
{children}
) } ## Input — https://presetui.com/r/input.txt — save as components/ui/input.js # Single-line text field with an invalid state. /** * Single-line text field. Renders a real ``, so `type`, `value`, * `onChange`, `ref` and the rest pass through unchanged. * * @param {object} props * @param {"sm" | "md" | "lg"} [props.size="md"] * @param {boolean} [props.invalid=false] Applies the danger border and sets `aria-invalid`. * @param {string} [props.className] */ export function Input({ size = "md", invalid = false, className = "", ...props }) { const base = "flex w-full rounded-md border bg-surface text-fg transition-colors outline-none " + "placeholder:text-muted focus-visible:ring-2 focus-visible:ring-offset-2 " + "focus-visible:ring-offset-surface disabled:cursor-not-allowed disabled:opacity-50" const sizes = { sm: "h-8 px-2.5 text-xs", md: "h-9 px-3 text-sm", lg: "h-11 px-4 text-base", } const state = invalid ? "border-danger focus-visible:ring-danger" : "border-border focus-visible:ring-accent" return ( ) } ## Label — https://presetui.com/r/label.txt — save as components/ui/label.js # Form label with an optional required marker. /** * Form label. Pair it with a control via `htmlFor` and the control's `id`, or * wrap the control in it. * * @param {object} props * @param {boolean} [props.required=false] Shows a marker after the text. * @param {string} [props.className] */ export function Label({ required = false, className = "", children, ...props }) { return ( ) } ## Progress — https://presetui.com/r/progress.txt — save as components/ui/progress.js # Determinate progress bar in three sizes. /** * Determinate progress bar. Built from two divs rather than ``, * because the native element's fill cannot be themed consistently across * browsers. * * @param {object} props * @param {number} [props.value=0] * @param {number} [props.max=100] * @param {"sm" | "md" | "lg"} [props.size="md"] * @param {string} [props.label="Progress"] Accessible name for the bar. * @param {string} [props.className] */ export function Progress({ value = 0, max = 100, size = "md", label = "Progress", className = "", ...props }) { const percent = Math.min(100, Math.max(0, (value / max) * 100)) const sizes = { sm: "h-1", md: "h-2", lg: "h-3", } return (
) } ## Radio — https://presetui.com/r/radio.txt — save as components/ui/radio.js # Native radio with the dot painted over. /** * A real `` with the native dot painted over. Give every * radio in a group the same `name` and the browser handles selection, arrow * keys, and form submission. * * @param {object} props * @param {"sm" | "md" | "lg"} [props.size="md"] * @param {string} [props.className] */ export function Radio({ size = "md", className = "", ...props }) { const base = "peer shrink-0 cursor-pointer appearance-none rounded-full border border-border bg-surface transition-colors outline-none checked:border-accent focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-surface disabled:cursor-not-allowed disabled:opacity-50" const sizes = { sm: "size-3.5", md: "size-4", lg: "size-5", } const dots = { sm: "size-1.5", md: "size-2", lg: "size-2.5", } return ( ) } ## Select — https://presetui.com/r/select.txt — save as components/ui/select.js # Native dropdown with the platform arrow replaced. /** * A real ` {children} ) } ## Separator — https://presetui.com/r/separator.txt — save as components/ui/separator.js # Dividing line, horizontal or vertical. /** * A dividing line. Decorative by default, so screen readers skip it — pass * `decorative={false}` when the divide carries real meaning. * * @param {object} props * @param {"horizontal" | "vertical"} [props.orientation="horizontal"] * @param {boolean} [props.decorative=true] * @param {string} [props.className] */ export function Separator({ orientation = "horizontal", decorative = true, className = "", ...props }) { const shapes = { horizontal: "h-px w-full", vertical: "h-full w-px", } return (
) } ## Skeleton — https://presetui.com/r/skeleton.txt — save as components/ui/skeleton.js # Placeholder block for content that hasn't arrived. /** * Placeholder block for content that hasn't arrived. Size it with utilities: * ``. * * @param {object} props * @param {string} [props.className] */ export function Skeleton({ className = "", ...props }) { return (