How theme settings declared in theme.json reach your CSS, and the
patterns themes use to keep them maintainable.
For the settings block declaration syntax (groups, field types,
defaults) see
theme-json-reference.md. For introductory
orientation see theme-development.md.
1. CSS custom properties
Every scalar setting is projected onto a CSS custom property named
--td-<group>-<field> (hyphen-separated, lowercased). So this schema:
"settings": {
"colors": { "fields": { "accent": { "type": "color", "default": "#8c2a3a" } } },
"layout": { "fields": { "content_width": { "type": "select", "default": "normal" } } }
}
…is rendered by $themeStyle->renderCssVariables() as:
:root {
--td-colors-accent: #8c2a3a;
--td-layout-content-width: normal;
}
textarea and image fields are skipped — they can't be scalar CSS
values. boolean is skipped too (CSS can't meaningfully express it).
Values are emitted verbatim. The core does not know what normal
means; your theme does. The pattern is:
/* Alias --td-* variables to the names used through the stylesheet. */
:root {
--color-accent: var(--td-colors-accent, #2563eb);
--color-on-accent: #ffffff;
--color-border: #e5e7eb;
--color-surface: #ffffff;
--content-width: 780px; /* fallback */
}
/* Branch on the setting value via body classes (base.latte appends
`width--<key>` to the body based on the setting). */
body.width--narrow { --content-width: 680px; }
body.width--normal { --content-width: 780px; }
body.width--wide { --content-width: 960px; }
This split keeps the core free of any vocabulary — no "narrow means 680px" mapping in PHP — while letting your theme express the semantics however it wants.
Use those semantic aliases throughout your component and plugin
component chrome. Do not hard-code a second design system for .td-form
or .td-social-*; let those selectors inherit the same tokens as the
rest of the theme. That keeps Form, Social, and future plugin components
in sync with /admin/theme-settings.
2. Switching assets per setting
For settings that need to change more than just colour — different
Google Fonts, different layout files — branch in base.latte before
emitting <link> tags:
{var $fontStyle = $theme->setting('typography.font_family', 'sans')}
{if $fontStyle === 'sans'}
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
{elseif $fontStyle === 'serif'}
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,[email protected],400..700&display=swap">
{/if}
Only load what you need — every extra family is bytes on the critical path and a licence surface.
3. The custom CSS escape hatch
If your theme ships with (say) three font styles, a user who wants a fourth — a Japanese operator who wants Noto Sans JP, say — has two options:
-
Override via custom CSS. Declare an
advanced.custom_cssfield of typetextarea. Emit its content as a<style>block inbase.latte, after your theme stylesheet, so it always wins:{var $customCss = $theme->setting('advanced.custom_css', '')} {if $customCss}<style>{$customCss|noescape}</style>{/if}The user can then paste, e.g.,
@import url(...Noto+Sans+JP...); body { font-family: 'Noto Sans JP', sans-serif; }. -
Fork the theme. Copy it, add a fourth preset, distribute.
The first is the recommended pattern for site-level tweaks. The second is the right answer when the theme needs real structural changes.
Source: https://github.com/typedock/core/blob/main/docs/theme-settings.md