Writing a theme
Build a theme from unstyled React components, data hooks, and overrides.
A theme is a small manifest plus React components that yaakdoc renders on the server with Vite. You write the layout and the styles; yaakdoc's unstyled components and hooks provide the content.
npm i @yaakdoc/react react react-domStart from Kiseki
The quickest start is often ejecting Kiseki and changing it. It's a complete theme that uses everything on this page.
Anatomy of a theme
my-theme/
├── theme.ts manifest, loaded by yaakdoc in Node
└── src/
├── entry.tsx server entry: { Layout, components?, Head? }
├── client.ts client entry: the islands to hydrate
└── styles.css styles (any CSS Vite supports: plain CSS, Tailwind, ...)Use it with "theme": "./my-theme".
The manifest
// Keep this file free of React imports: yaakdoc loads it in Node.
import { defineTheme } from "@yaakdoc/react/theme";
import { z } from "zod";
export default defineTheme({
name: "my-theme",
entry: "./src/entry.tsx",
client: "./src/client.ts",
styles: ["./src/styles.css"],
// Font files (from your styles) to preload, so the first paint doesn't use a fallback font.
// A role is skipped when the user sets `branding.fonts.sans` / `branding.fonts.mono`.
preloadFonts: {
sans: ["inter-latin-wght-normal.woff2"],
},
// Shiki themes for syntax highlighting.
highlight: {
light: "github-light",
dark: "github-dark",
},
// Validates `themeOptions` and fills in defaults.
options: z.object({
accent: z.string().default("violet"),
}),
// Vite plugins the theme needs, e.g. Tailwind CSS.
vitePlugins: async () => [
(await import("@tailwindcss/vite")).default()
],
// What `yaakdoc eject` copies, and the packages it lists.
eject: {
files: ["theme.ts", "src/**"],
dependencies: ["@yaakdoc/react"],
},
});Theme source is processed by Vite, so TSX works without a build step, and a theme package can ship its source as is.
Load fonts from styles
Load fonts from styles, not from the client entry. CSS imported by the client entry is only
applied once its script runs (in dev), so text first renders in a fallback font. To avoid a
visible switch when the font arrives, preload the fonts with preloadFonts and declare them with
font-display: optional.
The layout
entry.tsx default-exports a ThemeRuntime. Its Layout renders the page <body>. yaakdoc renders <html> and <head>: meta tags, assets, the branding CSS variables and the color mode script.
import {
Content,
Footer,
Logo,
NavTree,
type ThemeRuntime,
} from "@yaakdoc/react";
import { ColorModeToggle, NavFilter } from "@yaakdoc/react/islands";
const theme: ThemeRuntime = {
Layout: ({ page }) => (
<div className="layout">
<aside id="sidebar">
<Logo />
<NavFilter target="#sidebar [data-yd=nav]" />
<NavTree />
</aside>
<ColorModeToggle />
<Content />
<Footer />
</div>
),
};
export default theme;<Content /> renders whatever the current page is. page.kind tells you which kind it is:
kind | Page |
|---|---|
page | A custom page |
index | The API overview |
folder | A folder (with "pages": "per-request") |
request | A request (with "pages": "per-request") |
not-found | The 404 page |
In pages: "single" mode, the index page contains the whole reference. <Content /> handles both modes, but you can compose the pieces yourself.
Data hooks
| Hook | Returns |
|---|---|
useDocument() | The PreparedDocument: tree, requests, folders, workspace, environment, variables, pages, navigation, referenceBase. |
useSite() | Site data: title, description, logo, colors, links, llms settings, base, pages, ... |
usePage() | The current page (kind, id, path, markdownUrl, ...). |
useRequest(id), useFolder(id), usePageDoc(id) | A single prepared request, folder or custom page. |
useNavigation() | The navigation: tabs, each with items (pages, groups, and { type: "api" } for the folders and requests). |
useActiveTab() | The tab of the current page (the API reference's tab for the overview, folders and requests). |
usePageNeighbors(id) | The previous and next entries around a page, in navigation order. |
useThemeOptions<T>() | Your validated themeOptions. |
A prepared request already contains everything a theme needs:
hrefandmarkdownUrlresolvedUrl, with variables resolved and secrets as{{placeholders}}descriptionHtml, sanitized MarkdownbodyHtml,examples[].htmlandresponses[](Shiki-highlighted example responses withstatus,labelandlanguage)- headers, params and auth, each with
inheritedFrominformation
A prepared custom page has title, description, sidebarTitle, icon, href, markdownUrl, summary and html.
Components
All components are unstyled. They render semantic markup with stable data-yd="..." attributes, and accept className.
| Area | Components |
|---|---|
| Page structure | Content, Overview, TreeContent, FolderDoc, ChildLinks, NotFound, Heading |
| Custom pages | PageDoc, PageNav (previous and next links) |
| Requests | RequestDoc, RequestBody, MethodBadge, RequestUrl, AuthInfo, KeyValueTable, Variables |
| Code and Markdown | CodeBlock, CodeExamples, ResponseExamples, Markdown, Html |
| Environments | EnvironmentVariants, EnvironmentSelect |
| Navigation | Navigation (a tab's groups, pages and the API reference), NavTabs, NavTree (the API reference alone), NavIcon (renders nothing; override it to show icons), Breadcrumbs |
| Search | Search (a SearchTrigger and a SearchDialog) |
| Site chrome | Logo, SiteLinks, Footer, PageActions, RunInYaak, BrandingStyles |
A few attributes are useful when styling with plain CSS:
[data-method="get"]on method badges.[data-yd="url-variable"]on the{{variables}}inside aRequestUrl.[data-yd-section]on documented sections, which the scroll spy uses.aria-currenton the active navigation link.
Overriding components
Every component above can be replaced everywhere, including inside other components, by registering an override under its name. The override receives the same props, plus Default, the original component:
const theme: ThemeRuntime = {
Layout,
components: {
MethodBadge: ({ request, Default }) => {
return (
<Default request={request} className="badge" />
);
},
},
};Use overridable(name, Component) to make your own components overridable.
Override, don't re-implement
<Content /> decides what each page shows: a custom page, the API reference, a folder, a request
or the 404 page. Keep that logic and restyle the parts. Kiseki, for example, renders
<Content /> and overrides Overview, FolderDoc, RequestDoc, ChildLinks, PageDoc,
PageNav, NavIcon and NotFound.
Helpers
These keep themes consistent with the built-in components:
| Helper | Purpose |
|---|---|
usePageActions(markdownUrl) | Data for "Copy page/section" actions (null when disabled). |
copyActionLabel(level) | "Copy page" or "Copy section", depending on the heading level. |
useRunInYaak() | Name and export URL for the "Run in Yaak" button (null when disabled). |
toAbsoluteUrl(site, path) | The absolute URL of a site path, when branding.url is set. |
tabGroupIds, tabPanelProps, visibleTabPanelSelector | Ids, panel props and copy targets for server-rendered panels controlled by TabList. |
methodLabel, and grpcMethodPath, formFieldValue from @yaakdoc/core | Display values for methods, gRPC calls and form fields. |
Branding tokens
The user's branding arrives as CSS custom properties on :root:
--yd-color-primary,--yd-color-accent,--yd-color-backgroundand--yd-color-foreground, with per-mode values under[data-theme="light"]and[data-theme="dark"]--yd-font-sansand--yd-font-mono
Use them with fallbacks:
:root {
--primary: var(--yd-color-primary, #6d28d9);
}Color mode
The color mode is applied before first paint as data-theme="light|dark" on <html>, and color-scheme is set to match. Without JavaScript, follow prefers-color-scheme.
Highlighted code uses Shiki's dual themes. Pick the right variant with:
.shiki,
.shiki span {
color: light-dark(var(--shiki-light), var(--shiki-dark));
}Environments
When the config lists several environments, environment-dependent data is prepared once per environment:
request.variantsholdsresolvedUrlandexamplesfor each environment.document.variableSetsholds the variables table for each environment.document.environmentslists the options, default first.page.htmlof custom pages contains a variant of each block that uses variables, wrapped in[data-yd-env], so the switcher works on pages without extra code.
Render the variants with <EnvironmentVariants variants={...}>{(variant) => ...}</EnvironmentVariants>, and add a switcher with <EnvironmentSelect /> (or build your own with useEnvironment). yaakdoc adds a small head script and CSS, so a remembered choice shows up before first paint.
Entry points
| Import | Contents |
|---|---|
@yaakdoc/react | Components, hooks, overrides, types. |
@yaakdoc/react/islands | Islands and headless hooks (client-safe). |
@yaakdoc/react/client | hydrateIslands() (used by yaakdoc's client entry). |
@yaakdoc/react/server | renderPage() (used by yaakdoc's SSR entry). |
@yaakdoc/react/theme | defineTheme() and theme types (Node-safe). |