Theming
notectl ships with a complete theming system built on CSS custom properties. Two built-in themes (light & dark), automatic system-preference detection, and full custom-theme support are included out of the box.
For CSP runtime styling and nonce setup, see the Content Security Policy guide.
Quick Start
Section titled “Quick Start”Using a Preset
Section titled “Using a Preset”import { createEditor, ThemePreset } from '@notectl/core';
const editor = await createEditor({ theme: ThemePreset.Dark,});Available presets:
| Preset | Value | Description |
|---|---|---|
ThemePreset.Light | 'light' | Default light theme |
ThemePreset.Dark | 'dark' | Dark theme (Catppuccin-inspired) |
ThemePreset.System | 'system' | Follows OS prefers-color-scheme |
HTML Attribute
Section titled “HTML Attribute”<notectl-editor theme="dark"></notectl-editor>System Preference (Auto)
Section titled “System Preference (Auto)”const editor = await createEditor({ theme: ThemePreset.System,});The editor listens to prefers-color-scheme changes and switches automatically.
Runtime Switching
Section titled “Runtime Switching”// Switch to darkeditor.setTheme(ThemePreset.Dark);
// Read current themeconst current = editor.getTheme(); // 'light' | 'dark' | 'system' | Theme objectToggle example:
const toggle = document.getElementById('theme-toggle');toggle.addEventListener('click', () => { const next = editor.getTheme() === ThemePreset.Dark ? ThemePreset.Light : ThemePreset.Dark; editor.setTheme(next);});Custom Themes
Section titled “Custom Themes”Create a custom theme by extending a built-in base theme with partial overrides:
import { createTheme, LIGHT_THEME, createEditor } from '@notectl/core';import type { Theme } from '@notectl/core';
const corporate: Theme = createTheme(LIGHT_THEME, { name: 'corporate', primitives: { primary: '#6B21A8', primaryForeground: '#6B21A8', primaryMuted: 'rgba(107, 33, 168, 0.15)', borderFocus: '#6B21A8', focusRing: 'rgba(107, 33, 168, 0.2)', },});
const editor = await createEditor({ theme: corporate });Only specify the values you want to change — everything else falls back to the base theme.
Overriding Component Tokens
Section titled “Overriding Component Tokens”Component-level tokens (toolbar, code block, tooltip) can be overridden independently:
const myTheme: Theme = createTheme(DARK_THEME, { name: 'custom-dark', codeBlock: { background: '#0d1117', foreground: '#c9d1d9', }, tooltip: { background: '#21262d', foreground: '#f0f6fc', },});Sharing Themes
Section titled “Sharing Themes”Themes are plain objects — export them from a package:
import { createTheme, DARK_THEME } from '@notectl/core';import type { Theme } from '@notectl/core';
export const OCEAN_THEME: Theme = createTheme(DARK_THEME, { name: 'ocean', primitives: { primary: '#06b6d4', background: '#0c1222', surfaceRaised: '#1a2332', surfaceOverlay: '#1a2332', },});Theming Contract: Three-Tier Cascade
Section titled “Theming Contract: Three-Tier Cascade”Every component-level rule in notectl reads its color through a layered cascade:
border-color: var(--notectl-table-border, var(--notectl-border, #d0d7de));/* ^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^ ^^^^^^^^ component-scoped token global semantic hard-coded (target a single component) (theme-wide) (last resort) */| Tier | Example | Audience |
|---|---|---|
| Component-scoped token | --notectl-table-border | You only want to recolor a single component |
| Global semantic token | --notectl-border | You want a unified theme across the editor |
| Hard-coded fallback | #d0d7de | Used only when neither token is set |
Backwards compatibility. Setting --notectl-border continues to work exactly as before — it cascades to every component that doesn’t have a more specific override. The component-scoped tokens are purely additive; they exist for cases where the global token is too broad.
Example — theme just the table:
notectl-editor { --notectl-table-border: #6366f1; --notectl-table-header-bg: #eef2ff;}This is the exact ask from discussion #120: style the table without affecting any other component.
Shadow Parts
Section titled “Shadow Parts”For customization beyond what tokens cover, notectl exposes CSS Shadow Parts on every structural element. Parts let you target deep DOM internals from outside the shadow root without forking the editor or piercing the boundary.
notectl-editor::part(toolbar-button) { border-radius: 9999px;}
notectl-editor::part(toolbar-button-active) { background: linear-gradient(135deg, #6366f1, #8b5cf6);}
notectl-editor::part(blockquote) { font-style: italic;}Part Inventory
Section titled “Part Inventory”| Part | Element | Notes |
|---|---|---|
editor | <notectl-editor> wrapper | Root of the editor surface |
content | Editable content area | Where the user types |
plugin-container + plugin-container-top | Toolbar host | Two parts on the same element so consumers can target either |
plugin-container + plugin-container-bottom | Below-content slot | |
toolbar | Toolbar root | |
toolbar-button | Any toolbar button | |
toolbar-button + toolbar-button-active | Active toolbar button | Modifier part synced with aria-pressed |
toolbar-button + toolbar-overflow-button | The “more” button in burger-menu overflow mode | |
toolbar-divider | Group separator | |
table | Table wrapper | |
table-row | <tr> | |
table-cell | <td> | |
code-block | <pre> root | |
code-block-header | Header row (language label + actions) | |
code-block-content | <code> inside | |
blockquote | <blockquote> root |
State Expression
Section titled “State Expression”Stateful elements expose state via modifier parts (space-separated values). The modifier mirrors ARIA state for styling only — semantic state remains on attributes like aria-pressed.
<button part="toolbar-button toolbar-button-active" aria-pressed="true">B</button>notectl-editor::part(toolbar-button-active) { color: var(--my-accent);}This works in every browser that supports ::part() and does not depend on the newer CustomStateSet API.
CSS Custom Properties Reference
Section titled “CSS Custom Properties Reference”The theme engine sets all properties on :host inside the Shadow DOM. Every color in the editor references these variables.
Documented public tokens are declared with @property so DevTools surfaces them and invalid values fall back to a typed initial value instead of breaking downstream rules.
Primitives
Section titled “Primitives”These are the core tokens that all components derive their colors from.
| CSS Property | Token | Description |
|---|---|---|
--notectl-bg | background | Editor canvas background |
--notectl-fg | foreground | Main text color |
--notectl-fg-muted | mutedForeground | Secondary text (placeholders, labels, arrows) |
--notectl-border | border | Default borders (editor, toolbar, inputs, separators) |
--notectl-border-focus | borderFocus | Focus state border |
--notectl-primary | primary | Accent color (selection outlines, insert lines, active states) |
--notectl-primary-fg | primaryForeground | Text on primary-tinted backgrounds |
--notectl-primary-muted | primaryMuted | Subtle primary background (active toolbar button, selected cells) |
--notectl-surface-raised | surfaceRaised | Elevated surfaces (toolbar background) |
--notectl-surface-overlay | surfaceOverlay | Overlay surfaces (popups, context menus, dropdowns) |
--notectl-hover-bg | hoverBackground | Hover state background |
--notectl-active-bg | activeBackground | Active/pressed state background |
--notectl-danger | danger | Delete and error color |
--notectl-danger-muted | dangerMuted | Subtle danger background |
--notectl-success | success | Checked/success color (checklist checkmarks) |
--notectl-shadow | shadow | Box-shadow color |
--notectl-focus-ring | focusRing | Focus ring shadow (typically semi-transparent) |
Component: Toolbar
Section titled “Component: Toolbar”| CSS Property | Token | Fallback |
|---|---|---|
--notectl-toolbar-bg | toolbar.background | var(--notectl-surface-raised) |
--notectl-toolbar-border | toolbar.borderColor | var(--notectl-border) |
--notectl-toolbar-button-bg | — (CSS-only) | transparent |
--notectl-toolbar-button-fg | — (CSS-only) | var(--notectl-fg) |
--notectl-toolbar-button-hover-bg | — (CSS-only) | var(--notectl-hover-bg) |
--notectl-toolbar-button-active-bg | — (CSS-only) | var(--notectl-active-bg) |
--notectl-toolbar-button-active-fg | — (CSS-only) | var(--notectl-primary-fg) |
Component: Table
Section titled “Component: Table”| CSS Property | Description | Fallback |
|---|---|---|
--notectl-table-border | All table cell borders | var(--notectl-border) |
--notectl-table-cell-bg | Per-cell background | transparent |
--notectl-table-header-bg | Header row background (<th>) | var(--notectl-surface-raised) |
The per-table inline override set by the toolbar’s “Border color” action (--ntbl-border-color) still wins over --notectl-table-border so user customizations remain visible after a theme switch.
Component: Blockquote
Section titled “Component: Blockquote”| CSS Property | Description | Fallback |
|---|---|---|
--notectl-blockquote-border | Left bar color | var(--notectl-border) |
--notectl-blockquote-bg | Background | transparent |
--notectl-blockquote-fg | Foreground | inherit |
Component: Code Block
Section titled “Component: Code Block”| CSS Property | Token | Fallback |
|---|---|---|
--notectl-code-block-bg | codeBlock.background | var(--notectl-surface-raised) |
--notectl-code-block-color | codeBlock.foreground | var(--notectl-fg) |
--notectl-code-block-header-bg | codeBlock.headerBackground | var(--notectl-surface-raised) |
--notectl-code-block-header-color | codeBlock.headerForeground | var(--notectl-fg-muted) |
--notectl-code-block-header-border | codeBlock.headerBorder | var(--notectl-border) |
Component: Code Block Syntax Tokens
Section titled “Component: Code Block Syntax Tokens”When a ThemeSyntax object is provided in codeBlock.syntax, token colors are emitted as CSS custom properties. Each falls back to var(--notectl-code-block-color) when not set. There are 16 canonical token types; the theme engine derives all variables automatically from the SYNTAX_TOKEN_TYPES list.
| CSS Property | Token | Fallback |
|---|---|---|
--notectl-code-token-keyword | codeBlock.syntax.keyword | var(--notectl-code-block-color) |
--notectl-code-token-string | codeBlock.syntax.string | var(--notectl-code-block-color) |
--notectl-code-token-comment | codeBlock.syntax.comment | var(--notectl-code-block-color) |
--notectl-code-token-number | codeBlock.syntax.number | var(--notectl-code-block-color) |
--notectl-code-token-function | codeBlock.syntax.function | var(--notectl-code-block-color) |
--notectl-code-token-operator | codeBlock.syntax.operator | var(--notectl-code-block-color) |
--notectl-code-token-punctuation | codeBlock.syntax.punctuation | var(--notectl-code-block-color) |
--notectl-code-token-boolean | codeBlock.syntax.boolean | var(--notectl-code-block-color) |
--notectl-code-token-null | codeBlock.syntax.null | var(--notectl-code-block-color) |
--notectl-code-token-property | codeBlock.syntax.property | var(--notectl-code-block-color) |
--notectl-code-token-type | codeBlock.syntax.type | var(--notectl-code-block-color) |
--notectl-code-token-annotation | codeBlock.syntax.annotation | var(--notectl-code-block-color) |
--notectl-code-token-tag | codeBlock.syntax.tag | var(--notectl-code-block-color) |
--notectl-code-token-attribute | codeBlock.syntax.attribute | var(--notectl-code-block-color) |
--notectl-code-token-constant | codeBlock.syntax.constant | var(--notectl-code-block-color) |
--notectl-code-token-regex | codeBlock.syntax.regex | var(--notectl-code-block-color) |
Tokens whose value is a TokenStyle object (rather than a plain color string) additionally emit font-style and font-weight variables when those fields are set:
| CSS Property pattern | Emitted when |
|---|---|
--notectl-code-token-<type>-font-style | TokenStyle.fontStyle is set |
--notectl-code-token-<type>-font-weight | TokenStyle.fontWeight is set |
The built-in light and dark themes both include full syntax token definitions for all 16 types, so code blocks are styled automatically.
Component: Tooltip
Section titled “Component: Tooltip”| CSS Property | Token | Fallback |
|---|---|---|
--notectl-tooltip-bg | tooltip.background | var(--notectl-fg) |
--notectl-tooltip-fg | tooltip.foreground | var(--notectl-bg) |
| CSS Property | Description |
|---|---|
--notectl-content-min-height | Minimum height of the content area (default: 400px) |
--notectl-content-max-height | Maximum height of the content area before it scrolls internally (default: none) |
Fixed-size editor with internal scrolling
Section titled “Fixed-size editor with internal scrolling”By default the editor grows with its content. To embed it in a fixed-size frame where the toolbar stays pinned at the top, the bottom plugin container stays pinned at the bottom, and the content area scrolls internally, you have two options.

Option 1 — set a height on the host
Section titled “Option 1 — set a height on the host”Give the <notectl-editor> element a height; the toolbar/content/footer
distribute themselves automatically.
<notectl-editor style="display: block; height: 500px;"></notectl-editor>Option 2 — cap the content area only
Section titled “Option 2 — cap the content area only”Use --notectl-content-max-height if the editor itself should grow with
toolbar/footer but the editable area should scroll once it exceeds a given
size.
<notectl-editor style="--notectl-content-max-height: 320px;"></notectl-editor>Both approaches preserve full keyboard accessibility — caret navigation automatically scrolls the focused position into view.
Syntax Highlighting Tokens
Section titled “Syntax Highlighting Tokens”When a syntax highlighter is configured on the CodeBlockPlugin, token classes are applied to code content. The built-in light and dark themes include full syntax color definitions via CSS custom properties (see the Code Block Syntax Tokens reference above), so code blocks are styled automatically.
To customize syntax colors, override the codeBlock.syntax section in your custom theme. Each token accepts either a plain color string or a TokenStyle object for full font-weight and font-style control:
import { createTheme, LIGHT_THEME } from '@notectl/core';import type { Theme } from '@notectl/core';
const myTheme: Theme = createTheme(LIGHT_THEME, { name: 'custom-syntax', codeBlock: { syntax: { keyword: '#d73a49', string: '#032f62', number: '#005cc5', comment: { color: '#6a737d', fontStyle: 'italic' }, function: '#6f42c1', operator: '#d73a49', punctuation: '#24292e', boolean: '#005cc5', null: '#005cc5', property: '#005cc5', // New in 16-token system: type: '#e36209', annotation: '#6f42c1', tag: '#22863a', attribute: '#6f42c1', constant: '#005cc5', regex: '#032f62', }, },});You only need to specify tokens you want to override — unspecified tokens inherit from the base theme.
Theme API Types
Section titled “Theme API Types”interface ThemePrimitives { readonly background: string; readonly foreground: string; readonly mutedForeground: string; readonly border: string; readonly borderFocus: string; readonly primary: string; readonly primaryForeground: string; readonly primaryMuted: string; readonly surfaceRaised: string; readonly surfaceOverlay: string; readonly hoverBackground: string; readonly activeBackground: string; readonly danger: string; readonly dangerMuted: string; readonly success: string; readonly shadow: string; readonly focusRing: string;}
/** Per-token style: color plus optional font weight and style. */interface TokenStyle { readonly color: string; readonly fontWeight?: 'normal' | 'bold'; readonly fontStyle?: 'normal' | 'italic';}
/** A token style value is either a plain color string or a full TokenStyle object. */type TokenStyleValue = string | TokenStyle;
/** * Syntax highlighting styles for all 16 canonical token types. * Derived automatically from SYNTAX_TOKEN_TYPES — adding a new token type * here propagates to CSS variables and theme validation. */type ThemeSyntax = { readonly [K in SyntaxTokenType]: TokenStyleValue };
// The 16 canonical token types:// 'keyword' | 'string' | 'comment' | 'number' | 'function' | 'operator'// | 'punctuation' | 'boolean' | 'null' | 'property'// | 'type' | 'annotation' | 'tag' | 'attribute' | 'constant' | 'regex'
interface ThemeCodeBlock { readonly background: string; readonly foreground: string; readonly headerBackground: string; readonly headerForeground: string; readonly headerBorder: string; readonly syntax?: ThemeSyntax;}
interface Theme { readonly name: string; readonly primitives: ThemePrimitives; readonly toolbar?: Partial<ThemeToolbar>; readonly codeBlock?: Partial<ThemeCodeBlock>; readonly tooltip?: Partial<ThemeTooltip>;}Exports
Section titled “Exports”All theme-related exports from @notectl/core:
| Export | Kind | Description |
|---|---|---|
ThemePreset | Enum object | Light, Dark, System |
LIGHT_THEME | Constant | Built-in light theme |
DARK_THEME | Constant | Built-in dark theme |
SYNTAX_TOKEN_TYPES | Constant | Tuple of all 16 canonical token type names |
createTheme(base, overrides) | Function | Create custom theme from a base |
resolveTheme(preset | theme) | Function | Resolve a preset to a full Theme |
generateThemeCSS(theme) | Function | Generate CSS string from a Theme |
createThemeStyleSheet(theme) | Function | Create a CSSStyleSheet from a Theme |
Theme | Type | Full theme definition |
PartialTheme | Type | Partial overrides for createTheme() |
ThemePrimitives | Type | Primitive color palette |
ThemeToolbar | Type | Toolbar color overrides |
ThemeCodeBlock | Type | Code block color overrides (includes syntax) |
ThemeSyntax | Type | Syntax token styles — mapped type over all 16 token types |
ThemeTooltip | Type | Tooltip color overrides |
SyntaxTokenType | Type | Union of all 16 token type name strings |
TokenStyle | Type | Per-token style with color, optional fontWeight and fontStyle |
TokenStyleValue | Type | string | TokenStyle — accepted by every syntax token slot |
Forced-Colors Mode
Section titled “Forced-Colors Mode”notectl honors @media (forced-colors: active) (Windows High Contrast Mode, equivalent system-level accessibility settings). The editor falls back to system colors (CanvasText, Highlight, …) so structural elements stay legible even when a user’s OS overrides all color choices.
When writing custom styles for plugins, avoid setting background-color and color independently in a way that breaks the system palette — prefer system colors or currentColor inside forced-colors blocks.
For Plugin Authors
Section titled “For Plugin Authors”Plugins that create UI elements (popups, dialogs, overlays) should reference theme variables instead of hardcoding colors. Use context.registerStyleSheet() to inject CSS that references the theme custom properties:
// Register a stylesheet during plugin init()context.registerStyleSheet(` .my-popup { background: var(--notectl-surface-overlay); border: 1px solid var(--notectl-border); color: var(--notectl-fg); box-shadow: 0 4px 12px var(--notectl-shadow); }`);This ensures your plugin adapts automatically when the user switches themes, and remains CSP-compliant since all styles are injected via adopted stylesheets rather than inline style attributes.