Theming
Theme Ardo in app/root.tsx with hues, saturation, callout colors, fonts, and tokens, use imported assets for the logo and favicon, and add Vanilla Extract or Tailwind CSS for your own components.
On this page
Ardo's default theme is complete, and you change it where you build the rest of the site: in app/root.tsx, with React props and imported files. Start with a few hues and go deeper when you need to:
- The
themeprop: hues, saturation, callout colors, fonts, and any design token. - Logo and favicon: imported files, so Vite fingerprints them.
- CSS variables and Vanilla Extract: your own stylesheets, with Ardo's tokens.
- Tailwind CSS: utility classes for your own pages and components.
To replace the header, sidebar, or footer with your own React components, see Layout & Navigation.
Colors, Logo, and Favicon
app/root.tsx:
import { ArdoRoot } from "ardo/ui"
import logo from "./assets/logo.svg"
export default function Root() {
return (
<ArdoRoot theme={{ color: "blue", accent: "teal", neutral: "slate" }} logo={logo} icon={logo} />
)
}
theme sets three hues: color for links, buttons, and focus rings, accent for secondary highlights and code surfaces, and neutral for backgrounds, borders, text, header, and sidebar. Ardo derives every other color from them, in light and dark mode.
logo and icon take imported files. Vite gives them fingerprinted URLs, so browsers pick up a new logo after the next deploy instead of serving a cached one. Here the logo doubles as the favicon; pass a square mark to icon when your logo is a wordmark.
- Light and dark logos:
logo={{ light: logoLight, dark: logoDark }}. - iOS home screens:
appleTouchIcontakes a 180×180 PNG. - Without
icon: Ardo links its generated owl icons. Old clients that request/favicon.icodirectly get the owl too, unless you put your ownfavicon.icoinpublic/.
Ardo's config file holds no colors or assets. It covers what the build needs: title, base path, versions, and the API reference.
Hues and Presets
color, accent, and neutral take a preset name or an OKLCH hue angle from 0 to 360:
<ArdoRoot theme={{ color: 240, accent: 170, neutral: 260 }} />
Presets: berry (the default), red, orange, amber, green, teal, blue, indigo, purple, pink, slate, and gray. gray has no hue at all, and slate is a muted blue.
Without accent, Ardo picks a complement of color. Without neutral, the chrome takes the brand hue, faintly. neutral: "gray" gives chrome without any tint.
Saturation
chroma sets how saturated the hue-derived colors are. 1 keeps Ardo's palette, lower values calm it down, and 0 turns a role gray. A number applies to color and accent; the object form sets each role, neutral included:
<ArdoRoot
theme={{
color: "green",
accent: "orange",
neutral: 95,
chroma: { color: 0.4, accent: 2, neutral: 1.6 },
}}
/>
Values above 1 push some hues, such as teal, outside the sRGB gamut, so check link contrast when you raise color.
Callout Colors
Callouts keep their own hues, so a warning still reads as a warning next to any brand. When a callout hue clashes with your accent, move it with status. Ardo derives text, surface, and border for both color schemes:
<ArdoRoot theme={{ accent: 38, status: { warning: 72 } }} />
status takes tip, warning, danger (for caution), info (for important), and note, each as a preset or a hue. Every preset keeps callout text at 4.5:1 contrast or better.
Fonts
Load a font the way you load any other dependency, then name it in fonts. With Fontsource:
pnpm add @fontsource-variable/inter
import "@fontsource-variable/inter"
import { ArdoRoot } from "ardo/ui"
export default function Root() {
return <ArdoRoot theme={{ fonts: { body: '"Inter Variable", system-ui, sans-serif' } }} />
}
Headings follow body unless you set heading. mono sets the font of code blocks and inline code.
Design Tokens
tokens sets any of Ardo's design tokens for both color schemes. light and dark set them for one scheme only, which is where most colors belong:
import { ArdoRoot, type ArdoThemeProps } from "ardo/ui"
const theme: ArdoThemeProps = {
tokens: {
radius: { sm: "0px", base: "0px", lg: "0px" },
color: {
shadowSm: "0 0 0 0 transparent",
shadowMd: "0 0 0 0 transparent",
shadowLg: "0 0 0 0 transparent",
},
},
light: { color: { bg: "#fbfbf8", text: "#101010", brand: "#0038ff", brandGradient: "#0038ff" } },
dark: { color: { bg: "#101010", text: "#fbfbf8" } },
}
export default function Root() {
return <ArdoRoot theme={theme} />
}
TypeScript flags a token name Ardo doesn't know, and Ardo rejects it at render time, so a typo can't slip through quietly. shadowSm, shadowMd, shadowLg, and codeShadow are full box-shadow values; use 0 0 0 0 transparent for no shadow, because none breaks values that combine several layers. brandGradient also takes a plain color.
Ardo's own documentation uses all of it for the owl palette from its logo: olive brand, terracotta accent, a barely warm off-white, and a mustard warning:
const theme: ArdoThemeProps = {
color: 123,
accent: 38,
neutral: 95,
chroma: { color: 0.4, accent: 2, neutral: 1.6 },
status: { warning: 72 },
light: { color: { codeBg: "oklch(0.973 0.006 95)", codeBorder: "oklch(0.9 0.009 95)" } },
dark: { color: { codeBg: "oklch(0.205 0.01 95)", codeBorder: "oklch(0.31 0.014 95)" } },
}
Your Own Root
ArdoRoot renders the theme for you. A root that composes its own shell, for example from ArdoProvider and ArdoLayout, renders <ArdoTheme> with the same props, gives ArdoHeader its logo, and passes the favicon to ArdoRootLayout, which renders the HTML shell:
import type { ReactNode } from "ardo/react"
import { ArdoRootLayout, ArdoTheme } from "ardo/ui"
import icon from "./assets/icon.svg"
import { SiteShell } from "./site-shell"
export function Layout({ children }: { children: ReactNode }) {
return <ArdoRootLayout icon={icon}>{children}</ArdoRootLayout>
}
export default function Root() {
return (
<>
<ArdoTheme color="blue" neutral="slate" />
<SiteShell />
</>
)
}
CSS Variables
Every token is also a CSS variable, such as --ardo-hue-brand or --ardo-color-bg, so a plain stylesheet can set them too. Ardo ships its defaults in the ardo cascade layer, and your unlayered CSS beats them. A color set on :root therefore applies in dark mode as well; scope colors to one scheme:
:root:not(.dark) {
--ardo-color-bg: oklch(0.99 0.004 95);
}
.dark {
--ardo-color-bg: oklch(0.17 0.006 95);
}
Hue, chroma, layout, and typography variables are safe on :root. These are the ones most sites touch, with their defaults:
:root {
/* Hues */
--ardo-hue-brand: 356;
--ardo-hue-accent: 230;
--ardo-hue-neutral: 356;
--ardo-hue-tip: 155;
--ardo-hue-warning: 45;
--ardo-hue-danger: 25;
--ardo-hue-info: 220;
--ardo-hue-note: 270;
/* Chroma multipliers: 1 is the default saturation, 0 is gray */
--ardo-chroma-brand: 1;
--ardo-chroma-accent: 1;
--ardo-chroma-neutral: 1;
/* Layout */
--ardo-layout-sidebarWidth: 20rem;
--ardo-layout-tocWidth: 15rem;
--ardo-layout-contentMaxWidth: 36em; /* em: the measure follows each element's font size */
--ardo-layout-pageMaxWidth: 69rem; /* header, hero, features, footer on bare pages */
--ardo-layout-headerHeight: 3.5rem;
/* Typography */
--ardo-font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", ...;
--ardo-font-familyHeading: var(--ardo-font-family);
--ardo-font-mono: ui-monospace, SFMono-Regular, ...;
--ardo-font-lineHeight: 1.65;
/* Border radius */
--ardo-radius-sm: 0.25rem;
--ardo-radius-base: 0.5rem;
--ardo-radius-lg: 0.75rem;
}
Vanilla Extract Tokens
Ardo's UI is built with Vanilla Extract. All design tokens are available as type-safe imports from ardo/theme, which means you get full autocomplete and compile-time errors when referencing tokens.
The Ardo CLI includes the Vanilla Extract plugin, so .css.ts files work without extra Vite configuration. Install the styling API in your docs package before importing it:
pnpm add @vanilla-extract/css
Using tokens in your styles
Create a .css.ts file anywhere in your project:
// styles/custom.css.ts
import { style, globalStyle } from "@vanilla-extract/css"
import { vars } from "ardo/theme"
export const highlight = style({
background: vars.color.brandSubtle,
borderLeft: `3px solid ${vars.color.brand}`,
padding: "16px",
borderRadius: vars.radius.base,
})
// Override Ardo's default styles
globalStyle(".ardo-content a", {
color: vars.color.brand,
textDecoration: "underline",
})
Stylesheet ownership and cascade
Ardo loads ardo/ui/styles.css for every site, after the CSS your root module imports. It includes the complete UI, reset, and theme styles in the named ardo cascade layer. Component imports do not load another copy of the stylesheet, and neither does importing it yourself.
Normal unlayered app CSS takes precedence over Ardo's normal layered rules, regardless of stylesheet load order. You can override .ardo-footer directly without adding extra selectors. If your app uses layers, declare their order in the first stylesheet your root module imports, for example @layer ardo, app;.
Stable override hooks
Ardo's generated Vanilla Extract class names are implementation details. Use the stable hook classes for narrowly scoped overrides:
| Region | Classes |
|---|---|
| Header and navigation | .ardo-header, .ardo-nav, .ardo-mobile-panel |
| Sidebar | .ardo-sidebar, .ardo-sidebar-tabs, .ardo-sidebar-panel, .ardo-sidebar-header, .ardo-sidebar-nav |
| Document grid and optional right rail | .ardo-doc-page, .ardo-page-rail, .ardo-page-rail-section |
| Article and introduction | .ardo-content, .ardo-content-header, .ardo-content-title, .ardo-content-description, .ardo-content-body |
| Page outline | .ardo-toc |
| Code sheets | .ardo-code-block, .ardo-code-title, .ardo-code-wrapper, .ardo-copy-button |
| Footer | .ardo-footer |
These classes are override anchors, not a replacement styling API. Prefer tokens and component props first, then use the hooks for project-specific adjustments that need a stable selector.
Inside an existing design system, scope overrides to the site's own wrapper. A site that already offers section navigation can hide .ardo-sidebar-tabs and keep the sidebar's reading links, as long as every section stays reachable on desktop and mobile. Put theme variables that also need to reach body-level portals, such as search, on ArdoRootLayout's className.
Available token categories
The vars object provides tokens for:
vars.color— Brand colors, backgrounds, text, borders, shadows, and semantic colors (tip, warning, danger, info, note)vars.hue— Brand, accent, neutral, and callout hues the default color ramp readsvars.chroma— Brand, accent, and neutral chroma multipliers for the same rampvars.layout— Sidebar width, TOC width, content max width, page max width (the shared frame for header, hero, features, and footer on bare pages), header heightvars.transition— Fast, base, and slow transition durationsvars.font— Body, heading, and monospace families, base size, and line heightvars.fontSize— The type scale fromxsto4xlvars.space— The spacing scale fromxsto4xlvars.radius—sm,base,lg,xl, andfullborder radii
Tailwind CSS
Tailwind CSS v4 is an optional integration for custom landing pages, examples, component demos, and MDX content. Retain your existing styling setup when adopting Ardo.
Ardo handles the framework UI with Vanilla Extract. Tailwind is for your content, so you can style without leaving React or adding a second theme system.
Setup
To add Tailwind, install tailwindcss and @tailwindcss/vite as development dependencies and configure:
@tailwindcss/vitein thepluginsofvite.config.tstailwindcssas a devDependencyapp/app.csswith Tailwind's theme and utility layers
@layer theme, base, ardo, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);
/* Follow Ardo's theme toggle, which sets the .dark class on <html>. */
@custom-variant dark (&:where(.dark, .dark *));
Import this file in app/root.tsx. Ardo loads its own stylesheet after it, so the layer order declared here applies: Ardo ships its styles in the ardo cascade layer, and naming it before components and utilities lets your utility classes win over Ardo's content styles.
Tailwind's dark: variant follows the operating system by default. The @custom-variant line makes it follow Ardo's theme toggle instead, so dark: classes switch together with the rest of the site.
Examples
Callout box in MDX
Drop this into any .mdx file — it works immediately:
<div
className="flex gap-3 rounded-lg border border-blue-200 bg-blue-50 p-4 dark:border-blue-800 dark:bg-blue-950"
>
<span className="text-xl">💡</span>
<div>
<p className="font-semibold text-blue-900 dark:text-blue-100">Good to know</p>
<p className="text-sm text-blue-700 dark:text-blue-300">
Tailwind classes work in MDX files, TSX components, and anywhere else in your Ardo project.
</p>
</div>
</div>
Custom hero section
Build a landing page header without writing a single line of CSS:
import { ArdoLink } from "ardo/ui"
export default function Home() {
return (
<div className="flex min-h-[60vh] flex-col items-center justify-center gap-6 text-center">
<h1 className="text-5xl font-bold tracking-tight text-gray-900 dark:text-white">
My Project
</h1>
<p className="max-w-2xl text-lg text-gray-600 dark:text-gray-400">
A short description that tells visitors exactly what this is and why they should care.
</p>
<div className="flex gap-3">
<ArdoLink
to="/guide/getting-started"
className="rounded-lg bg-blue-600 px-5 py-2.5 text-sm font-medium text-white hover:bg-blue-700"
>
Get Started
</ArdoLink>
<a
href="https://github.com/your/repo"
className="rounded-lg border border-gray-300 px-5 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
>
GitHub
</a>
</div>
</div>
)
}
Why no Preflight?
Ardo provides its own base styles for documentation content — typography, headings, links, lists, and code blocks. Tailwind's Preflight reset would conflict with these styles and break the default look.
If you're building a heavily custom site where you control all styling, you can opt in to Preflight by adding one line to app/app.css:
@layer theme, base, ardo, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/preflight.css" layer(base);
@import "tailwindcss/utilities.css" layer(utilities);
Ardo's content styles set list markers, table borders, code styles, and spacing explicitly, so Markdown content stays readable even when a host reset clears browser defaults. Because the ardo layer sits after base, Preflight cannot undo Ardo's styles, and because it sits before utilities, your utility classes still win where you apply them.