@farming-labs/docs

Creating Your Own Theme

Everything starts with createTheme(). No CSS required — just a config object.

Quick Start

my-theme.ts
import { createTheme } from "@farming-labs/docs";

export const myTheme = createTheme({
  name: "my-theme",
  ui: {
    colors: {
      primary: "#e11d48",
      background: "#09090b",
      muted: "#71717a",
      border: "#27272a",
    },
  },
});
docs.config.tsx
import { defineDocs } from "@farming-labs/docs";
import { myTheme } from "./my-theme";

export default defineDocs({
  entry: "docs",
  theme: myTheme(),
});

That's it. Users can override any of your defaults:

theme: myTheme({
  ui: { colors: { primary: "#3b82f6" } },
})

Config Options Reference

Here's everything you can configure in createTheme().

name

Unique identifier for your theme. Used for debugging and CSS scoping.

name: "my-theme"

ui.colors

Color tokens mapped to --color-fd-* CSS variables at runtime. Any valid CSS color value works (hex, rgb, hsl, oklch).

colors: {
  primary: "#e11d48",               // brand color — links, buttons, active states
  primaryForeground: "#ffffff",      // text on primary backgrounds
  background: "#09090b",            // page background
  foreground: "#fafafa",            // default text color
  muted: "#71717a",                 // muted backgrounds (badges, tags)
  mutedForeground: "#a1a1aa",       // secondary text, descriptions
  border: "#27272a",               // borders and dividers
  card: "#18181b",                 // card / callout backgrounds
  cardForeground: "#fafafa",        // text inside cards
  accent: "#27272a",               // hover backgrounds
  accentForeground: "#fafafa",      // text on hover backgrounds
  secondary: "#27272a",            // secondary button backgrounds
  secondaryForeground: "#fafafa",   // text on secondary
  popover: "#18181b",              // dropdown / dialog backgrounds
  popoverForeground: "#fafafa",     // text inside popovers
  ring: "#e11d48",                 // focus ring on interactive elements
}

You only need to set the ones you want to change — the rest are inherited from the base preset.

ui.typography

Font families, heading sizes, weights, line heights, and letter spacing.

typography: {
  font: {
    // Font families
    style: {
      sans: "Inter, system-ui, sans-serif",
      mono: "JetBrains Mono, monospace",
    },
    // Heading styles
    h1: { size: "2.25rem", weight: 700, lineHeight: "1.2", letterSpacing: "-0.02em" },
    h2: { size: "1.5rem", weight: 600, lineHeight: "1.3", letterSpacing: "-0.01em" },
    h3: { size: "1.25rem", weight: 600, lineHeight: "1.4" },
    h4: { size: "1.125rem", weight: 600, lineHeight: "1.4" },
    // Body and small text
    body: { size: "1rem", weight: 400, lineHeight: "1.75" },
    small: { size: "0.875rem", weight: 400, lineHeight: "1.5" },
  },
}

Each heading/text style accepts these properties:

PropertyTypeExample
sizestring"2.25rem", "36px"
weightnumber | string700, "bold"
lineHeightstring"1.2", "28px"
letterSpacingstring"-0.02em"

Tip: Use CSS variables for fonts so users can load them with next/font:

style: {
  sans: "var(--font-geist-sans, system-ui, sans-serif)",
  mono: "var(--font-geist-mono, ui-monospace, monospace)",
}

ui.radius

Global border radius. Maps to CSS --radius.

radius: "0.5rem"   // rounded
radius: "0px"      // sharp corners
radius: "0.75rem"  // more rounded

ui.layout

Content area dimensions and structural options.

layout: {
  contentWidth: 768,       // max width of docs content (px)
  sidebarWidth: 280,       // sidebar width (px)
  tocWidth: 220,           // table of contents width (px)
  toc: {
    enabled: true,         // show table of contents
    depth: 3,              // heading depth (2 = h2 only, 3 = h2 + h3)
  },
  header: {
    height: 64,            // header height (px)
    sticky: true,          // stick header on scroll
  },
}

ui.codeBlock

Code block rendering options.

codeBlock: {
  showLineNumbers: false,  // show line numbers
  showCopyButton: true,    // show copy button
  theme: "github-dark",    // shiki syntax theme
  darkTheme: "github-dark", // dark mode shiki theme (for dual-theme)
}

ui.sidebar

Sidebar visual style.

sidebar: {
  style: "default",        // "default" | "bordered" | "floating"
  background: undefined,   // override sidebar background
  borderColor: undefined,  // override sidebar border color
}
StyleDescription
"default"Standard fumadocs sidebar
"bordered"Visible bordered sections (inspired by better-auth.com)
"floating"Floating card sidebar with subtle shadow

ui.card

Card styling.

card: {
  bordered: true,          // show card borders
  background: undefined,   // override card background
}

ui.components

Default props for built-in MDX components.

components: {
  Callout: { variant: "outline" },   // or "soft"
  CodeBlock: { showCopyButton: true },
  Tabs: { style: "default" },
}

Full Example

Here's a complete theme with every option:

my-theme.ts
import { createTheme } from "@farming-labs/docs";

export const MyThemeDefaults = {
  colors: {
    primary: "#8b5cf6",
    background: "#0f0f11",
    foreground: "#e4e4e7",
    muted: "#1e1e2e",
    mutedForeground: "#71717a",
    border: "#27272a",
    card: "#18181b",
    accent: "#27272a",
    ring: "#8b5cf6",
  },
  typography: {
    font: {
      style: {
        sans: "Inter, system-ui, sans-serif",
        mono: "Fira Code, monospace",
      },
      h1: { size: "2.5rem", weight: 800, letterSpacing: "-0.03em" },
      h2: { size: "1.75rem", weight: 700, letterSpacing: "-0.02em" },
      h3: { size: "1.25rem", weight: 600 },
      body: { size: "1rem", lineHeight: "1.8" },
    },
  },
  radius: "0.5rem",
  layout: {
    contentWidth: 800,
    sidebarWidth: 260,
    toc: { enabled: true, depth: 3 },
    header: { height: 56, sticky: true },
  },
  sidebar: { style: "bordered" as const },
  codeBlock: { showCopyButton: true, theme: "github-dark" },
  components: {
    Callout: { variant: "outline" },
  },
};

export const myTheme = createTheme({
  name: "my-theme",
  ui: MyThemeDefaults,
});

// Export defaults so others can extend your theme
export { MyThemeDefaults };

Extending an Existing Theme

Don't want to start from scratch? Use extendTheme() to build on top of a built-in preset:

my-theme.ts
import { extendTheme } from "@farming-labs/docs";
import { fumadocs } from "@farming-labs/fumadocs";

export const myTheme = extendTheme(fumadocs(), {
  name: "my-fumadocs-variant",
  ui: {
    colors: { primary: "#22c55e", background: "#0c0c0c" },
    sidebar: { style: "bordered" },
  },
});

Works with any built-in theme:

import { darksharp } from "@farming-labs/fumadocs/darksharp";

export const myDark = extendTheme(darksharp(), {
  name: "my-dark-variant",
  ui: {
    colors: { primary: "#f97316" },
    radius: "0.75rem",
  },
});

extendTheme() returns a DocsTheme directly (not a factory). Use createTheme() when building a reusable preset.


Cherry-Picking from Built-in Themes

Each built-in theme exports its defaults object:

import { DefaultUIDefaults } from "@farming-labs/fumadocs/default";
import { DarksharpUIDefaults } from "@farming-labs/fumadocs/darksharp";
import { PixelBorderUIDefaults } from "@farming-labs/fumadocs/pixel-border";
import { createTheme } from "@farming-labs/docs";

export const myTheme = createTheme({
  name: "my-hybrid-theme",
  ui: {
    colors: PixelBorderUIDefaults.colors,
    typography: DefaultUIDefaults.typography,
    layout: DarksharpUIDefaults.layout,
    sidebar: { style: "floating" },
  },
});

Publishing as an npm Package

my-fumadocs-theme/
  src/
    index.ts       ← createTheme() + exported defaults
    theme.css      ← CSS overrides (optional)
  package.json
package.json
{
  "name": "my-fumadocs-theme",
  "version": "1.0.0",
  "type": "module",
  "exports": {
    ".": { "import": "./dist/index.mjs", "types": "./dist/index.d.mts" },
    "./css": "./src/theme.css"
  },
  "peerDependencies": {
    "@farming-labs/docs": ">=0.0.1"
  }
}

Users install and use:

npm install my-fumadocs-theme
docs.config.tsx
import { myTheme } from "my-fumadocs-theme";
export default defineDocs({ entry: "docs", theme: myTheme() });
app/global.css
@import "tailwindcss";
@import "my-fumadocs-theme/css";

Advanced: CSS Customization

The config options above cover most use cases. But if you need pixel-level control over specific components — like how the sidebar active state looks, code block borders, table styling, or callout appearance — you can write a CSS file alongside your theme.

How it Works

Every theme can ship a CSS file that sits on top of the config. The CSS file:

  1. Imports a color preset (which includes fumadocs-ui core styles)
  2. Overrides CSS variables for light (:root) and dark (.dark) modes
  3. Targets specific component selectors to customize their appearance

Starting Point

my-theme/theme.css
/* Pick a color preset as your base */
@import "@farming-labs/fumadocs/presets/black";
/* Or: @import "@farming-labs/fumadocs/presets/neutral"; */

/* Override CSS variables */
.dark {
  --color-fd-primary: #22d3ee;
  --color-fd-background: #0a0a0f;
  --color-fd-border: #1e293b;
  --radius: 0.5rem;
}

Two presets are available:

PresetImportBest for
Neutral@farming-labs/fumadocs/presets/neutralLight-first themes
Black@farming-labs/fumadocs/presets/blackDark-first themes

Component CSS Selectors

Here are all the selectors you can target to style individual components:

/* Container */
.dark aside#nd-sidebar {
  background: hsl(0 0% 2%);
  border-color: hsl(0 0% 12%);
}

/* All sidebar links */
.dark aside a[data-active] {
  font-size: 0.875rem;
  border-radius: 0.2rem;
  color: hsl(0 0% 50%);
}

/* Active link (current page) */
.dark aside a[data-active="true"] {
  background: hsl(0 0% 14%);
  color: var(--color-fd-primary);
  font-weight: 600;
}

/* Hover on inactive links */
.dark aside a[data-active="false"]:hover {
  color: hsl(0 0% 90%);
}

/* Folder toggle buttons */
.dark aside button.text-fd-muted-foreground {
  color: hsl(0 0% 90%);
  font-weight: 500;
}

/* Child links inside folders */
.dark aside div.overflow-hidden[data-state] a[data-active] {
  font-size: 0.785rem;
  padding-left: 2rem;
}

/* Search button */
.dark aside button[class*="bg-fd-secondary"] {
  background: transparent;
  border-top: 1px solid hsl(0 0% 12%);
  border-bottom: 1px solid hsl(0 0% 12%);
}

/* Active link indicator bar */
.dark aside a[data-active="true"]::before {
  background-color: var(--color-fd-primary);
  width: 2px;
}

Code Blocks

/* Container */
figure.shiki {
  border: 1px solid var(--color-fd-border);
  border-radius: 0.5rem;
}

/* Title bar */
figure.shiki > div:first-child {
  border-radius: 0.5rem 0.5rem 0 0;
}

/* Copy button */
figure.shiki button {
  border-radius: 0.25rem;
}

Inline Code

code:not(pre code) {
  border-radius: 0.25rem;
}

.dark code:not(pre code) {
  background: hsl(0 0% 10%);
  border: 1px solid hsl(0 0% 16%);
  color: hsl(0 0% 85%);
}

Tables

.dark table {
  border: 1px solid hsl(0 0% 15%);
}

.dark th {
  background: hsl(0 0% 6%);
  color: hsl(0 0% 80%);
}

.dark td {
  border-color: hsl(0 0% 12%);
}

Callouts

[class*="bg-fd-card"],
[role="alert"] {
  border-radius: 0.5rem;
}

.dark [class*="bg-fd-card"] {
  border-color: hsl(0 0% 14%);
}

Tabs

.dark [role="tablist"] {
  border-radius: 0.5rem;
}

.dark [role="tab"] {
  border-radius: 0.25rem;
}

Prev/Next Navigation Cards

.dark article a[class*="border"] {
  border-color: hsl(0 0% 15%);
  border-radius: 0.5rem;
}

.dark article a[class*="border"]:hover {
  background: hsl(0 0% 6%);
}

Search Dialog

[role="dialog"] {
  border-radius: 0.5rem;
}
.fd-breadcrumb { font-size: 0.75rem; }
.fd-breadcrumb-link { text-decoration: none; }
.fd-breadcrumb-current { font-weight: 500; }

Page Action Buttons

.fd-page-action-btn {
  border-radius: 0.375rem;
  background: var(--color-fd-secondary);
  border: 1px solid var(--color-fd-border);
}

.fd-page-action-menu {
  border-radius: 0.5rem;
  background: var(--color-fd-popover);
}

Scrollbar

.dark {
  --scrollbar-thumb: var(--color-fd-border);
  --scrollbar-thumb-hover: var(--color-fd-ring);
  --scrollbar-track: transparent;
}

Selection & Misc

::selection {
  background: var(--color-fd-foreground);
  color: var(--color-fd-background);
}

.dark hr {
  border-color: hsl(0 0% 15%);
}

Tips

  • Inspect in DevTools — the fastest way to find the right selector is to right-click an element, inspect it, and check its classes/attributes.
  • Use !important sparingly — fumadocs uses Tailwind utilities, so some overrides need !important. Check the built-in theme CSS files for examples.
  • Scope with .dark — use .dark for dark mode styles. Fumadocs uses class-based dark mode.
  • Reference CSS variables — always use var(--color-fd-*) in your styles so they respond to user color overrides.
  • Look at built-in themes — the pixel-border and darksharp CSS files are excellent references showing every component selector in use.

On this page

No Headings