suiss/ui
Theming
The standard theme variables, written in oklch for light and dark, and exposed as Tailwind classes such as bg-background and text-foreground.
How the theme works
Your stylesheet holds the whole theme. init from the suiss/ui CLI writes it to app/globals.css (Next.js) or src/index.css (Vite), and projects that use the registry CLI already have one. It has four parts:
- Color values under
:root(light) and.dark(dark). - The base
--radius. - The font, set as
--font-sans. - An
@theme inlineblock that maps each value to a Tailwind name, such as--color-background: var(--background);.
It imports tw-animate-css for the enter and exit animations, and shadcn/tailwind.css for the shared variants (data-open, data-checked and the like), the accordion keyframes and the no-scrollbar, scroll-fade-* and shimmer utilities. Text sizes, shadows and spacing are the Tailwind defaults.
The variable names are the common theme variables used across Tailwind component libraries, so a theme written for one of them works here, and ours works there. The values are our own: a white and true-black canvas, gray text in the system style and a blue primary color.
The theme item
Adding components with the registry CLI keeps the colors your project already has. To use the suiss/ui values instead, add the theme item:
The theme item is the same for every style and primitive base. It writes every light and dark value under :root and .dark, including --radius, the chart colors and the sidebar colors, and adds the radius scale and the font stack to @theme inline. It also installs @fontsource-variable/inter and imports it from your stylesheet. Projects set up with npx @esuiss/ui init already have all of it.
Using variables
Every color becomes a Tailwind color. Use it with any color utility.
Colors come in pairs: a background and the text that sits on it. For example, bg-primary takes text-primary-foreground, and bg-card takes text-card-foreground. A variable without the -foreground suffix is the background; the one with it is the text.
Interaction states
There is no separate variable per state. States use the same variables with an opacity, and disabled elements are dimmed:
| State | Pattern |
|---|---|
| Hover on a filled element | hover:bg-primary/80, hover:bg-secondary/80 |
| Hover on a transparent element | hover:bg-accent hover:text-accent-foreground |
| Highlighted menu item | focus:bg-accent focus:text-accent-foreground |
| Tinted background | bg-primary/10, bg-destructive/10 |
| Keyboard focus | focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 |
| Invalid | aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 |
| Disabled | disabled:opacity-50 or data-disabled:opacity-50 |
The button component uses them this way:
Variables
Values are from the default stylesheet.
Base
| Variable | Used for | Light | Dark |
|---|---|---|---|
background | The page | oklch(1 0 0) | oklch(0 0 0) |
foreground | Text on the page | oklch(0.2316 0.0038 286.09) | oklch(0.9707 0.0026 286.29) |
card | Cards and raised panels | oklch(1 0 0) | oklch(0.2316 0.0038 286.09) |
card-foreground | Text on cards | oklch(0.2316 0.0038 286.09) | oklch(0.9707 0.0026 286.29) |
popover | Menus, popovers, dialogs, sheets and toasts | oklch(1 0 0) | oklch(0.2316 0.0038 286.09) |
popover-foreground | Text on popovers | oklch(0.2316 0.0038 286.09) | oklch(0.9707 0.0026 286.29) |
Primary and secondary
| Variable | Used for | Light | Dark |
|---|---|---|---|
primary | The brand color: default buttons, checked controls, links | oklch(0.5629 0.1933 256.16) | oklch(0.669 0.1808 251.84) |
primary-foreground | Text on primary | oklch(1 0 0) | oklch(1 0 0) |
secondary | Fills for secondary buttons, fields and tracks | oklch(0 0 0 / 4%) | oklch(1 0 0 / 10%) |
secondary-foreground | Text on secondary | oklch(0.2316 0.0038 286.09) | oklch(0.9707 0.0026 286.29) |
Muted and accent
| Variable | Used for | Light | Dark |
|---|---|---|---|
muted | Quiet backgrounds: filled cards, skeletons, grouped lists | oklch(0.9707 0.0026 286.29) | oklch(0.2006 0.002 286.21) |
muted-foreground | Secondary text, descriptions and placeholders | oklch(0.5399 0.0077 286.13) | oklch(0.7105 0.0072 286.2) |
accent | Hovered, highlighted and selected items | oklch(0 0 0 / 6%) | oklch(1 0 0 / 10%) |
accent-foreground | Text on accent | oklch(0.2316 0.0038 286.09) | oklch(0.9707 0.0026 286.29) |
Destructive, borders and focus
| Variable | Used for | Light | Dark |
|---|---|---|---|
destructive | Destructive actions and errors | oklch(0.5329 0.2255 27.27) | oklch(0.7033 0.2236 28.29) |
border | Lines, dividers and borders | oklch(0.8652 0.0068 286.24) | oklch(0.3802 0.005 286.15) |
input | Stronger borders and the unchecked switch track | oklch(0.6217 0.0074 286.17) | oklch(0.5399 0.0077 286.13) |
ring | The keyboard focus outline | oklch(0.5629 0.1933 256.16) | oklch(0.5629 0.1933 256.16) |
Charts
Series colors. Charts reference them as var(--chart-1) to var(--chart-5) in their config.
| Variable | Light | Dark |
|---|---|---|
chart-1 | oklch(0.6028 0.2177 257.42) | oklch(0.6243 0.2056 255.49) |
chart-2 | oklch(0.6998 0.1109 212.72) | oklch(0.7706 0.1181 211.98) |
chart-3 | oklch(0.5295 0.191 278.34) | oklch(0.5564 0.2032 278.15) |
chart-4 | oklch(0.7652 0.1752 62.57) | oklch(0.7824 0.1711 67.22) |
chart-5 | oklch(0.6483 0.0073 286.18) | oklch(0.6483 0.0073 286.18) |
Sidebar
The sidebar component has its own set, so it can differ from the page.
| Variable | Light | Dark |
|---|---|---|
sidebar | oklch(0.9707 0.0026 286.29) | oklch(0.2006 0.002 286.21) |
sidebar-foreground | oklch(0.2316 0.0038 286.09) | oklch(0.9707 0.0026 286.29) |
sidebar-primary | oklch(0.5629 0.1933 256.16) | oklch(0.669 0.1808 251.84) |
sidebar-primary-foreground | oklch(1 0 0) | oklch(1 0 0) |
sidebar-accent | oklch(0 0 0 / 6%) | oklch(1 0 0 / 10%) |
sidebar-accent-foreground | oklch(0.2316 0.0038 286.09) | oklch(0.9707 0.0026 286.29) |
sidebar-border | oklch(0.8652 0.0068 286.24) | oklch(0.3802 0.005 286.15) |
sidebar-ring | oklch(0.5629 0.1933 256.16) | oklch(0.5629 0.1933 256.16) |
Status and other colors
Success, warning and decorative colors use the Tailwind palette directly, with a lighter shade in dark mode for text, for example text-green-600 dark:text-green-400 and bg-green-500/10. The dimmed layer behind dialogs, sheets and drawers is bg-black/50.
Mobile
React Native, SwiftUI and Compose use the same variables with the same light and dark values, generated from one token file.
| Platform | Theme color | Palette color |
|---|---|---|
| Uniwind and NativeWind | bg-muted, text-muted-foreground | text-green-600 dark:text-green-400 |
| StyleSheet | colors.muted, colors.mutedForeground from useTheme() | palette.green600 |
| SwiftUI | EsuissColor.muted, EsuissColor.mutedForeground | Palette.green600 |
| Compose | EsuissTheme.colors.muted, EsuissTheme.colors.mutedForeground | Palette.green600 |
Pressed elements fade to 80% opacity and disabled elements to 50%, on every platform.
Text sizes and shadows are the Tailwind defaults on every platform: xs is 12 points on a 16-point line, up to 3xl at 30 on 36, and shadow-md and shadow-lg keep both of their layers. SwiftUI and Compose name 2xl and 3xl as xl2 and xl3.
| Platform | Text size | Shadow |
|---|---|---|
| Uniwind and NativeWind | text-sm | shadow-md |
| StyleSheet | fontSize.sm | shadow.md |
| SwiftUI | .textStyle(.sm) | .shadow(.md) |
| Compose | EsuissTheme.typography.sm | Modifier.shadow(EsuissTheme.shadows.md, shape) |
Focused fields in Uniwind and NativeWind draw the same border-ring ring-3 ring-ring/50 ring as the web. SwiftUI and Compose keep the system focus.
Radius
--radius is 0.625rem by default. Every rounded component scales from it.
| Class | Value |
|---|---|
rounded-xs | calc(var(--radius) * 0.3) |
rounded-sm | calc(var(--radius) * 0.5) |
rounded-md | calc(var(--radius) * 0.8) |
rounded-lg | var(--radius) |
rounded-xl | calc(var(--radius) * 1.2) |
rounded-2xl | calc(var(--radius) * 1.8) |
rounded-3xl | calc(var(--radius) * 2.8) |
Customize
- Change a color. Edit its value under
:root(light) and.dark(dark). - Use another theme. Themes written with the same variable names drop in: replace the
:rootand.darkblocks with theirs and keep the@theme inlineblock. - Add a color. Define it under
:rootand.dark, then map it in@theme inline, for example--color-brand: var(--brand);. Classes such asbg-brandandtext-brandbecome available. - Change the font. Set
--font-sansin@theme inline. - Change the corner radius. Set
--radiusunder:root.
Dark mode
Dark mode is enabled by the dark class on <html>. Every token switches with it.
For a theme switch, add the theme component. It is not added by add --all, so projects that stay in one appearance never get it.
Or with the suiss/ui CLI:
It gives you ThemeProvider, useTheme, ThemeToggle and ThemeScript. Adding it also appends the keyframes for the animated theme switch to your stylesheet. The provider remembers the choice (light, dark or system) and follows the system setting when it is system. ThemeScript sets the class before the page paints, so there is no flash of the wrong theme.
With Vite, wrap your app in ThemeProvider and put the same script in index.html, before your app script:
ThemeToggle switches with an animated reveal built on the View Transitions API. Choose the effect (circle, rectangle, polygon or circle-blur), where it starts with origin, and add blur for a softer edge.
To build your own control, use setTheme from useTheme. Pass a transition as the second argument to animate it, or leave it out to switch instantly. Browsers without View Transitions, and visitors who prefer reduced motion, switch instantly either way.