How to make a HomeGlow theme: a folder of data (a manifest, pictures and fonts) that recolors and reshapes the dashboard and can animate a scene behind it. Themes never contain code, so they are safe to share.
Related reading:
- Theme Architecture: the design and the rules behind it.
reef/andstarship/in jherforth/HomeGlowThemes: two complete themes to copy from.
aurora/
theme.json the manifest
assets/ pictures: .svg, .png, .webp or .jpg
fonts/ .woff2 files, with their license
Install the folder with Add a theme folder (§7) and choose the theme
under Admin → Look → Appearance. To build a theme into the app instead, put
the folder in client/src/themes/; HomeGlow finds it on the next build. The
folder name must match the manifest's id.
{
"manifestVersion": 1,
"id": "aurora",
"name": "Aurora",
"version": "1.0.0",
"author": "Your name",
"description": "Northern lights over a dark sky.",
"extends": "classic",
"modes": ["dark"],
"variety": "load",
"colors": { "primary": "#05070f", "secondary": "#7ff5c8", "accent": "#7ff5c8" },
"fonts": [{ "family": "Aurora Sans", "weight": 400, "src": "fonts/aurora-400.woff2" }],
"tokens": { "all": {}, "light": {}, "dark": {} },
"mui": {},
"ambience": []
}| Field | Required | Meaning |
|---|---|---|
manifestVersion |
yes | 1, or 2 for a theme that uses ornaments, the meter tokens or clumps. A HomeGlow older than the version refuses the theme. |
id |
yes | Lowercase slug (a-z, 0-9, hyphens), the same as the folder name. |
name |
yes | Shown in the theme picker. |
version, author, description |
As in a plugin manifest. The author shows as "by ...". | |
extends |
Another theme's id. Its tokens, colors and MUI options apply first, then yours. Most themes extend classic. |
|
modes |
["light", "dark"] by default. A dark-only theme lists ["dark"] and shows dark whatever the display's mode. |
|
variety |
How often the scene behind the widgets changes: load (each page load, the default), day (every display shows the same scene, and it changes daily) or fixed. |
|
colors |
primary, secondary and accent as hex. Plugins are told these. |
|
fonts |
.woff2 files in your fonts/ folder: family, weight (100 to 900), optional style (normal or italic), src. A font downloads only when text uses it. |
|
tokens |
Values for the CSS custom properties, in all, light and dark. See §3. |
|
mui, muiModes |
Options for buttons, inputs, dialogs and sliders; muiModes.light and .dark override per mode. See utils/themes.js, MUI_TYPES. |
|
ambience |
Layers drawn behind the widgets. See §4. | |
ornaments |
Pictures drawn on every widget frame. See §4a. | |
confetti |
The theme's own celebration confetti: colors (a list, or per mode), shapes (some of square, circle, streamer), pictures (as in a sprites layer, height in px) and mix (the share of pieces that are pictures, 0 to 1). Without it, a theme gets Classic's confetti. |
Every token has a type, and a value that doesn't fit is rejected. Values may
never contain ;, braces, url( or similar, so a theme can't inject a
stylesheet. The full list is THEME_TOKENS in client/src/utils/themes.js;
the most useful:
| Token | Type | What it sets |
|---|---|---|
--background, --surface, --text, --text-secondary, --border |
color | The page and its text |
--accent, --accent-rgb |
color, r, g, b |
Highlights |
--hg-page-image |
gradient | An image over the page background |
--hg-frame-bg, --hg-frame-radius, --hg-frame-shadow |
color, lengths, shadow | Each widget's frame |
--hg-frame-decoration-width, -style, -color |
lengths, keyword, up to four colors | A border drawn over the frame's edge (Starship's elbows) |
--hg-frame-inset |
0 to 24px a side |
Room inside the frame for its decoration and ornaments; widget content, plugins included, sits inside it |
--hg-frame-image, --hg-frame-overlay |
gradient | A tint under the frame's content, a reflection over it (Reef's glass) |
--dock-bg, --dock-active-bg, --dock-active-image |
color, gradient | The dock |
--hg-font-body, --hg-font-heading |
font list | Type |
--hg-grid-gap |
0, 8px, 16px or 24px |
The gap between widgets; rows keep their pitch |
--hg-meter-track, --hg-meter-fill |
color | The empty and filled parts of anything showing a level or progress |
--hg-meter-thickness, --hg-meter-cap |
1px to 12px; round, square or butt |
A meter's line and its ends |
Themes never change spacing, font size or line height: widget heights are fixed, and content must still fit.
ambience is a list of layers, drawn in order (later ones on top). Each
names a building block in layer, may limit itself to modes, and sets that
block's options. Numbers may be a range, [low, high]: HomeGlow picks a value
for each item, so with variety set to load or day the scene varies.
Pictures (src) are files in your assets/ folder. A picture can differ by
mode, { "light": "assets/a-day.svg", "dark": "assets/a-night.svg" }, and so can
a tint. A one-color silhouette can be recolored with tint, so one file
serves both modes; give multi-color pictures one file per mode.
A sprites layer can mix kinds of thing with pictures: a list of
{ src, aspect, height, tint, weight }. Each copy picks one, by weight, and
taller copies are drawn behind shorter ones. Reef scatters eight kinds of coral
this way, still ("motion": "none") and randomly mirrored ("flip": true), so
every load is a different reef.
Any layer can also set chance, from 0 to 1: how likely it is to be in a
scene at all. Starship's planets have a chance of 0.75 and its galaxies 0.6, so
some nights there is a ringed planet and the Milky Way, and some nights only
stars.
| Layer | Draws | Options |
|---|---|---|
image |
A still picture, or one of a list | src (a picture or a list), anchor (bottom, top, center, fill), height (e.g. "42vh"), angle (turns it, growing it to cover the screen), flip, opacity |
sprites |
Copies of a picture, or a mix of pictures, spread across the screen | src or pictures, count, height (vh), aspect (width ÷ height), span ([from, to] %), base (distance from the bottom), clumps and clumpWidth (gather copies into that many patches, each that many percent wide, with open ground between, as plants grow), lift (percent of the screen above base, drawn per copy), hue (degrees to turn each copy's colors), motion (sway, bob, pulse, none), angle (sway degrees) or distance (bob px), seconds, tint, flip, current, opacity |
particles |
Many small things | src or colors (plain dots), motion (rise, fall, twinkle), count, size (px), seconds, drift (px), span, opacity |
dots |
A still field of dots, painted once | count, colors, size, opacity. Each dot's size and opacity follow one brightness, mostly dim, so dim dots are small; the brightest get a halo. |
field |
Drifting light (caustics) | strength (soft, bright), spacing, seconds |
streaks |
An occasional streak on a random path | color, length, every (pause, seconds), seconds, burst (streaks per pass: a meteor storm) |
flyby |
Now and then, one picture crossing the screen | pictures, height (vh), every (pause, seconds), seconds (to cross), span ([from, to] % from the top), tilt (degrees of climb), spin (degrees of tumble per crossing), opacity. Draw pictures facing right. |
blobs |
Soft color blobs drifting and swelling | colors, count, size (vmin), seconds, opacity |
colors and tint take a list, or { "color": "#...", "weight": 3 } entries
for a weighted pick.
Each sprite moves on two periods that don't divide evenly, so its motion never
quite repeats. With "current": true, a slow third motion rolls across the
sprites like a passing current.
Limits: 12 layers, 40 particles and 16 sprites per layer, 400 dots, durations of 1 to 120 seconds (a flyby's pause, up to an hour).
ornaments is a list (at most 8) of pictures from your assets/ folder, drawn
on every widget frame, native and plugin alike, above the frame and under its
decoration. They are still, take no pointer events, and are clipped to the
frame's shape.
| Option | Meaning |
|---|---|
src |
A picture, or { "light", "dark" } pictures |
anchor |
top-left, top, top-right, right, bottom-right, bottom, bottom-left, left, center, or corners: one picture drawn for the top-left corner, mirrored into all four |
size |
Height of the picture (an edge picture's thickness), e.g. "28px"; default 24px |
aspect |
Width ÷ height, default 1 |
stretch |
For an edge: run the picture along the whole edge |
tint |
Recolor a one-color silhouette, as for sprites; may differ by mode |
opacity |
0 to 1 |
Ornaments take no space. Make room for them with --hg-frame-inset, at most
24px a side, since every pixel comes out of a fixed-height widget. Keep each
ornament inside that room, or it will sit over the widget's content.
"ornaments": [
{ "src": "assets/bezel.svg", "anchor": "corners", "size": "26px", "tint": { "light": ["#0b6e8a"], "dark": ["#4fe3d6"] } }
]A wall display can be a Raspberry Pi. The engine keeps every layer to these rules, and a theme should not fight them:
- Only
transformandopacityanimate. - Nothing animates for someone who prefers reduced motion, and nothing renders while the photo screensaver covers the dashboard.
- No live blur (
backdrop-filter) over a moving scene, and no full-screen blend modes: either one costs a slow GPU most of its frames. Reef's glass is a still tint and highlight instead. - Check your theme in Chromium with the CPU slowed 4× (DevTools, Performance, CPU throttling). It should hold about 60 frames a second.
validateThemePackage(manifest, { assets }) in client/src/utils/themes.js
reports every problem as a readable sentence, and npm test runs it over every
theme folder. Then pick the theme in Admin and look at it in light and dark,
on a desktop and a phone width.
HomeGlow builds in only Classic. Other themes are installed in Admin → Look → Themes:
- Get themes lists the themes repository
(jherforth/HomeGlowThemes),
one folder per theme at its root, named by the theme's
id. Apreview.pngbesidetheme.jsonshows in the list. - Add a theme folder installs a folder from your computer. The admin page
runs
validateThemePackagebefore sending it, and shows what's wrong.
Installed themes are kept by the server and offered to every display. Each
display runs the same check before using one, so a theme that fails is listed
as unusable and never partly applied. Files other than theme.json,
assets/ and fonts/ (a README, a preview) are ignored. SVGs may not carry
script. A theme may hold 64 files, 2 MB each, 8 MB in all.
An installed theme with a built-in theme's id replaces it, except Classic. To share a theme, open a pull request adding its folder to the themes repository.
All are in the themes repository.
- Starship (
starship/) layers a sometimes-galaxy (image, picked from three, at a random angle, with achance), a starfield in stellar colors (dots), up to three planets (spriteswithliftandhue), twinkles, shooting stars and meteor storms (streakswithburst), and twoflybylayers: five kinds of ship, and the occasional tumbling alien. Its confetti mixes tinted sparkles, tiny ships and an alien into LCARS-colored streamers; Reef's throws fish, starfish, shells and bubbles. - Reef (
reef/) uses six layers:fieldcaustics,particlesbubbles frombubble.svg, a sea floor per mode (image), a stillspriteslayer scattering eight kinds of coral, swaying tinted kelp with a current, and a swaying mix of fans, whips and anemones. Every load is a new reef. - Spring, Summer, Autumn and Winter (
spring/,summer/,autumn/,winter/) are the simplest kind of theme: no pictures or fonts, only tokens and twoblobslayers per mode, a few large slow washes of seasonal color under smaller, quicker ones. Each has a light and a dark palette, and confetti in the season's colors.