Skins
On this page
A skin sets the look of the editor chrome (toolbar, menus, popovers, dialogs, find panel, status bar, source view, character picker, selection toolbar) and the content area. Skins live in @better-rich-text/skins. Each is a stylesheet scoped by [data-brt-skin="<id>"] on .brt-editor and .brt-popover-layer, inside the brt cascade layer, so your own styles always win.
Choosing a skin
import { createRichTextEditor } from '@better-rich-text/core';
const editor = createRichTextEditor('#editor', { skin: 'galley', theme: 'auto' });
editor.setSkin('bevel');
editor.setTheme('dark');skin defaults to studio. theme (light, dark or auto) picks the colour scheme for skins that have both; single-scheme skins ignore it. EditorUi takes the same skin and theme options and has setSkin() and setTheme(). A skin's CSS is added to the document (or shadow root) the first time it is used. Switching skins never recreates the editor.
| Skin | Schemes | Description |
|---|---|---|
studio |
light, dark | Default. Black ink, white stock and a vermilion signal. Square, high contrast, toggled buttons invert, and the selection toolbar is printed in reverse. |
galley |
light, dark | Warm proof paper, ink and blue pencil, with a book serif for the writing area. |
native |
light, dark | System colours, fonts and form controls. Follows the OS accent colour and appearance. |
meadow |
light | Glossy blue frame, cream panels and a green primary button, after the 2001-era desktop. |
bevel |
light | Grey faces, two-pixel bevels and navy highlights, after the 1995-era desktop. |
midnight |
dark | Blue-black ground with sodium-lamp amber. |
sorbet |
dark | Warm charcoal with strawberry, tangerine, lemon and lime, after fruit-toned code editor palettes. |
sepia |
light, dark | Cream paper, brown-black ink and an oxblood accent, with a book serif for long reading. |
phosphor |
dark | Green phosphor terminal: reverse video, double-line frames, one fixed-pitch face. |
contrast |
light, dark | WCAG AAA high contrast: 7:1 text, heavy rules, 3px focus rings. |
skins, skinIds and getSkin(id) describe each skin: label, description, reference, colorSchemes and the token values in tokens. Use them to build a skin picker.
Overriding tokens
Every colour, font and size comes from a --brt-* custom property. Set the ones you want on the editor. Rules outside the brt layer always beat skin rules, and inline styles are copied to the popover layer:
.my-app .brt-editor, .my-app .brt-popover-layer {
--brt-accent: #0b6e4f;
--brt-focus: #0b6e4f;
--brt-content-font: "Iowan Old Style", Georgia, serif;
}Colour tokens: canvas, surface, surface-raised, surface-inset, border, border-strong, text, text-muted, accent, accent-soft, accent-contrast, danger, warning, success, focus, selection, selection-text, insertion, deletion, comment, placeholder, mark, search-match, search-current, search-current-text, pressed, pressed-text, pressed-border, hover, menu-active, menu-active-text, menu-active-bar.
Other tokens: ui-font, content-font, code-font, radius, ui-size, content-size, focus-width, control (minimum control size: 32px, or 44px on coarse pointers; leave it to the editor).
Optional tokens have defaults in the base theme, so a skin only sets them to change them:
| Token | Default | Use |
|---|---|---|
--brt-caret |
var(--brt-text) |
Caret colour in the writing area. Studio uses the accent. |
--brt-measure |
none (46rem in fullscreen) |
Maximum line length of the writing area. The content is centred when it is narrower than the editor. |
--brt-elevation |
a short, tight shadow | box-shadow for popovers, menus and the table bar. Set none for flat skins. |
--brt-duration |
120ms |
Length of hover, press and popover transitions. Reduced motion turns them off. |
.my-app .brt-editor { --brt-measure: 40rem; }Writing a custom skin
Use defineSkin and registerSkin, then pass the id:
import { defineSkin, registerSkin, skins } from '@better-rich-text/skins';
registerSkin(
defineSkin({
id: 'harbour',
label: 'Harbour',
description: 'Navy and brass.',
reference: 'Ship chandlery signage: navy enamel and brass.',
base: { ...skins.studio.tokens.base, radius: '4px' },
light: { ...skins.studio.tokens.light!, accent: '#1d3f6e', focus: '#1d3f6e', pressed: '#1d3f6e', 'pressed-border': '#1d3f6e', 'menu-active': '#1d3f6e' },
components: (s, editor, layer) => `
${s} .brt-toolbar { border-block-end: 2px solid #b08d3c; }
${editor} { border-radius: 8px; }
`,
}),
);
createRichTextEditor('#editor', { skin: 'harbour' });A skin must define every colour token for each scheme it supports and every base token. Prefix component selectors with s (editor and popover layer), editor or layer. The dark palette only emits tokens that differ from the light one.
You can also ship plain CSS for an id that is not registered. The editor sets data-brt-skin="harbour" and leaves the styling to you:
@layer brt {
:is(.brt-editor, .brt-popover-layer)[data-brt-skin="harbour"] { --brt-accent: #1d3f6e; }
}Keep these when you change a skin:
Text at 4.5:1 against every surface it sits on; focus rings and input borders at 3:1.
Pressed, mixed and disabled states that differ in shape or value, not hue alone. The
pressed-bordertoken, or a fill at 3:1 against the toolbar, covers pressed.Logical properties (
inline-start,block-end) so right-to-left layouts mirror.No
--brt-controlbelow 32px. Forced-colours and reduced-motion handling comes from the base theme; if you draw state withbox-shadowor gradients (which forced colours removes), add a@media (forced-colors: active)fallback.
The skin tests in packages/skins/test check scoping, required tokens and contrast for every built-in skin.