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-- and text-- classes through the utility remaps: theme-slots.utility-* and the bulk utility-remap-* helpers.
  • Optionally give icons their own color with theme-slots.semantic-icon; without it, image--adaptive icons 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 .

#

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-token helper.
#

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.