Creating a Theme
There are two approaches to creating a theme: extending Lumina (start with full visual coverage, then customize) or building from scratch (full control from the ground up).
Extending Lumina
The quickest way to create a theme is to start from Lumina and override the parts you want to change. You get a complete, styled theme immediately and can customize incrementally.
Config
Use mergeThemeConfig with Lumina's config as the base:
// src/config.ts
import { mergeThemeConfig } from '@refrakt-md/transform';
import { luminaConfig } from '@refrakt-md/lumina/transform';
export const myThemeConfig = mergeThemeConfig(luminaConfig, {
// Override icons
icons: {
hint: {
note: '<svg ...>...</svg>',
warning: '<svg ...>...</svg>',
},
},
// Override specific rune configs
runes: {
Hint: {
modifiers: { hintType: { source: 'meta', default: 'info' } },
},
},
});
CSS
Import all of Lumina's CSS, then layer your overrides after it:
/* index.css */
@import '@refrakt-md/lumina'; /* full Lumina styles */
@import './tokens/overrides.css'; /* your custom tokens */
@import './styles/runes/hint.css'; /* override specific runes */
Because CSS cascades, your overrides replace Lumina's rules for the same selectors. You only need to write CSS for the parts you want to change.
What to override
- Tokens only — change the visual language (colors, typography, radii) while keeping all rune layouts
- Specific rune CSS — restyle individual runes while keeping others as-is
- Icons — provide your own SVGs via the config
- Config tweaks — change modifier defaults or add structural elements
Building from scratch
This guide walks through building a custom theme package from the ground up, using the base configuration as a foundation and adding your own visual identity.
Prerequisites
- Familiarity with CSS and BEM naming
- Basic knowledge of Node.js and npm
- Understanding of the theme overview and configuration reference
Create the package
Set up a new package in your project or monorepo:
mkdir packages/my-theme cd packages/my-theme npm init -yConfigure
package.jsonwith the required exports:{ "name": "@my-org/my-theme", "version": "0.1.0", "type": "module", "main": "dist/transform.js", "types": "dist/transform.d.ts", "exports": { ".": "./index.css", "./transform": { "types": "./dist/transform.d.ts", "default": "./dist/transform.js" }, "./manifest": "./manifest.json", "./svelte": { "svelte": "./svelte/index.ts", "default": "./svelte/index.ts" } }, "scripts": { "build": "tsc" }, "dependencies": { "@refrakt-md/runes": "0.4.0", "@refrakt-md/transform": "0.4.0", "@refrakt-md/svelte": "0.4.0", "@refrakt-md/types": "0.4.0" } }The key exports:
.— Your CSS entry point (tokens + rune styles)./transform— Your theme config, compiled to JS./manifest— Theme metadata./svelte— SvelteKit adapter (element overrides, behaviors, component registry). Optional if you only target the HTML adapter.
Write your config
Create
src/config.ts:import { baseConfig } from '@refrakt-md/runes'; import { mergeThemeConfig } from '@refrakt-md/transform'; export const myThemeConfig = mergeThemeConfig(baseConfig, { // Optional: use a different BEM prefix // prefix: 'mt', // Add icon SVGs for runes that use them icons: { hint: { note: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">...</svg>', warning: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">...</svg>', caution: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">...</svg>', check: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">...</svg>', }, }, // Optional: override specific rune configs runes: { // Example: change the default hint type // Hint: { // modifiers: { hintType: { source: 'meta', default: 'info' } }, // }, }, });The base config defines all core rune configurations. Your config only needs to provide:
- Icons for runes that display them (currently just Hint)
- Overrides for runes where you want different defaults or behavior
Community packages (e.g.,
@refrakt-md/marketing,@refrakt-md/docs) contribute their own theme config entries alongside their rune schemas. These are merged in automatically bymergePackages()when packages are loaded — you don't need to add config entries for community package runes in your theme.noteIf you change the
prefix(e.g., from'rf'to'mt'), all your CSS selectors must use the new prefix:.mt-hintinstead of.rf-hint. Most themes keep'rf'for compatibility.Define design tokens
Create
tokens/base.csswith your visual language::root { /* Typography */ --rf-font-sans: 'Your Font', system-ui, sans-serif; --rf-font-mono: 'Your Mono Font', monospace; /* Primary color scale */ --rf-color-primary-50: #faf5ff; --rf-color-primary-100: #f3e8ff; --rf-color-primary-500: #a855f7; --rf-color-primary-600: #9333ea; --rf-color-primary-900: #581c87; /* Core palette */ --rf-color-text: #1a1a2e; --rf-color-muted: #64748b; --rf-color-border: #e2e8f0; --rf-color-bg: #ffffff; --rf-color-primary: var(--rf-color-primary-500); --rf-color-primary-hover: var(--rf-color-primary-600); /* Surfaces */ --rf-color-surface: #f8fafc; --rf-color-surface-hover: #f1f5f9; --rf-color-surface-active: #e2e8f0; --rf-color-surface-raised: #ffffff; /* Semantic */ --rf-color-info: #3b82f6; --rf-color-info-bg: #eff6ff; --rf-color-info-border: #bfdbfe; --rf-color-warning: #f59e0b; --rf-color-warning-bg: #fffbeb; --rf-color-warning-border: #fde68a; --rf-color-danger: #ef4444; --rf-color-danger-bg: #fef2f2; --rf-color-danger-border: #fecaca; --rf-color-success: #10b981; --rf-color-success-bg: #ecfdf5; --rf-color-success-border: #a7f3d0; /* Radii */ --rf-radius-sm: 6px; --rf-radius-md: 10px; --rf-radius-lg: 16px; --rf-radius-full: 9999px; /* Shadows */ --rf-shadow-sm: 0 1px 3px rgba(0,0,0,0.06); --rf-shadow-md: 0 4px 12px rgba(0,0,0,0.07); --rf-shadow-lg: 0 8px 24px rgba(0,0,0,0.08); }Create
tokens/dark.cssfor dark mode:[data-theme="dark"] { --rf-color-text: #e2e8f0; --rf-color-muted: #94a3b8; --rf-color-border: #334155; --rf-color-bg: #0f172a; --rf-color-surface: #1e293b; --rf-color-surface-hover: #334155; --rf-color-surface-active: #475569; --rf-color-surface-raised: #1e293b; /* Override all semantic colors for dark backgrounds */ } @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { /* Same overrides as above */ } }Write rune CSS
Create a
styles/runes/directory. Start with a simple rune and build from there.styles/runes/grid.css:.rf-grid { margin: 1.5rem 0; } .rf-grid [data-layout="grid"] { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; }styles/runes/hint.css:.rf-hint { --hint-color: var(--rf-color-info); --hint-bg: var(--rf-color-info-bg); border-left: 3px solid var(--hint-color); padding: 0.875rem 1.25rem; margin: 1.5rem 0; background: var(--hint-bg); } .rf-hint__header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.375rem; } .rf-hint__icon { display: flex; color: var(--hint-color); } .rf-hint__title { font-weight: 600; text-transform: capitalize; color: var(--hint-color); } /* Variant colors */ .rf-hint--note { --hint-color: var(--rf-color-info); --hint-bg: var(--rf-color-info-bg); } .rf-hint--warning { --hint-color: var(--rf-color-warning); --hint-bg: var(--rf-color-warning-bg); } .rf-hint--caution { --hint-color: var(--rf-color-danger); --hint-bg: var(--rf-color-danger-bg); } .rf-hint--check { --hint-color: var(--rf-color-success); --hint-bg: var(--rf-color-success-bg); }Dimension CSS
Before writing per-rune CSS, create the dimension layer — generic rules that handle cross-rune patterns. Create a
styles/dimensions/directory with files for each dimension:metadata.css— badge styling via[data-meta-type],[data-meta-sentiment],[data-meta-rank]density.css— spacing and visibility via[data-density]sections.css— structural anatomy via[data-section]state.css— interactive states via[data-state]media.css— image treatments via[data-media]surfaces.css— container groupings (card, inline, banner, inset)checklist.css— checkbox items via[data-checked]sequence.css— ordered list styles via[data-sequence]
This layer handles ~70% of visual styling generically. See Universal Theming Dimensions for the complete CSS patterns you can adapt.
Working through per-rune CSS
The base config defines 74 rune configurations. With dimension CSS in place, many runes are already styled. Per-rune CSS files only need to cover rune-specific styling that the dimensions don't handle (e.g., Hint's colored left border, Nav's tree layout).
A good order:
- Layout basics: grid, tabs, accordion, details
- Content blocks: hint, steps, figure, cta, hero, feature
- Structural runes: recipe, api, event, howto
- Everything else: design tokens, code, data, creative runes
Add icons
Icons are SVG strings organized by group in the theme config. There are two types of icon groups:
Structural icons — used by the identity transform's
StructureEntry.iconconfig to inject icons into rune headers. The Hint rune expects icons for each hint type:icons: { hint: { note: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>', // ... other variants }, }Content icons — the
globalgroup provides icons that content authors reference with{% icon name="..." /%}. Lumina ships ~80 curated Lucide icons in itsglobalgroup. Your theme can provide its own set or extend Lumina's:icons: { hint: { /* structural icons */ }, global: { 'rocket': '<svg ...>...</svg>', 'shield': '<svg ...>...</svg>', // ... curated icon set for content authors }, }Use
stroke="currentColor"so icons inherit color from CSS.Create your manifest
Create
manifest.jsonat the package root:{ "name": "My Theme", "description": "A custom theme for refrakt.md", "version": "0.1.0", "author": "Your Name", "prefix": "rf", "tokenPrefix": "--rf", "darkMode": { "attribute": "data-theme", "values": { "dark": "dark", "light": "light" }, "systemPreference": true } }The manifest declares your theme's identity and capabilities to tooling and documentation generators.
Create the CSS entry point
Create
index.cssthat imports everything:@import './tokens/base.css'; @import './tokens/dark.css'; @import './styles/global.css'; /* Dimension CSS — generic cross-rune rules */ @import './styles/dimensions/metadata.css'; @import './styles/dimensions/density.css'; @import './styles/dimensions/sections.css'; @import './styles/dimensions/state.css'; @import './styles/dimensions/media.css'; @import './styles/dimensions/surfaces.css'; @import './styles/dimensions/checklist.css'; @import './styles/dimensions/sequence.css'; /* Per-rune CSS — rune-specific overrides */ @import './styles/runes/grid.css'; @import './styles/runes/hint.css'; @import './styles/runes/recipe.css'; /* Add imports as you create more rune CSS files */Adapter integration
Your theme needs adapter-specific exports so it can be used with a particular adapter. The most common is the SvelteKit adapter.
SvelteKit
Create
svelte/index.tsto re-export adapter utilities from theme-base:export { elements } from '@refrakt-md/svelte'; export { behaviors } from '@refrakt-md/svelte'; export { registry } from '@refrakt-md/svelte';The
elementsexport is empty by default but available as an extension point for user-defined element overrides (table and code block wrapping is handled by Markdoc node schemas in@refrakt-md/runes). Thebehaviorsaction wires up progressive enhancement (including copy-to-clipboard on code blocks). Theregistryis empty by default but available if you need custom Svelte components for specific runes.HTML
The HTML adapter doesn't require a separate export. It uses the theme's
./transformexport (config) and.export (CSS) directly. See the HTML adapter page for usage.Define layouts
Your theme needs layout configs that describe page structure. The simplest approach is to use the built-in layouts from
@refrakt-md/transform:// svelte/index.ts import { registry } from '@refrakt-md/svelte'; import { defaultLayout, docsLayout, blogArticleLayout } from '@refrakt-md/transform'; import type { SvelteTheme } from '@refrakt-md/svelte'; import { myThemeConfig } from '../src/config.js'; export { registry }; export const theme: SvelteTheme = { config: myThemeConfig, registry, layouts: { 'default': defaultLayout, 'docs': docsLayout, 'blog-article': blogArticleLayout, }, };import type { HtmlTheme } from '@refrakt-md/html'; import { defaultLayout, docsLayout, blogArticleLayout } from '@refrakt-md/transform'; export const theme: HtmlTheme = { manifest: { /* from manifest.json */ }, layouts: { 'default': defaultLayout, 'docs': docsLayout, 'blog-article': blogArticleLayout, }, };To customize a layout, create your own
LayoutConfigobject:import type { LayoutConfig } from '@refrakt-md/transform'; import { docsLayout } from '@refrakt-md/transform'; // Start from the docs layout and override slots export const myDocsLayout: LayoutConfig = { ...docsLayout, // Remove the toolbar (no breadcrumbs) slots: { ...docsLayout.slots, toolbar: undefined as any, }, };Layout CSS goes in
styles/layouts/. The built-in layouts use classes like.rf-mobile-panel,.rf-docs-header,.rf-docs-sidebar, etc. See the layouts reference for all generated classes and data attributes.Build and test
Add a
tsconfig.json:{ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "dist", "rootDir": "src" }, "include": ["src"] }Build:
npm run buildTesting CSS coverage
The quickest way to check coverage is the CLI audit:
# Audit a single rune refrakt inspect hint --audit # Full-theme audit refrakt inspect --all --auditThis reports which generated selectors have matching CSS rules and which are missing. See the tooling guide for details on audit output and workflow.
For automated CI testing, you can add CSS coverage tests similar to Lumina's. Create
test/css-coverage.test.tsthat:- Reads the base config
- Parses your CSS files with PostCSS
- Asserts that expected selectors exist
See Lumina's
packages/lumina/test/css-coverage.test.tsfor the full pattern.
Using your theme in a site
Import the theme config and CSS in your project. The exact integration depends on your adapter — see the SvelteKit adapter and HTML adapter pages for setup details.
Both adapters use the same theme config:
import { myThemeConfig } from '@my-org/my-theme/transform';
import { createTransform } from '@refrakt-md/transform';
const transform = createTransform(myThemeConfig);
Final directory structure
packages/my-theme/
├── src/
│ └── config.ts
├── svelte/
│ └── index.ts
├── tokens/
│ ├── base.css
│ └── dark.css
├── styles/
│ ├── global.css
│ ├── dimensions/ # Generic cross-rune rules
│ │ ├── metadata.css
│ │ ├── density.css
│ │ ├── sections.css
│ │ ├── state.css
│ │ ├── media.css
│ │ ├── surfaces.css
│ │ ├── checklist.css
│ │ └── sequence.css
│ ├── runes/ # Per-rune overrides
│ │ ├── hint.css
│ │ ├── grid.css
│ │ └── ...
│ └── layouts/
│ ├── mobile.css
│ └── on-this-page.css
├── test/
│ └── css-coverage.test.ts
├── index.css
├── manifest.json
├── tsconfig.json
└── package.json