Theme Customizer
Some of you may have seen the “My view” and “Board setup” theme customizer that is often shipped with some of my themes. This is a lightweight solution giving some options to themes. The idea is to bring something more advanced inside the ACP and dev boards will be able to register and wire certain settings to their theme. This is coming somewhere in the near future. In this topic, I’ll explain how it works.
Everything below runs inside your theme's layout HTML and CSS.
The ideas is to have one CSS variable, with three levels of override:
Your theme CSS should never use a raw color twice. Define a small set of theme variables, and give each one a fallback chain:
body.myt {
--myt-accent: var(--mytp-accent, #3ec9a7);
}
body.myt .btn-primary {
background: var(--myt-accent);
}Read it right to left:
1. #3ec9a7 is your theme default, shipped in the CSS.
2. --mytp-accent is the admin override. It only exists when the admin saved one (see Board setup below), otherwise the fallback wins.
3. A member override is applied on top by setting --myt-accent inline on body from JavaScript, which beats the rule above.
Convention: use one prefix for the variables your CSS consumes myt-) and a second one for the admin-saved properties mytp-). That separation is what makes the three levels compose.
Board setup: theme custom properties
Admin choices are stored on the theme row as custom properties, a simple name to value map. The platform renders them into your theme stylesheet automatically:
:root {
--mytp-accent: #7a5cf0;
--mytp-heads: crown;
}So a saved property is just a CSS variable that exists on every page load, before anything paints. No JavaScript involved in applying it.
Rules the platform enforces:
- Names: lowercase letters, digits and dashes, up to 40 characters, stored without the leading -- (you save mytp-accent, it renders as --mytp-accent).
- Values: a hex color, var(--something), a url("/storage/..."), a theme resource reference, or plain text up to 80 characters (letters, digits, spaces, . , % -).
- At most 40 properties per theme.
Reading and saving from the panel
There is one JSON endpoint for it, on the board's admin surface:
GET /admin/themes/{id}/appearance
PUT /admin/themes/{id}/appearanceGET returns the current state:
{
"theme": { "id": 66, "name": "Nimbus" },
"defaultColorScheme": "dark",
"memberBackdrops": false,
"customProperties": { "mytp-accent": "#7a5cf0", "mytp-heads": "crown" },
"resources": [ { "name": "hero.jpg", "url": "..." } ]
}PUT accepts partial updates. Send only the keys you are changing; unsent keys are untouched. null (or an empty string) deletes a property, which returns it to your theme default through the fallback chain:
fetch('/admin/themes/' + themeId + '/appearance', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json'
},
credentials: 'same-origin',
body: JSON.stringify({
properties: { 'mytp-accent': '#7a5cf0', 'mytp-heads': null },
defaultColorScheme: 'dark'
})
});Saving bumps the theme's version hash, so every visitor's cached stylesheet refreshes on their next page load. You never need to bust caches yourself.
Two practical details from for example Nimbus:
- Finding your theme id in JS: read it from your own stylesheet link, document.querySelector('link[href*="/main.css"]') and match themes/(\d+)/main.css.
- Showing the tab only to admins: just try the GET. It succeeds for board admins and fails for everyone else, so reveal the Board setup tab when the response is OK and keep it hidden otherwise. No separate permission check needed.
After saving, apply it live
A save only affects the next page load, so mirror the change immediately for the admin who clicked:
document.documentElement.style.setProperty('--mytp-accent', '#7a5cf0');
document.documentElement.style.removeProperty('--mytp-heads');My view: member choices in localStorage
Member preferences never touch the server. Store them in localStorage and apply them from a small script at the very top of your layout, before any content markup. That placement matters: it runs before the page paints, so the saved look is there on the first frame and nothing flashes.
<script>(function () {
var b = document.body;
var accent = null;
try { accent = localStorage.getItem('myt-accent'); } catch (e) {}
if (accent) { b.style.setProperty('--myt-accent', accent); }
var motion = 'alive';
try { motion = localStorage.getItem('myt-motion') || 'alive'; } catch (e) {}
if (motion === 'calm' || window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
b.classList.add('myt-calm');
}
})();</script>Pair class toggles like myt-calm with CSS that stills your animations:
body.myt.myt-calm .myt-hero { animation: none; }Recipes
An admin feature toggle (absent means on)
Store off when disabled and delete the property when enabled. Your boot script reads it straight from CSS, so the saved state needs no extra round trip:
var spineOff = getComputedStyle(document.body).getPropertyValue('--mytp-spine').trim() === 'off';
if (! spineOff) { document.body.classList.add('myt-spine-on'); }Panel wiring: toggle on sends { properties: { 'mytp-spine': null } }, toggle off sends { properties: { 'mytp-spine': 'off' } }.
A permission the admin grants to members
Nimbus has a "Members may restyle" switch. It is just another property nbp-members-restyle: 0 when revoked. The member boot script checks it before applying any saved member accent, and the panel hides the member section when it is 0. Members keep their stored choice; it simply stops applying until the admin allows it again.
A board default pair with a member override
The admin saves mytp-a1 and mytp-a2 (Board setup). A member picking their own pair writes to localStorage and sets --myt-a1 / --myt-a2 inline on body (My view). Clearing the member choice removes the inline properties and the admin defaults show through again. This is exactly the chain from the first section, in practice.
Checklist
Theme CSS consumes
--myt-*variables withvar(--mytp-*, default)fallbacks. No repeated raw values.Boot script at the top of the layout applies member state before first paint.
Board setup tab appears only when
GET .../appearancesucceeds.Saves send only changed keys, use
nullto delete, and apply the change locally as well.A Calm or reduced-motion path exists for every animation.
Property names fit
[a-z][a-z0-9-]*and values fit the allowed formats (hex, var, url, resource, short plain text).
If there’s any questions, happy to answer them.
ForumSpark Support