Every control on Appearance → Theme options is declared in the active theme's theme.json. This page explains the schema, where values are stored, how a page can override them, and how they end up in CSS.
theme.json "defaults" --ThemeOptionsSchema--> Theme options screen (one tab per "tab")
| save
v
settings.theme_options[{theme}] (ThemeOptionStore)
| |
theme_option('key') in Blade ThemeCssCompiler (only if a CSS option changed)
|
built Vite CSS (resources/themes/{theme}/assets/sass/app.scss)
+ option values -> storage/app/public/css/themes/{theme}/app.css
|
FrontTheme shares $themeStylesheetUrl -> layout links itEach key of the defaults object is one option. Echo declares 161 of them.
resources/themes/echo/theme.json (two entries)
"primary_color": {
"default": "#345D32",
"type": "color",
"tab": "light-colors",
"label": "Primary color",
"helpTooltip": "Sets the main brand color used by primary controls, active states, focus rings, and derived primary variants."
},
"dropdown_background_color": {
"default": "#FFFFFF",
"type": "color",
"tab": "header",
"label": "Dropdown background (custom menu colors)",
"conditions": [{ "name": "theme[menu_color_mode]", "value": ["custom"] }],
"helpTooltip": "Used only when Menu colors is set to Custom colors."
}| Key | Meaning |
|---|---|
type | One of color, text, textarea, number, range, select, image-select, radio, switch, font, spacing, file (the settings field types) |
default | The value used until the site saves one. spacing defaults are objects ({"top": "2.5rem", "right": "0", ...}); switch values are the strings yes/no |
tab | The tab the field appears on. Echo uses light-colors, dark-colors, typography, layout, components, header, top-bar, navigation, breadcrumbs, footer; the core adds an Advanced tab with reset and regenerate buttons |
label, helpTooltip, help | Field label, tooltip and help text |
options, images | Choices for select, radio and image-select (value → label, value → preview image URL) |
validation | Laravel rules; when omitted a rule is derived from the type |
condition + conditionValue, or conditions | Show the field only while another option has a value. Refer to other options as theme[key] |
allow_alpha, allow_transparent | Colour picker extras |
min, max, step, rows, accepted | Passed to the field (range, textarea, file) |
persist | false keeps the option out of the stored defaults |
The schema is cached per file fingerprint, so edits to theme.json show up on the next request. The full list of Echo's options is in the Echo theme options catalogue.
App\Services\Themes\ThemeOptionStore keeps all values in one database setting, theme_options, keyed by theme name. Only values the site has saved are stored; everything else comes from the schema.
| Call | Does |
|---|---|
theme_option('header_preset', 'logo-left') | Stored value → schema default → your fallback, for the active front theme |
theme_nested_option('primary_color') | Reads a key inside a nested option array (legacy colour groups) |
app(ThemeOptionStore::class)->forTheme('echo') | All values for a theme, including request overrides |
->put('echo', $options) | Persist values and flush the response cache. It replaces the theme's whole stored set, so merge with persistedForTheme('echo') first when you change one key |
->forget('echo') | Reset a theme to its defaults |
A page can change options for itself only. The page form stores choices such as header preset, floating or sticky header, menus and breadcrumb mode in the page's settings JSON; when the page renders, PageSlugRenderer calls override_theme_options($page->settings). Overrides live for the current request and are never saved.
php — in your own controller
set_theme_option('header_preset', 'centered'); // this request only
override_theme_options(['show_breadcrumbs' => 'no', 'footer_widgets' => 'no']);
return view('testimonials::front.wall');A page that pins primary_menu keeps that menu even when the site's primary menu changes. When menu changes "do nothing", check the page's settings first.
App\Services\Themes\ThemeCssCompiler does not run Sass. It takes the stylesheet Vite already built for the theme (resources/themes/{theme}/assets/sass/app.scss in public/build/manifest.json), rewrites its CSS custom properties with the saved values, appends a few component overrides, and writes the result to the public disk.
--bs-* variables inside the compiler. A new option you add to theme.json appears on the screen and is readable with theme_option(), but it does not change any CSS by itself.app-editor.scss → app-editor.css) so the page builder canvas shows the site's real colours.After npm run build, regenerate the theme CSS
The public site serves the compiled copy, not the fresh Vite build. After changing Echo's Sass and rebuilding, open Appearance → Theme options → Advanced and click Regenerate Theme CSS (route admin.settings.theme-options.compile-css), or save the options again. Until then the site keeps the old compiled stylesheet.
Because new options do not reach the compiled CSS, apply them where you render. Validate the value in the schema and still escape it:
theme.json + Blade
{{-- theme.json --}}
"harbor_ribbon_color": { "default": "#0E7490", "type": "color", "tab": "components",
"label": "Ribbon colour", "helpTooltip": "Background of the announcement ribbon.",
"validation": ["nullable", "regex:/^#[0-9a-fA-F]{6}$/"] }
{{-- components/ribbon.blade.php --}}
<div class="harbor-ribbon" style="--harbor-ribbon: {{ theme_option('harbor_ribbon_color') }}">
{{ $slot }}
</div>In your Sass, read the variable (background: var(--harbor-ribbon, var(--bs-primary));) so the default still follows the theme's primary colour.
var(--bs-primary), var(--bs-body-color), var(--bs-border-color)) and color-mix() for tints. Never hard-code hex values: they break as soon as a site changes its palette or uses a dark palette.--bs-header-* tokens (header-bg, header-color, header-hover-color) so menus and dropdowns match the header the site chose.data-bs-theme. Test your plugin with a dark palette from one of the website templates.use App\Services\Themes\ThemeOptionStore;
it('uses the configured ribbon colour', function (): void {
$store = app(ThemeOptionStore::class);
$store->put('echo', [...$store->persistedForTheme('echo'), 'harbor_ribbon_color' => '#123456']);
expect(theme_option('harbor_ribbon_color'))->toBe('#123456');
});