Traven

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

  1. Duplicate a Base Theme: Copy packages/core/assets/skins/skin-light.css and rename it (e.g., skin-forest.css).
  2. 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.
  3. Adjust Editor Typography: Map heading styles and inline code elements. Make sure to apply !important to headings padding.
  4. Set Up Shortcode Styles: Target the CodeMirror widget containers (.cm-wysiwym-*) and the HTML Preview equivalents (.traven-preview *) using the cheat sheet selectors.
  5. Test Dark Mode: Ensure variables and overrides resolve correctly when .cm-wysiwym-dark is toggled.
  6. 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:

  1. Create a custom override stylesheet (e.g., theme-overrides.css).
  2. 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">
    
  3. 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]
  1. 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).
  2. 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).
  3. 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.
  4. Dark mode — controlled by the .cm-wysiwym-dark class 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, and skin-academic.css @import from Google Fonts by default. skin-starter.css and skin-custom.css load zero web fonts on initial request. Custom font loading for skin-custom.css is handled dynamically at runtime by the host page. See §10 for self-hosting setups.
  • First-load fonts. skin-light.css, skin-dark.css, and skin-academic.css import Atkinson Hyperlegible, Fira Code, Source Serif 4, and/or Courier Prime from Google Fonts. skin-starter.css uses system fonts only.
  • Blockquote treatment. The skin-light, skin-colorful, skin-dark, skin-modern, and skin-starter themes use a thick left bar. The skin-editorial theme uses a decorative ::before curly-quote mark. The skin-academic theme uses a transparent background with a simple left bar.
  • Info / warning cards. The skin-light, skin-colorful, skin-dark, skin-modern, and skin-starter themes render these as soft rounded/bordered cards. The skin-editorial theme uses the "hand-drawn" organic border-radius. The skin-academic theme uses clean rectangular left-bordered callout boxes matching the academic position paper callout style.
  • Pullquote dividers. Only the skin-editorial theme 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 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-h1h6 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 ![alt](src) 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*, _, ~~, ` markers
  • tok-meta / cmt-meta
  • tok-punctuation / cmt-punctuation
  • tok-list / cmt-list — list bullet characters
  • tok-comment / cmt-comment<!-- … -->
  • tok-keyword / cmt-keyword
  • tok-string / cmt-string
  • tok-number / cmt-number, tok-boolean / cmt-boolean
  • tok-variableName / cmt-variableName
  • tok-propertyName / cmt-propertyName
  • tok-operator / cmt-operator
  • tok-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 h1h6 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 use float: 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: left or float: 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-top or margin-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: swap for the editor's body font.

  • For self-hosted fonts, preload the .woff2 files 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:9 aspect-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-body and .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 set opacity: 0.6 !important on 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-editor and .traven-preview set the base font, color, and background.
  • Headings use padding (never margin), with !important.
  • Blockquote first / mid / last lines are styled with :has() siblings.
  • Blank line after headings collapses to ~0.6 em.
  • No float and no vertical margin on any .cm-wysiwym-*-container in the editor; alignment uses auto-margins.
  • .cm-wysiwym-collapsed-fence is zero-height.
  • .cm-wysiwym-h1.h6 span descendants don't have an underline.
  • .cm-fat-cursor has opacity: 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-card styling.
  • figcaption.traven-image-caption, figcaption.traven-video-caption, figcaption.traven-audio-caption, .traven-figure-caption are styled.
  • First-child heading margin-top matches the editor's padding-top (§5.1).
  • .traven-preview first-child blockquote neighbors have reduced margins (§5.2).
  • Tables: th and td have height: 38px and box-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:

  1. 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">
    
  2. Use !important to win specificity wars (the shipping skins use !important heavily, which is intentional to defeat page-level resets).

  3. 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;
    }
    
  4. 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:

  1. Static check. Open the file and grep for the rules in the §8.3 checklist. Any miss is a bug.
  2. 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.
  3. 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 float somewhere — see §4.
  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.
  5. Build pipeline. The theme is referenced by <link> tags in the demo pages only. The dist/traven.css bundle (built from src/style.css) provides only the dark-mode base styles and the math / Vim / scrollbar rules; the live themes live entirely in packages/core/assets/skins/. So you can iterate on a theme without ever running npm 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. See docs/key-features.md and 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-editor and .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; see customization-styling.md.
  • Shortcode — a Traven-extended Markdown construct ([image], [video], [audio], [figure], [component]) parsed by a custom Lezer grammar in src/*.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-dark toggled on the editor host DOM by setTheme("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 in wysiwym.js.
  • ../installation-setup.md — how to wire the editor into a host page.
  • ../api-reference.md — full constructor options and instance methods (notably setTheme(), 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.