Browse the docs

Installation

PresetUI is copy-first. Each component is a single JavaScript file that imports nothing but React and the kit's own primitives — you copy it into your project and it becomes your code, with nothing upstream to update. The same components are also published as a package for projects that prefer npm install.

Most components import nothing at all; a few reach for a React hook, and the behavior-bearing ones import one of the kit's own primitives. Nothing foreign is ever imported: no clsx, no tailwind-merge, no third-party headless library. npm run check enforces that in this repo.

1. Requirements

Plain JavaScript, no TypeScript. Props are documented with JSDoc comments, so editors still autocomplete them.

2. Add the theme

Paste this into your global stylesheet, after @import "tailwindcss";. This is the only shared code in PresetUI. Every component reads these tokens and nothing else, so changing a value here rebrands the entire kit. The core tier is required; each optional status color ships with its readable foreground (success/success-fg, warning/warning-fg) plus the dialog backdrop color (overlay) — drop a pair and only the variants that read it stop working.

Sizes are tokens too: Button and every text control read their heights from the control scale (--spacing-control-sm/md/lg), so resizing the kit's controls is one edit in the theme — exactly like recoloring it.

registry/theme.css
/* 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 eight. */
  --color-surface: #ffffff;
  --color-fg: #0a0a0a;
  --color-muted: #737373;
  --color-border: #e5e5e5;
  --color-accent: #0a0a0a;
  --color-accent-fg: #ffffff;
  --color-danger: #dc2626;
  --color-danger-fg: #ffffff;

  /* Optional — each powers exactly the variants listed beside it. Drop a
     pair you don't need and only those variants stop working:
       --color-success / --color-success-fg  →  Alert and Badge, variant="success"
       --color-warning / --color-warning-fg  →  Alert and Badge, variant="warning"
       --color-overlay                       →  Dialog backdrop
     Every status color travels with its readable foreground — keep each
     pair above 4.5:1 contrast. 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-success-fg: #03190b;
  --color-warning: #d97706;
  --color-warning-fg: #241503;
  --color-overlay: rgb(0 0 0 / 0.5);

  /* Sizing — the control scale. Button and every text control read their
     height from these (as h-control-sm / h-control-md / h-control-lg), so
     resizing the kit's controls is one edit here, exactly like recoloring
     it is. Not part of the palettes: sizes don't change with the theme. */
  --spacing-control-sm: 2rem;
  --spacing-control-md: 2.25rem;
  --spacing-control-lg: 2.75rem;
}

.dark {
  --color-surface: #0a0a0a;
  --color-fg: #fafafa;
  --color-muted: #a3a3a3;
  --color-border: #262626;
  --color-accent: #fafafa;
  --color-accent-fg: #0a0a0a;
  --color-danger: #ef4444;
  --color-danger-fg: #0a0a0a;

  --color-success: #22c55e;
  --color-success-fg: #0a0a0a;
  --color-warning: #f59e0b;
  --color-warning-fg: #0a0a0a;
  --color-overlay: rgb(0 0 0 / 0.6);
}

/* Respect the user's motion preference. Components animate with transition-*
   and animate-* utilities; this turns them all off. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

Dark mode

The dark values live under a .dark class on <html> — toggling that class is yours to wire (a button, a stored preference, or on page load):

document.documentElement.classList.toggle("dark")

If the rest of your app uses Tailwind's default media-query dark: variant, align the two by registering the class strategy in your stylesheet:

@custom-variant dark (&:where(.dark, .dark *));

The theme also ships a prefers-reduced-motion guard, so copied components respect the visitor's motion preference wherever they land.

3. Copy a component

Open any component page, switch to the Source tab, and copy the file into your project. A common home is components/ui/, but nothing depends on the location.

The Usage tab next to it shows how the component is used once you've copied it — the same code you'd write if it had come from a package.

If you prefer the terminal:

curl -o components/ui/button.js https://presetui.com/r/button.txt

Components built on a primitive

The behavior-bearing components (Tabs, Accordion, Popover, Dropdown Menu, Combobox, Select Menu, Toast, Sheet, Tooltip (JS)) sit on the kit's headless primitives and import one. Two ways to satisfy that import — their pages say which primitive, and both stay zero-foreign-dependency:

4. Or: install the package

Everything above — all the components, with the primitives bundled in at @presetui/ui/primitives/<name> — is also published, built from the same source files. One install, zero dependencies:

npm install @presetui/ui

Point your global stylesheet at the package instead of pasting the theme, and let Tailwind see the package's class names:

@import "tailwindcss";
@import "@presetui/ui/theme.css";
/* adjust the relative path from this file to node_modules */
@source "../node_modules/@presetui/ui";

Then import per component (or from the root barrel):

import { Button } from "@presetui/ui/button"
import { DropdownMenu } from "@presetui/ui/dropdown-menu"
import { Toaster, toast } from "@presetui/ui/toast"

Choose per project and stay consistent: copy when you want to own and rewrite the files, install when you want updates via npm update. The trade-offs are the ones in the comparison on the landing page — as a package, PresetUI is on the package side of that table.

One barrel-only detail: the CSS-only tooltip owns the Tooltip name; the positioned one is TooltipJs in the barrel, while its own subpath (@presetui/ui/tooltip-js) still exports Tooltip.

For AI agents

Point your agent at these plain-text endpoints:

A useful instruction: "Read https://presetui.com/llms.txt and write these components into components/ui/ as .js files. Do not install any packages."

One trade-off worth knowing

PresetUI does not use tailwind-merge. Your className is appended after the component's own classes, so it usually wins — but when two utilities target the same CSS property, the winner is decided by Tailwind's stylesheet order rather than by which one you wrote.

In practice this means <Button className="px-8"> works, while overriding something the component already sets in the same category may need ! :

<Button className="bg-blue-600!">Custom</Button>

This is the cost of having no dependencies. If it bothers you, the components are yours — add tailwind-merge to your copy.