Authoring Themes
A theme maps framework slots to different tokens and never touches the paint pipeline, so a themed screen keeps its device-capability rendering. This page walks the workflow: start from the boilerplate, map your slots, register the id, and lint.
The contract
A theme sets token references through the theme-slot mixins and never touches the paint pipeline directly. The screen's device mode still decides how every token renders: dither patterns on 1-bit, palette images on limited color, solids on full color.
- Allowed: semantic channel refs (
theme-slots.semantic-bg/text/stroke/border) - Allowed: component slot refs (
theme-slots.bg-slot/text-slot/border-level-slot/border-token-slot) - Allowed: utility token remaps (
theme-slots.utility-*and the bulkutility-remap-*helpers) - Optional: icon paint for adaptive images (
theme-slots.semantic-icon). When omitted,image--adaptiveicons follow the theme's semantic text-primary paint. - Disallowed: root palette overrides (
--white,--black,--gray-*,--color-*) - Disallowed: framework-owned paint variables (
--bg-*,--text-*,--border-*)
The full vocabulary of channels, slots, and remaps lives on Theme Slots Theme Slots Every themable surface: semantic channels, component slots, utility remaps, border lines, and the chart ramp Every surface a theme can re-point, with the mixin that sets it: semantic channels, component slots, utility remaps, border lines, and the chart ramp. Slots take token references, so each one still resolves through the device mode at render time. .
Start from the boilerplate
framework/themes/_theme-boilerplate.scss is the
starting point. A theme is one file: load the theme-slot mixins, emit the framework layer order, open the
tn--themes layer, and scope every rule to
.trmnl .screen--theme-<name>.
Keep @include layers.order; above the layer
block. Cascade layer order is fixed by whichever stylesheet the browser parses first. Without it, a theme
link placed before plugins.css loses to the
framework defaults.
@use "../config/layers" as layers;
@use "../mixins/theme-slots" as theme-slots;
@include layers.order;
@layer tn--themes {
.trmnl .screen--theme-example {
// 1. Semantic channels: the whole screen in a few lines.
@include theme-slots.semantic-bg("canvas", "yellow");
@include theme-slots.semantic-text("text-primary", "black");
// 2. Component slots: tune specific surfaces.
@include theme-slots.bg-slot("title-bar", "yellow-40");
// 3. Utility remaps: re-point the raw bg--/text-- utilities.
@include theme-slots.utility-remap-grayscale("yellow");
// Optional: give image--adaptive icons their own paint
// (they follow text-primary when this is omitted)
// @include theme-slots.semantic-icon("yellow-20");
}
}
Work in that order: semantic channels first, then component slots, then utility remaps. The shipped
themes follow the same section order, so they read as reference implementations:
black-and-yellow-theme.scss is a dark-side
grayscale remap, white-and-red-theme.scss a
bright-side remap with one targeted utility exception.
Register and lint
The file name, the registry id, and the screen class stay in sync:
themes/<id>-theme.scss registers as
<id> in
lib/framework/themes.rb and applies as
screen--theme-<id>.
rake framework:themes:lint enforces the
contract on every theme file:
- The theme files on disk match the registry ids.
- No framework-owned paint variables (
--bg-*,--text-*,--border-*). - No root palette overrides (
--white,--black,--gray-*,--color-*). - No calls to the deprecated
role-tokenhelper.
Compile and ship
Each theme compiles to its own stylesheet, never into
plugins.css. It ships as a second
<link> next to the framework, and the
screen opts in with its theme class; see
Themes
Themes
Opt-in stylesheets that re-theme screens while preserving device-capability rendering
Themes are standalone stylesheets that re-point semantic channels, component slots, and utility tokens at different palette tokens. A themed screen still renders through its device mode: dither patterns on 1-bit, palette images on limited color, solids on full color.
for usage and
Compiling the Framework
Compiling the Framework
Compile plugins.css and the theme stylesheets from source with Dart Sass
Compile the framework from source with Dart Sass: one entrypoint, one load path, plus a stylesheet per theme. The result is the same plugins.css the release pipeline produces. You only need a source build when a released one is not enough, for modified source or custom device profiles.
for the compile command.
Dark mode
Themed screens are exempt from the framework's dark-mode remaps: every dark rule carries a
zero-specificity :where(:not([class*="screen--theme-"]))
gate, so a theme is a complete color statement and
screen--dark-mode has no effect on it.
A theme that wants a dark variant styles the combination itself, with the same plain mixins as the rest of the theme. There is no dark remap to work around, so the device mode still resolves every token: dither patterns on 1-bit, palette tiles on limited color, solids on full color.
.trmnl .screen--theme-example.screen--dark-mode {
@include theme-slots.semantic-bg("canvas", "black");
@include theme-slots.semantic-bg("surface", "black");
@include theme-slots.semantic-text("text-primary", "yellow");
}
Do not use $raw: true in a theme.
It skips the mode pipeline, so the surface renders as one flat color with no dither on 1-bit and no
palette tile on screen--color-4bwry. The
framework uses it only for its own unthemed dark defaults.
Expose values to JavaScript
Themes never ship JavaScript. A theme that wants to hand extra values to plugin code publishes its own
public --* custom properties on the screen,
and JS reads them back with TRMNLPaint.cssVar().
See
Paint API
Paint API
TRMNLPaint: read the live CSS cascade from JavaScript to resolve framework colors and tile patterns
TRMNLPaint is the framework's public JavaScript paint API. It reads the live CSS cascade (bit depth, dark mode, theme, limited palette, and tiles all resolved) and hands back a canonical Fill, so token mappings are never duplicated in JavaScript. Charts are just one consumer; any plugin can resolve framework colors from JS for any purpose.
.