Authoring Themes
Build your own theme by filling in slots: named parts of the screen, like the background, the text, or the title bar, that you point at new colors. This page walks through the workflow: start from the boilerplate, map your slots, register the id, and lint.
The Contract
A theme picks colors by name, through the theme-slot mixins; it never writes raw color values. Because of that, every color still renders correctly on every device: dithered down to the panel's inks where needed, exact on full color.
- Set the whole screen's colors at once with the semantic channels:
theme-slots.semantic-bg/text/stroke/border. - Recolor a single part, like the title bar, with a component slot:
theme-slots.bg-slot/text-slot/border-level-slot/border-token-slot. - Recolor what plugins set with
bg--andtext--classes through the utility remaps:theme-slots.utility-*and the bulkutility-remap-*helpers. - Optionally give icons their own color with
theme-slots.semantic-icon; without it,image--adaptiveicons follow the theme's primary text color. - Pick the colors charts draw their series with:
theme-slots.chart-series-ramp. - Do not override the base palette (
--white,--black,--gray-*,--color-*) or the framework's own paint variables (--bg-*,--text-*,--border-*); the linter rejects both.
The full list of channels, slots, and remaps lives on Theme Slots Theme Slots Every part of a screen a theme can recolor, from whole-screen colors down to single components, utilities, borders, and chart series This page lists every slot: each part of a screen a theme can recolor, and the mixin that sets it. A slot takes a framework token, not a raw color, so whatever you map still renders correctly on every device. .
Start from the Boilerplate
framework/themes/_theme-boilerplate.scss is the
starting point. A theme is one file: load the theme-slot mixins, declare 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. The browser locks in layer order from the first stylesheet it reads, so without that line a theme
loaded 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 order, so read them as worked examples:
black-and-yellow-theme.scss turns the
screen dark, white-and-red-theme.scss keeps
it bright and makes 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
Visually customize any plugin with a drop-in stylesheet that recolors the whole screen
Themes are a simple way to visually customize any TRMNL plugin. A theme adjusts the framework's colors (backgrounds, text, borders, chart colors) without touching plugin markup, and gracefully adapts to every supported device, from 1-bit ePaper to 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
screen--dark-mode has no effect on a themed
screen: a theme already decides every color, so the framework's own dark rules step aside on
their own.
A theme that wants a dark variant styles the combination itself, with the same mixins as the rest of the theme.
.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 bypasses device rendering, so the surface paints one flat color instead of dithering down to
the panel's inks where needed.
Expose Values to JavaScript
A theme hands extra values to plugin code through CSS alone: set your own
--* variables on the screen,
and JavaScript reads them back with TRMNLPaint.cssVar().
See
Paint API
Paint API
TRMNLPaint: read the exact colors and patterns CSS paints right now, from JavaScript
TRMNLPaint is the framework's JavaScript API for its paint: colors, border lines, and text styles. Ask it for any framework color and it returns what CSS would actually paint right now, with the device and the active theme already applied. Use it wherever JavaScript draws: charts, canvases, or your own rendering.
.