Skip to content

Custom Theme

Override a color, swap a component, or build an entirely new theme — it's your site.

Custom Theme

Ardo's default theme is intentionally not a black box. Nothing is locked down, nothing requires workarounds. You have four levels of customization, and you can mix them freely:

  1. CSS variables — Change colors, spacing, and typography without touching components
  2. Vanilla Extract tokens — Type-safe styling with ardo/theme imports and .css.ts files
  3. Component overrides — Replace individual pieces (header, sidebar, footer) with your own React components
  4. Full custom theme — Build your entire layout from scratch, using Ardo's runtime hooks for data

Start simple. Go deeper when you need to.

Brand Color

The fastest way to customize Ardo's look. Every color in the theme derives from a single OKLCH hue value. Change the hue and all backgrounds, text, borders, and brand colors update together — keeping the carefully tuned lightness and contrast relationships intact.

Create a .css.ts file:

// styles/brand.css.ts
import { applyBrandTheme } from "ardo/theme"

applyBrandTheme(240) // deep blue

Import it in your root.tsx after Ardo's styles:

import "ardo/ui/styles.css"
import "./styles/brand.css"

The default hue is 170 (teal). Some starting points:

HueColor
170Teal (default)
240Blue
260Indigo
290Purple
330Pink
25Red
45Orange
130Green

For full control over individual tokens, use createTheme() instead — it returns the complete { light, dark } token sets for a given hue, which you can modify before passing to createGlobalTheme.

CSS Variables

The fastest way to make the theme yours. Override a few CSS custom properties and the entire site updates:

Create a custom CSS file:

/* styles/custom.css */
:root {
  --ardo-color-brand: oklch(0.6 0.2 260);
  --ardo-color-brandLight: oklch(0.7 0.15 260);
  --ardo-color-brandDark: oklch(0.5 0.25 260);
}

Import it in your app:

import "./styles/custom.css"
import "ardo/ui/styles.css"

Available Variables

Here's the full set of variables you can override. You don't need to set all of them — just the ones you want to change:

:root {
  /* Brand colors */
  --ardo-color-brand: oklch(0.62 0.19 260);
  --ardo-color-brandLight: oklch(0.7 0.14 260);
  --ardo-color-brandDark: oklch(0.55 0.24 260);

  /* Background colors */
  --ardo-color-bg: oklch(1 0 0);
  --ardo-color-bgSoft: oklch(0.97 0.005 260);
  --ardo-color-bgMute: oklch(0.95 0.008 260);

  /* Text colors */
  --ardo-color-text: oklch(0.25 0.02 260);
  --ardo-color-textLight: oklch(0.4 0.02 260);
  --ardo-color-textLighter: oklch(0.5 0.02 260);

  /* Layout */
  --ardo-layout-sidebarWidth: 280px;
  --ardo-layout-tocWidth: 240px;
  --ardo-layout-contentMaxWidth: 800px;
  --ardo-layout-headerHeight: 64px;

  /* Typography */
  --ardo-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", ...;
  --ardo-font-mono: ui-monospace, SFMono-Regular, ...;

  /* Border radius */
  --ardo-radius-base: 8px;
  --ardo-radius-sm: 4px;
}

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() Vite plugin automatically includes the Vanilla Extract plugin, so .css.ts files work in your project without extra configuration.

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",
})

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.layout — Sidebar width, TOC width, content max width, header height
  • vars.transition — Fast, base, and slow transition durations
  • vars.font — Font family and monospace family
  • vars.radius — Small, base, and large border radii

Component Overrides

When CSS isn't enough, replace individual components with your own. Since everything in Ardo is a React component, this works exactly how you'd expect — write a component, use it in your layout:

// components/MyHeader.tsx
import { useArdoConfig } from "ardo/runtime"

export function MyHeader() {
  const config = useArdoConfig()

  return (
    <header className="my-custom-header">
      <h1>{config.title}</h1>
      {/* Your custom header content */}
    </header>
  )
}

Then use it in your layout. Mix your custom components with Ardo's defaults — replace the pieces you want, keep the rest:

// routes/__root.tsx
import { MyHeader } from "../components/MyHeader"
import { ArdoSidebar, ArdoFooter } from "ardo/ui"

export function Layout({ children }) {
  return (
    <div>
      <MyHeader />
      <ArdoSidebar />
      <main>{children}</main>
      <ArdoFooter />
    </div>
  )
}

Runtime Hooks

When building custom components, you'll need access to site configuration, sidebar data, and page metadata. Ardo provides React hooks for all of it:

useArdoConfig

import { useArdoConfig } from "ardo/runtime"

function MyComponent() {
  const config = useArdoConfig()
  return <h1>{config.title}</h1>
}

useArdoSiteConfig

Access cross-cutting content settings (editLink, lastUpdated, TOC label) set on <ArdoRoot>:

import { useArdoSiteConfig } from "ardo/runtime"

function MyEditLink() {
  const { editLink } = useArdoSiteConfig()
  if (!editLink) return null
  return <a href={editLink.pattern}>{editLink.text ?? "Edit this page"}</a>
}

useArdoSidebar

import { useArdoSidebar } from "ardo/runtime"

function MySidebar() {
  const sidebar = useArdoSidebar()
  // Render sidebar items
}

useArdoPageData

import { useArdoPageData } from "ardo/runtime"

function MyContent() {
  const pageData = useArdoPageData()
  return (
    <article>
      <h1>{pageData?.title}</h1>
      {/* content */}
    </article>
  )
}

useArdoTOC

import { useArdoTOC } from "ardo/runtime"

function MyTOC() {
  const toc = useArdoTOC()
  return (
    <nav>
      {toc.map((item) => (
        <a href={`#${item.id}`}>{item.text}</a>
      ))}
    </nav>
  )
}

Full Custom Theme

If you need complete control, you can build your own theme from scratch. Create your components, export them, and use them in your app. Ardo's runtime hooks give you all the data you need — you just decide how to render it:

// theme/index.tsx
export { MyLayout as Layout } from "./Layout"
export { MyHeader as Header } from "./Header"
export { MySidebar as Sidebar } from "./Sidebar"
export { MyTOC as TOC } from "./TOC"
export { MyContent as Content } from "./Content"
export { MyFooter as Footer } from "./Footer"

Use your custom theme components in root.tsx and you have a fully custom documentation site — with all of Ardo's content processing, routing, and search still working underneath.