CSS Variables
The whole theme fits in six custom properties (--bg, --card-bg, --card-border, --text, --text-muted, --accent) that the .active class redefines at once; every color that reads them glides to the new value in 0.6 s. One class to set, no per-element rule to duplicate; 150 ms when the user reduces motion.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Transitions has 52 effects, including 9 free. Explore the category →
3 usage examples



How it works
The scene .th-morph-scene (300 × 220 px, 16 px corners) declares six custom properties: --bg: #0f172a, --card-bg (5% white), --card-border (8% white), --text: #fff, --text-muted (50% white) and --accent: #8b5cf6. Everything else reads them: the scene background (background: var(--bg)), the title and the line of text, the inner card (background and border), the accent square and its label. The .th-morph-scene.active rule redefines all six in one block (#f1f5f9, white, #e2e8f0, #0f172a, #64748b, #6366f1): switching themes means setting a class on a single element, and the children inherit the new values.
An unregistered custom property does not animate: the browser does not know --bg is a color and swaps it at once. So the properties that consume it carry the transition: background-color .6s on the scene and the square, background-color .6s, border-color .6s on the card, color .6s on the three texts. When --bg changes, the computed value of background-color changes, and that is what gets interpolated. Measured 300 ms after the click: scene background rgb(196, 201, 208) in Chromium 151, rgb(190, 195, 202) in Firefox 153, rgb(194, 199, 206) in WebKit 26, then exactly #f1f5f9 at 1.5 s; the way back follows the same path. Transitions are listed property by property, never all: if you change the card size, it will not animate by accident.
The script (0.6 KB) arms every .th-morph-scene declared role="button": click, Enter and Space toggle .active and aria-pressed. A scene without that role is left alone, and that is the real use case: the class is set by your own control (a property picker, a shift time), and every component inside the scene follows, as long as each property reading a variable carries its transition. The variable set extends freely: add --ok or --warn for status colors, they will switch with the others. The effect does not read prefers-color-scheme and remembers nothing.
Accessibility
- Keyboard and role: the demo scene is a focusable
role="button", named 'Light theme', witharia-pressed. Measured: reached with the Tab key in Chromium and WebKit, toggled by Enter and Space in all three engines, violet outline on keyboard focus. In production, drive the class from a real button or a picker, as in the examples. - Reduced motion: nothing moves, only colors change; under
prefers-reduced-motion: reduce, the duration drops to 150 ms. Measured: at 60 ms the background is alreadyrgb(166, 171, 180)in Chromium, final at 300 ms. - Contrast: secondary line at 5.2:1 on dark and 4.3:1 on light (
#64748bon#f1f5f9): move it to#5b6678if it carries important information. During the 0.6 s, text and background go through mid grays together: do not trigger the switch while the user is typing.
Browser compatibility
CSS (custom properties, color transitions) + 0.6 KB of JavaScript, no dependency. Measured in Chromium 151, Firefox 153 and WebKit 26: the same intermediate colors within a few units, same way back, same keys. Custom properties exist since Chrome 49, Firefox 31 and Safari 9.1. The effect does not use @property: it also works in browsers that cannot animate a variable. The minimum versions come from gap in flex boxes: Chrome and Edge 84, Firefox 63, Safari 14.1 (iOS 14.5); before that, the items touch.
A browser without custom properties ignores the var() declarations: the scene loses its colors (transparent background, default text). Add a fixed color before each var() if you target such an old browser. Without JavaScript, the scene stays in its starting theme; setting active in the HTML serves the second one directly.
The code
HTML structure to paste into your page (the CSS + JS come with Premium):
<div class="th-morph-root">
<div class="th-morph-scene" role="button" tabindex="0" aria-pressed="false" aria-label="Light theme">
<h3>…</h3>
<p>…</p>
<div class="th-morph-card">
<div class="th-morph-dot"></div>
<span>…</span>
</div>
</div>
</div>
The full HTML + CSS + JS of CSS Variables, ready to paste — plus all 840 effects. Or $299 once, lifetime access.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
The two variable sets (CSS — .th-morph-scene and .th-morph-scene.active) |
6 variables, dark then light | Change the values, not the rules: everything that reads a variable follows. Add as many as needed (--ok, --danger), declaring them in both sets. |
Duration (CSS — .6s on each transition) |
0.6 s | All durations should stay equal, otherwise the text arrives before its background. 0.3 s for a brisk change, 1 s for a mood. |
New components (CSS — their own transition) |
only the demo elements transition | An added element reading var(--accent) changes color, but at once unless it declares transition: background-color .6s (or color, border-color, fill). |
Trigger (HTML — role="button" on the scene) |
the whole scene is the button | Remove role, tabindex and aria-pressed and set .active from your control: the script no longer touches the scene. |
Reduced motion (CSS — transition-duration: .15s) |
150 ms | Use 0s for an instant change. |
FAQ
@property --bg { syntax: '<color>'; inherits: true; initial-value: #0f172a; } and then transition: --bg .6s on the scene: the browser then knows --bg is a color and interpolates it, and the children no longer need their own transition (measured in Chromium 151, Firefox 153 and WebKit 26). But every variable must be declared that way, and @property only exists since Chrome 85, Safari 16.4 and Firefox 128: without it, the color jumps. Transitions set on the consuming properties work everywhere; that is the choice made here.prefers-color-scheme and stores nothing. Variables lend themselves well to it: a @media (prefers-color-scheme: light) { .th-morph-scene { … the light set … } } block serves the right theme with no JavaScript; to remember a different choice, write it to localStorage and set the class on load, before the first paint, otherwise the transition plays when the page opens..active rule per piece of the card (background, avatar, name, text, two buttons): explicit and self-contained, but each new piece needs its pair of rules. CSS Variables writes a single value set that the whole component reads: adding an element only needs a transition. Choose CSS Variables for a theme covering several components or several brands, Card Theme for an isolated widget that must share nothing with the page.