suiss/ui
  • Docs
  • Components
  • Patterns
  • Interactions
  • Charts
  • Blocks

Search

Search documentation

GitHubGet started
DocsComponentsPatternsInteractionsChartsBlocks
Docs4
Get Started
  • Introduction
  • Installation
  • Theming
  • CLI

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 inline block 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.

app/globals.css
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "@fontsource-variable/inter";

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

:root {
  --radius: 0.625rem;
  --background: oklch(1 0 0);
  --foreground: oklch(0.2316 0.0038 286.09);
  --primary: oklch(0.5629 0.1933 256.16);
  --primary-foreground: oklch(1 0 0);
}

.dark {
  --background: oklch(0 0 0);
  --foreground: oklch(0.9707 0.0026 286.29);
  --primary: oklch(0.669 0.1808 251.84);
  --primary-foreground: oklch(1 0 0);
}

@theme inline {
  --font-sans: -apple-system, BlinkMacSystemFont, "Inter Variable", sans-serif;
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
}

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:

$ pnpm dlx shadcn@latest add @esuiss/theme-suiss

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.

<div className="rounded-lg border border-border bg-muted p-4">
  <p className="text-foreground">Title</p>
  <p className="text-muted-foreground">Supporting text</p>
</div>

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:

StatePattern
Hover on a filled elementhover:bg-primary/80, hover:bg-secondary/80
Hover on a transparent elementhover:bg-accent hover:text-accent-foreground
Highlighted menu itemfocus:bg-accent focus:text-accent-foreground
Tinted backgroundbg-primary/10, bg-destructive/10
Keyboard focusfocus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50
Invalidaria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20
Disableddisabled:opacity-50 or data-disabled:opacity-50

The button component uses them this way:

"bg-primary text-primary-foreground hover:bg-primary/80 disabled:opacity-50"

Variables

Values are from the default stylesheet.

Base

VariableUsed forLightDark
backgroundThe pageoklch(1 0 0)oklch(0 0 0)
foregroundText on the pageoklch(0.2316 0.0038 286.09)oklch(0.9707 0.0026 286.29)
cardCards and raised panelsoklch(1 0 0)oklch(0.2316 0.0038 286.09)
card-foregroundText on cardsoklch(0.2316 0.0038 286.09)oklch(0.9707 0.0026 286.29)
popoverMenus, popovers, dialogs, sheets and toastsoklch(1 0 0)oklch(0.2316 0.0038 286.09)
popover-foregroundText on popoversoklch(0.2316 0.0038 286.09)oklch(0.9707 0.0026 286.29)

Primary and secondary

VariableUsed forLightDark
primaryThe brand color: default buttons, checked controls, linksoklch(0.5629 0.1933 256.16)oklch(0.669 0.1808 251.84)
primary-foregroundText on primaryoklch(1 0 0)oklch(1 0 0)
secondaryFills for secondary buttons, fields and tracksoklch(0 0 0 / 4%)oklch(1 0 0 / 10%)
secondary-foregroundText on secondaryoklch(0.2316 0.0038 286.09)oklch(0.9707 0.0026 286.29)

Muted and accent

VariableUsed forLightDark
mutedQuiet backgrounds: filled cards, skeletons, grouped listsoklch(0.9707 0.0026 286.29)oklch(0.2006 0.002 286.21)
muted-foregroundSecondary text, descriptions and placeholdersoklch(0.5399 0.0077 286.13)oklch(0.7105 0.0072 286.2)
accentHovered, highlighted and selected itemsoklch(0 0 0 / 6%)oklch(1 0 0 / 10%)
accent-foregroundText on accentoklch(0.2316 0.0038 286.09)oklch(0.9707 0.0026 286.29)

Destructive, borders and focus

VariableUsed forLightDark
destructiveDestructive actions and errorsoklch(0.5329 0.2255 27.27)oklch(0.7033 0.2236 28.29)
borderLines, dividers and bordersoklch(0.8652 0.0068 286.24)oklch(0.3802 0.005 286.15)
inputStronger borders and the unchecked switch trackoklch(0.6217 0.0074 286.17)oklch(0.5399 0.0077 286.13)
ringThe keyboard focus outlineoklch(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.

VariableLightDark
chart-1oklch(0.6028 0.2177 257.42)oklch(0.6243 0.2056 255.49)
chart-2oklch(0.6998 0.1109 212.72)oklch(0.7706 0.1181 211.98)
chart-3oklch(0.5295 0.191 278.34)oklch(0.5564 0.2032 278.15)
chart-4oklch(0.7652 0.1752 62.57)oklch(0.7824 0.1711 67.22)
chart-5oklch(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.

VariableLightDark
sidebaroklch(0.9707 0.0026 286.29)oklch(0.2006 0.002 286.21)
sidebar-foregroundoklch(0.2316 0.0038 286.09)oklch(0.9707 0.0026 286.29)
sidebar-primaryoklch(0.5629 0.1933 256.16)oklch(0.669 0.1808 251.84)
sidebar-primary-foregroundoklch(1 0 0)oklch(1 0 0)
sidebar-accentoklch(0 0 0 / 6%)oklch(1 0 0 / 10%)
sidebar-accent-foregroundoklch(0.2316 0.0038 286.09)oklch(0.9707 0.0026 286.29)
sidebar-borderoklch(0.8652 0.0068 286.24)oklch(0.3802 0.005 286.15)
sidebar-ringoklch(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.

PlatformTheme colorPalette color
Uniwind and NativeWindbg-muted, text-muted-foregroundtext-green-600 dark:text-green-400
StyleSheetcolors.muted, colors.mutedForeground from useTheme()palette.green600
SwiftUIEsuissColor.muted, EsuissColor.mutedForegroundPalette.green600
ComposeEsuissTheme.colors.muted, EsuissTheme.colors.mutedForegroundPalette.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.

PlatformText sizeShadow
Uniwind and NativeWindtext-smshadow-md
StyleSheetfontSize.smshadow.md
SwiftUI.textStyle(.sm).shadow(.md)
ComposeEsuissTheme.typography.smModifier.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.

ClassValue
rounded-xscalc(var(--radius) * 0.3)
rounded-smcalc(var(--radius) * 0.5)
rounded-mdcalc(var(--radius) * 0.8)
rounded-lgvar(--radius)
rounded-xlcalc(var(--radius) * 1.2)
rounded-2xlcalc(var(--radius) * 1.8)
rounded-3xlcalc(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 :root and .dark blocks with theirs and keep the @theme inline block.
  • Add a color. Define it under :root and .dark, then map it in @theme inline, for example --color-brand: var(--brand);. Classes such as bg-brand and text-brand become available.
  • Change the font. Set --font-sans in @theme inline.
  • Change the corner radius. Set --radius under :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.

$ pnpm dlx shadcn@latest add @esuiss/theme

Or with the suiss/ui CLI:

$ pnpm dlx @esuiss/ui add theme

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.

app/layout.tsx
import { ThemeProvider, ThemeScript } from "@/components/ui/theme"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <ThemeScript />
      </head>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}

With Vite, wrap your app in ThemeProvider and put the same script in index.html, before your app script:

index.html
<script>
  (()=>{try{var t=localStorage.getItem("theme")||"system";var d=t==="dark"||(t==="system"&&matchMedia("(prefers-color-scheme: dark)").matches);var r=document.documentElement;r.classList.toggle("dark",d);r.style.colorScheme=d?"dark":"light"}catch(e){}})()
</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.

<ThemeToggle effect="circle" origin="top-right" />
<ThemeToggle effect="rectangle" origin="bottom-up" blur />

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.

const { theme, setTheme } = useTheme()
setTheme("system", { effect: "circle", origin: "top-center" })
NextCLIPreviousInstallation

On this page

  1. 01How the theme works
  2. 02The theme item
  3. 03Using variables
  4. 04Interaction states
  5. 05Variables
    • Base
    • Primary and secondary
    • Muted and accent
    • Destructive, borders and focus
    • Charts
    • Sidebar
    • Status and other colors
  6. 06Mobile
  7. 07Radius
  8. 08Customize
  9. 09Dark mode