A gallery of what's possible when you combine <pattern-grid>, modern CSS, and the seeded <seed-context> companion. Each tile is a live element β view source or expand the details below it to see exactly how it's built.
CrystalsA parametric gem facet β every vertex driven by a seeded random, so each cell is a distinct crystal.<seed-context seed="cryst" count="5">
<pattern-grid cells="10x10"></pattern-grid>
</seed-context>
#sc-crystals pattern-grid > i {
background: linear-gradient(135deg,
hsl(calc(var(--rand-1) * 360deg) 70% 62%),
hsl(calc(var(--rand-1) * 360deg) 70% 42%));
clip-path: polygon(
50% 0%,
calc(64% + var(--rand-0) * 36%) calc(32% + var(--rand-4) * 26%),
calc(30% + var(--rand-3) * 40%) 100%,
calc(36% - var(--rand-2) * 36%) calc(32% + var(--rand-4) * 26%));
}
Random staircaseVertical offset and hue both from seeded randoms β no sibling-index(), so it works in every browser.<seed-context seed="stair">
<pattern-grid cells="12x12"></pattern-grid>
</seed-context>
#sc-staircase pattern-grid > i {
background: hsl(calc(var(--rand-1) * 360) 70% 55%);
transform: translateY(calc((var(--rand-0) - 0.5) * 40%));
}
Unicode rainA seeded-random glyph per cell, falling in a downward cascade β both driven by <seed-context> randoms, all-CSS.<seed-context seed="rain">
<pattern-grid cells="12x12">
<!-- 10 stacked glyph layers per cell -->
<template><span class="rc"><i></i>β¦<i></i></span></template>
</pattern-grid>
</seed-context>
/* random glyph: --g picks which stacked layer shows (no if()/abs()) */
#sc-rain .rc { --g: mod(var(--randi-0), 10); }
#sc-rain .rc > i:nth-child(1)::before { content: "β"; } /* β¦10 glyphs */
#sc-rain .rc > i:nth-child(1) { opacity: calc(1 - min(1, max(var(--g) - 0, 0 - var(--g)))); }
/* downward cascade: --row (set via nth-child ranges) phases the fall */
#sc-rain .rc { animation: rain-fall 2.6s linear infinite; }
#sc-rain .rc { animation-delay: calc(var(--row,0) * 0.22s + var(--rand-1) * 0.12s); }
Hover rippleHover any cell to light up its neighbors.<pattern-grid cells="10x10"></pattern-grid>
#sc-ripple pattern-grid > i {
background: var(--bg-tertiary);
transition: background 0.4s, transform 0.4s;
}
#sc-ripple pattern-grid > i:hover,
#sc-ripple pattern-grid > i:hover + i,
#sc-ripple pattern-grid > i:has(+ i:hover) {
background: hsl(200 80% 55%);
transform: scale(1.1);
}
Matrix rainVertical columns of falling katakana characters.<pattern-grid shim="sibling" cols="14" rows="1">
<template><span class="col"></span></template>
</pattern-grid>
#sc-matrix .col {
/* repeating linear-gradient of katakana glyphs scrolls vertically */
background: linear-gradient(180deg, transparent, #0f3 50%, transparent);
animation: fall 6s linear infinite;
animation-delay: calc(var(--i, sibling-index()) * -0.5s);
}
@keyframes fall { to { background-position-y: 200%; } }
MandalaThree nested pattern-grids form concentric rings.<div class="mandala-stack">
<pattern-grid shim="sibling" cells="8"></pattern-grid> <!-- inner -->
<pattern-grid shim="sibling" cells="12"></pattern-grid> <!-- middle -->
<pattern-grid shim="sibling" cells="24"></pattern-grid> <!-- outer -->
</div>
/* Each ring positions its cells via polar math with a
different --r (radius) per ring. */
Goo blobsDrifting cells merge via an SVG feGaussianBlur + feColorMatrix.<filter id="sc-goo-filter">
<feGaussianBlur stdDeviation="6" />
<feColorMatrix values="... 0 0 0 22 -10" />
</filter>
#sc-goo .tile { filter: url(#sc-goo-filter); }
#sc-goo pattern-grid > i {
/* cells drift around with a per-cell offset animation */
animation: drift 5s ease-in-out infinite;
animation-delay: calc(var(--i, sibling-index()) * -80ms);
}
Sierpinski fractalChaos game: random walk halfway to a vertex, lighting up cells.<pattern-grid id="sc-fractal-grid" cells="50x50"></pattern-grid>
<script>
const grid = document.getElementById('sc-fractal-grid');
const W = 50;
/* three vertices of a triangle, in [0,1] grid coords */
const V = [
{ x: 0.5, y: 0.05 },
{ x: 0.05, y: 0.95 },
{ x: 0.95, y: 0.95 },
];
let px = 0.5, py = 0.5;
function step() {
for (let n = 0; n < 60; n++) {
const v = V[Math.floor(Math.random() * 3)];
px = (px + v.x) / 2;
py = (py + v.y) / 2;
const cell = grid.cellElements[((py * W) | 0) * W + ((px * W) | 0)];
if (cell) cell.classList.add('hit');
}
}
grid.addEventListener('pattern-grid:render', () => step(), { once: true });
setInterval(step, 50);
</script>
Spiral pulse400 rounded cells, each scaling on a delay derived from its own polar coordinates β atan2() angle plus radial distance β so the pulse sweeps outward as a rotating spiral. Pure CSS, no JS.<pattern-grid cells="20x20" shim="sibling"></pattern-grid>
#sc-swirl pattern-grid > i {
--sw-i: calc(var(--i, sibling-index()) - 1); /* 0-based; --i from shim */
--sw-x: calc(mod(var(--sw-i), var(--pg-cols)) - (var(--pg-cols) - 1) / 2);
--sw-y: calc((var(--pg-cols) - 1) / 2 - round(down, var(--sw-i) / var(--pg-cols)));
--sw-t: calc(var(--sw-i) / (var(--n, sibling-count()) - 1));
--sw-hue: calc(mod(sin(var(--sw-t) * 90deg) * 3600, 1) * 360);
--sw-dist: calc(sqrt(var(--sw-x) * var(--sw-x) + var(--sw-y) * var(--sw-y)) / sqrt(200));
--sw-ang: calc(atan2(var(--sw-x), var(--sw-y)) / -360deg);
--sw-delay: calc(var(--sw-dist) * 9s + var(--sw-ang) * 3s - 12s);
translate: calc(var(--sw-x) * var(--size)) calc(var(--sw-y) * var(--size));
background: hsl(var(--sw-hue) 100% calc(80% - var(--sw-dist) * 60%));
animation: sc-swirl-pulse 1.5s var(--sw-delay) infinite ease-in-out alternate;
}
Modern platform showcase
The previous pieces match what css-doodle does. The next ten show what only a light-DOM component can do β real semantic cells, View Transitions, Anchor Positioning, Popover, scroll-driven animations, :has(), container queries, Houdini Paint Worklet β features that live outside the Shadow-DOM wall.
Step sequencerReal <button> cells with ARIA pressed state; Web Audio plays the toggled cells on a 120 BPM transport.<pattern-grid cols="16" rows="4">
<template><button type="button" aria-pressed="false"></button></template>
</pattern-grid>
Morph layoutPer-cell view-transition-name animates a reshuffle natively via the View Transitions API.document.startViewTransition(() => {
// reorder cells; each has a unique view-transition-name
});
Scroll-reveal mosaicEach cell uses animation-timeline: view() β entry/exit driven entirely by scroll position. The Replay button retriggers the same keyframes on a time-based fallback.#sc-scroll pattern-grid > i {
animation: sc-reveal both;
animation-timeline: view();
animation-range: entry 0% cover 30%;
animation-delay: calc((var(--i, sibling-index())) * -8ms);
}
Anchored popoversEach cell is a <button popovertarget>. Popovers dock beside their cell via CSS Anchor Positioning.<button popovertarget="pop-N" style="anchor-name: --c-N"></button>
<div id="pop-N" popover style="position-anchor: --c-N; left: anchor(right); top: anchor(top)">...</div>
Neighbor glowHover one cell β its left/right siblings glow via :has() with zero JavaScript.#sc-neighbor i:hover,
#sc-neighbor i:has(+ i:hover),
#sc-neighbor i:hover + i { background: var(--accent); }
Parametric clip shapesSix-pointed stars from trig-driven clip-path: polygon() β pattern-grid's answer to css-doodle's @shape().#sc-shape pattern-grid > i {
clip-path: polygon(...trig...);
rotate: calc(var(--i, sibling-index()) * 8deg);
}
Recursive gridA <pattern-grid> whose <template> contains another <pattern-grid>. Custom elements upgrade naturally inside cloned templates β Sierpinski carpet without a DSL.<pattern-grid cells="3x3">
<template>
<div><pattern-grid shim="sibling" cells="3x3"></pattern-grid></div>
</template>
</pattern-grid>
Houdini paint() isnβt supported in this browser. Open in Chrome to see the worklet.
Paint worklet swirlEach cell calls paint(swirl) β a Houdini Paint Worklet registered declaratively via <paint-worklet src>. The platform answer to css-doodle's @shaders(). Chrome-only; Firefox/Safari show a "not supported" notice.<paint-worklet src="worklets/swirl.js"></paint-worklet>
#sc-paint pattern-grid > i { background: paint(swirl); }
/* No paint() API (Firefox, Safari): hide the demo, show a notice */
@supports not (background: paint(id)) {
#sc-paint seed-context { display: none; }
#sc-paint .sc-no-support { display: grid; }
}
Contribution calendar53Γ7 real <button> cells β keyboard-focusable, screen-reader-readable, real dates, color-mix() for perceptual gradients. css-doodle's Shadow DOM cannot expose this surface.<pattern-grid cols="53" rows="7">
<template><button aria-label="day"></button></template>
</pattern-grid>
/* color via color-mix(in oklch, low, high calc(--randi-0 * 1%)) */
Lissajous point cloud200 cells placed by cos(3t)/sin(4t) β pattern-grid's answer to css-doodle's @plot().#sc-lissajous pattern-grid > i {
--t: calc(var(--i, sibling-index()) / var(--pg-cols) * 6.2832);
translate: calc(cos(var(--t) * 3) * 40%) calc(sin(var(--t) * 4) * 40%);
}
Tributes β abstract art & 8-bit sprites
A small gallery tour: three hard-edge geometric tributes (Mondrian, Albers, Sol LeWitt), three painterly ones (Kandinsky, Kelly, Kusama), six pixel-sprite pieces (single sprites, an animated chomp, a hover-swap, a CSS-only sprite-sheet picker, a palette-cycling sprite, and an interactive paint scratchpad), and two pop-and-illusion crossovers (Escher tessellation, Lichtenstein POW with popover and CSS Anchor Positioning).
Mondrian β Composition with Red, Yellow & BlueHand-authored cells with grid-area spans; primary colours, off-white fields, thick black gutters via gap + black host background. The grid here is plain CSS β <pattern-grid> would re-render uneven cell counts..sc-mondrian-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-template-rows: repeat(6, 1fr);
gap: 6px;
background: #111;
aspect-ratio: 1;
padding: 6px;
}
.sc-mondrian-grid > [data-fill="red"] { background: #dd2222; }
.sc-mondrian-grid > [data-fill="blue"] { background: #1f3fbf; }
.sc-mondrian-grid > [data-fill="yellow"] { background: #f0d040; }
.sc-mondrian-grid > [data-fill="white"] { background: #f4f0e6; }
Albers β Homage to the SquareFour nested squares, each offset downward by uneven padding. Warm palette, no recursion of <pattern-grid> needed here β pure nesting demonstrates the same compositional grammar.<div class="sc-albers-layer sc-albers-l1">
<div class="sc-albers-layer sc-albers-l2">
<div class="sc-albers-layer sc-albers-l3">
<div class="sc-albers-layer sc-albers-l4"></div>
</div>
</div>
</div>
.sc-albers-layer { aspect-ratio: 1; display: grid; }
.sc-albers-l1 { background: #6a2818; padding: 8% 8% 14% 8%; }
.sc-albers-l2 { background: #b25a26; padding: 8% 8% 14% 8%; }
.sc-albers-l3 { background: #d77a37; padding: 8% 8% 14% 8%; }
.sc-albers-l4 { background: #f0c266; }
Sol LeWitt β algorithmic line workEach cell is a single 2px-thick diagonal whose angle is calc(sibling-index() * 17deg). Zero JavaScript art that feels hand-drawn but is entirely algorithmic.#sc-lewitt pattern-grid > i {
aspect-ratio: 1;
background: #f4ede2;
position: relative;
}
#sc-lewitt pattern-grid > i::before {
content: "";
position: absolute;
inset: 0;
background: #1a1a1a;
clip-path: polygon(0 49%, 100% 49%, 100% 51%, 0 51%);
rotate: calc(var(--i, sibling-index()) * 17deg);
}
Kandinsky β Squares with Concentric Circles4Γ3 cells, each a stack of four nested radial gradients driven by three seeded randoms per cell. Same painting twice in a row because the seed is stable.<seed-context seed="kandinsky" count="3">
<pattern-grid cols="4" rows="3"></pattern-grid>
</seed-context>
#sc-kandinsky pattern-grid > i {
background:
radial-gradient(circle at center,
hsl(calc(var(--rand-0) * 360) 75% 55%) 0 18%,
transparent 19%),
radial-gradient(circle at center,
hsl(calc(var(--rand-1) * 360) 75% 55%) 0 30%,
transparent 31%),
radial-gradient(circle at center,
hsl(calc(var(--rand-2) * 360) 75% 55%) 0 42%,
transparent 43%),
hsl(calc(var(--rand-0) * 360) 30% 90%);
}
Ellsworth Kelly β Colors for a Large Wall8Γ8 flat saturated tiles, palette from seeded random hues mixed with paper-white via color-mix(in oklch).<seed-context seed="kelly" count="2">
<pattern-grid cols="8" rows="8"></pattern-grid>
</seed-context>
#sc-kelly pattern-grid > i {
aspect-ratio: 1;
background: color-mix(in oklch,
hsl(calc(var(--rand-0) * 360) 85% 55%),
white calc(var(--rand-1) * 25%));
}
Yayoi Kusama β infinity dots16Γ16 red-on-white dots whose radius comes from --rand-0. Hover any cell to make its dot bloom.<seed-context seed="kusama" count="1">
<pattern-grid cols="16" rows="16"></pattern-grid>
</seed-context>
#sc-kusama pattern-grid > i {
aspect-ratio: 1;
background:
radial-gradient(circle at center,
#c00 0 calc(var(--rand-0) * 40%),
#fff calc(var(--rand-0) * 40% + 1px) 100%);
transition: scale 200ms;
}
#sc-kusama pattern-grid > i:hover { scale: 1.4; z-index: 2; }
Space-invader-style sprite11Γ8 grid where each cell carries data-px="0|1". CSS maps data-px="1" to bright green. The pixel string is the source of truth.<pattern-grid class="pixel-grid" cols="11" rows="8"></pattern-grid>
const pixels =
"00100000100" +
"00010001000" +
"00111111100" +
"01101110110" +
"11111111111" +
"10111111101" +
"10100000101" +
"00011011000";
grid.innerHTML = [...pixels].map(c => `<i data-px="${c}"></i>`).join('');
#sc-invader [data-px="1"] { --px: #2eff2e; }
Pac-Man-style chompTwo pixel arrays β mouth open and closed β swapped every 200 ms by re-rendering innerHTML. CSS handles the colour.const frames = [
/* closed */ "...",
/* open */ "...",
];
let f = 0;
const draw = () => grid.innerHTML = [...frames[f]].map(c => `<i data-px="${c}"></i>`).join('');
setInterval(() => { f = 1 - f; draw(); }, 200);
#sc-chomp [data-px="1"] { --px: #ffeb00; }
Hover-swap spriteTwo 8Γ8 sprites stacked in the tile. Hovering the tile fades one out and the other in. Mario-style in spirit, generic in art.<div class="tile sc-hoverswap-tile">
<pattern-grid class="pixel-grid" cols="8" rows="8" id="..."></pattern-grid>
<pattern-grid class="pixel-grid" cols="8" rows="8" id="..."></pattern-grid>
</div>
.sc-hoverswap-tile { position: relative; }
.sc-hoverswap-tile > pattern-grid { position: absolute; inset: 0; transition: opacity 200ms; }
#sc-hover-jump { opacity: 0; }
.sc-hoverswap-tile:hover #sc-hover-idle { opacity: 0; }
.sc-hoverswap-tile:hover #sc-hover-jump { opacity: 1; }
Sprite-sheet atlas19 distinct sprites packed into one tile, like an NES game's tile palette β two 16Γ16 heroes spanning 2Γ2 slots and seventeen 8Γ8 items filling the rest. Each sprite is its own <pattern-grid> with its own pixel string and palette.<div class="sc-sheet-atlas">
<pattern-grid class="pixel-grid sc-hero" cols="16" rows="16" style="grid-area: 1/1/3/3"></pattern-grid>
<pattern-grid class="pixel-grid" cols="8" rows="8" style="grid-area: 1/3"></pattern-grid>
β¦ 17 more small sprites β¦
</div>
.sc-sheet-atlas {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(5, 1fr);
gap: 4px;
aspect-ratio: 1;
}
#sc-sp-h1 [data-px="1"] { --px: #f4c290; } /* skin */
#sc-sp-h1 [data-px="2"] { --px: #4a6cd4; } /* armor */
β¦
Palette-cycle invaderSame invader pixels as #50, but the foreground colour cycles through the hue wheel via @property --hue + @keyframes. CSS-only colour cycling.@property --hue { syntax: '<angle>'; inherits: true; initial-value: 0deg; }
#sc-cycle { animation: sc-hue-cycle 4s linear infinite; }
@keyframes sc-hue-cycle { to { --hue: 360deg; } }
#sc-cycle [data-px="1"] { --px: hsl(var(--hue) 85% 60%); }
click cycles 0 β 1 β 2 β 3 β 0
Pixel-paint scratchpad16Γ16 of real <button> cells. Click cycles data-px through four palette slots. No save, no load β pure play.grid.addEventListener('click', e => {
const btn = e.target.closest('button[data-px]');
if (!btn) return;
btn.dataset.px = (Number(btn.dataset.px) + 1) % 4;
});
#sc-paint-pad [data-px="0"] { --px: #1a1a24; }
#sc-paint-pad [data-px="1"] { --px: #ff5470; }
#sc-paint-pad [data-px="2"] { --px: #ffd166; }
#sc-paint-pad [data-px="3"] { --px: #06d6a0; }
Escher β tessellating birdsEvery cell is a full square painted with one seamless gull tile, offset row to row so the flock tiles the sky with no gaps. A per-cell clip-path can't tessellate β any shape that recedes from the cell edges leaves background showing β so the motif lives in a tiling background-image instead.<pattern-grid cols="11" rows="11"></pattern-grid>
#sc-escher pattern-grid > i {
/* full square; a seamless SVG gull tile, offset every other row */
background: url("data:image/svg+xml,…three gull paths…") 0 0 / 100% 100%;
}
POW!
Lichtenstein β POWBen-Day dots painted on every cell with a CSS radial gradient. One outlined cell opens a comic-book "POW!" speech balloon (a popover). Click the outlined cell.<pattern-grid cols="6" rows="4"></pattern-grid>
<div id="sc-pow-balloon" popover>POW!</div>
// make one existing cell the trigger (a <button> grid item lays out
// outside the grid in Chrome/Safari, so keep the <i> and wire it up)
const hero = grid.cellElements[10];
hero.className = 'sc-pow-hero';
hero.addEventListener('click', () => popover.togglePopover());
// position beside the cell before it paints (no anchor() in the top layer yet)
popover.addEventListener('beforetoggle', e => {
if (e.newState !== 'open') return;
const r = hero.getBoundingClientRect();
popover.style.top = r.top - 80 + 'px';
popover.style.left = r.right + 8 + 'px';
});