Gradient Shift
The mood background goes from a blue-violet night gradient to a golden one in 0.8 s, its violet glow turns amber and the text follows: the gold gradient lives in a layer laid over the night and its opacity is what animates, so the switch is continuous in every browser. 0.6 KB of JavaScript, 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-gradient-scene (300 × 220 px, 16 px corners) holds a .th-gradient-bg background with position: absolute; inset: 0: linear-gradient(135deg, #0f172a, #1e1b4b, #312e81), a night going from slate blue to indigo. On top, its ::after pseudo-element covers the same area with the golden linear-gradient(135deg, #fef3c7, #fde68a, #fcd34d), at opacity 0. In the center, the content layer (z-index: 1) stacks an 80 px glow, the 1.2 rem bold title and a line of text. The glow follows the same principle: a 40% violet radial-gradient below, a 50% amber one in its ::after, invisible at rest.
A gradient does not interpolate from one value to another: declared directly in place of the first under .active, it replaces the old one at once (measured: the final gradient shows 60 ms after the click in Chromium and Firefox, despite a 0.8 s transition). Hence the two layers: .active takes the opacity of both ::afters from 0 to 1 with transition: opacity .8s, while the title goes from #fff to #78350f and the line from 50% white to #92400e (transition: color .8s). Since both gradients share the same geometry and the golden layer is opaque, the blend at each pixel is exactly what you would get if each color slid to its counterpart. Measured 300 ms after the click: golden layer at 0.65 opacity in Chromium 151, 0.61 in Firefox 153, 0.66 in WebKit 26, then 1 at 1.5 s; the second click goes back to night.
The script (0.6 KB) arms every .th-gradient-scene declared role="button": click, Enter and Space toggle .active and aria-pressed. Without that role, the scene is left alone and you set the class from your own control (a time slot, an hour, a timer ending), as in the examples. The two gradients are swapped in CSS without touching anything else: only the two background rules of the backdrop and its ::after decide the mood. Opacity is animated by the compositor, without repainting the gradients every frame. The effect does not read prefers-color-scheme and remembers nothing.
Accessibility
- Keyboard and role: the demo scene is a focusable
role="button"named 'Golden mood', witharia-pressed. Measured: reached with the Tab key in Chromium and WebKit, toggled by Enter and Space in all three engines; a 2 px amber outline on keyboard focus. In production, prefer a visible button that drives the mood. - Reduced motion: nothing moves; under
prefers-reduced-motion: reduce, the fade and the text colors take 150 ms. Measured: golden layer at 0.67 (Chromium) and 0.35 (Firefox) opacity 60 ms after the click, 1 at 300 ms. - Contrast: on the night, line of text at 5.0:1 and white title well above; on gold,
#78350ftitle at 7.3:1 and#92400eline at 5.7:1. During the 0.8 s, text and background go through mid tones together: avoid switching while critical information is being read. Color alone should not carry meaning (off-peak or peak): keep a label.
Browser compatibility
CSS (::after pseudo-element, opacity and color transitions) + 0.6 KB of JavaScript, no dependency, no @property, no prefix. Measured in Chromium 151, Firefox 153 and WebKit 26: the same continuous fade, the same values within a few hundredths, same way back. Opacity transitions on pseudo-elements are much older; the minimum versions come from inset: 0, which spreads the gradient over the whole scene: Chrome and Edge 87, Firefox 66, Safari 14.1 (iOS 14.5).
Without JavaScript, the scene stays on the night and does not react; setting active in the HTML serves the golden mood directly.
The code
HTML structure to paste into your page (the CSS + JS come with Premium):
<div class="th-gradient-root">
<div class="th-gradient-scene" role="button" tabindex="0" aria-pressed="false" aria-label="Golden mood">
<div class="th-gradient-bg"></div> <!-- night + golden ::after -->
<div class="th-gradient-content">
<div class="th-gradient-orb"></div> <!-- glow + amber ::after -->
<h3>…</h3>
<p>…</p>
</div>
</div>
</div>
The full HTML + CSS + JS of Gradient Shift, 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 moods (CSS — background of .th-gradient-bg and .th-gradient-bg::after) |
night #0f172a → #312e81, gold #fef3c7 → #fcd34d, at 135° |
Any gradients work; keep the same geometry (angle, stop positions) in both for a change that looks like colors sliding rather than an overlay. |
Glow (CSS — .th-gradient-orb and its ::after) |
80 px, 40% violet then 50% amber | Enlarge it (150 to 200 px) for a sun or a moon; remove it for a plain background. |
Text colors (CSS — h3 and p under .active) |
#78350f and #92400e on gold |
Check 4.5:1 against the area of the gradient closest to the text, not just its average color: for these dark texts, that is the darkest one, #fcd34d, where #92400e drops to 4.92:1 (6.37:1 on #fef3c7). |
Duration (CSS — .8s on the fade and colors) |
0.8 s | A mood can take 1.2 s; under 0.4 s, the change looks like a jump. |
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 | 0s for an instant change. |
FAQ
@property, you declare each stop color as a <color> (--g1, --g2, --g3) and animate those variables; that takes three declarations per gradient, and the gradient jumps in browsers without @property (before Chrome 85, Safari 16.4 and Firefox 128). The fading layer works everywhere, animates only opacity, and, with both gradients sharing the same geometry, gives the same blend at every pixel. On the other hand, it can only switch between two moods per pseudo-element..th-gradient-bg, each with position: absolute; inset: 0, its gradient and transition: opacity .8s, and make only the current one opaque (one class per state, for instance .is-noon). The base background stays visible when all layers are at 0.active: remove role="button" from the scene so it no longer reacts to clicks. On load, read the time (var h = new Date().getHours(); scene.classList.toggle('active', h >= 6 && h < 22);) before the first paint, otherwise the fade plays when the page opens; then schedule the next switch with a setTimeout computed up to 6 am or 10 pm: set live, the class plays the 0.8 s fade in front of the user. A tab in the background delays timers: redo the calculation on the visibilitychange event. The effect does not read prefers-color-scheme: it is a mood tied to content, not an interface theme.