Theme Development Guide
Guide for theme designers, UI/UX designers, and front-end engineers who want to build Traven themes from scratch, customize an existing skin, or ship a Traven-aware theme for a CMS or static site.
Traven's skinning model is intentionally decoupled: themes are plain CSS files with no JavaScript and no build step. The editor engine and the shortcode widgets are class-driven, so every visual decision — fonts, colors, borders, spacing, alignment, and dark-mode behavior — lives in your theme. The trade-off is that the WYSIWYM (live) editor and the HTML preview share the same content but use two different DOM scopes, and a complete theme must style both.
This document is a comprehensive guide to styling Traven, detailing the side-by-side skin comparisons, the canonical selector reference, a "what every theme must include" QA checklist, and recipes for styling elements in both the editor and preview DOM scopes (including the raw Markdown pane, Vim's fat cursor, scrollbars, and LaTeX math widgets).
Quick Start: How to Build or Extend a Skin
Building a Skin from Scratch
- Duplicate a Base Theme: Copy
packages/core/assets/skins/skin-light.cssand rename it (e.g.,skin-forest.css). - Define Fonts and Variables: Import your preferred typography (e.g., from Google Fonts, or locally loaded for better reader privacy and no telemetry) and update the main CSS color variables.
- Adjust Editor Typography: Map heading styles and inline code elements. Make sure to apply
!importantto headings padding. - Set Up Shortcode Styles: Target the CodeMirror widget containers (
.cm-wysiwym-*) and the HTML Preview equivalents (.traven-preview *) using the cheat sheet selectors. - Test Dark Mode: Ensure variables and overrides resolve correctly when
.cm-wysiwym-darkis toggled. - Load the Skin: Link your stylesheet in the
<head>of your host application:<link rel="stylesheet" href="packages/core/assets/skins/skin-forest.css">
Extending an Existing Skin
If you just want to tweak colors or minor settings on an existing skin without modifying its source file:
- Create a custom override stylesheet (e.g.,
theme-overrides.css). - Import or load the main skin first:
<link rel="stylesheet" href="packages/core/assets/skins/skin-editorial.css"> <link rel="stylesheet" href="theme-overrides.css"> - Apply specific overrides using CSS variables or classes:
/* Tweak Editorial theme default link colors */ .cm-wysiwym-link-anchor, .traven-preview a { color: #059669 !important; /* Emphatic emerald green */ }
1. Mental model: two scopes, one CSS file
A Traven skin is a single .css file that targets two cooperating DOM trees rendered by the same editor:
graph TD
A[packages/core/assets/skins/skin-yourname.css] -->|styles| B[Editor Scope: .cm-editor]
A -->|styles| C[Preview Scope: .traven-preview]
B -->|WYSIWYM| W[Writer's live view]
C -->|Compiled HTML| P[Reader's preview]
A -.->|optional| D[Raw Markdown Pane: .raw-editor-mount]
A -.->|always| E[Dark-mode: .cm-wysiwym-dark]
- Editor scope (
.cm-editor) — the live WYSIWYM canvas. Traven translates Markdown tokens into visual elements using CodeMirror 6 decorations. Live editor classes are prefixed with.cm-wysiwym-*(for example.cm-wysiwym-bold,.cm-wysiwym-blockquote,.cm-wysiwym-inline-code). - Preview scope (
.traven-preview) — the compiled HTML output displayed in the preview pane. It is styled using normal native HTML selectors nested inside the wrapper class (for example.traven-preview h1,.traven-preview blockquote,.traven-preview img). - Raw editor scope (
.raw-editor-mount) — optional split-pane that shows raw Markdown source. Most themes restyle the raw pane to use a monospace font. - Dark mode — controlled by the
.cm-wysiwym-darkclass on the editor host node (and the preview container, when present). See §7 for details.
IMPORTANT: A complete theme styles both the editor and the preview. If a heading looks right in the live editor but cropped in the preview, or vice-versa, the writer will assume the theme is broken.
2. The eight shipping skins, side by side
All eight skins live in packages/core/assets/skins/ and are auto-discovered by the customization dropdown (includes/_customization-dropdowns.php); any new file you add to that directory shows up in the demo page's skin picker with no further wiring.
| Skin | Design intent | Body font | Mono font | Gutter | Headings | Caret | Accent |
|---|---|---|---|---|---|---|---|
skin-starter.css ⭐ |
Modern Georgia — base (bundled default) | Georgia (serif) |
System monospace | Visible, soft slate | System sans, bold | Slate 900 | Slate 600 |
skin-light.css |
Neutral Slate — baseline | Atkinson Hyperlegible Next (Google Fonts) |
Fira Code (Google Fonts) |
Visible, soft slate | Bold sans, tight | Slate 900 | Slate 600 |
skin-colorful.css |
Warm Rust — expressive | Atkinson Hyperlegible Next (Google Fonts) |
Fira Code (Google Fonts) |
Visible, indigo active | Bold sans, bright rust caret | #cc4a0a rust |
#3b82f6 indigo |
skin-dark.css |
Premium Dark Slate | Atkinson Hyperlegible Next (Google Fonts) |
Fira Code (Google Fonts) |
Visible, dim slate | Bold sans, near-white | Sky #38bdf8 |
Sky #38bdf8 |
skin-editorial.css |
Minimalist Focus — no chrome | Goudy Bookletter 1911 (serif) |
Victor Mono |
Visible, soft slate | Macondo (h1–h3), Goudy (h4–h6) |
Ink black | None — pure paper |
skin-modern.css |
Modern Clean — premium | Epunda Slab (serif) |
JetBrains Mono |
Visible, borderless | Saira Condensed (sans) |
Zinc 900 | #115e59 teal |
skin-academic.css |
Classic LaTeX Booktabs | Source Serif 4 (serif) |
Courier Prime |
Visible, soft slate | Serif, booktabs tables | Oxblood #7a2226 |
Oxblood #7a2226 |
skin-custom.css |
Dynamic parameterized fonts | Configurable via --traven-font-body |
Configurable via --traven-font-mono |
Inherits starter | Configurable via --traven-font-display |
Slate 900 | Slate 600 |
Other dimensions worth knowing:
- External requests.
skin-light.css,skin-dark.css,skin-colorful.css,skin-editorial.css,skin-modern.css, andskin-academic.css@importfrom Google Fonts by default.skin-starter.cssandskin-custom.cssload zero web fonts on initial request. Custom font loading forskin-custom.cssis handled dynamically at runtime by the host page. See §10 for self-hosting setups. - First-load fonts.
skin-light.css,skin-dark.css, andskin-academic.cssimport Atkinson Hyperlegible, Fira Code, Source Serif 4, and/or Courier Prime from Google Fonts.skin-starter.cssuses system fonts only. - Blockquote treatment. The
skin-light,skin-colorful,skin-dark,skin-modern, andskin-starterthemes use a thick left bar. Theskin-editorialtheme uses a decorative::beforecurly-quote mark. Theskin-academictheme uses a transparent background with a simple left bar. - Info / warning cards. The
skin-light,skin-colorful,skin-dark,skin-modern, andskin-starterthemes render these as soft rounded/bordered cards. Theskin-editorialtheme uses the "hand-drawn" organic border-radius. Theskin-academictheme uses clean rectangular left-bordered callout boxes matching the academic position paper callout style. - Pullquote dividers. Only the
skin-editorialtheme renders the decorative wave SVG above and below.traven-component-pullquote; the other themes use simple top/bottom rules.
When in doubt, copy the theme that is closest to your goal and patch its variables.
3. The selector reference
The list below covers every selector a complete theme should consider. Anything in bold is required; anything in italics is optional but recommended; the rest is "if the feature is visible in your demo, style it."
3.1 Editor scope — .cm-editor
Chrome
| Selector | Required? | Notes |
|---|---|---|
.cm-editor |
Yes | Base wrapper. Set font-family, background-color, color. |
.cm-editor:focus, .cm-editor.cm-focused |
Recommended | Strip the default focus ring. |
.cm-scroller |
Recommended | Inner scroll viewport. Inherit font, set line-height and vertical padding. |
.cm-content |
Recommended | Sets horizontal padding (typical: 0 24px). |
.cm-line |
Optional | General line. Never apply padding: 0 here unless you also re-pad every heading variant. |
.cm-gutters, .cm-gutterElement |
Optional | Line-number column. Hide with display: none !important for a paper feel. |
.cm-activeLine |
Optional | Soft tint of the active line. |
.cm-activeLineGutter |
Optional | Active line indicator in the gutter. |
.cm-selectionBackground, .cm-native-selection |
Yes | Selection highlight. The focused variant uses .cm-editor.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground. |
.cm-cursor |
Yes | Caret color. |
.cm-fat-cursor |
Yes | Vim Normal-mode block cursor. Always set opacity: 0.6 !important so the character underneath is readable. |
Scrollbars: .cm-editor ::-webkit-scrollbar* |
Optional | Skinnable per theme. |
Inline marks (all required)
| Selector | Purpose |
|---|---|
.cm-wysiwym-bold |
Bold text |
.cm-wysiwym-italic |
Italic text |
.cm-wysiwym-strikethrough |
|
.cm-wysiwym-highlight |
==highlight== mark |
.cm-wysiwym-inline-code |
`inline code` pill |
.cm-wysiwym-link-anchor |
Live link anchor (Ctrl/Cmd-click) |
.cm-wysiwym-bullet |
Custom list bullet glyph |
cmt-heading, tok-heading, span inside .cm-wysiwym-h1–h6 |
Strip any default underline from the highlighter. |
Block-level live elements
| Selector | Purpose |
|---|---|
.cm-wysiwym-h1 … .cm-wysiwym-h6 |
Live heading lines (one class per level). Use padding (not margin) and add !important. |
.cm-wysiwym-blockquote |
Live blockquote lines. Use the :has() sibling selectors (see §4.2) to style first, middle, and last lines. |
.cm-wysiwym-frontmatter |
YAML frontmatter block. |
.cm-wysiwym-frontmatter-active |
Active frontmatter line (the one the cursor is on). |
.cm-wysiwym-hr-widget |
Horizontal rule replacement. |
.cm-wysiwym-codeblock-line |
One line inside a fenced code block. Use -first / -last modifiers for rounded corners. |
.cm-wysiwym-collapsed-fence |
A fence line whose text is hidden but the empty container still occupies space. Set height: 0 !important. |
.cm-wysiwym-image-widget-container |
Legacy plain  Markdown image widget. |
.cm-wysiwym-image-shortcode-container |
Advanced [image ...] shortcode widget. Same alignment helpers (.align-left, etc.) as the preview. |
.cm-wysiwym-image-caption |
Caption text under the legacy image widget. |
.cm-wysiwym-image-shortcode-container .shortcode-meta |
The meta row under the advanced shortcode widget. |
.cm-wysiwym-image-shortcode-container .meta-badge |
"Tag name" pill ([IMAGE], etc.). Use .tag-name for the first badge. |
.cm-wysiwym-image-uploading |
Optimistic upload pill (green dashed border). |
.cm-wysiwym-video-shortcode-container |
[video] widget. |
.cm-wysiwym-video-shortcode-container .video-placeholder, .video-placeholder-icon-wrap, .video-placeholder-details, .video-placeholder-platform, .video-placeholder-url |
Pieces of the video placeholder card. |
.cm-wysiwym-audio-shortcode-container |
[audio] widget, same shape as the video container. |
.cm-wysiwym-component-shortcode |
Generic [component] block, plus variants .component-blockquote, .component-pullquote, .component-info, .component-warning. |
.cm-wysiwym-component-shortcode .component-body |
Inner body container. |
.cm-wysiwym-component-shortcode .component-body p |
Paragraphs inside the body. |
.cm-wysiwym-component-shortcode cite |
The "— Author, Source" line on blockquote components. |
.cm-wysiwym-figure-shortcode |
[figure] widget. Contains .component-body and .figure-caption. |
.cm-wysiwym-table-row |
GFM table source line in raw editing mode. |
.cm-wysiwym-table-widget |
Rendered WYSIWYM table (when the cursor is outside). Style .cm-wysiwym-table-widget table, th, td normally. |
.cm-wysiwym-inline-math-widget |
Live LaTeX inline equation. |
.cm-wysiwym-block-math-widget |
Live LaTeX display equation. |
.katex-inline-fallback, .katex-display-fallback |
Fallback rendering when KaTeX is not loaded. |
Hover-edit icons
All block widgets have an absolute-positioned icon that appears on hover:
.cm-wysiwym-image-shortcode-container .image-edit-icon.cm-wysiwym-video-shortcode-container .video-edit-icon.cm-wysiwym-audio-shortcode-container .audio-edit-icon.cm-wysiwym-component-shortcode .image-edit-icon.cm-wysiwym-figure-shortcode .figure-edit-icon
Style them like 24 × 24 px round buttons with a subtle border, hidden by opacity: 0 and shown on :hover of the parent.
Syntax highlighting (optional)
The CodeMirror Markdown highlighter tags tokens with classes in two parallel namespaces: tok-* and cmt-*. Style both for safety. The complete list is short:
tok-markup/cmt-markup—*,_,~~,`markerstok-meta/cmt-metatok-punctuation/cmt-punctuationtok-list/cmt-list— list bullet characterstok-comment/cmt-comment—<!-- … -->tok-keyword/cmt-keywordtok-string/cmt-stringtok-number/cmt-number,tok-boolean/cmt-booleantok-variableName/cmt-variableNametok-propertyName/cmt-propertyNametok-operator/cmt-operatortok-url/cmt-url,tok-link/cmt-link
The shipping skins style only the markdown-relevant subset (markup, meta, punctuation, list, comment, keyword, string, number). For a content-focused theme that is plenty.
3.2 Raw editor scope — .raw-editor-mount
If the host page mounts a raw Markdown pane (most demos do), the entire CodeMirror tree lives inside a .raw-editor-mount wrapper. The default themes override the font and line height:
.raw-editor-mount .cm-editor,
.raw-editor-mount .cm-content,
.raw-editor-mount .cm-scroller,
.raw-editor-mount .cm-line {
font-family: 'Victor Mono', Courier, monospace !important;
font-size: 14px !important;
line-height: 2 !important;
background-color: transparent !important;
color: #1a1a1a !important;
}
Themes that keep the same mono font as the inline code (e.g. Fira Code) can drop this block, but the raw pane should always use monospace at a slightly larger size with generous line-height for readability.
3.3 Preview scope — .traven-preview
Targets compiled HTML. Almost all of these are standard selectors nested inside .traven-preview.
| Selector | Maps to |
|---|---|
.traven-preview |
Preview wrapper. Set the base font, color, and background. |
.traven-preview h1 … h6 |
Compiled headings. Set sizes, weights, margins, and family. |
.traven-preview > h1:first-child, > h2:first-child, > h3:first-child |
First-child headings — match the editor's padding-top to avoid a visual jump. |
.traven-preview p |
Paragraphs. line-height and margin-bottom only. |
.traven-preview ul, ol, li |
Lists. padding-left, li::marker (color). |
.traven-preview blockquote:not(.traven-component-pullquote) |
Native blockquotes. (The :not() keeps the [pullquote] shortcode from picking these styles up.) |
.traven-preview pre, .traven-preview code |
Code blocks and inline code. |
.traven-preview a |
Links. |
.traven-preview mark |
==highlight== output. |
.traven-preview hr |
Horizontal rule. |
.traven-preview table, th, td |
GFM tables. Set height: 38px on th/td to keep blank cells from collapsing. |
.traven-preview img.traven-image-shortcode |
The advanced image shortcode, no caption. |
.traven-preview figure.traven-image-figure |
The advanced image shortcode, with caption. |
.traven-preview figure.traven-image-figure figcaption.traven-image-caption |
Caption text. |
.traven-preview .traven-video-container, figure.traven-video-figure |
[video] output. |
.traven-preview figure.traven-video-figure .traven-video-container, figcaption.traven-video-caption |
Inner video container + caption. |
.traven-preview .traven-audio-container, figure.traven-audio-figure |
[audio] output. |
.traven-preview .traven-audio-figure figcaption.traven-audio-caption |
Audio caption. |
.traven-preview .traven-component |
Generic [component] card wrapper. |
.traven-preview .traven-component-blockquote |
The [quote] / [blockquote] / [component="blockquote"] block. |
.traven-preview .traven-component-blockquote::before |
Decorative opening quote (the editorial/write themes draw it with a ::before; default/colorful/dark omit it). |
.traven-preview .traven-component-blockquote footer, cite |
The optional citation. |
.traven-preview .traven-component-pullquote |
The [pullquote] block. Optional decorative ::before / ::after. |
.traven-preview .traven-component-info |
The [info] notice block. |
.traven-preview .traven-component-warning |
The [warning] notice block. |
.traven-preview .traven-figure |
The [figure] block wrapper. |
.traven-preview .traven-figure-caption |
The caption inside .traven-figure. |
.traven-preview .traven-figure.align-fullbleed |
Breakout. Apply the same 100vw / calc(-50vw + 50%) trick used elsewhere. |
.traven-preview figure.traven-image-figure figcaption.traven-image-caption |
Caption of the [image] figure. |
Alignment helpers (shared between editor and preview)
Every shortcode accepts align="left|right|center|fullbleed" and size="small|medium|large|full". Both the editor widget and the preview HTML emit these as classes, so a single rule typically covers both scopes:
.cm-wysiwym-image-shortcode-container.align-left,
.traven-preview figure.traven-image-figure.align-left { ... }
/* Or in a combined block: */
.cm-wysiwym-image-shortcode-container,
.cm-wysiwym-video-shortcode-container,
.cm-wysiwym-audio-shortcode-container,
.cm-wysiwym-figure-shortcode {
/* shared card chrome */
}
.size-small { width: 150px; }
.size-medium { width: 300px; }
.size-large { width: 600px; }
.size-full { width: 100%; }
WARNING: Inside the editor, alignment uses auto-margins (e.g.
margin: 0 auto 0 0 !important). Inside the preview, you may usefloat: left/right. Mixing them up breaks CodeMirror's coordinate mapping. See §4.4 for the full rule.
3.4 Modal dialog scope
Traven's modal overlay (link, image, video, audio, component, figure, table editor, help, etc.) is rendered in a separate overlay class .traven-modal-overlay. The default toolbar stylesheet (packages/core/assets/toolbars/toolbar-default.css) handles most of the modal chrome. A theme normally only needs to override these:
.traven-modal-overlay— outer backdrop..traven-modal-overlay.cm-wysiwym-dark— dark-mode modal..traven-modal-btn.btn-primary/.btn-secondary— action buttons..traven-modal-input,.traven-modal-textarea,.traven-modal-select— form fields..traven-modal-dropzone— drag-and-drop file zone.
If you ship a dark skin, the toolbar stylesheet's existing .traven-modal-overlay.cm-wysiwym-dark rules will look right if the theme's body background and text colors match the editor. If they don't, override the toolbar variables in your theme.
4. CodeMirror 6 layout-engine rules
A handful of CSS assumptions that work everywhere else on the web break the WYSIWYM editor. They are not bugs; they are consequences of how CodeMirror 6 measures characters and maps coordinates. Every shipping skin obeys them, and a custom theme that violates them will see cursor clicks land on the wrong lines.
4.1 Never use vertical margins on line elements
CodeMirror stacks .cm-line elements sequentially and re-measures the stack on every layout pass. A margin-top on a heading pushes the next line down by an amount CodeMirror does not see, so posAtCoords returns a stale position and clicks land above or below where the user actually clicked.
Always use padding and add !important so a generic .cm-line { padding: 0 } reset cannot win:
.cm-editor .cm-wysiwym-h1 {
font-size: 2em;
font-weight: 800;
line-height: 1.2;
padding-top: 24px !important;
padding-bottom: 12px !important;
margin: 0 !important;
}
The same rule applies to every other block element that lives on its own line: blockquotes, code-block lines, table rows, list items, and (in §4.4) block widgets.
4.2 Blockquotes and the :has() trick
A blockquote in CodeMirror is rendered as multiple .cm-line siblings that all share the .cm-wysiwym-blockquote class. The first line needs top padding, the last line needs bottom padding, and the middle lines need neither, otherwise you get a double-thick bar at the top.
The shipping skins use:
.cm-editor .cm-wysiwym-blockquote {
padding-top: 2px !important;
padding-bottom: 2px !important;
padding-left: 16px !important;
border-left: 3px solid #cbd5e1;
margin: 0 !important;
}
/* First line of the block */
.cm-editor .cm-wysiwym-blockquote { padding-top: 10px !important; }
/* Subsequent lines in the same block */
.cm-editor .cm-wysiwym-blockquote + .cm-wysiwym-blockquote { padding-top: 2px !important; }
/* Last line of the block */
.cm-editor .cm-wysiwym-blockquote:not(:has(+ .cm-wysiwym-blockquote)) {
padding-bottom: 10px !important;
}
The same trick is used in the preview: .traven-preview blockquote:not(.traven-component-pullquote) { ... } and the equivalent :not(:has(+ ...)) rule for the last line.
4.3 Blank-line collapse after headings
A blank Markdown line directly after a heading renders as a full-height empty .cm-line in the editor, but the preview parser collapses it away. The shipping themes close the gap with:
.cm-editor .cm-wysiwym-h1 + .cm-line:has(br:only-child),
.cm-editor .cm-wysiwym-h2 + .cm-line:has(br:only-child),
.cm-editor .cm-wysiwym-h3 + .cm-line:has(br:only-child) {
height: 0.6em !important;
min-height: 0.6em !important;
}
Apply the same pattern around blockquotes (+ .cm-line:has(br:only-child) before and after) and after code-block fences (see §4.5).
4.4 No floats or vertical margins in the editor
Block widgets (.cm-wysiwym-image-shortcode-container, .cm-wysiwym-video-shortcode-container, .cm-wysiwym-audio-shortcode-container, .cm-wysiwym-figure-shortcode, and the component shortcode variants) are rendered as CodeMirror block decorations. CodeMirror's coordinate mapping assumes they sit in the normal document flow.
-
No
float: leftorfloat: right— floated elements wrap text around themselves, which CodeMirror doesn't expect. The result: mouse clicks below a floated widget land on the wrong line. -
No
margin-topormargin-bottom— same family of issues as §4.1. -
Use auto-margins for horizontal alignment:
.cm-wysiwym-image-shortcode-container.align-left { margin: 0 auto 0 0 !important; } .cm-wysiwym-image-shortcode-container.align-right { margin: 0 0 0 auto !important; } .cm-wysiwym-image-shortcode-container.align-center { margin: 0 auto !important; } .cm-wysiwym-image-shortcode-container.align-fullbleed { width: 100% !important; margin: 0 !important; border: none !important; border-radius: 0 !important; } -
In the preview, floats are fine.
.traven-preview img.traven-image-shortcode.align-left { float: left; ... }works without breaking anything, because the preview is plain HTML and never goes through CodeMirror's coordinate mapper.
4.5 Fenced code blocks
When a fenced code block is collapsed (cursor outside), the opening and closing fence lines become empty .cm-line containers that still occupy vertical space. The shipping themes mark them with a .cm-wysiwym-collapsed-fence class and squash them:
.cm-editor .cm-wysiwym-collapsed-fence {
height: 0 !important;
min-height: 0 !important;
padding: 0 !important;
margin: 0 !important;
border: none !important;
overflow: hidden !important;
visibility: hidden !important;
}
For the first / last / middle content lines, use the -first / -last modifiers to apply rounded corners and border rules that stitch the block into a single visual unit.
4.6 Character width caching
CodeMirror measures and caches character widths on first paint. If your theme loads custom fonts asynchronously (Google Fonts, a self-hosted font-display: swap face, etc.), the cache can be populated with the fallback's widths and then never re-measured when the real font loads, causing cursor misalignment.
Mitigations:
-
Defer editor construction until fonts are ready:
document.fonts.ready.then(() => { new TravenEditor({ element: document.getElementById("editor") }); }); -
Avoid
font-display: swapfor the editor's body font. -
For self-hosted fonts, preload the
.woff2files in the<head>so the measurement cache is correct from the first paint.
For runtime font swapping (changing typefaces after the editor is mounted), see the Custom Typography Guide which covers the CSS custom property pattern (--traven-font-display, --traven-font-body, --traven-font-mono) and the requestMeasure() re-measurement step.
5. WYSIWYM/Preview parity
The preview and the editor should look as similar as possible. The shipping skins accomplish this with the following patterns.
5.1 First-child heading margin
CodeMirror's first line is flush with the editor's top; the heading's padding-top provides the breathing room. In the preview, the same heading's margin-top is collapsed with the preview container's padding, so the first heading appears at the wrong offset unless you compensate:
.cm-editor .cm-wysiwym-h1 { padding-top: 27px !important; }
.traven-preview > h1:first-child { margin-top: 27px !important; }
.traven-preview > h2:first-child { margin-top: 12px !important; }
.traven-preview > h3:first-child { margin-top: 4.5px !important; }
The numbers in editorial/write themes are larger because their padding-top values are larger; whatever you pick, the editor and the preview must agree.
5.2 Blockquote spacing parity
Reduce the preview's blockquote vertical neighbors with a :has() selector to mirror the editor's collapsed blank line:
.traven-preview p:has(+ blockquote:not(.traven-component-pullquote)),
.traven-preview ul:has(+ blockquote:not(.traven-component-pullquote)),
.traven-preview pre:has(+ blockquote:not(.traven-component-pullquote)) {
margin-bottom: 0.6em !important;
}
.traven-preview blockquote:not(.traven-component-pullquote) + p,
.traven-preview blockquote:not(.traven-component-pullquote) + pre {
margin-top: 0.6em !important;
}
The same pattern is used around the [component="blockquote"] and [info]/[warning] blocks. (Search your theme for :has(+ .traven- to see them.)
5.3 Tables
The live table widget (.cm-wysiwym-table-widget table) and the preview table (.traven-preview table) should share the same border colors, padding, and minimum cell height. All shipping themes set height: 38px on th and td so that an empty cell never collapses to zero.
5.4 Code blocks
Match the editor's .cm-wysiwym-codeblock-line colors and the preview's .traven-preview pre. The -first / -last borders on the editor side correspond to a border-top / border-bottom on the preview <pre>.
5.5 Font Family Inheritance on Nested Elements (The CSS Cascade Override)
In Traven, the default starter skin (skin-starter.css which is bundled inside dist/traven.css) defines high-priority overrides for standard body elements like paragraphs:
.traven-preview p {
font-family: var(--traven-font-body) !important;
}
If your custom theme has elements that wrap nested paragraphs — such as standard blockquotes, blockquote components ([component="blockquote"]), and notice blocks ([info], [warning]) — those nested paragraph tags will ignore the parent container's custom font declarations. Instead, they will inherit the starter skin's default body typeface (e.g. Georgia), breaking visual consistency.
The Fix
To enforce your theme's custom typography on all nested structures, apply the font-family declaration using a selector targeting both the parent container and all its descendants (using *) along with !important:
/* Editor scope */
.cm-wysiwym-component-shortcode.component-info,
.cm-wysiwym-component-shortcode.component-info * {
font-family: 'Atkinson Hyperlegible Next', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
}
/* Preview scope */
.traven-preview .traven-component-info,
.traven-preview .traven-component-info * {
font-family: 'Atkinson Hyperlegible Next', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
}
This ensures the cascade correctly forces nested block contents to render using the theme's designated body font.
6. Shortcode markup reference (cheat sheet)
The fallback renderer emits zero inline styles; every visual decision is delegated to your theme. This section is a copy of the canonical markup in docs/dev/shortcodestyles.css, condensed for theme authors. Use the comment blocks in that file as your authoritative reference.
6.1 [image ...]
<!-- No caption -->
<img class="traven-image-shortcode align-[alignment] size-[size] [custom]" src="..." alt="...">
<!-- With caption -->
<figure class="traven-image-figure align-[alignment] size-[size] [custom]">
<img class="traven-image-shortcode" src="..." alt="...">
<figcaption class="traven-image-caption">Caption</figcaption>
</figure>
- Editor wrapper:
.cm-wysiwym-image-shortcode-container - Preview wrapper:
.traven-preview img.traven-image-shortcode,.traven-preview figure.traven-image-figure - Caption:
figcaption.traven-image-caption - Alignment:
align-left/align-right/align-center/align-fullbleed - Sizes:
size-small(~150 px),size-medium(~300 px),size-large(~600 px),size-full(100%)
For align-fullbleed, use the standard 100vw breakout:
.traven-preview img.traven-image-shortcode.align-fullbleed,
.traven-preview figure.traven-image-figure.align-fullbleed {
width: 100vw !important;
max-width: 100vw !important;
margin-left: calc(-50vw + 50%) !important;
margin-right: calc(-50vw + 50%) !important;
border-radius: 0 !important;
display: block !important;
float: none !important;
clear: both !important;
}
6.2 [video ...]
<!-- YouTube / Vimeo / direct file, no caption -->
<div class="traven-video-container align-[a] size-[s] [custom]">
<iframe src="https://www.youtube.com/embed/..." ...></iframe>
<!-- or: -->
<video src="..." controls></video>
</div>
<!-- With caption -->
<figure class="traven-video-figure align-[a] size-[s] [custom]">
<div class="traven-video-container">
<iframe ...></iframe> <!-- or <video> -->
</div>
<figcaption class="traven-video-caption">Caption</figcaption>
</figure>
- Editor wrapper:
.cm-wysiwym-video-shortcode-container(plus.video-placeholder,.video-placeholder-icon-wrap,.video-placeholder-details,.video-placeholder-platform,.video-placeholder-url). - Preview wrapper:
.traven-preview .traven-video-container(16:9aspect-ratio),.traven-preview figure.traven-video-figure. - Caption:
figcaption.traven-video-caption.
6.3 [audio ...]
<div class="traven-audio-container align-[a] size-[s] [custom]">
<audio src="..." controls></audio>
</div>
<!-- With caption -->
<figure class="traven-audio-figure align-[a] size-[s] [custom]">
<div class="traven-audio-container">
<audio src="..." controls></audio>
</div>
<figcaption class="traven-audio-caption">Caption</figcaption>
</figure>
Same alignment helpers as image/video.
6.4 [figure ...]...[/figure]
Wraps arbitrary block content (tables, code blocks, images, etc.) in a captioned figure.
<figure class="traven-figure align-[a] size-[s] [custom]">
<!-- block content here -->
<figcaption class="traven-figure-caption">Caption</figcaption>
</figure>
- Editor wrapper:
.cm-wysiwym-figure-shortcode(with.component-bodyand.figure-caption). - Preview wrapper:
.traven-preview .traven-figure.
6.5 [component]...[/component] and its aliases
The [component] shortcode has a structural base class plus one variant class derived from the name attribute. Aliases normalize to the same variants:
| Author writes | Compiles to |
|---|---|
[component name="blockquote"]...[/component] |
.traven-component-blockquote |
[quote]...[/quote], [blockquote]...[/blockquote] |
.traven-component-blockquote |
[component="blockquote"]...[/component] |
.traven-component-blockquote |
[pullquote]...[/pullquote] |
.traven-component-pullquote |
[info]...[/info], [component="info"]...[/component] |
.traven-component-info |
[warning]...[/warning], [component="warning"]...[/component] |
.traven-component-warning |
[component="my-card"]...[/component] |
.traven-component.traven-component-my-card |
[highlight]...[/highlight] |
<mark>...</mark> |
HTML output by variant:
<!-- Blockquote (with author/source) -->
<blockquote class="traven-component-blockquote">
<p>Quote...</p>
<footer><cite>— Author, Source</cite></footer>
</blockquote>
<!-- Pullquote -->
<blockquote class="traven-component-pullquote"><p>Editorial emphasis.</p></blockquote>
<!-- Info / Warning notice -->
<div class="traven-component traven-component-info">
<p>Heads-up text…</p>
</div>
<!-- Generic / unknown name -->
<div class="traven-component traven-component-my-card">
<p>Anything you like.</p>
</div>
The editorial and write themes draw the [info] and [warning] blocks with the "hand-drawn" border radius:
.traven-preview .traven-component-info,
.traven-preview .traven-component-warning {
border: 2px solid #1a1a1a !important;
border-radius: 255px 15px 225px 15px/15px 225px 15px 255px !important;
}
The other themes use clean rounded rectangles. Either is a valid aesthetic — pick one and stay consistent.
6.6 LaTeX math (when enabled)
The math parser uses $...$ for inline and $$...$$ for display. When KaTeX is loaded the output is .katex / .katex-display. When it isn't, the fallback classes are:
.katex-inline-fallback— inline equation rendered as monospaced text on a soft background..katex-display-fallback— display equation, centered, monospaced, with rounded background.
In the editor, the live widgets are:
.cm-wysiwym-inline-math-widget.cm-wysiwym-block-math-widget
The shipping editorial and write themes define these; the default/colorful/dark themes inherit the rules from the built-in dist/traven.css.
7. Dark mode
Dark mode is decoupled between the editor and the surrounding page. The editor's toggle adds a .cm-wysiwym-dark class to its own host DOM nodes (and, in split-pane setups, to the preview container too). Themes handle the resulting rules in two ways.
7.1 The minimal approach (used by the default skin)
Define light values at the top of the stylesheet and re-declare the same selectors with the dark prefix for the dark variants. The shipping pattern is to keep both halves of every selector adjacent, e.g.:
/* Light */
.cm-editor .cm-wysiwym-blockquote { border-left-color: #cbd5e1; color: #475569; }
.cm-editor .cm-wysiwym-blockquote,
.cm-editor .cm-wysiwym-blockquote * { color: #475569; }
/* Dark — same selectors under .cm-editor.cm-wysiwym-dark */
.cm-editor.cm-wysiwym-dark .cm-wysiwym-blockquote { border-left-color: #475569; }
.cm-editor.cm-wysiwym-dark .cm-wysiwym-blockquote,
.cm-editor.cm-wysiwym-dark .cm-wysiwym-blockquote * { color: #cbd5e1 !important; }
This is verbose but explicit, and every rule is right next to its light counterpart — easy to diff.
7.2 The CSS-variables approach (recommended for new themes)
Define a small palette of variables at :root and re-declare them under the dark class. Every other rule references the variables. The cheat-sheet at docs/dev/shortcodestyles.css demonstrates this pattern with a --traven-color-* family:
:root {
--traven-color-text-main: #0f172a;
--traven-color-text-muted: #64748b;
--traven-color-text-blockquote: #475569;
--traven-color-border: #e2e8f0;
--traven-blockquote-border: #cbd5e1;
--traven-color-info-border: #1a1a1a;
--traven-color-info-bg: #fafafa;
--traven-color-warning-border: #1a1a1a;
--traven-color-warning-bg: rgba(250, 204, 21, 0.25);
}
.cm-wysiwym-dark,
.traven-preview.cm-wysiwym-dark {
--traven-color-text-main: #f8fafc;
--traven-color-text-muted: #94a3b8;
--traven-color-text-blockquote: #cbd5e1;
--traven-color-border: #334155;
--traven-blockquote-border: #475569;
/* …dark info / warning overrides… */
}
.traven-preview .traven-component-blockquote {
border-left: 3px solid var(--traven-blockquote-border) !important;
color: var(--traven-color-text-blockquote);
}
You can drive the same variables with @media (prefers-color-scheme: dark) to get an automatic dark mode that does not depend on the editor's toggle, but be aware that the editor's class-based toggle wins when both are active.
7.3 The two dark scopes are independent
- Editor dark: scoped under
.cm-editor.cm-wysiwym-dark …. - Preview dark: scoped under
.traven-preview.cm-wysiwym-dark ….
A complete theme declares both. If you only style one, the editor and the preview will drift out of sync when the user toggles dark mode.
7.4 Dark-mode pitfalls
-
The Vim "fat cursor" (
.cm-fat-cursor) is shared across modes; always setopacity: 0.6 !importanton it (the base stylesheet does this for you, but reinforce it in your theme if you override background colors). -
Selection colors: the focused and unfocused variants use different selector paths. Mirror both:
.cm-editor.cm-wysiwym-dark .cm-selectionBackground, .cm-editor.cm-wysiwym-dark .cm-native-selection { background-color: rgba(56, 189, 248, 0.35) !important; } .cm-editor.cm-wysiwym-dark.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground { background-color: rgba(56, 189, 248, 0.6) !important; } -
The "active" frontmatter line uses a different border color than the inactive variant — see
.cm-wysiwym-frontmatter-active. -
The image / video / audio / figure / component hover edit icons get their own dark backgrounds. The shipping themes re-tint them per skin.
8. Building a new theme
8.1 File placement
Drop a new file into packages/core/assets/skins/ with the skin- prefix:
packages/core/assets/skins/skin-aurora.css
The dropdown script (includes/_customization-dropdowns.php) auto-discovers any *.css file in that directory, sorts skin-light first and everything else alphabetically, and registers a <select> option labeled "Aurora Skin" (it strips the prefix, title-cases the remainder, and appends "Skin"). For the new option to actually load when picked, the host page must contain a <link id="editor-skin-link" rel="stylesheet" href="packages/core/assets/skins/skin-light.css"> tag; the demo's applySelection() script will rewrite that href at runtime.
8.2 Walkthrough: from zero to a working skin
The minimum viable skin is short:
/* 1. Optional font import (or use a system stack). */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&family=JetBrains+Mono:wght@400;700&display=swap');
/* 2. Editor base. */
.cm-editor {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
background-color: #fbfaf6 !important;
color: #1f1d1a !important;
}
.cm-editor:focus, .cm-editor.cm-focused { outline: none !important; }
.cm-scroller { font-family: inherit !important; line-height: 1.7 !important; padding: 12px 0 !important; }
.cm-content { padding: 0 28px !important; }
/* 3. Selection, caret, fat cursor. */
.cm-selectionBackground, .cm-native-selection {
background-color: rgba(180, 132, 60, 0.18) !important;
}
.cm-editor.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground {
background-color: rgba(180, 132, 60, 0.28) !important;
}
.cm-editor .cm-cursor { border-left-color: #1f1d1a !important; }
.cm-editor .cm-fat-cursor { background-color: #1f1d1a !important; opacity: 0.6 !important; }
/* 4. Inline marks. */
.cm-wysiwym-bold { font-weight: 700; color: #0e0d0b; }
.cm-wysiwym-italic { font-style: italic; color: #2a2724; }
.cm-wysiwym-strikethrough { text-decoration: line-through; color: #6c6660; }
.cm-wysiwym-highlight { background-color: rgba(247, 200, 80, 0.45); border-radius: 3px; padding: 1px 0; }
.cm-wysiwym-inline-code {
font-family: 'JetBrains Mono', ui-monospace, monospace !important;
background-color: #f3efe6;
color: #1f1d1a;
padding: 2px 6px; border-radius: 6px; font-size: 0.88em; font-weight: 500;
border: 1px solid #e6dfd1;
}
/* 5. Headings — padding only, !important, never margin. */
.cm-editor .cm-wysiwym-h1, .cm-editor .cm-wysiwym-h2,
.cm-editor .cm-wysiwym-h3, .cm-editor .cm-wysiwym-h4,
.cm-editor .cm-wysiwym-h5, .cm-editor .cm-wysiwym-h6 {
font-family: 'Inter', sans-serif; color: #0e0d0b;
letter-spacing: -0.02em; text-decoration: none;
}
.cm-editor .cmt-heading, .cm-editor .tok-heading,
.cm-editor .cm-wysiwym-h1 span, .cm-editor .cm-wysiwym-h2 span,
.cm-editor .cm-wysiwym-h3 span, .cm-editor .cm-wysiwym-h4 span,
.cm-editor .cm-wysiwym-h5 span, .cm-editor .cm-wysiwym-h6 span {
text-decoration: none;
}
.cm-editor .cm-wysiwym-h1 { font-size: 2em; font-weight: 800; line-height: 1.2; padding-top: 24px !important; padding-bottom: 12px !important; margin: 0; }
.cm-editor .cm-wysiwym-h2 { font-size: 1.5em; font-weight: 700; line-height: 1.3; padding-top: 20px !important; padding-bottom: 10px !important; margin: 0; }
.cm-editor .cm-wysiwym-h3 { font-size: 1.25em; font-weight: 600; line-height: 1.4; padding-top: 18px !important; padding-bottom: 8px !important; margin: 0; }
.cm-editor .cm-wysiwym-h4 { font-size: 1.1em; font-weight: 600; line-height: 1.5; padding-top: 14px !important; padding-bottom: 6px !important; margin: 0; }
.cm-editor .cm-wysiwym-h5 { font-size: 1em; font-weight: 600; line-height: 1.5; padding-top: 12px !important; padding-bottom: 4px !important; margin: 0; }
.cm-editor .cm-wysiwym-h6 { font-size: 0.9em; font-weight: 600; line-height: 1.5; padding-top: 10px !important; padding-bottom: 2px !important; margin: 0; }
/* 6. Blockquote — first / mid / last using :has() (see §4.2). */
.cm-editor .cm-wysiwym-blockquote {
border-left: 3px solid #c8b78a;
padding-top: 2px !important; padding-bottom: 2px !important;
padding-left: 16px !important; margin: 0 !important; position: relative;
}
.cm-editor .cm-wysiwym-blockquote { padding-top: 10px !important; }
.cm-editor .cm-wysiwym-blockquote + .cm-wysiwym-blockquote { padding-top: 2px !important; }
.cm-editor .cm-wysiwym-blockquote:not(:has(+ .cm-wysiwym-blockquote)) { padding-bottom: 10px !important; }
.cm-editor .cm-wysiwym-blockquote,
.cm-editor .cm-wysiwym-blockquote * { font-style: italic; color: #5b4f3c; line-height: 1.5 !important; }
/* 7. HR, code blocks, and the collapsed-fence squash. */
.cm-wysiwym-hr-widget { border: 0; border-top: 2px dashed #c8b78a; margin: 0 !important; padding: 12px 0 !important; display: block; width: 100%; }
.cm-editor .cm-wysiwym-codeblock-line {
background-color: #f3efe6; font-family: 'JetBrains Mono', monospace !important;
font-size: 0.88em; padding-left: 24px !important; padding-right: 24px !important;
}
.cm-editor .cm-wysiwym-codeblock-line-first { padding-top: 14px !important; border-top-left-radius: 10px; border-top-right-radius: 10px; border: 1px solid #e6dfd1; border-bottom: none; }
.cm-editor .cm-wysiwym-codeblock-line-last { padding-bottom: 14px !important; border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; border: 1px solid #e6dfd1; border-top: none; }
.cm-editor .cm-wysiwym-codeblock-line:not(.cm-wysiwym-codeblock-line-first):not(.cm-wysiwym-codeblock-line-last) { border-left: 1px solid #e6dfd1; border-right: 1px solid #e6dfd1; }
.cm-editor .cm-wysiwym-collapsed-fence {
height: 0 !important; min-height: 0 !important; padding: 0 !important; margin: 0 !important;
border: none !important; overflow: hidden !important; visibility: hidden !important;
}
/* 8. Empty-line collapse after headings. */
.cm-editor .cm-wysiwym-h1 + .cm-line:has(br:only-child),
.cm-editor .cm-wysiwym-h2 + .cm-line:has(br:only-child),
.cm-editor .cm-wysiwym-h3 + .cm-line:has(br:only-child) { height: 0.6em !important; min-height: 0.6em !important; }
/* 9. Shortcode widgets — image, video, audio, figure, component. */
/* (Use the editor auto-margin alignment helpers from §4.4.) */
.cm-wysiwym-image-shortcode-container,
.cm-wysiwym-video-shortcode-container,
.cm-wysiwym-audio-shortcode-container,
.cm-wysiwym-figure-shortcode {
display: flex; flex-direction: column; max-width: 100%;
margin: 0 !important; padding: 12px 0;
border: 1px solid #e6dfd1; border-radius: 12px; overflow: hidden;
background-color: #ffffff; position: relative;
}
.size-small { width: 150px; }
.size-medium { width: 300px; }
.size-large { width: 600px; }
.size-full { width: 100%; }
.align-left { margin: 0 auto 0 0 !important; }
.align-right { margin: 0 0 0 auto !important; }
.align-center { margin: 0 auto !important; }
.align-fullbleed { width: 100% !important; margin: 0 !important; border: none !important; border-radius: 0 !important; }
/* 10. Components. */
.cm-wysiwym-component-shortcode {
position: relative; margin: 0 !important; padding: 28px 16px !important;
border-left: 4px solid #c8b78a; background-color: #fbfaf6; color: #1f1d1a;
}
.cm-wysiwym-component-shortcode.component-info {
border: 1px solid #e6dfd1 !important; border-left: 4px solid #2f80ed !important;
border-radius: 8px !important; background-color: #f3f8ff !important;
color: #1f1d1a !important; padding: 16px !important; margin: 0 !important;
}
.cm-wysiwym-component-shortcode.component-warning {
border: 1px solid #f6e0a4 !important; border-left: 4px solid #d4a017 !important;
border-radius: 8px !important; background-color: rgba(247, 200, 80, 0.18) !important;
color: #1f1d1a !important; padding: 16px !important; margin: 0 !important;
}
.cm-wysiwym-component-shortcode.component-pullquote {
border-left: none !important; border-top: 2px solid #c8b78a !important;
border-bottom: 2px solid #c8b78a !important; padding: 24px 16px !important;
margin: 0 !important; font-size: 1.2em !important; font-weight: bold;
text-align: center; color: #0e0d0b; background-color: transparent !important;
}
/* 11. Preview parity (mirror every visual decision above for .traven-preview). */
.traven-preview { background-color: #fbfaf6; color: #1f1d1a; font-family: 'Inter', sans-serif; }
.traven-preview h1, .traven-preview h2, .traven-preview h3,
.traven-preview h4, .traven-preview h5, .traven-preview h6 {
font-family: 'Inter', sans-serif; color: #0e0d0b; letter-spacing: -0.02em;
margin-top: 24px; margin-bottom: 12px; line-height: 1.25;
}
.traven-preview > h1:first-child { margin-top: 24px !important; }
.traven-preview > h2:first-child { margin-top: 20px !important; }
.traven-preview > h3:first-child { margin-top: 18px !important; }
.traven-preview p { margin-top: 0; margin-bottom: 16px; line-height: 1.7; }
.traven-preview blockquote:not(.traven-component-pullquote) { border-left: 3px solid #c8b78a; padding: 10px 16px; font-style: italic; color: #5b4f3c; }
.traven-preview code { font-family: 'JetBrains Mono', monospace; background-color: #f3efe6; padding: 2px 6px; border-radius: 4px; border: 1px solid #e6dfd1; }
.traven-preview pre { background-color: #f3efe6; border: 1px solid #e6dfd1; border-radius: 10px; padding: 14px 20px; overflow-x: auto; margin: 16px 0; }
.traven-preview a { color: #8a5a2c; text-decoration: underline; }
.traven-preview hr { border: none; border-top: 1px solid #e6dfd1; margin: 24px 0; }
.traven-preview mark { background-color: rgba(247, 200, 80, 0.45); border-radius: 3px; padding: 1px 4px; }
/* …and one block per preview shortcode, mirroring the editor rules. */
/* 12. Dark mode — re-declare each rule under .cm-editor.cm-wysiwym-dark
and .traven-preview.cm-wysiwym-dark, or use CSS variables (see §7.2). */
That is the entire skeleton. A real theme fills in the remaining shortcodes, the bullet list marker, the frontmatter / table styles, the syntax highlighting tokens, the modal overlay overrides, and the dark variants.
8.3 Theme checklist
Use this when reviewing a finished theme before publishing it.
-
.cm-editorand.traven-previewset the base font, color, and background. - Headings use
padding(nevermargin), with!important. - Blockquote first / mid / last lines are styled with
:has()siblings. - Blank line after headings collapses to ~0.6 em.
- No
floatand no verticalmarginon any.cm-wysiwym-*-containerin the editor; alignment uses auto-margins. -
.cm-wysiwym-collapsed-fenceis zero-height. -
.cm-wysiwym-h1….h6spandescendants don't have an underline. -
.cm-fat-cursorhasopacity: 0.6 !important. - All four image alignments and four image sizes work in both scopes.
- All four video and audio alignments and four sizes work in both scopes.
- The
[component]aliases (blockquote,pullquote,info,warning) are styled. - A
[component="my-custom-card"](generic / unknown) picks up the.traven-component+.traven-component-my-custom-cardstyling. -
figcaption.traven-image-caption,figcaption.traven-video-caption,figcaption.traven-audio-caption,.traven-figure-captionare styled. - First-child heading
margin-topmatches the editor'spadding-top(§5.1). -
.traven-previewfirst-child blockquote neighbors have reduced margins (§5.2). - Tables:
thandtdhaveheight: 38pxandbox-sizing: border-box. - Dark mode rules exist for every light rule that defines a color.
- The raw editor pane (if used) uses a monospace font.
- The "active" frontmatter variant is visibly different from the inactive one.
- The "uploading" pill is recognizable (green dashed border).
- All four hover edit icons are styled and dark-mode aware.
- Scrollbars (
:-webkit-scrollbar*) match the theme. - LaTeX math widgets (if KaTeX is enabled) are styled.
8.4 Loading the theme
Static load (recommended for production):
<link rel="stylesheet" href="packages/core/assets/skins/skin-aurora.css" id="editor-skin-link">
<link rel="stylesheet" href="packages/core/assets/toolbars/toolbar-default.css">
Dynamic load (matches the demos' skin dropdown):
<link rel="stylesheet" href="packages/core/assets/skins/skin-aurora.css" id="editor-skin-link">
<script>
document.getElementById("editor-skin-link").href =
"packages/core/assets/skins/" + (localStorage.getItem("traven-selected-skin") || "skin-aurora") + ".css";
</script>
The applySelection() helper bundled with the demo dropdowns does this for you — see includes/_customization-dropdowns.php.
9. Extending an existing skin
If you just want to recolor or restyle a few elements without forking the file:
-
Ship an override stylesheet that is loaded after the base skin:
<link rel="stylesheet" href="packages/core/assets/skins/skin-editorial.css"> <link rel="stylesheet" href="packages/core/assets/css/site-overrides.css"> -
Use
!importantto win specificity wars (the shipping skins use!importantheavily, which is intentional to defeat page-level resets). -
Prefer overriding CSS custom properties when the base skin exposes them. A future-friendly extension pattern is to wrap the base skin in your own override file that re-declares variables and a small number of selectors, like:
:root { --my-brand: #5e3023; --my-accent: #b6794d; } .cm-wysiwym-link-anchor, .traven-preview a { color: var(--my-brand) !important; } .cm-editor .cm-cursor, .cm-editor .cm-fat-cursor { background-color: var(--my-accent) !important; border-left-color: var(--my-accent) !important; } -
If you need a custom alignment or size (say
size-hero), add the new utility classes to your overrides and add the corresponding attribute to the toolbar modal for that shortcode.
10. Telemetry & offline self-hosting
Traven ships no analytics, no tracking, no cookies, and no third-party calls from the core editor.
That said, some themes load web fonts from Google Fonts. The default posture is:
| Skin | Network footprint | How to go offline |
|---|---|---|
skin-light.css |
Google Fonts CDN | Replace the @import url(...Google Fonts...) at the top of the file with @import url('/path/to/your/local-fonts.css'); (and provide your own binary files). |
skin-dark.css |
Google Fonts CDN | Same. |
skin-colorful.css |
Google Fonts CDN | Remove or replace the @import url(...Google Fonts...) at the top of the file with @import url('/path/to/your/local-fonts.css');. |
skin-editorial.css |
Google Fonts CDN | Same. |
skin-modern.css |
Google Fonts CDN | Same. |
skin-academic.css |
Google Fonts & jsDelivr (GitHub CDN) | Replace the @import urls at the top of the file with local paths. |
skin-custom.css |
None | Statically inherits local system fallback font stacks. Dynamics font injection is driven entirely by client-side JS on the host page. |
If you ship a custom skin, you decide whether to @import from a CDN. The recommended pattern for a privacy-respecting theme is the same as the dark/default skin: use a system font stack in the CSS, and let the host page load self-hosted fonts via <link rel="preload"> tags so that CodeMirror's first measurement cache is correct.
10.1 Dynamic Parameterized Font Customization
The custom overlay theme skin-custom.css implements font variables that can be overridden at runtime. This enables client-side selectors to hot-swap Display, Body, and Monospace fonts using standard CSS variables:
:root {
--traven-font-display: 'Your Custom Display Font', sans-serif;
--traven-font-body: 'Your Custom Body Font', serif;
--traven-font-mono: 'Your Custom Monospace Font', monospace;
}
To update fonts dynamically and ensure CodeMirror layout accuracy, perform the update and request a viewport measurement:
// 1. Swap font variables dynamically
document.documentElement.style.setProperty('--traven-font-body', "'Atkinson Hyperlegible Next', sans-serif");
// 2. Trigger CodeMirror re-measurement once fonts are fully rendered
document.fonts.ready.then(() => {
if (window.editor) {
const view = window.editor.getView();
if (view) view.requestMeasure();
}
});
11. Validation
You can sanity-check a finished skin without writing tests:
- Static check. Open the file and grep for the rules in the §8.3 checklist. Any miss is a bug.
- Visual check. Load the theme in one of the demos (
demo-inline.php,demo-form.php,demo-hybrid.php,demo-unified.php,demo-editorial.php). The default dropdown at the top will list the new file automatically. Toggle dark mode, scroll a long document, and visit each shortcode. - Cursor accuracy. Click around headings, blockquotes, and the shortcode widgets. The cursor should land precisely on the character under the mouse. If it doesn't, you almost certainly introduced a vertical margin or a
floatsomewhere — see §4. - Preview parity. Toggle the preview tab (or open the split-pane demo) and compare. Headings should align; first-child headings should not jump; blockquote spacing should match the editor.
- Build pipeline. The theme is referenced by
<link>tags in the demo pages only. Thedist/traven.cssbundle (built fromsrc/style.css) provides only the dark-mode base styles and the math / Vim / scrollbar rules; the live themes live entirely inpackages/core/assets/skins/. So you can iterate on a theme without ever runningnpm run build.
12. Reference: existing themes, file by file
A quick lookup of the assets that each shipping theme overrides or extends.
| Concern | skin-light |
skin-colorful |
skin-dark |
skin-editorial |
skin-modern |
skin-academic |
skin-custom |
|---|---|---|---|---|---|---|---|
External @import |
Google Fonts | Google Fonts | Google Fonts | Google Fonts | Google Fonts | Google Fonts + jsDelivr | None |
| Body font | Atkinson + system | Atkinson + system | Atkinson + system | Goudy Bookletter 1911 | Epunda Slab + system | Computer Modern Serif | Configurable |
| Mono font | Fira Code | Fira Code | Fira Code | Victor Mono | JetBrains Mono | Computer Modern Typewriter | Configurable |
| Gutter visible? | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Caret color | Slate 900 | Rust #cc4a0a |
Sky #38bdf8 |
Ink black | Zinc 900 | Oxblood #7a2226 |
Slate 900 |
| Selection color | Slate 18% | Indigo 15% | Sky 35% | Slate 18% | Teal 15% | Oxblood 22% | Slate 18% |
| Blockquote treatment | Left bar | Rust left bar | Slate left bar | Curly quote ::before |
Teal left bar | Muted left bar | Left bar |
| Pullquote treatment | Top/bottom rules | Top/bottom rust rules | Top/bottom slate rules | Wavy SVG lines | Top/bottom teal rules | Top/bottom rules | Top/bottom rules |
| Info / Warning cards | Soft rounded | Soft tinted | Slate variants | Hand-drawn border-radius | Teal / Orange borders | Clean left-border | Soft rounded |
| Image borders | 1 px slate, 12 px radius | Same | Same | Same | No border, 8px radius | No border | Same |
| Component blockquote | Slate left bar | Rust left bar | Slate left bar | Curly quote | Teal left bar | Muted left bar | Left bar |
| Math fallback colors | Inherits from base | Inherits from base | Inherits from base | Light slate | Light zinc | Inherits from base | Inherits from base |
| Hidden gutter padding | n/a | n/a | n/a | n/a | n/a | n/a | n/a |
| Raw editor override | n/a | n/a | n/a | Victor Mono 14 px / lh 2 | n/a | n/a | n/a |
When you build a new theme, picking a "donor" from this table gets you 80% of the way there.
13. Glossary
- WYSIWYM — "What You See Is What You Mean." Markdown syntax markers (the
**around bold, the[]around links, the```fences) collapse into a clean visual representation while the cursor is outside, and re-expand for editing when the cursor enters. Seedocs/key-features.mdand the knowledgebase §5. - Scope — a wrapper class that isolates CSS so the same selectors can mean different things in the live editor vs. the preview. The two scopes are
.cm-editorand.traven-preview. - Skin — a single CSS file under
packages/core/assets/skins/that styles a theme. Auto-discovered, hot-swappable at runtime. - Toolbar — a separate concern, governed by
packages/core/assets/toolbars/*.css. Toolbar styles live in their own files; seecustomization-styling.md. - Shortcode — a Traven-extended Markdown construct (
[image],[video],[audio],[figure],[component]) parsed by a custom Lezer grammar insrc/*.js. Shortcodes compile to clean semantic HTML in the preview. - Decoration — a CodeMirror 6 visual transformation of a range. Inlined in
wysiwym.js. Decorations can be marks (.cm-wysiwym-bold) or block-replacement widgets (the[image]card). - Dark class —
.cm-wysiwym-darktoggled on the editor host DOM bysetTheme("dark")and on the preview container by the demo's theme switcher.
14. Where to go next
customization-styling.md— toolbar styling, button CSS identifiers, how to hide buttons.shortcodes-architecture.md— what each shortcode compiles to, attribute parsing, and how to register a brand-new shortcode (parser, widget, and skin).shortcodes.md— technical blueprint for adding custom shortcode support, including the regex/scanner pattern used inwysiwym.js.../installation-setup.md— how to wire the editor into a host page.../api-reference.md— full constructor options and instance methods (notablysetTheme(),setVimMode(),getUploadHandler()).shortcodestyles.css— the canonical copy-paste cheat sheet for every shortcode's HTML output and CSS variables, kept in sync with the cheat sheet summary in §6.knowledgebase.md§2 and §6 — the engineering rules behind this guide (CodeMirror 6 pitfalls, dark-mode parity tips, list-parsing constraints). Read this if you plan to extend the editor itself.