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
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.
Updated
This effect is free — the Liquid category contains 31 effects total, including 6 free. Effect.Labs has 840 vanilla effects. Explore the category →
The color changes with the viewing angle: interference pigments under three coats of clear.
Rainbow mode: the five zones move through the hues together, evenly spaced, one full turn in 20 s.
Five panes, evenly spaced hues: watch the combinations go by, then stop on the one you like.
Ask for a quoteThe .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.
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.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.
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.
Copy the three blocks into your page. No dependencies.
<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>
.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;
}
(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();
});
})();
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
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. |
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.
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.
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%.