LiquidFree

AI Color Shift

Five OKLCH color tiles, 72° apart on the hue wheel, turning together one full revolution every 20 s while their lightness gently ripples from tile to tile: a computed rainbow, with no randomness and no artificial intelligence. CSS and 1.6 KB of JavaScript, frozen when the user reduces motion.

CSSOKLCHCouleurs

Updated

Hover or click the scene to interact

This effect is free — the Liquid category contains 31 effects total, including 6 free. Effect.Labs has 840 vanilla effects. Explore the category →

Usage examples

Custom paint · finish 3 of 8

Iris Flip

The color changes with the viewing angle: interference pigments under three coats of clear.

  • full body, 5 working days
  • sample plate sent on request
Sample plate preview

① Chameleon paint — a car customization shop's configurator

WhenThe customer browses the finishes: on the "Iris Flip" page, the sample plate shows five color bands gliding, captioned "the color changes with the viewing angle".
WhyThe hue turning without jumps, at controlled brightness, mimics the shifting look of a paint with interference pigments.
SettingsTiles side by side (gap: 0, square corners) in a plate with 18 px corners; a sheen in ::after (white to black gradient) on each tile, since the script rewrites their background; finish details on the left.

RGB LED strip, 5 m, 5 zones

€39.90
SteadyBreathingRainbowMusic

Rainbow mode: the five zones move through the hues together, evenly spaced, one full turn in 20 s.

② Rainbow mode of an LED strip — a lighting shop's product page

WhenOn the page for the 5 m strip split into five zones, the box shows the five zones changing color like the programmed mode; the "Rainbow" mode is selected.
WhyFive tiles = five segments; OKLCH keeps hues of comparable brightness (a blue and a yellow at the same L look equally light), like a well-tuned strip.
SettingsA 64 px tall band, tiles with 4 px radius and 4 px gap in a gray casing; light dots in ::after (a radial gradient repeated every 22 px); 1 m to 5 m marks under the zones.
Stained glass workshop

Design your stained glass

Five panes, evenly spaced hues: watch the combinations go by, then stop on the one you like.

Ask for a quote

③ A five-pane stained glass panel — a stained glass workshop's page

WhenUnder the heading "Design your stained glass", an arched window split into five panes whose colors slowly shift; the visitor watches the combinations go by before asking for a quote.
WhyEvenly spaced hues stay in harmony with each other: the visitor sees possible pane combinations go by.
SettingsArched root (border-radius: 999px 999px 6px 6px), lead came suggested by the 5 px gap on a dark brown background; glass texture in ::after (slanted streaks and a vertical gradient); text on the right.

How it works

The .ai-color-shift block is a grid of five equal columns (grid-template-columns: repeat(5, 1fr), 8 px gap, 20 px padding) that fills its 250 px tall frame; each .ai-color-swatch has 12 px corners and no color in the CSS. The script does the painting: on every frame, it writes an oklch(L% 0.2 H) color into each tile's style.background. The base hue moves 18° per second (one turn in 20 s) and each tile offsets it by 72°: at any moment, the five tiles cover the color wheel at even intervals. Lightness L ripples between 35 and 65% along a slow sine (one cycle in 17.5 s), offset from one tile to the next: the wave of brightness travels across the tiles.

Time is the clock, not the frame count: the script adds up the time elapsed between frames (capped at 100 ms to avoid a jump after a pause) and derives hue and lightness from it. Scrolling therefore runs at the same speed on a 60 or 120 Hz screen, and lightness does not jump when the hue wraps past 0°. The colors go through no CSS transition: written on every frame, they show as they are, the same way in Chromium, Firefox and WebKit (measured: 15% of pixels change in 500 ms in all three).

Why OKLCH? In this space, the L value follows the brightness the eye perceives: a yellow and a blue at equal L look equally light, which hsl() does not guarantee (its 50% yellow looks far lighter than its 50% blue). The brightness differences you see between tiles therefore come only from the intended wave, not from the hues. At a chroma of 0.2, some hues fall outside the sRGB space of an ordinary screen: the browser maps them to the nearest displayable color. The catalog name says "AI", but there is no model and no generated harmony: a computed rotation, five hues fixed relative to each other. The requestAnimationFrame loop stops when the block leaves the screen (IntersectionObserver) or the tab is hidden, and resumes where it left off.

Accessibility

  • Reduced motion: under prefers-reduced-motion: reduce, the script paints the five tiles once and does not start the loop: a still rainbow (hues 0, 72, 144, 216 and 288°). Measured in Chromium 151, Firefox 153 and WebKit 26: 0% of pixels changing in 500 ms, identical colors one second apart.
  • Color alone: the tiles carry no text and no role. If a color has to be chosen or named (a paint shade, a glass pane), write its name next to it: color alone cannot be read by a screen reader, nor by someone who has trouble telling some hues apart.
  • Flashing: the colors glide with no flash or fast alternation (one turn in 20 s), far from the WCAG flash thresholds.

Browser compatibility

CSS (grid) + 1.6 KB of JavaScript (requestAnimationFrame, IntersectionObserver, oklch() colors), no dependency. Measured in Chromium 151, Firefox 153 and WebKit 26: same scrolling speed, same still picture under reduced motion, off-screen pause checked in Chromium and Firefox. oklch() exists since Chrome 111, Firefox 113 and Safari 15.4.

Chrome 111+✓ Full
Firefox 113+✓ Full
Safari 15.4+✓ Full
Edge 111+✓ Full
Mobile iOS 15.4+✓ Full
Android Chrome 111+✓ Full

A browser that does not know oklch() ignores the color written: the tiles stay empty. For those older browsers, replace the color line with hsl(H, 80%, L%): the scrolling stays the same, perceived brightness varies a little with the hue. Without JavaScript, the tiles stay empty: give them a starting color in the CSS if the block must stay readable.

The code

Copy the three blocks into your page. No dependencies.

HTML
<div class="fx-0414-root">
  <div class="ai-color-shift">
    <div class="ai-color-swatch"></div>
    <div class="ai-color-swatch"></div>
    <div class="ai-color-swatch"></div>
    <div class="ai-color-swatch"></div>
    <div class="ai-color-swatch"></div>
  </div>
</div>
CSS
.fx-0414-root,
.fx-0414-root *,
.fx-0414-root *::before,
.fx-0414-root *::after {
  box-sizing: border-box
}

.fx-0414-root {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.ai-color-shift {
  width: 100%;
  height: 100%;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 8px;
  padding: 20px;
}

.ai-color-swatch {
  border-radius: 12px;
}
JavaScript (fx-0414)
(function() {
  // AI Color Shift: five OKLCH hues 72° apart, rotating together
  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
  document.querySelectorAll('.ai-color-shift').forEach(function(container) {
    var swatches = container.querySelectorAll('.ai-color-swatch');
    var elapsed = 0; // seconds of animation so far
    var last = 0;
    var raf = 0;
    var onScreen = true;

    function paint() {
      var hueBase = (elapsed * 18) % 360; // 18° per second: one turn in 20 s
      swatches.forEach(function(swatch, i) {
        var hue = (hueBase + i * 72) % 360;
        var lightness = 50 + Math.sin(elapsed * 0.36 + i) * 15;
        swatch.style.background = 'oklch(' + lightness.toFixed(1) + '% 0.2 ' + hue.toFixed(1) + ')';
      });
    }

    function frame(now) {
      elapsed += Math.min(now - last, 100) / 1000;
      last = now;
      paint();
      raf = requestAnimationFrame(frame);
    }

    function update() {
      // Reduced motion, off screen or hidden tab: colors stay still
      var run = onScreen && !document.hidden && !reduce.matches;
      if (run && !raf) {
        last = performance.now();
        raf = requestAnimationFrame(frame);
      } else if (!run && raf) {
        cancelAnimationFrame(raf);
        raf = 0;
      }
    }

    paint();
    if ('IntersectionObserver' in window) {
      new IntersectionObserver(function(entries) {
        onScreen = entries[0].isIntersecting;
        update();
      }).observe(container);
    }
    document.addEventListener('visibilitychange', update);
    update();
  });
})();

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Speed (JS — elapsed * 18) 18° per second, one turn in 20 s 9 for one turn in 40 s, almost still; 36 for 10 s. Lightness keeps its own pace (elapsed * 0.36).
Gap between tiles (JS — i * 72) 72°, i.e. 360° split in five 30° gives a range of neighboring hues (a gliding gradient); 180° alternates complementary colors. With more or fewer tiles, keep 360 divided by their number.
Lightness wave (JS — 50 + Math.sin(…) * 15) between 35 and 65% * 0 gives five tiles of equal brightness (L fixed at 50%); * 25 a stronger wave. Stay between 25 and 80% to keep vivid colors.
Chroma (JS — 0.2 in oklch()) 0.2 0.1 gives pastel tones, all displayable; above 0.25, many hues fall outside the sRGB screen and the browser clips them.
Tile shape (CSS — .ai-color-shift, .ai-color-swatch) 5-column grid, 8 px gap, 12 px corners The script only touches the background: change the grid, the corners, add textures in ::after. For 3 or 7 tiles, adjust repeat(…) and the hue gap.

FAQ

Is there any artificial intelligence in this effect?

No. The name comes from the catalog, but the script only does arithmetic: a base hue that moves with time, five tiles offset by 72°, a lightness that follows a sine. No randomness, no model, no network call: two visitors at the same point in the animation see the same colors.

How do I freeze the colors on a chosen combination?

The loop already stops on its own off screen and under reduced motion, in the update() function. Add a paused variable to the condition (var run = !paused && onScreen …), toggle it when the block is clicked and call update() again: the tiles keep the last color painted, and scrolling resumes from the same point on the next click.

Why don't all the tiles have the same brightness?

That is intended: a wave of lightness (35 to 65%) travels across the tiles, offset from one to the next, to give the scrolling some depth. OKLCH only guarantees that at equal values, two hues look equally light. For five tiles of identical brightness, set the sine factor to 0: lightness stays at 50%.