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
- React 19 or later — components rely on
refbeing a normal prop, so noforwardRefwrapper is needed. - Tailwind CSS v4 — the theme below uses the
@themedirective.
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.
/* 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.txtComponents 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:
- Copy the primitive too: fetch
/r/primitives/<name>.txt(one file, shared core embedded) and save it asprimitives/<name>.jsnext tocomponents/ui/. The Source tab's import matches this layout. - Install the package:
npm install @presetui/ui— the primitives are bundled, no second package — and take the Source · npm tab instead: the same file with its import pointing at@presetui/ui/primitives/<name>. Updates then arrive withnpm updaterather than by re-copying.
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/uiPoint 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:
/llms.txt— the entire kit, theme and all components, in one document/r/button.txt,/r/input.txt,/r/card.txt,/r/badge.txt— individual sources
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.