Skip to content

Custom Theme

Change the brand color, swap components, or build a custom React documentation theme.

Ardo starts with a polished default theme, but it is not a black box. React teams should be able to carry their brand, design system, and component choices into documentation without fighting the framework. You have five levels of customization, and you can mix them freely:

  1. Brand shortcut — Set brand, accent, neutral, and logo values in ardo()
  2. CSS variables — Change colors, spacing, and typography without touching components
  3. Vanilla Extract tokens — Type-safe styling with ardo/theme imports and .css.ts files
  4. Component overrides — Replace individual pieces (header, sidebar, footer) with your own React components
  5. 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 Shortcut

Use brand in vite.config.ts for the common case: one primary brand color, one accent color, a neutral chrome tone, and a logo.

import { defineConfig } from "vite"
import { ardo } from "ardo/vite"

export default defineConfig({
  plugins: [
    ardo({
      title: "Acme Docs",
      brand: {
        color: "blue",
        accent: "teal",
        neutral: "slate",
        logo: "./app/assets/logo.svg",
      },
    }),
  ],
})

color, accent, and neutral accept either preset names or OKLCH hue numbers:

ardo({
  title: "Acme Docs",
  brand: {
    color: 240,
    accent: 170,
    neutral: 260,
  },
})

Presets: berry, red, orange, amber, green, teal, blue, indigo, purple, pink, slate, and gray.

When color is set without accent, Ardo derives a complementary accent. When neutral is omitted, neutral chrome follows the brand hue. brand.logo is used by the default header and, for local SVG paths, as the default source for generated favicons. An explicit logo on ArdoHeader and icons.source still win when you need different assets.

Brand Color Helper

Use the TypeScript helper when you prefer defining theme values from app code instead of the ardo() config. The default theme uses OKLCH hue tokens for brand, accent, and neutral chrome colors, so you can change the brand while keeping backgrounds, borders, header, sidebar, and text on a separate neutral base.

Create a .css.ts file:

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

applyBrandTheme(240) // deep blue

Use the object form when your brand hue should not tint neutral chrome:

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

applyBrandTheme({
  primary: 130, // green brand
  neutral: 260, // neutral slate chrome
})

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

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

The default hue is 356 (a muted berry tone). Some starting points:

HueColor
356Berry (default)
170Teal
240Blue
270Indigo
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 set, which you can modify before passing to createGlobalTheme.

You can also pass token overrides directly. Shared overrides apply to both light and dark themes; light and dark override one mode only:

import { applyBrandTheme } from "ardo/theme"

applyBrandTheme({
  color: {
    bg: "#fbfbf8",
    text: "#101010",
    brand: "#0038ff",
    brandGradient: "#0038ff",
    shadowSm: "0 0 0 0 transparent",
    shadowMd: "0 0 0 0 transparent",
    shadowLg: "0 0 0 0 transparent",
  },
  radius: {
    sm: "0px",
    base: "0px",
    lg: "0px",
  },
  dark: {
    color: {
      bg: "#101010",
      text: "#fbfbf8",
    },
  },
})

shadowSm, shadowMd, shadowLg, and codeShadow are full box-shadow values. Use 0 0 0 0 transparent when a brand wants no shadow; none can invalidate declarations that combine multiple shadow layers. brandGradient also accepts a plain color when gradients are not part of your design system.

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-hue-brand: 260;
  --ardo-hue-accent: 134;
  --ardo-hue-neutral: 260;

  --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 "ardo/ui/styles.css"
import "./styles/custom.css"

The --ardo-* variables are public override anchors. Ardo ships defaults on :root and dark-mode values on .dark; app CSS can override either selector and those raw variable overrides remain supported even when the higher-level theme helpers evolve.

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 {
  /* Hue knobs */
  --ardo-hue-brand: 260;
  --ardo-hue-accent: 134;
  --ardo-hue-neutral: 260;

  /* 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;
}

Most color tokens reference the hue variables, so retinting the neutral chrome usually only needs one declaration:

:root {
  --ardo-hue-neutral: 260;
}

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

Stable override hooks

Ardo's generated Vanilla Extract class names are implementation details. Use the stable hook classes for narrowly scoped overrides:

ClassLandmark
.ardo-headerDefault header shell
.ardo-navDefault navigation container
.ardo-mobile-panelMobile navigation dialog
.ardo-sidebarSidebar shell
.ardo-footerDefault footer shell
.ardo-contentRendered Markdown/MDX content

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.

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, and neutral hue custom properties used by the default color ramp
  • 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:

// app/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>
  )
}

Footer message and copyright props render as React content, so plain strings are escaped. If you need footer markup from a trusted source, use trustedMessageHtml or trustedCopyrightHtml; do not pass user- or CMS-provided content to those props.

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

useArdoContext

Use the full runtime context when a custom component needs several pieces of Ardo state at once:

import { useArdoContext } from "ardo/runtime"

function CurrentRouteDebug() {
  const { config, currentPage, activeContextId } = useArdoContext()
  return (
    <pre>{JSON.stringify({ site: config.title, page: currentPage?.title, activeContextId })}</pre>
  )
}

These helpers are useful when you replace the default document footer or sidebar behavior:

import {
  findCurrentSidebarItem,
  getPrevNextLinks,
  useArdoPageData,
  useArdoSidebar,
} from "ardo/runtime"
import { useLocation } from "react-router"

function MyPrevNext() {
  const location = useLocation()
  const page = useArdoPageData()
  const sidebar = useArdoSidebar()
  if (!page) return null

  const current = findCurrentSidebarItem(sidebar, location.pathname)
  const { prev, next } = getPrevNextLinks(sidebar, location.pathname)

  return (
    <nav>
      <span>{current?.text}</span>
      {prev && <a href={prev.link}>{prev.text}</a>}
      {next && <a href={next.link}>{next.text}</a>}
    </nav>
  )
}

ArdoProvider and ArdoSiteConfigProvider

Most apps get these providers through <ArdoRoot>. Import them directly only when you are building a fully custom root layout or an isolated component preview.

Icon Registration

MDX content and UI components can refer to registered icon names. Register icons once in app/root.tsx, then use the names through ArdoIcon, component icon props, or the MDX <Icon /> component:

import { ArdoIcon, registerIcons } from "ardo/ui"
import { Code2, Rocket } from "lucide-react"

registerIcons({ Code2, Rocket })

export function LaunchLink() {
  return (
    <a href="/guide/getting-started">
      <ArdoIcon name="Rocket" aria-hidden /> Get started
    </a>
  )
}

getRegisteredIconNames() returns the current registry keys when you need to validate user-facing configuration.

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.