Cast Docs.
Craft CMS 5.10+ and PHP 8.2+.
composer require bensomething/craft-cast:^1.0.0-beta
If Composer refuses, your project's minimum-stability is stable. Set it to beta with "prefer-stable": true, or pin the exact version.
Settings → Plugins → Cast sets the site-wide default and which themes users can pick from. Users choose their own under Account → Preferences → Colour mode.
Auto is a pair, not a theme. Anyone set to Auto gets the two themes configured on the Auto tab, so the Default theme has no effect on them.
If the picker doesn't appear on the preferences screen, run
php craft clear-caches/compiled-templates. Cast renders it through Craft'scp.users.edit.prefshook, and a stale compiled template silently skips it.
| Theme | Scheme | Notes |
|---|---|---|
| Dark | Dark | Neutral dark CP. The reference implementation. |
| Dim | Dark | Softer and lower contrast, for long sessions. |
| Stone | Light | A warm neutral, based on Tailwind's stone ramp. |
| Stone Dark | Dark | The same warm neutral, but.. dark. |
| High Contrast | Light | Near-black text, solid borders, widened focus ring. |
| High Contrast Dark | Dark | The same treatment on a near-black canvas. |
Repoints the fill of Craft's primary buttons at one of nine colours. Beyond primary buttons, --bg-primary reaches only the Plugin Store's cart badge and the installer's step dots.
Each is shaded per mode so the white label clears WCAG AA against the fill (≥4.5:1 light, ≥4:1 dark). Black/White carries its own label colour so it can swap ends with the mode. Amber is a knowing compromise at 3.19:1 — the only warm colour distinct enough from the default that keeps a white label. Status colours stay red.
Drop a .css file in cast-themes/, alongside config/ and templates/. The filename is the handle, a header comment supplies the rest.
/**
* Theme Name: Midnight
* Color Scheme: dark
* Description: Near-black, for late sessions.
*/
html[data-cast-theme="midnight"] {
--body-bg: #05070d;
--text-color: #e6ecff;
}
Only Color Scheme really matters: it decides whether _dark-base.css loads ahead of your theme, and which side of Auto it sits on. The name falls back to the filename and the scheme to light, so a file with no header is still a theme. Files starting with an underscore are partials and skipped.
The file is the registration, so a theme can't exist in one environment and not another. Settings → Plugins → Cast → Themes lists what was found and any file that couldn't be read, with the reason. The folder is published to cpresources — it needn't sit in the web root, and editing a theme busts its own cache. Point elsewhere with themesPath in config/cast.php.
Bundled handles win, so a dark.css won't quietly redefine Dark for everyone already using it. To genuinely replace one, register it in code.
Craft's palette derives almost entirely from a --gray-*-hsl ramp plus semantic properties (--pane-bg, --text-color, …), and most themes are a page of variables. Dark themes get _dark-base.css ahead of them — the inverted ramp plus patches — so they only declare deltas. Cast also sets data-cast-scheme to light or dark, so several of your own themes can share a base the same way.
Never
@importa base from a theme. The preview screens load every theme at once, and a second import would re-declare the base after the first theme's overrides, flattening it.
The expense isn't light versus dark, and it isn't how much of the ramp you redeclare. It's how far you move the hue. Dim rewrites all fourteen ramp steps but keeps Craft's blue-grey, and needs two extra rules. Stone shifts to a warm neutral and needs nine — as does Stone Dark, base or no base. Craft and the dark base both write blues straight into rules in a few dozen places, and those don't follow a retinted ramp. The most visible is --fg-input, which every control background derives from at 25%, 30% and 50% alpha.
Move the hue and stone.css is the worked example; each patch carries a note on what was pinned and why.
For a plugin or module shipping its own:
use bensomething\cast\events\RegisterThemesEvent;
use bensomething\cast\models\Theme;
use bensomething\cast\services\Themes;
use yii\base\Event;
Event::on(Themes::class, Themes::EVENT_REGISTER_THEMES, function(RegisterThemesEvent $event) {
$event->themes['midnight'] = new Theme([
'handle' => 'midnight',
'name' => 'Midnight',
'colorScheme' => Theme::SCHEME_DARK,
'url' => Craft::$app->getAssetManager()->getPublishedUrl(
'@mymodule/resources', true, 'midnight.css',
),
]);
});
Use a bundled handle as the key to replace that theme.
Most plugin UIs get Cast for free, building on Craft's components. One shipping its own compiled stylesheet with colours baked in doesn't, and lands as dark text on a dark canvas.
<div data-cast-ignore>
{# your app #}
</div>
Everything inside gets Craft's palette back, plus color-scheme: light so native controls follow. It's the whole subtree, so mark the outermost element the region owns. Menus opened from inside come with it — Garnish moves them to <body>, which would otherwise hand them back to the theme. Craft's Plugin Store is exempted this way out of the box.
Reach for it only when a region genuinely can't follow the palette; fixing the stylesheet to read Craft's properties earns dark mode rather than opting out of it.
Caveat: the region is restored through Cast's variables, so anything reading them corrects itself. A few patches carry literal colours — Prism syntax highlighting mainly — and those still need overriding by hand inside an ignored region.
Override per environment in config/cast.php:
return [
'buttonColor' => 'red',
'defaultTheme' => 'auto',
'allowUserOverride' => true,
'enabledThemes' => ['dark', 'high-contrast'],
'autoLightTheme' => '',
'autoDarkTheme' => 'dark',
'themesPath' => '@root/cast-themes',
];
An empty handle means Craft's stock appearance. Copy src/config.php for a commented starting point.
MIT.