# Introduction
Source: https://desyne.dev/docs
> Accessible React components built on React Aria, styled with shadcn tokens, installed as source you own.
Desyne is a copy-paste component library for React. Every component is built on
[React Aria Components](https://react-aria.adobe.com/) and styled with Tailwind CSS v4
using the same CSS variables as [shadcn/ui](https://ui.shadcn.com) (`--background`,
`--primary`, `--border`, …). You install components with the shadcn CLI, and they land
in your repo as plain `.tsx` files.
```tsx title="dialog/demo.tsx"
"use client";
import { Form } from "react-aria-components";
import { Button } from "@/components/ui/button";
import {
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { TextField } from "@/components/ui/text-field";
export default function DialogDemo() {
return (
{({ close }) => (
)}
);
}
```
## What you get
* **Accessible behaviour out of the box.** Keyboard navigation, focus management,
screen reader announcements and ARIA patterns come from React Aria, not from
hand-written `onKeyDown` handlers.
* **Your code, not a dependency.** The CLI copies source into `components/ui`. There is
no `@desyne/*` package to upgrade and nothing you can't change.
* **shadcn-compatible tokens.** Components read the standard shadcn variables, so they
inherit an existing shadcn theme and sit next to shadcn components without clashing.
* **One design language.** Controls share a 28 / 32 / 40px height scale, one focus ring,
one field system and one tone system, so screens stay consistent as they grow.
* **Real examples.** Every component page has live previews you can copy, including
recipes that combine several components into a working pattern.
## How it fits together
| Piece | What it is |
| ------------------ | ----------------------------------------------------------------------------------------- |
| Components | `components/ui/*.tsx`, one file per component, installed by name |
| `lib/utils.ts` | shadcn's `cn()` helper (`clsx` + `tailwind-merge`) |
| `lib/primitive.ts` | Shared helpers: the focus ring, the `tones` color system and `composeTailwindRenderProps` |
| Tokens | CSS variables with shadcn names, plus `--brand`, `--success`, `--warning` and `--info` |
| Registry | A shadcn registry at `/r/{name}.json` under the `@desyne` namespace |
Components style React Aria states with Tailwind's native data variants
(`data-hovered:`, `data-pressed:`, `data-focus-visible:`, `data-selected:`,
`data-invalid:`), so you don't need a Tailwind plugin.
## Requirements
* React 19
* Tailwind CSS v4
* TypeScript (recommended; the source is typed)
* Any React framework: Next.js, Vite, React Router or TanStack Start
## Acknowledgements
Desyne stands on the work of others, and we're grateful for it:
* **[React Aria Components](https://react-aria.adobe.com/)** by Adobe. Every interactive
component is built on its primitives, and the accessibility, keyboard and
internationalisation behaviour you get comes from that work.
* **[shadcn/ui](https://ui.shadcn.com)** by shadcn. The copy-the-source model, the CLI and
registry format we install through, and the token names we theme with all come from
shadcn/ui.
* **[Intent UI](https://intentui.com)** by Irsyad A. Panjaitan. It showed what a polished,
React Aria–based take on the shadcn approach can look like, and inspired parts of our
component APIs, block catalogue and docs.
We also rely on [Tailwind CSS](https://tailwindcss.com), [Lucide](https://lucide.dev),
[Recharts](https://recharts.org), [Motion](https://motion.dev) and
[Fumadocs](https://fumadocs.dev). If you like Desyne, go star their repos too.
## Where to next
---
# Installation
Source: https://desyne.dev/docs/installation
> Set up Desyne in a Next.js, Vite or existing shadcn project, in a monorepo, or by copying files by hand.
Desyne installs through the [shadcn CLI](https://ui.shadcn.com/docs/cli). The CLI
reads the registry, copies component source into your project, installs npm
dependencies and adds any CSS variables a component needs.
React 19, Tailwind CSS v4 and a `@/*` import alias. Components import
`@/lib/utils`, `@/lib/primitive` and `@/components/ui/*`, the same paths a
shadcn project already uses.
## Pick your setup
### Create the app
```bash
npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app
```
### Initialise shadcn
```bash
npx shadcn@latest init
```
This writes `components.json`, `lib/utils.ts` and the base tokens in `app/globals.css`.
### Register the namespace
Add the Desyne registry to `components.json`:
```json title="components.json"
{
"registries": {
"@desyne": "https://desyne.dev/r/{name}.json"
}
}
```
### Add a component
```bash
npx shadcn@latest add @desyne/button
```
```tsx title="app/page.tsx"
import { Button } from "@/components/ui/button";
export default function Page() {
return ;
}
```
Components that use hooks or React Aria are marked `"use client"`, so they work in
App Router pages without extra wrappers. See [Server components](/docs/server-components).
### Create the app
```bash
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install tailwindcss @tailwindcss/vite
```
Replace `src/index.css` with:
```css title="src/index.css"
@import "tailwindcss";
```
### Add the `@` alias
shadcn needs a path alias in both TypeScript and Vite.
```json title="tsconfig.json"
{
"files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
"compilerOptions": {
"baseUrl": ".",
"paths": { "@/*": ["./src/*"] }
}
}
```
Add the same `baseUrl` and `paths` to `tsconfig.app.json`, then:
```ts title="vite.config.ts"
import path from "node:path";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: { "@": path.resolve(__dirname, "./src") },
},
});
```
### Initialise shadcn and register the namespace
```bash
npx shadcn@latest init
```
```json title="components.json"
{
"registries": {
"@desyne": "https://desyne.dev/r/{name}.json"
}
}
```
### Add components
```bash
npx shadcn@latest add @desyne/button @desyne/text-field
```
Desyne components use the same token names and import paths as shadcn/ui, so they
drop into an existing project.
1. Add the namespace to `components.json`:
```json title="components.json"
{
"registries": {
"@desyne": "https://desyne.dev/r/{name}.json"
}
}
```
2. Add components by their namespaced name:
```bash
npx shadcn@latest add @desyne/select
```
3. Your existing theme applies immediately. The first component that needs it also
installs `lib/primitive.ts` and adds the extra tokens (`--brand`, `--success`,
`--warning`, `--info`, `--destructive-foreground`) to your CSS.
Desyne and shadcn/ui both write to `components/ui/.tsx`. Adding
`@desyne/button` over an existing shadcn `button.tsx` asks before
overwriting. Desyne's Button accepts shadcn's variant names (`default`,
`destructive`, `secondary`, `outline`, `ghost`, `link`), but it is a React Aria
button: handlers take `onPress`, and `disabled` is `isDisabled`. Check call sites
before you replace a component.
## Monorepos
In a Turborepo or pnpm/Bun workspace, keep components in a shared package and point
each app at it.
1. Run `npx shadcn@latest init` in the shared UI package and in each app, so every
workspace has its own `components.json` with the `@desyne` registry entry.
2. Install into the shared package with `--cwd` (`-c`):
```bash
npx shadcn@latest add @desyne/dialog -c packages/ui
```
3. In each app, map the aliases the components import to the shared package:
```json title="apps/web/tsconfig.json"
{
"compilerOptions": {
"paths": {
"@/components/ui/*": ["../../packages/ui/src/components/ui/*"],
"@/lib/utils": ["../../packages/ui/src/lib/utils"],
"@/lib/primitive": ["../../packages/ui/src/lib/primitive"],
"@/*": ["./*"]
}
}
}
```
4. Tell Tailwind to scan the package and, in Next.js, transpile it:
```css title="apps/web/app/globals.css"
@import "tailwindcss";
@source "../../../packages/ui/src";
```
```js title="apps/web/next.config.mjs"
export default { transpilePackages: ["@acme/ui"] };
```
This is exactly how this documentation site consumes the library.
## Manual installation
You can skip the CLI and copy files yourself. Every component page has a **Manual** tab
with its source, its npm dependencies and the other components it imports.
### Install the shared dependencies
```bash
npm install react-aria-components tailwind-variants clsx tailwind-merge lucide-react tw-animate-css
```
Some components need more (`sonner` for Toast, `recharts` for Chart,
`embla-carousel-react` for Carousel, `input-otp` for Input OTP). Their pages list them.
### Add the helpers
```ts title="lib/utils.ts"
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
```
Then copy `lib/primitive.ts` from the [registry](/r/primitive.json) (the `files[0].content`
field) or from any component's Manual tab.
### Add the tokens
Your global CSS needs Tailwind, the animation utilities and the tokens:
```css title="app/globals.css"
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
:root {
--radius: 0.625rem;
--background: oklch(0.984 0.002 286);
--foreground: oklch(0.205 0.006 286);
/* …the rest of the tokens */
}
```
The full set, with dark values and the `@theme inline` mapping, is on
[Colors & tokens](/docs/colors).
### Copy components
Copy each component into `components/ui/` and fix the imports if your aliases differ.
## Check it works
```tsx
"use client";
import { Form } from "react-aria-components";
import { Button } from "@/components/ui/button";
import { TextField } from "@/components/ui/text-field";
export function Smoke() {
return (
);
}
```
Tab to the field: you should see the soft colored focus ring. Submit it empty: focus
moves to the field and the required-field message appears under it.
## Next steps
---
# CLI & registry
Source: https://desyne.dev/docs/cli
> How the @desyne namespace works, what the CLI writes, how to update components, and how to connect the private Pro registry.
Desyne is distributed as a [shadcn registry](https://ui.shadcn.com/docs/registry): a
set of JSON files, one per item, served at `/r/{name}.json`. The shadcn CLI fetches an
item, copies its files into your project, installs its npm dependencies, follows its
`registryDependencies` and merges any CSS variables it declares.
## Namespaces
Registries are configured in `components.json` under `registries`. The key is the
namespace you type on the command line; the value is a URL template where `{name}` is
replaced by the item name.
```json title="components.json"
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"tsx": true,
"tailwind": { "css": "app/globals.css", "baseColor": "neutral", "cssVariables": true },
"aliases": {
"components": "@/components",
"ui": "@/components/ui",
"lib": "@/lib",
"utils": "@/lib/utils"
},
"registries": {
"@desyne": "https://desyne.dev/r/{name}.json"
}
}
```
| Namespace | Contents | Access |
| ------------- | ------------------------------ | ------------------------------------------------ |
| `@desyne` | Components and `lib/primitive` | Public |
| `@desyne-pro` | Pro blocks | License key ([below](#the-private-pro-registry)) |
## Commands
```bash
# Add one or more components
npx shadcn@latest add @desyne/button @desyne/dialog
# Inspect an item before installing it (files, dependencies, CSS)
npx shadcn@latest view @desyne/date-picker
# Search the namespace
npx shadcn@latest search @desyne -q date
# Install straight from a URL, no namespace needed
npx shadcn@latest add https://desyne.dev/r/combobox.json
```
## What an item contains
Each component item lists its source file, npm dependencies and the other registry
items it imports. Dependencies are inferred from the source, so they are always in
sync with the code.
```json title="/r/text-field.json (abridged)"
{
"name": "text-field",
"type": "registry:ui",
"title": "Text Field",
"dependencies": ["react-aria-components"],
"registryDependencies": ["@desyne/field", "@desyne/primitive"],
"files": [{ "path": "components/ui/text-field.tsx", "type": "registry:ui" }]
}
```
Adding `text-field` therefore also installs `field.tsx` and `lib/primitive.ts`, and
`primitive` in turn depends on shadcn's own `utils` item (`lib/utils.ts`).
### The `primitive` item
`lib/primitive.ts` is the only shared library file. It exports:
* `composeTailwindRenderProps(className, tw)`: merges a React Aria `className` (a string or a render-prop function) with Tailwind classes.
* `focusRing`: the keyboard focus style, a 3px glow in `--ring` on `data-focus-visible`.
* `tones` and the `Tone` type: the color system behind every `color` prop ([Theming](/docs/theming#tones)).
The item also carries `cssVars`, so the first install adds `--brand`, `--success`,
`--warning`, `--info` and `--destructive-foreground` (light and dark) to your CSS and
maps them to Tailwind colors.
## Updating components
Installed components are your code, so the CLI never updates them behind your back. To
pull a newer version:
Commit or stash your work so you can review the change.
Re-add the component with `--overwrite`:
```bash
npx shadcn@latest add @desyne/select --overwrite
```
Review the diff with `git diff` and re-apply any local edits you want to keep.
Prefer passing `className`, wrapping a component, or adding a variant over
rewriting internals. Small, local edits survive updates with an easy merge.
The [Changelog](/changelog) lists what changed in each release.
## The private Pro registry
Pro blocks are served from the Pro site's own registry, which checks a license key on
every request. Keys look like `dsp_` followed by 32 characters; you'll find yours on
your account page after purchase.
### Store the key in an environment variable
```bash title=".env.local"
DESYNE_PRO_LICENSE=dsp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
Never commit the key. In CI, add it as a secret with the same name.
### Add the registry with an Authorization header
```json title="components.json"
{
"registries": {
"@desyne": "https://desyne.dev/r/{name}.json",
"@desyne-pro": {
"url": "https://pro.desyne.dev/r/{name}.json",
"headers": {
"Authorization": "Bearer ${DESYNE_PRO_LICENSE}"
}
}
}
}
```
The CLI expands `${DESYNE_PRO_LICENSE}` from your environment and `.env` files when it makes
the request.
### Install blocks by name
```bash
npx shadcn@latest add @desyne-pro/pricing-02
```
Block files land in `components/blocks//`, and the components they use come from
`@desyne` automatically.
| Response | Meaning |
| -------- | ---------------------------------------------------------------------------------------- |
| `200` | The block JSON, with file contents |
| `401` | Missing, invalid or inactive key. Check the variable is set in the shell running the CLI |
| `404` | No block with that name |
A handful of blocks are free teasers and install without a key. See
[Installing blocks](/docs/pro/blocks) for the full workflow.
---
# Project structure
Source: https://desyne.dev/docs/project-structure
> Where Desyne files land in your project, what each one does, and how to organise your own code around them.
After `init` and a few `add` commands, a Next.js project looks like this:
```txt
app/
globals.css Tailwind, tw-animate-css and the tokens
layout.tsx Root layout: providers,
components/
ui/ Desyne components, one file each
button.tsx
field.tsx Shared by every text-like control
text-field.tsx
dialog.tsx
…
blocks/ Pro blocks, one folder each (if you use Pro)
pricing-02/
lib/
utils.ts cn(): clsx + tailwind-merge
primitive.ts focusRing, tones, composeTailwindRenderProps
components.json shadcn config and registry namespaces
```
In Vite the same tree lives under `src/`.
## The files
| File | Role | Edit it? |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------ |
| `components/ui/*.tsx` | Component source. Each file exports the component, its parts and usually a `*Variants` function built with `tailwind-variants` | Yes. It's yours |
| `components/ui/field.tsx` | `Label`, `Description`, `FieldError`, `FieldGroup`, `Input` and `fieldVariants` (outline, filled, underlined × sm, md, lg). Text Field, Select, Combobox, Number Field and the date fields build on it | Yes, and the change applies to every field |
| `lib/primitive.ts` | Focus ring, tones and the render-prop class helper | Rarely. Add a tone here |
| `lib/utils.ts` | shadcn's `cn()` | No need |
| `app/globals.css` | Tokens and Tailwind setup | Yes. This is where theming happens |
## Anatomy of a component
Components follow one pattern, so once you've read one you can read them all:
```tsx title="components/ui/switch.tsx (shape)"
"use client";
import { Switch as SwitchPrimitive } from "react-aria-components";
import { tv } from "tailwind-variants";
import { composeTailwindRenderProps } from "@/lib/primitive";
// 1. Styles: a tv() recipe with variants and sizes, using data-* state variants
const switchVariants = tv({ /* … */ });
// 2. Props: the React Aria props plus our style props
export interface SwitchProps extends SwitchPrimitiveProps { /* … */ }
// 3. Component: the React Aria primitive with classes applied
export function Switch({ className, ...props }: SwitchProps) {
return (
);
}
```
* **`data-slot`** attributes name each part (`data-slot="dialog-content"`), so you can
target parts from a parent: `[&_[data-slot=card-header]]:pb-0`.
* **State styling** uses React Aria's data attributes: `data-hovered:`, `data-pressed:`,
`data-focus-visible:`, `data-selected:`, `data-disabled:`, `data-invalid:`,
`data-entering:` and `data-exiting:`. Group variants (`group-data-invalid/field:`)
style children from a parent's state.
* **`className`** accepts a string or a function of render props, like every React
Aria component: `className={({ isSelected }) => isSelected ? "font-semibold" : ""}`.
## Organising your own code
Keep `components/ui` for primitives and put product code beside it:
```txt
components/
ui/ Desyne (and any shadcn) primitives
blocks/ Pro blocks you've installed
settings/ Your feature components, composed from ui/
notifications.tsx
```
When you need a variation that every screen should share, edit the primitive. When only
one screen needs it, wrap the primitive in a feature component instead. That keeps
`components/ui` easy to update with `--overwrite` (see [CLI & registry](/docs/cli#updating-components)).
---
# Theming
Source: https://desyne.dev/docs/theming
> shadcn token names, an ink primary with an indigo brand accent, color tones, radius and density, dark mode and multiple themes.
Every component reads CSS variables with shadcn's names, so theming is a CSS job: change
values in `globals.css` and every component follows. There's no theme provider and no
JavaScript involved.
The default theme is "enterprise-crisp": layered neutral surfaces, an **ink** primary for
the main action, an **indigo brand** accent for selection, focus and links, a 10px base
radius and compact 28 / 32 / 40px controls.
## How the tokens are used
| Token | Used for |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `--background` / `--foreground` | Page canvas and body text |
| `--card`, `--popover` | Raised surfaces: cards, menus, dialogs, popovers |
| `--primary` / `--primary-foreground` | Solid buttons and the main call to action |
| `--brand` / `--brand-foreground` | Selection and emphasis: checked checkboxes, switches, radios, selected tabs and dates, links |
| `--secondary`, `--muted` / `--muted-foreground` | Quiet fills, inset panels and secondary text |
| `--accent` / `--accent-foreground` | Hovered and highlighted items in lists and menus |
| `--destructive` / `--destructive-foreground` | Errors, invalid fields and destructive actions |
| `--success`, `--warning`, `--info` (+ `-foreground`) | Status colors used by tones |
| `--border`, `--input` | Hairlines and field outlines |
| `--ring` | Focus glow (`ring-ring/25`) and the focused-field border |
| `--radius` | Base radius that the whole radius scale derives from |
| `--chart-1` … `--chart-5` | Chart series |
| `--sidebar-*` | The Sidebar component's own surface set |
The full list with default values is on [Colors & tokens](/docs/colors).
## Build a brand theme
Most products only need three changes: the brand color, the primary action color and
the radius.
```css title="app/globals.css"
:root {
--brand: oklch(0.56 0.14 160); /* emerald */
--brand-foreground: oklch(0.99 0 0);
--ring: oklch(0.56 0.14 160);
--primary: oklch(0.56 0.14 160); /* make solid buttons brand-colored too */
--primary-foreground: oklch(0.99 0 0);
--radius: 0.5rem;
}
.dark {
--brand: oklch(0.72 0.14 160);
--ring: oklch(0.72 0.14 160);
--primary: oklch(0.72 0.14 160);
--primary-foreground: oklch(0.2 0.05 160);
}
```
Tips that keep a theme looking finished:
* **Use OKLCH.** Equal lightness steps look equal, so a dark-mode brand is usually the
same hue and chroma with lightness raised by about 0.12–0.18.
* **Tint the neutrals.** The default neutrals carry a trace of the brand hue (`286`).
Change the hue on `--background`, `--muted`, `--border` and friends to match your
brand and keep chroma below `0.01`.
* **Keep `--ring` close to `--brand`.** The focus glow is the brand color at 25%
opacity, which reads as part of the design rather than a browser default.
* **Check contrast.** Text on `--primary` and `--brand` should reach 4.5:1. Amber and
yellow brands usually need a dark `-foreground`.
The default keeps `--primary` near-black so the main action stands out without
competing with selected states. If your brand is the action color, set `--primary`
to the brand value as above.
## Tones
Components with a `color` prop (Button, Badge, Alert, Tag Group, Slider, Progress Bar,
Progress Circle, Meter, Spinner, Rating, Timeline) share one tone system. A tone is a
class that sets two variables, `--tone` and `--tone-fg`, and the variant styles are
written against them:
```ts title="lib/primitive.ts"
export const tones = {
primary: "[--tone-fg:var(--primary-foreground)] [--tone:var(--primary)]",
brand: "[--tone-fg:var(--brand-foreground)] [--tone:var(--brand)]",
neutral: "[--tone-fg:var(--background)] [--tone:var(--foreground)]",
danger: "[--tone-fg:var(--destructive-foreground)] [--tone:var(--destructive)]",
success: "[--tone-fg:var(--success-foreground)] [--tone:var(--success)]",
warning: "[--tone-fg:var(--warning-foreground)] [--tone:var(--warning)]",
info: "[--tone-fg:var(--info-foreground)] [--tone:var(--info)]",
} as const;
```
So the soft style is `bg-(--tone)/10 text-(--tone)` for every color, and one variant
definition covers all seven.
```tsx
LiveYour trial ends in 3 days.
```
### Add a tone
Define the color in your CSS and expose it to Tailwind:
```css title="app/globals.css"
:root { --violet: oklch(0.55 0.22 300); --violet-foreground: oklch(0.99 0 0); }
.dark { --violet: oklch(0.7 0.18 300); --violet-foreground: oklch(0.2 0.05 300); }
@theme inline {
--color-violet: var(--violet);
--color-violet-foreground: var(--violet-foreground);
}
```
Add it to `tones`:
```ts title="lib/primitive.ts"
export const tones = {
// …
violet: "[--tone-fg:var(--violet-foreground)] [--tone:var(--violet)]",
} as const;
```
Add it to the `color` variant of the components that should accept it, for example in
`button.tsx`:
```ts
color: {
// …
violet: tones.violet,
},
```
`Tone` is derived from `tones`, so the prop types update automatically.
## Radius
One variable drives the whole scale, defined in the `@theme inline` block:
| Utility | Value | Used by |
| ------------ | ---------------- | ------------------------------ |
| `rounded-xs` | `--radius` − 6px | Tiny details |
| `rounded-sm` | `--radius` − 4px | `xs` buttons, small tags |
| `rounded-md` | `--radius` − 2px | Buttons, fields, menu items |
| `rounded-lg` | `--radius` | Popovers, menus and list boxes |
| `rounded-xl` | `--radius` + 4px | Cards and dialogs |
With the default `--radius: 0.625rem`, controls are 8px, popovers 10px and cards 14px. Set
`--radius: 0.375rem` for a sharper look or `0.875rem` for a softer one; the steps
keep their relationship because they all derive from the same base. (Checkboxes keep a
fixed 4px corner.)
## Density
Controls share one height scale so mixed rows line up:
| Size | Height | Use it for |
| -------------- | ------ | ------------------------------------------------- |
| `sm` | 28px | Toolbars, table filters, dense settings |
| `md` (default) | 32px | Most forms and dialogs |
| `lg` | 40px | Marketing pages, touch-first and onboarding flows |
```tsx
```
Text-like fields also take a `variant`: `outline` (default), `filled` or `underlined`.
For larger regions, Card takes `size` (`sm`, `md`, `lg`) and Table takes `density`
(`compact`, `default`, `comfortable`).
## Theme builder & styles
The [theme builder](/themes) puts every token on one page: pick a **style**, a **base**
neutral, a **brand** color (a preset or any custom color), the **radius**, **density** and
**font**, and watch a live dashboard of real components update. Share the result as a link
(`/themes?design=…`) or apply it to the previews on this site.
| Style | Character | Defaults |
| ------- | ------------------------------------------------------------ | ---------------------------------- |
| Default | Crisp hairlines, soft depth and medium radii | 0.625rem · default density · Inter |
| Soft | Rounder shapes, filled fields, gentle diffused shadows | 0.875rem · default · Inter |
| Sharp | Square corners, hairline borders, dense controls, no shadows | 0.25rem · compact · Geist |
| Bold | Thick borders, strong contrast, chunky controls, heavy type | 0.5rem · comfortable · Inter |
A style is a set of structural tokens: radius levels (`--radius-control`, `--radius-box`,
`--radius-overlay`…), `--border-width`, field chrome (`--field-bg`, `--field-border`),
shadows, `--ring-width` and heading weights. Picking a style resets radius, density and font
to that style's defaults and keeps your base and brand. Bases are neutral palettes (zinc,
neutral, slate, stone, olive, mauve); the brand drives `--brand`, `--ring` and `--chart-1`,
with the dark-mode variant and the text color on it derived for you.
### Install a theme
Press **Install** in the builder to get a command for your design. It points at a
`registry:theme` item that the shadcn CLI merges into your global CSS (`:root`, `.dark`
and the font stack in `@theme inline`):
```bash
npx shadcn@latest add https:///r/themes/1~soft~zinc~indigo~0.875~default~inter.json
```
The **CSS** tab gives the same variables to paste by hand. Components read only tokens,
so the ones you've already installed pick up a new theme without being reinstalled. If the
theme uses a font other than Inter, load that font too (the CLI adds a Google Fonts
`@import`; with Next.js you can use `next/font` instead).
## Dark mode
Dark values live under a `.dark` class, and the tokens file declares a class-based
`dark:` variant. Put `class="dark"` on `` (or any ancestor) and everything inside
switches. See [Dark mode](/docs/dark-mode) for the toggle and `next-themes` setup.
## Multiple themes
Because themes are just variables, you can scope one to any element.
```css title="app/globals.css"
[data-theme="ocean"] {
--brand: oklch(0.6 0.13 230);
--ring: oklch(0.6 0.13 230);
--radius: 1rem;
}
.dark [data-theme="ocean"],
[data-theme="ocean"].dark {
--brand: oklch(0.74 0.12 230);
--ring: oklch(0.74 0.12 230);
}
```
```tsx
… {/* whole app */}
… {/* one area, e.g. a customer's portal */}
```
You can also set variables inline for a live preview, the way the theme studio on the
home page does:
```tsx
Uses the preview accent
```
Menus, popovers, dialogs and toasts render at the end of ``, outside the
element where you scoped a theme. Put theme attributes on `` or `` when
overlays should match, or wrap the portal content in its own themed element.
---
# Colors & tokens
Source: https://desyne.dev/docs/colors
> Every design token with its light and dark value, the Tailwind utilities it maps to, and the complete CSS to paste into a project.
Tokens use [shadcn's names](https://ui.shadcn.com/docs/theming) and OKLCH values. Each
token is exposed to Tailwind as a color through `@theme inline`, so `--brand` becomes
`bg-brand`, `text-brand`, `border-brand`, `ring-brand` and so on, with opacity
modifiers like `bg-brand/10`.
These values come from `theme.css` in the library. The swatches show the live values
on this site, so toggle the site theme to compare light and dark.
## Color tokens
### Surfaces
| | Token | Light | Dark |
| ------------------------------------- | ---------------------- | ------------------------ | ------------------------ |
| | `--background` | `oklch(0.984 0.002 286)` | `oklch(0.145 0.004 286)` |
| | `--foreground` | `oklch(0.205 0.006 286)` | `oklch(0.97 0.002 286)` |
| | `--card` | `oklch(1 0 0)` | `oklch(0.185 0.005 286)` |
| | `--card-foreground` | `oklch(0.205 0.006 286)` | `oklch(0.97 0.002 286)` |
| | `--popover` | `oklch(1 0 0)` | `oklch(0.205 0.006 286)` |
| | `--popover-foreground` | `oklch(0.205 0.006 286)` | `oklch(0.97 0.002 286)` |
### Actions and emphasis
| | Token | Light | Dark |
| --------------------------------------- | ------------------------ | ------------------------ | ------------------------ |
| | `--primary` | `oklch(0.215 0.006 286)` | `oklch(0.965 0.002 286)` |
| | `--primary-foreground` | `oklch(0.985 0 0)` | `oklch(0.2 0.006 286)` |
| | `--brand` | `oklch(0.54 0.21 277)` | `oklch(0.67 0.18 277)` |
| | `--brand-foreground` | `oklch(0.99 0.005 277)` | `oklch(0.99 0.005 277)` |
| | `--secondary` | `oklch(0.967 0.002 286)` | `oklch(0.235 0.006 286)` |
| | `--secondary-foreground` | `oklch(0.25 0.006 286)` | `oklch(0.95 0.002 286)` |
| | `--muted` | `oklch(0.967 0.002 286)` | `oklch(0.225 0.006 286)` |
| | `--muted-foreground` | `oklch(0.54 0.012 286)` | `oklch(0.7 0.012 286)` |
| | `--accent` | `oklch(0.955 0.003 286)` | `oklch(0.255 0.006 286)` |
| | `--accent-foreground` | `oklch(0.205 0.006 286)` | `oklch(0.97 0.002 286)` |
### Status
| | Token | Light | Dark |
| ----------------------------------------- | -------------------------- | ---------------------- | ---------------------- |
| | `--destructive` | `oklch(0.585 0.22 25)` | `oklch(0.66 0.2 22)` |
| | `--destructive-foreground` | `oklch(0.99 0 0)` | `oklch(0.99 0 0)` |
| | `--success` | `oklch(0.62 0.16 150)` | `oklch(0.72 0.15 150)` |
| | `--success-foreground` | `oklch(0.99 0 0)` | `oklch(0.2 0.05 150)` |
| | `--warning` | `oklch(0.76 0.16 70)` | `oklch(0.8 0.15 75)` |
| | `--warning-foreground` | `oklch(0.25 0.06 70)` | `oklch(0.25 0.06 70)` |
| | `--info` | `oklch(0.6 0.16 240)` | `oklch(0.7 0.14 240)` |
| | `--info-foreground` | `oklch(0.99 0 0)` | `oklch(0.2 0.05 240)` |
### Lines and focus
| | Token | Light | Dark |
| ------------------------- | ---------- | ------------------------ | ---------------------- |
| | `--border` | `oklch(0.925 0.003 286)` | `oklch(1 0 0 / 8%)` |
| | `--input` | `oklch(0.895 0.004 286)` | `oklch(1 0 0 / 13%)` |
| | `--ring` | `oklch(0.54 0.21 277)` | `oklch(0.67 0.18 277)` |
### Charts
| | Token | Light | Dark |
| -------------------------- | ----------- | ---------------------- | ---------------------- |
| | `--chart-1` | `oklch(0.54 0.21 277)` | `oklch(0.67 0.18 277)` |
| | `--chart-2` | `oklch(0.7 0.13 185)` | `oklch(0.74 0.12 185)` |
| | `--chart-3` | `oklch(0.78 0.15 75)` | `oklch(0.8 0.14 75)` |
| | `--chart-4` | `oklch(0.65 0.2 350)` | `oklch(0.7 0.18 350)` |
| | `--chart-5` | `oklch(0.7 0.12 235)` | `oklch(0.74 0.12 235)` |
### Sidebar
| | Token | Light | Dark |
| --------------------------------------------- | ------------------------------ | ------------------------ | ------------------------ |
| | `--sidebar` | `oklch(0.975 0.002 286)` | `oklch(0.165 0.005 286)` |
| | `--sidebar-foreground` | `oklch(0.205 0.006 286)` | `oklch(0.97 0.002 286)` |
| | `--sidebar-primary` | `oklch(0.215 0.006 286)` | `oklch(0.965 0.002 286)` |
| | `--sidebar-primary-foreground` | `oklch(0.985 0 0)` | `oklch(0.2 0.006 286)` |
| | `--sidebar-accent` | `oklch(1 0 0)` | `oklch(0.225 0.006 286)` |
| | `--sidebar-accent-foreground` | `oklch(0.205 0.006 286)` | `oklch(0.97 0.002 286)` |
| | `--sidebar-border` | `oklch(0.925 0.003 286)` | `oklch(1 0 0 / 8%)` |
| | `--sidebar-ring` | `oklch(0.54 0.21 277)` | `oklch(0.67 0.18 277)` |
Neutrals use hue `286` with very low chroma, which gives the grays a faint cool tint
that matches the indigo brand. In dark mode, `--border` and `--input` are translucent
white, so they adapt to whatever surface they sit on.
## Radius
| Token | Default | Utilities |
| ---------- | ---------- | ------------------------------------------------------------------------------------------------ |
| `--radius` | `0.625rem` | `rounded-xs` (−6px), `rounded-sm` (−4px), `rounded-md` (−2px), `rounded-lg`, `rounded-xl` (+4px) |
## Additive tokens
shadcn/ui doesn't define these. When you install a component through the CLI, the
`primitive` item adds them to your CSS automatically:
| Token | Purpose |
| ----------------------------------- | ------------------------------------- |
| `--brand`, `--brand-foreground` | Accent for selection, focus and links |
| `--destructive-foreground` | Text on solid destructive fills |
| `--success`, `--success-foreground` | Positive states |
| `--warning`, `--warning-foreground` | Caution states |
| `--info`, `--info-foreground` | Neutral information |
## Using tokens in your own code
```tsx
// Utilities
// Arbitrary values for anything without a utility
```
Prefer semantic tokens over raw palette colors (`bg-card`, not `bg-white`). Semantic
tokens switch with dark mode and with any theme you scope to an element.
## The complete CSS
Paste this into your global stylesheet when you aren't using `shadcn init`, or when you
want Desyne's default look instead of your current shadcn theme.
```css title="app/globals.css"
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
:root {
--radius: 0.625rem;
/* Surfaces: canvas < card (white) ; muted = inset panels inside cards */
--background: oklch(0.984 0.002 286);
--foreground: oklch(0.205 0.006 286);
--card: oklch(1 0 0);
--card-foreground: oklch(0.205 0.006 286);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.205 0.006 286);
/* Ink primary for the main action; brand (indigo) for selection, focus, links */
--primary: oklch(0.215 0.006 286);
--primary-foreground: oklch(0.985 0 0);
--brand: oklch(0.54 0.21 277);
--brand-foreground: oklch(0.99 0.005 277);
--secondary: oklch(0.967 0.002 286);
--secondary-foreground: oklch(0.25 0.006 286);
--muted: oklch(0.967 0.002 286);
--muted-foreground: oklch(0.54 0.012 286);
--accent: oklch(0.955 0.003 286);
--accent-foreground: oklch(0.205 0.006 286);
--destructive: oklch(0.585 0.22 25);
--destructive-foreground: oklch(0.99 0 0);
--success: oklch(0.62 0.16 150);
--success-foreground: oklch(0.99 0 0);
--warning: oklch(0.76 0.16 70);
--warning-foreground: oklch(0.25 0.06 70);
--info: oklch(0.6 0.16 240);
--info-foreground: oklch(0.99 0 0);
--border: oklch(0.925 0.003 286);
--input: oklch(0.895 0.004 286);
--ring: oklch(0.54 0.21 277);
--chart-1: oklch(0.54 0.21 277);
--chart-2: oklch(0.7 0.13 185);
--chart-3: oklch(0.78 0.15 75);
--chart-4: oklch(0.65 0.2 350);
--chart-5: oklch(0.7 0.12 235);
--sidebar: oklch(0.975 0.002 286);
--sidebar-foreground: oklch(0.205 0.006 286);
--sidebar-primary: oklch(0.215 0.006 286);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(1 0 0);
--sidebar-accent-foreground: oklch(0.205 0.006 286);
--sidebar-border: oklch(0.925 0.003 286);
--sidebar-ring: oklch(0.54 0.21 277);
}
.dark {
--background: oklch(0.145 0.004 286);
--foreground: oklch(0.97 0.002 286);
--card: oklch(0.185 0.005 286);
--card-foreground: oklch(0.97 0.002 286);
--popover: oklch(0.205 0.006 286);
--popover-foreground: oklch(0.97 0.002 286);
--primary: oklch(0.965 0.002 286);
--primary-foreground: oklch(0.2 0.006 286);
--brand: oklch(0.67 0.18 277);
--brand-foreground: oklch(0.99 0.005 277);
--secondary: oklch(0.235 0.006 286);
--secondary-foreground: oklch(0.95 0.002 286);
--muted: oklch(0.225 0.006 286);
--muted-foreground: oklch(0.7 0.012 286);
--accent: oklch(0.255 0.006 286);
--accent-foreground: oklch(0.97 0.002 286);
--destructive: oklch(0.66 0.2 22);
--destructive-foreground: oklch(0.99 0 0);
--success: oklch(0.72 0.15 150);
--success-foreground: oklch(0.2 0.05 150);
--warning: oklch(0.8 0.15 75);
--warning-foreground: oklch(0.25 0.06 70);
--info: oklch(0.7 0.14 240);
--info-foreground: oklch(0.2 0.05 240);
--border: oklch(1 0 0 / 8%);
--input: oklch(1 0 0 / 13%);
--ring: oklch(0.67 0.18 277);
--chart-1: oklch(0.67 0.18 277);
--chart-2: oklch(0.74 0.12 185);
--chart-3: oklch(0.8 0.14 75);
--chart-4: oklch(0.7 0.18 350);
--chart-5: oklch(0.74 0.12 235);
--sidebar: oklch(0.165 0.005 286);
--sidebar-foreground: oklch(0.97 0.002 286);
--sidebar-primary: oklch(0.965 0.002 286);
--sidebar-primary-foreground: oklch(0.2 0.006 286);
--sidebar-accent: oklch(0.225 0.006 286);
--sidebar-accent-foreground: oklch(0.97 0.002 286);
--sidebar-border: oklch(1 0 0 / 8%);
--sidebar-ring: oklch(0.67 0.18 277);
}
@theme inline {
--radius-xs: calc(var(--radius) - 6px);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-brand: var(--brand);
--color-brand-foreground: var(--brand-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground antialiased;
}
}
```
---
# Typography
Source: https://desyne.dev/docs/typography
> Fonts, the text scale components use, and how to style headings and long-form content.
Components don't ship a font. They inherit `font-family` from your page and use
Tailwind's text scale, so the type system is whatever your app sets on ``.
For headings, body text, lists, inline code and long-form content, use the
[Typography](/docs/components/typography) component. It renders plain elements
with no `"use client"`, so it works in server components.
## Set a font
The examples, blocks and templates use [Inter](https://rsms.me/inter/) through
`next/font`, exposed as Tailwind's `--font-sans`:
```tsx title="app/layout.tsx"
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"], variable: "--font-sans" });
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
```css title="app/globals.css"
@theme inline {
--font-sans: var(--font-sans), ui-sans-serif, system-ui, sans-serif;
}
```
Any neutral sans works. Fonts with tabular figures (Inter, Geist, IBM Plex Sans) suit
data-heavy screens: tables and stats in the library use `tabular-nums`.
## The scale components use
| Where | Class | Size |
| ------------------------------------------------- | ------------------------- | -------- |
| Buttons, fields, menu items, table cells (`md`) | `text-sm` | 14px |
| `sm` controls, badges, descriptions, field errors | `text-xs` | 12px |
| `lg` controls | `text-base` | 16px |
| Dialog titles | `text-base font-semibold` | 16px |
| Card titles | `text-sm font-medium` | 14px |
| Keyboard shortcuts, code | `font-mono` | inherits |
Body copy sits at 14px in dense product UI. For reading-heavy pages, set a larger base
on the page container (`text-base`) and leave controls at their own sizes.
## Hierarchy without extra sizes
Most screens need only three levels: a title, body text and muted secondary text.
```tsx
Billing
Manage your plan, payment method and invoices.
```
* Use `text-muted-foreground` for secondary text instead of a smaller size.
* Use `tracking-tight` on headings 18px and larger.
* Use `text-balance` on headings and `text-pretty` on paragraphs to avoid orphans.
* Keep line length around 60–75 characters (`max-w-prose`) for long text.
## Labels and descriptions
Form text comes from `field.tsx`, so all fields match:
| Part | Style |
| ------------- | ---------------------------------------------------------------- |
| `Label` | `text-sm font-medium`, with a red `*` when the field is required |
| `Description` | `text-xs text-muted-foreground` |
| `FieldError` | `text-xs text-destructive` |
Change those once in `field.tsx` to restyle every form.
---
# Dark mode
Source: https://desyne.dev/docs/dark-mode
> Class-based dark mode with next-themes, a theme toggle, and how to preview or force a mode for part of a page.
Every token has a dark value under the `.dark` selector, and the tokens file declares a
class-based variant:
```css
@custom-variant dark (&:is(.dark *));
```
So dark mode is on wherever an ancestor has `class="dark"`. Components never check the
mode in JavaScript, which means no flash, no context and no re-render when it changes.
## Next.js with next-themes
[next-themes](https://github.com/pacocoursey/next-themes) toggles the class on ``,
follows the system setting and remembers the choice.
```bash
npm install next-themes
```
```tsx title="app/providers.tsx"
"use client";
import { ThemeProvider } from "next-themes";
export function Providers({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
```tsx title="app/layout.tsx"
import { Providers } from "./providers";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
`suppressHydrationWarning` is needed because next-themes sets the class before React
hydrates. `disableTransitionOnChange` stops every color transition from animating at once
when the mode flips.
## A theme toggle
```tsx title="components/theme-toggle.tsx"
"use client";
import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { ToggleButton } from "@/components/ui/toggle-button";
import { ToggleButtonGroup } from "@/components/ui/toggle-button-group";
export function ThemeToggle() {
const { theme = "system", setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return null; // the theme is unknown on the server
return (
setTheme(String([...keys][0]))}
>
);
}
```
For a single button, a `Button` with `variant="ghost" size="icon"` calling
`setTheme(resolvedTheme === "dark" ? "light" : "dark")` works too. Give it an
`aria-label`.
## Vite and other setups
Without next-themes, set the class yourself before first paint so there's no flash:
```html title="index.html"
```
Then toggle `document.documentElement.classList` and write `localStorage.theme` from
your toggle.
## Forcing a mode for part of a page
Because the variant matches descendants of `.dark`, you can render one region dark on a
light page. The final call-to-action on the home page does exactly this:
```tsx
Ship the next screen this afternoon.
```
Inside the section, `bg-background` and every component use dark tokens. To force light
inside a dark page, re-declare the light values on a class such as `.light` in your CSS.
## Writing dark-aware styles
* Prefer tokens (`bg-card`, `border-border`) over `dark:` overrides. Tokens already
switch.
* Use `dark:` for the few things tokens can't express, such as removing a shadow that
disappears on dark surfaces: `shadow-sm dark:shadow-none`.
* Images and screenshots need two sources. Toggle them with `dark:hidden` and
`hidden dark:block`.
* Set the browser's own UI to match with `color-scheme`:
```css
:root { color-scheme: light; }
.dark { color-scheme: dark; }
```
---
# Icons
Source: https://desyne.dev/docs/icons
> Lucide icons by default, automatic sizing inside components, and labelling icon-only controls.
Components use [Lucide](https://lucide.dev) (`lucide-react`) for their built-in icons
(chevrons, checks, close buttons) and the examples use it throughout. Any React icon
set that renders an `