Theming
The @domternal/theme package provides ready-made light and dark themes for the editor, toolbar, bubble menu, and all UI components. Every visual property is a CSS custom property, so you can customize anything by overriding a single variable. The same package carries the print stylesheet, so what the document looks like on paper is a theming concern too.
When to use
Section titled “When to use”Customize Theming when you need:
- Brand colors, fonts, or spacing that match a design system
- Dark mode that follows a custom toggle (not just
prefers-color-scheme) - Theme overrides scoped to a specific editor instance (multiple editors with different themes on one page)
- Control over the printed page: which tokens still reach the paper, and where the print partial sits in the cascade (see Printing)
Notes:
- Domternal exposes 150+ CSS custom properties for fine-grained control without touching the SCSS source
- Pair with
@domternal/themefor the default styling, then override CSS variables in your own stylesheet
Installation
Section titled “Installation”pnpm add @domternal/themeImport the theme in your application entry point:
// JavaScript/TypeScript bundler (Vite, Webpack, esbuild)import '@domternal/theme';// SCSS@use '@domternal/theme';<!-- HTML link tag --><link rel="stylesheet" href="node_modules/@domternal/theme/dist/domternal-theme.css" />The package exports multiple entry points:
| Import | Path | Description |
|---|---|---|
@domternal/theme | dist/domternal-theme.css | Default: compiled CSS |
@domternal/theme/css | dist/domternal-theme.css | Explicit CSS import |
@domternal/theme/scss | src/index.scss | SCSS source (for Sass pipelines) |
Light and dark mode
Section titled “Light and dark mode”Light mode is the default. Dark mode is activated by adding a CSS class to the editor or any ancestor element.
Three modes
Section titled “Three modes”| Class | Behavior |
|---|---|
| (none) | Light theme (default) |
dm-theme-dark | Dark theme, always |
dm-theme-light | Light theme, always (use to force light inside a dark context) |
dm-theme-auto | Follows the user’s system preference via prefers-color-scheme |
.dm-theme-light is not identical to the untouched default. The base tokens on .dm-editor are the implicit light theme; the explicit .dm-theme-light class applies a self-contained mixin that diverges on five properties:
| Property | Implicit default | Under .dm-theme-light |
|---|---|---|
--dm-border-color | #e5e7eb | #e0e0e0 |
--dm-hover | rgba(0,0,0,0.04) | rgba(0,0,0,0.06) |
--dm-active | rgba(0,0,0,0.1) | rgba(0,0,0,0.12) |
--dm-code-surface | #f0f0f0 | #f5f5f5 |
--dm-code-color | inherit | #d63384 |
The last one is the visible one: inline code renders pink under .dm-theme-light and inherits the surrounding text color without it.
<!-- Always dark --><div class="dm-theme-dark"> <div class="dm-toolbar">...</div> <div class="dm-editor">...</div></div>
<!-- Follows system preference --><div class="dm-theme-auto"> <div class="dm-toolbar">...</div> <div class="dm-editor">...</div></div>
<!-- Force light inside a dark page --><div class="dm-theme-light"> <div class="dm-toolbar">...</div> <div class="dm-editor">...</div></div>Toggle dark mode at runtime
Section titled “Toggle dark mode at runtime”function toggleTheme() { document.body.classList.toggle('dm-theme-dark');}Or use dm-theme-auto and let the OS handle it.
Customization
Section titled “Customization”Override any CSS custom property on .dm-editor, .dm-toolbar, or any parent element:
/* Custom accent color */.my-editor { --dm-accent: #e11d48; --dm-accent-hover: #be123c; --dm-accent-surface: rgba(225, 29, 72, 0.1);}
/* Custom editor appearance */.my-editor .dm-editor { --dm-editor-bg: #fefce8; --dm-editor-border-radius: 0; --dm-editor-shadow: none;}
/* Custom toolbar */.my-editor .dm-toolbar { --dm-toolbar-bg: #fef9c3; --dm-button-size: 2.25rem;}Since CSS custom properties cascade, you can set them on any ancestor and they flow down to all editor components.
CSS custom properties reference
Section titled “CSS custom properties reference”All properties are defined on .dm-editor unless noted otherwise. Light values are the defaults on .dm-editor (see the note above for the five tokens .dm-theme-light sets differently). Dark values are applied when dm-theme-dark or dm-theme-auto (in dark mode) is active.
Semantic base tokens
Section titled “Semantic base tokens”These are the core design tokens that other properties reference via var().
| Property | Light | Dark | Description |
|---|---|---|---|
--dm-bg | #ffffff | #1e1e1e | Base background |
--dm-text | #1a1a1a | #e0e0e0 | Base text color |
--dm-muted | #6b7280 | #9ca3af | Secondary/muted text |
--dm-surface | #f8f9fa | #2a2a2a | Elevated surface background |
--dm-border-color | #e5e7eb | #3a3a3a | Border color |
--dm-hover | rgba(0,0,0,0.04) | rgba(255,255,255,0.08) | Hover state background |
--dm-active | rgba(0,0,0,0.1) | rgba(255,255,255,0.15) | Active state background |
--dm-accent | #2563eb | #60a5fa | Primary accent (blue) |
--dm-accent-hover | #1d4ed8 | #93c5fd | Accent on hover |
--dm-accent-surface | rgba(37,99,235,0.1) | rgba(96,165,250,0.15) | Accent tint background |
--dm-focus-color | rgba(66,133,244,0.3) | rgba(96,165,250,0.3) | Focus ring color |
--dm-selection | rgba(66,133,244,0.2) | rgba(96,165,250,0.25) | Text selection color |
--dm-popover-shadow | 0 12px 28px -8px rgba(0,0,0,0.22), 0 4px 10px -2px rgba(0,0,0,0.12) | 0 10px 25px rgba(0,0,0,0.4), 0 4px 10px rgba(0,0,0,0.25) | Shared drop shadow for every popover surface: floating menu, slash command menu, block context menu, link popover, image popover, math popover |
--dm-scrollbar-thumb | rgba(0,0,0,0.18) | rgba(255,255,255,0.18) | Thin-scrollbar thumb color shared by every scrollable popup: the ToC card, emoji suggestion list, slash command menu, block context menu, floating menu and mention suggestion list |
--dm-scrollbar-thumb-hover | rgba(0,0,0,0.28) | rgba(255,255,255,0.32) | Thumb color on hover |
--dm-code-surface | #f0f0f0 | #2d2d2d | Code background surface |
--dm-code-color | inherit | inherit | Code text color |
--dm-color-scheme | light | dark | Feeds the CSS color-scheme property on .dm-editor, so native form controls, scrollbars and the caret follow the theme |
All three of --dm-popover-shadow, --dm-scrollbar-thumb and --dm-scrollbar-thumb-hover default to their light values on .dm-editor, and every consumer falls back to a light-toned value when the token is missing. A custom dark theme that overrides --dm-bg but skips these keeps a light shadow on dark surfaces, and gets near-invisible scrollbars in six popups at once.
Editor
Section titled “Editor”| Property | Default | Description |
|---|---|---|
--dm-editor-bg | var(--dm-bg) | Editor background |
--dm-editor-text | var(--dm-text) | Editor text color |
--dm-editor-font-family | System font stack | Font family |
--dm-editor-font-size | 1rem | Base font size |
--dm-editor-line-height | 1.6 | Line height |
--dm-editor-padding | 1rem | Content padding (any padding shorthand) |
--dm-editor-padding-top | var(--dm-editor-padding) | Top padding as a single length. Set it whenever --dm-editor-padding uses a multi-value shorthand, because a shorthand cannot participate in the internal padding-top calc(). |
--dm-editor-padding-top-extra | 0.5rem | Extra length added on top of the resolved top padding (gives the first line breathing room). The effective top padding is calc(padding-top + padding-top-extra). |
--dm-editor-border | 1px solid var(--dm-border-color) | Border |
--dm-editor-border-radius | 0.75rem | Corner radius |
--dm-editor-focus-ring | none | Focus ring (override for custom, e.g., 0 0 0 2px var(--dm-focus-color)) |
--dm-editor-shadow | 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02) | Box shadow |
Block UI and z-index
Section titled “Block UI and z-index”| Property | Light | Dark | Description |
|---|---|---|---|
--dm-z-handle | 25 | (same) | Z-index for BlockHandle. Sits below popovers so opening a menu overlays the handle. |
--dm-z-popover | 50 | (same) | Z-index for the floating menu, the slash command menu, the block context menu and the math edit popover. Other floating surfaces in @domternal/theme hardcode their own value and ignore this token: bubble menu 50, toolbar dropdown panel 60 (deliberately above the bubble menu, like the link popover: at parity the bubble menu painted over it and swallowed clicks on its items), table controls dropdown 50 (table handles 10), link popover 60, image popover 60, Notion color picker 60, mention and emoji suggestion lists 100. The floating ToC uses its own --dm-toc-z-index (10). Note that the math popover falls back to 60, not 50, when the token is unset. |
--dm-block-handle-gutter | 3rem | (same) | Gutter reserved on .ProseMirror when BlockHandle is active |
--dm-block-handle-left | 0.25rem | (same) | Horizontal offset of the handle from the editor’s left edge. Negative values place it in the surrounding whitespace and require .dm-editor--has-block-handle { overflow: visible }. Notion mode replaces the constant with a calc derived from the reading measure, so the handle tracks a centred column. |
--dm-block-inline-padding | 0.375rem | (same) | Inline padding pre-applied to colorable blocks. Paragraphs and headings get it on both sides via a zero-specificity :where() rule; blockquotes get it as their right padding (left stays 0.8em for the border bar); ul/ol get it as their reading-end padding. Pre-applying it means toggling a block background color only paints, it never shifts the text. Changing it moves content in all three places at once. |
--dm-block-children-indent | calc(1.5 * var(--dm-editor-font-size, 1rem)) | (same) | Indent for children-zone blocks under a list/task item label. Scales with the editor font size (was a fixed 1.5rem before v0.9.1). |
--dm-block-selected-halo | rgba(112, 207, 248, 0.25) | rgba(112, 207, 248, 0.35) | Halo behind the block a drag handle is targeting |
--dm-block-context-active-bg | rgba(55, 53, 47, 0.06) | rgba(255, 255, 255, 0.05) | Tint applied to the target block while its context menu is open |
Geometry tokens (--dm-block-handle-gutter, --dm-block-handle-left, --dm-block-inline-padding) are deliberately not overridden by the dark theme: geometry is identical in light and dark.
Placeholder
Section titled “Placeholder”| Property | Default | Description |
|---|---|---|
--dm-placeholder-color | var(--dm-muted) | Placeholder text color |
| Property | Default | Description |
|---|---|---|
--dm-link-color | var(--dm-accent) | Link color |
--dm-link-hover-color | var(--dm-accent-hover) | Link hover color |
Inline code
Section titled “Inline code”| Property | Default | Description |
|---|---|---|
--dm-code-bg | var(--dm-code-surface) | Inline code background |
--dm-code-text | var(--dm-code-color) | Inline code text color |
--dm-code-font | "SF Mono", "Fira Code", Consolas, "Liberation Mono", Menlo, monospace | Monospace font |
--dm-code-border-radius | 0.25rem | Inline code corner radius |
Code blocks
Section titled “Code blocks”| Property | Default | Description |
|---|---|---|
--dm-code-block-bg | var(--dm-code-surface) | Code block background |
--dm-code-block-text | var(--dm-text) | Code block text color |
Syntax highlighting
Section titled “Syntax highlighting”Used by Code Block Lowlight for highlight.js token colors.
| Property | Light | Dark | Description |
|---|---|---|---|
--dm-syntax-keyword | #c72031 | #ff7b72 | Keywords, types |
--dm-syntax-entity | #6f42c1 | #d2a8ff | Functions, class names |
--dm-syntax-constant | #005cc5 | #79c0ff | Constants, numbers |
--dm-syntax-string | #032f62 | #a5d6ff | Strings, regex |
--dm-syntax-variable | #d35400 | #ffa657 | Variables, built-ins |
--dm-syntax-comment | #57606a | #8b949e | Comments |
--dm-syntax-tag | #22863a | #7ee787 | HTML/XML tags |
--dm-syntax-addition | #22863a | #aff5b4 | Diff additions |
--dm-syntax-addition-bg | #f0fff4 | #033a16 | Diff addition background |
--dm-syntax-deletion | #b31d28 | #ffdcd7 | Diff deletions |
--dm-syntax-deletion-bg | #ffeef0 | #67060c | Diff deletion background |
Blockquote
Section titled “Blockquote”| Property | Light | Dark | Description |
|---|---|---|---|
--dm-blockquote-border | 3px solid #6a6a6a | 3px solid #555555 | Left border |
--dm-blockquote-color | #6a6a6a | #a0a0a0 | Text color |
Horizontal rule
Section titled “Horizontal rule”| Property | Default | Description |
|---|---|---|
--dm-hr-color | var(--dm-border-color) | Rule color |
| Property | Default | Description |
|---|---|---|
--dm-table-border | 1px solid var(--dm-border-color) | Cell border |
--dm-table-header-bg | var(--dm-surface) | Header cell background |
--dm-table-selected-bg | rgba(66, 133, 244, 0.15) | Selected cell background |
Highlight
Section titled “Highlight”Highlight has no dedicated CSS variable. There are two paths, depending on how the highlight was applied:
-
Hex highlights (the
==text==input rule, the keyboard shortcut, and the hex swatches in the highlight palette) are written as an inlinestyle="background-color: ..."on thetextStylemark, so no theme override can reach them. Change the color used by the shortcut and the input rule with the extension option instead:Highlight.configure({ defaultColor: '#fef08a' })See Highlight for
defaultColor,colorsand the default 25-color palette. -
Named-token highlights (applied through the Notion Color Picker) render as
data-bg-color="yellow"and resolve through the--dm-block-bg-*variables. Override those to restyle them: see Named color tokens below.
Mention
Section titled “Mention”| Property | Default | Description |
|---|---|---|
--dm-mention-bg | var(--dm-accent-surface) | Mention background |
--dm-mention-color | var(--dm-accent) | Mention text color |
--dm-mention-border-radius | 0.25rem | Mention corner radius |
Details / accordion
Section titled “Details / accordion”| Property | Default | Description |
|---|---|---|
--dm-details-border | 1px solid var(--dm-border-color) | Details border |
--dm-details-bg | var(--dm-surface) | Summary background |
--dm-details-summary-font-weight | 600 | Summary text weight |
Task list
Section titled “Task list”| Property | Default | Description |
|---|---|---|
--dm-task-checkbox-left | -1.15em | Horizontal position of the absolutely-positioned checkbox label, hanging it in the bullet column |
--dm-task-checkbox-top | 0.45em | Vertical position, tuned to the first text line’s x-height |
Both are tuned against the default --dm-editor-line-height. Retune them whenever you override that token, so the checkbox stays aligned with the first label glyph.
Toolbar
Section titled “Toolbar”Defined on .dm-toolbar:
| Property | Default | Description |
|---|---|---|
--dm-toolbar-bg | var(--dm-bg, #ffffff) | Toolbar background |
--dm-toolbar-border | none | Toolbar border |
--dm-toolbar-padding | 0.375rem 0.5rem | Toolbar padding |
--dm-toolbar-gap | 0.125rem | Gap between items |
--dm-toolbar-border-radius | 0.75rem 0.75rem 0 0 | Toolbar corner radius |
Toolbar buttons
Section titled “Toolbar buttons”Defined on .dm-toolbar:
| Property | Default | Description |
|---|---|---|
--dm-button-size | 2rem | Button width and height |
--dm-button-border-radius | 0.375rem | Button corner radius |
--dm-button-color | var(--dm-text, #374151) | Button icon/text color |
--dm-button-hover-bg | var(--dm-hover) | Button hover background |
--dm-button-active-bg | var(--dm-accent-surface) | Active button background |
--dm-button-active-color | var(--dm-accent) | Active button icon color |
--dm-button-disabled-opacity | 0.35 | Disabled button opacity |
Toolbar separator
Section titled “Toolbar separator”Defined on .dm-toolbar:
| Property | Default | Description |
|---|---|---|
--dm-separator-color | var(--dm-border-color) | Separator color |
--dm-separator-margin | 0.375rem | Separator vertical margin |
Notion mode (v0.7.0)
Section titled “Notion mode (v0.7.0)”Apply the .dm-notion-mode CSS class to the .dm-editor element itself (the theme rule is .dm-editor.dm-notion-mode, so the class does nothing on a parent) to switch into the Notion-style layout: a centered 44rem reading column inside a full-width editor host, no card frame, generous line-height, and the block handle pulled out into the side gutter. Body font-size is unchanged (--dm-editor-font-size stays 1rem), so toggling between modes shifts layout, not text scale.
<div class="dm-editor dm-notion-mode">...</div>The class is shipped by @domternal/theme and combines with the Block Menu and Table of Contents extensions for the full Notion experience. See the Notion Mode guide for the complete setup.
The class overrides exactly five tokens:
| Property | Global default | In .dm-notion-mode | Notes |
|---|---|---|---|
--dm-editor-line-height | 1.6 | 1.7 | Generous reading rhythm |
--dm-editor-padding | 1rem | 0 | Zeroed because the page wrapper supplies the white space around the column |
--dm-block-handle-gutter | 3rem | 0 | Column reserved on .ProseMirror for BlockHandle. Notion mode reclaims it because the handle sits fully outside the content column. |
--dm-notion-column-width | not set | 44rem | The reading measure. Notion mode reads it twice, as the column width and halved inside the handle offset, so overriding this one property moves both. |
--dm-block-handle-left | 0.25rem | calc(...) derived from the measure | Horizontal offset of the 40px handle cluster. Walks in to the centred column’s edge and backs off, so the cluster ends 4px before the text at any host width, and follows a docked panel that slides the column. |
Named color tokens (v0.7.0)
Section titled “Named color tokens (v0.7.0)”The Notion Color Picker and Block Color extensions use a 9-color named-token palette. Each token has two CSS variables in the theme (text and background). Override these to customize the palette.
| Variable | Purpose |
|---|---|
--dm-block-text-{gray,brown,orange,yellow,green,blue,purple,pink,red} | Per-token TEXT color (used for inline colorToken and block textColor) |
--dm-block-bg-{gray,brown,orange,yellow,green,blue,purple,pink,red} | Per-token BACKGROUND color (used for inline backgroundColorToken and block bgColor) |
Table of Contents (v0.7.0)
Section titled “Table of Contents (v0.7.0)”The FloatingTocOutline and TableOfContentsBlock extensions are styled with these tokens:
| Property | Purpose |
|---|---|
--dm-toc-mid-top | Sticky top (editor mode, middle state, default 50vh) |
--dm-toc-editor-top | Sticky top (editor mode, frozen state, default 1rem) |
--dm-toc-right-offset | Right margin (viewport mode, default 24px) |
--dm-toc-card-bg/shadow/radius | Expanded card visual |
--dm-toc-tick-{h1-h6}-width | Per-level tick width (h1 widest, h6 narrowest) |
--dm-toc-tick-height/radius/gap | Tick visual |
CSS classes
Section titled “CSS classes”The theme styles these CSS classes. Use them when building custom UI or vanilla JS editors.
| Class | Description |
|---|---|
.dm-editor | Main editor wrapper. Defines all design tokens. Must have position: relative (set by theme). |
.dm-editor .ProseMirror | The contenteditable area. Inherits editor tokens. |
.dm-toolbar | Toolbar container. Flex layout with grouped buttons. |
.dm-toolbar-group | Group of related toolbar buttons. |
.dm-toolbar-button | Individual toolbar button. |
.dm-toolbar-separator | Vertical separator between button groups. |
.dm-toolbar-dropdown-panel | Dropdown panel (headings, font family, colors). |
.dm-toolbar-dropdown-item | Item inside a dropdown panel. |
Floating UI
Section titled “Floating UI”| Class | Description |
|---|---|
.dm-bubble-menu | Inline formatting toolbar (appears on text selection). Compact button sizing. |
.dm-floating-menu | Block-level menu (appears on empty lines). |
.dm-link-popover | URL input popover for links. |
.dm-image-popover | URL input popover for images. |
.dm-slash-command-menu | Root popup for the / command menu. |
.dm-slash-command-item | A single command row (-icon, -label, -description, -shortcut, -text sub-elements). |
.dm-slash-command-group | Grouping wrapper, with .dm-slash-command-group-label as its heading. |
.dm-slash-command-query | Inline decoration on the active /query text in the document. |
.dm-slash-command-empty | Shown when the query matches nothing. |
Block UI
Section titled “Block UI”| Class | Description |
|---|---|
.dm-notion-mode | Opt-in Notion layout preset, applied to the .dm-editor element itself. |
BlockHandle and BlockContextMenu classes (.dm-block-handle, .dm-block-handle-btn, .dm-block-handle-dragging, .dm-block-drop-indicator, .dm-block-context-menu and its -group, -group-label, -item, -item-icon, -item-label children, .dm-block-context-active, .dm-editor--has-block-handle) are listed on Block Controls. Math classes (.dm-math-popover and friends) are listed on Math, ToC classes on Table of Contents, and .dm-notion-color-picker / .dm-ncp-* on Notion Color Picker.
Content elements
Section titled “Content elements”| Class | Description |
|---|---|
.mention | Inline mention node. |
.dm-mention-suggestion | Mention autocomplete dropdown. |
.dm-emoji-picker | Emoji picker panel. |
.dm-emoji-suggestion | Emoji autocomplete dropdown. |
.dm-color-palette | Color grid for text color and highlight pickers. |
.dm-image-resizable | Resizable image wrapper with handles. |
.invisible-char | Invisible character indicator (paragraph marks, spaces). |
Table controls
Section titled “Table controls”| Class | Description |
|---|---|
.dm-table-container | Wrapper for table handles and content. |
.dm-table-col-handle | Column selection handle. |
.dm-table-row-handle | Row selection handle. |
.dm-table-cell-handle | Cell corner handle (opens cell toolbar). |
.dm-table-cell-toolbar | Floating toolbar above selected cells. |
.dm-table-controls-dropdown | Dropdown for table operations. |
State classes
Section titled “State classes”| Class | Description |
|---|---|
.ProseMirror-selectednode | Outline on selected atom nodes (images, horizontal rules). Uses --dm-accent. |
.ProseMirror-gapcursor | Animated blinking cursor in gap positions. |
.selectedCell | Background overlay on selected table cells. Uses --dm-table-selected-bg. |
.is-empty | Added to empty nodes for placeholder display. |
.is-editor-empty | Added to the editor when the document is empty. |
.has-focus | Added by the Focus extension to the focused node. |
.is-open | Added to open details/accordion elements. |
| Class | Description |
|---|---|
.dm-printing | On <body> while printDocument() runs. Gates the isolation rules. |
.dm-print-root | The element being printed, .dm-editor by default. |
.dm-print-ancestor | Every ancestor of that element up to the document element. Their other children are hidden. |
All three are added and removed by the Print extension around one print, and the rules that key on them are documented there under isolation.
Printing
Section titled “Printing”@domternal/theme ships a paper stylesheet in _print.scss, and everything in it applies to the reader’s own Ctrl/Cmd+P with no JavaScript in the loop. What it hides, reveals and neutralises is documented with the Print extension, together with the marking classes that extension adds to isolate the document from the surrounding application.
This section is the theme author’s half: where the partial sits in the cascade, and which tokens still reach the page.
Where it sits in the cascade
Section titled “Where it sits in the cascade”@use 'print' is the last partial in index.scss, after themes/_light.scss and themes/_dark.scss and just before the closing prefers-reduced-motion block. The position is load-bearing. @media print adds no specificity of its own, so a print rule and a screen rule written with the same selector are separated by source order alone; importing print last means it wins that tie without having to escalate. A hand-assembled build that puts the partial earlier flips every one of those ties silently, because nothing errors.
The !important declarations that are in the file are aimed at something source order cannot reach: styles the node views write inline on the element (the table handles’ display, the stored column widths and the table’s min-width, the image handles). An inline style beats any rule that is not important, whatever its position.
Two rules are written against body rather than against a descendant selector, because no element is a descendant of itself: the ancestor isolation rules never reach body’s own box, so the page canvas has to be named on its own selector. The unconditional one drops the canvas background on every print. body.dm-printing additionally forces color: #000, gated to the command path, where everything but the document is already hidden and the declaration can reach nothing the host still shows.
What you can still control
Section titled “What you can still control”The print pass does not touch typography, so these tokens still shape the page: --dm-editor-font-family, --dm-editor-font-size, --dm-editor-line-height, --dm-code-font, the --dm-syntax-* palette and the named --dm-block-text-* / --dm-block-bg-* colours.
These are overridden on paper, so setting the token alone changes nothing there:
| Token | What print does instead |
|---|---|
--dm-editor-bg, --dm-editor-border, --dm-editor-border-radius, --dm-editor-shadow | Dropped as literal declarations on .dm-editor. The sheet is already the frame. |
--dm-editor-text | Forced to #000. The text colour has to come with the background: a dark theme sets it near-white, and dropping only the dark panel behind it leaves light grey on white paper. Colours an author applied are inline on their own spans and still win. |
--dm-editor-padding, --dm-editor-padding-top, --dm-editor-padding-top-extra | .ProseMirror padding is zeroed. |
--dm-block-handle-gutter | Zeroed with the rest of that padding. It reserves room for a drag handle that is not printed, and costs about 48px on every page. |
the page canvas (body background, whether a token of yours sets it or not) | Dropped, always, not only under printDocument(). The document’s text is forced to #000, so a dark canvas left standing prints black on near-black. The rule kills body’s transition first, because a theme toggle animates the background and a running transition outranks !important. |
No @page rule ships in the free theme, so paper size, orientation and margins come from the browser’s print dialog. Add your own when you need them fixed:
@page { size: A4; margin: 20mm;}To change something the print pass sets, write your own @media print block after the theme import, matching its selector so specificity ties and your rule wins on order:
@import '@domternal/theme';
@media print { /* the theme declares `.dm-editor { color: #000 !important }` */ .dm-editor { color: #1a1a1a !important; }}A running transition outranks !important
Section titled “A running transition outranks !important”While a CSS transition is running, the transitioned value comes from the transition origin of the cascade, which sits above every author declaration, !important included. A print rule that fights an animated property loses for exactly as long as the animation lasts, and a print is a snapshot that can land right inside that window.
There are two cases in the theme. The first is the block context menu. Opening it tints the target block through .dm-block-context-active, which transitions background-color and box-shadow over 0.12s, so a print taken just after the menu opened would carry a fading grey block behind the text however loudly the print rule declared background: none !important. The transition has to stop first, in the same rule:
@media print { .dm-editor .ProseMirror .dm-block-context-active { transition: none !important; /* first, or the rest of this block loses */ background: none !important; border: none !important; box-shadow: none !important; }}The second is the page canvas. A theme toggle transitions body’s background, so a print taken while that toggle is still running would carry the outgoing canvas onto the sheet behind text the print layer has already forced to black. The print rule zeroes the transition in the same block, before dropping the background:
@media print { body { transition: none !important; /* first, or the drop below loses */ background: none !important; }}The same applies to any print rule you write against your own animated UI. Killing the transition is not a nicety, it is the precondition for the declarations underneath it.
Building a custom theme
Section titled “Building a custom theme”You don’t need @domternal/theme to use Domternal. The editor works without any styles. You can build everything from scratch.
Minimum required styles
Section titled “Minimum required styles”At minimum, ProseMirror requires these styles to function correctly:
/* ProseMirror content area */.dm-editor .ProseMirror { position: relative; outline: none; word-wrap: break-word; white-space: pre-wrap; white-space: break-spaces;}
/* Gapcursor (if using Gapcursor extension) */.dm-editor .ProseMirror-gapcursor { display: none; pointer-events: none; position: absolute;}.dm-editor .ProseMirror-gapcursor::after { content: ''; display: block; position: absolute; top: -2px; width: 20px; border-top: 1px solid currentColor; animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;}@keyframes ProseMirror-cursor-blink { to { visibility: hidden; }}.dm-editor .ProseMirror-focused .ProseMirror-gapcursor { display: block;}
/* Hide selection on drag */.ProseMirror-hideselection *::selection { background: transparent;}
/* Table column resize handle (if using tables) */.dm-editor .column-resize-handle { position: absolute; right: -1.5px; top: 0; bottom: 0; width: 2px; background-color: #2563eb; pointer-events: none;}Approach: override the default theme
Section titled “Approach: override the default theme”The fastest way to customize is to import the default theme and override specific variables:
@import '@domternal/theme';
.my-app .dm-editor { --dm-accent: #e11d48; --dm-editor-border-radius: 0; --dm-editor-font-family: 'Inter', sans-serif;}
.my-app .dm-toolbar { --dm-toolbar-bg: #f1f5f9; --dm-toolbar-border-radius: 0;}Approach: build from SCSS source
Section titled “Approach: build from SCSS source”Import the SCSS source and use the theme’s structure:
// Import just the variables and base styles you need@use '@domternal/theme/scss';Dark mode for custom themes
Section titled “Dark mode for custom themes”If building a custom dark theme, override the same semantic tokens:
.my-dark-theme .dm-editor { --dm-color-scheme: dark; --dm-bg: #0d1117; --dm-text: #c9d1d9; --dm-surface: #161b22; --dm-border-color: #30363d; --dm-accent: #58a6ff; --dm-accent-hover: #79c0ff; --dm-accent-surface: rgba(88, 166, 255, 0.15); --dm-hover: rgba(255, 255, 255, 0.06); --dm-active: rgba(255, 255, 255, 0.12); --dm-code-surface: #161b22;}
/* Apply the same tokens to floating UI that escapes the editor */.my-dark-theme .dm-toolbar,.my-dark-theme .dm-bubble-menu,.my-dark-theme .dm-emoji-picker,.my-dark-theme .dm-mention-suggestion,.my-dark-theme .dm-table-controls-dropdown { --dm-bg: #0d1117; --dm-text: #c9d1d9; --dm-surface: #161b22; --dm-border-color: #30363d; --dm-accent: #58a6ff; --dm-hover: rgba(255, 255, 255, 0.06);}Source files
Section titled “Source files”The theme is composed of SCSS partials:
| File | Description |
|---|---|
_variables.scss | All CSS custom property definitions |
_base.scss | .dm-editor wrapper styles, fade-in animation |
_content.scss | Content typography (headings, lists, links, code, images) |
_prosemirror.scss | ProseMirror required styles (gapcursor, selected nodes, tables) |
_toolbar.scss | Toolbar, buttons, dropdowns, separators |
_bubble-menu.scss | Compact bubble menu |
_floating-menu.scss | Floating menu |
_link-popover.scss | Link URL input popover |
_block-handle.scss | Hover gutter, drag handle, drop indicator |
_slash-command.scss | / command menu popup |
_context-menu.scss | Block context menu |
_color-palette.scss | Color grid for text color and highlight pickers |
_mention.scss | Mention nodes and suggestion dropdown |
_image.scss | Resizable images, handles, image popover |
_details.scss | Accordion/details with CSS grid toggle |
_math.scss | Inline and block math plus the math edit popover |
_placeholder.scss | Placeholder text |
_task-list.scss | Task list with checkboxes |
_invisible-chars.scss | Invisible character indicators |
_syntax.scss | Syntax highlighting for code blocks |
_emoji-picker.scss | Emoji picker and suggestions |
_table-controls.scss | Table handles, cell toolbar, dropdowns |
_block-colors.scss | Block-level named-token colors |
_inline-colors.scss | Inline named-token colors and the Notion color picker. Must load after _block-colors.scss: the inline override depends on cascade order to win over the block-level [data-bg-color] rule |
_toc.scss | Floating outline and the inline /toc block |
_notion-mode.scss | The opt-in .dm-notion-mode preset |
themes/_light.scss | Light theme class and mixin |
themes/_dark.scss | Dark theme class, auto mode, and mixin |
_print.scss | The whole @media print layer. Imported last of all the partials, after both themes, because @media print adds no specificity of its own and only source order breaks a tie. See Printing |
index.scss ends with a @media (prefers-reduced-motion: reduce) block that strips animations and transitions from the menu, popover, dropdown and button classes it lists. A theme built from scratch that reuses the same class names has to reimplement it, and the same goes for _print.scss.
See also
Section titled “See also”- Configuration - editor and extension configuration
- Toolbar - toolbar configuration and custom items
- Text Color - inline text color via TextStyle
- Print - the
printDocumentcommand, the toolbar button and the isolation that pairs with the print stylesheet