Transitions✨ Premium

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.

CSSVariablesMorph

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

Each campsite's colors — a white-label booking module — CSS Variables example 1

① Each campsite's colors — a white-label booking module

WhenIn the admin, the manager switches from the Les Pins campsite to La Plage: the same module (July 12 to 19, mobile home at €89 a night, €623 total) takes on the other site's colors.
WhyA single variable set drives the whole module: changing clients means redefining six values, and each color glides to the new one instead of jumping. The panel on the right reads the same variables and changes with the module.
Settings508 × 272 px scene, without role="button"; dark forest-green 'Pins' set (--bg: #16271d, --accent: #7cc792) and light sand 'Plage' set (--bg: #f5e9d0, --accent: #0c6480); a two-button picker in the admin bar; a 0.6 s transition on every element of the module.
Night shift theme — a hospital pharmacy's software — CSS Variables example 2

② Night shift theme — a hospital pharmacy's software

WhenAt 10 pm, the list of preparations (amoxicillin in room 214, heparin in 108, paracetamol in 317) switches from the day theme to the dimmed night-team theme.
WhyThe variables change together, so no element is forgotten, not even the status chips; the 0.6 s transition avoids the glare of a sudden jump in a dark room.
SettingsFull-page scene; set extended to eight variables with --ok and --warn for statuses, dimmed at night (#5f9c70, #b48a4f); 'Day / Night shift' picker and a scheduled switch.
Home jersey to away jersey — a basketball club's app — CSS Variables example 3

③ Home jersey to away jersey — a basketball club's app

WhenThe day before a road game, the club app goes from the red and white of the home jersey to the black and gold of the away jersey, drawn jersey included.
WhyThe club maintains only one stylesheet: the 'away' variable set is enough to repaint the whole app, even the SVG jersey, whose fill reads var(--accent).
SettingsFull-page scene; added variable --on-accent for text on the accent; SVG jersey with transition: fill .6s; 'Home / Away' picker.

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', with aria-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 already rgb(166, 171, 180) in Chromium, final at 300 ms.
  • Contrast: secondary line at 5.2:1 on dark and 4.3:1 on light (#64748b on #f1f5f9): move it to #5b6678 if 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.

Chrome 84+✓ Full
Firefox 63+✓ Full
Safari 14.1+✓ Full
Edge 84+✓ Full
Mobile iOS 14.5+✓ Full
Android Chrome 84+✓ Full

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):

index.html — structure
<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>
Unlock the full code — $9.90/mo incl. tax

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 / propertyDefaultEffect
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

You can, by declaring them with @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.
The effect does not read 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.
The result looks alike, the upkeep does not. Card Theme writes one .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.