Home /

docs

Creating Your Own Theme

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

Quick Start

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:

quick-start.tsx
theme: myTheme({
  ui: { colors: { primary: "#3b82f6" } },
});

Prompt: Match an Existing Website

Use this prompt when you already have a product or marketing website and want the docs to feel like the same brand.

Create a docs theme from my website

Copy this into your coding agent when you want docs that match an existing site.

The copy button asks for the website URL, fetches Brandfetch brand details, defaults the docs entry folder to docs, defaults the framework to Next.js, and fills those placeholders before copying. If the website uses a very expressive landing page, ask for a docs interpretation of the brand instead of a literal copy. Documentation should feel connected to the product while staying calmer, denser, and easier to scan.


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

ui-colors.ts
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.

ui-typography.ts
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:

example.ts
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.

ui-radius.ts
radius: "0.5rem"; // rounded
radius: "0px"; // sharp corners
radius: "0.75rem"; // more rounded

ui.layout

Content area dimensions and structural options.

ui-layout.ts
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.

ui-codeblock.ts
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.

ui-sidebar.ts
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.

ui-card.ts
card: {
  bordered: true,          // show card borders
  background: undefined,   // override card background
}

ui.components

Default props for built-in MDX components.

ui-components.ts
components: {
  Callout: { variant: "outline" },   // or "soft"
  CodeBlock: { showCopyButton: true },
  Tabs: { style: "default" },
  HoverLink: {
    linkLabel: "Open page",
    showIndicator: false,
    align: "start",
  },
}

ui.components changes the default props for built-in MDX components. For example, HoverLink can keep the built-in popover UI while changing its CTA label, indicator, or alignment across your docs. If you want to replace the component completely, use components.HoverLink in docs.config.tsx instead.


Full Example

Here's a complete theme with every option:

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" },
    HoverLink: { linkLabel: "Open page", showIndicator: false },
  },
};

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:

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

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

Works with any built-in theme:

extending-an-existing-theme.ts
import { darksharp } from "@farming-labs/theme/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:

cherry-picking-from-built-in-themes.ts
import { DefaultUIDefaults } from "@farming-labs/theme/default";
import { DarksharpUIDefaults } from "@farming-labs/theme/darksharp";
import { PixelBorderUIDefaults } from "@farming-labs/theme/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:

terminal
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/theme/presets/black";
/* Or: @import "@farming-labs/theme/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/theme/presets/neutralLight-first themes
Black@farming-labs/theme/presets/blackDark-first themes

Component CSS Selectors

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

radius.css
/* 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

example.css
/* 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

inline-code.css
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

tables.css
.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

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

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

Tabs

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

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

Prev/Next Navigation Cards

prev-next-navigation-cards.css
.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

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

Page Action Buttons

page-action-buttons.css
.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

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

Selection & Misc

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

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

Tips