Popcorn
React + Tailwind
import type { CSSProperties } from 'react';
const css = `
/* Each kernel bursts out of the pile, puffs open on the way up and falls back in. */
@keyframes loader-popcorn-x { from { transform: translateX(0); } to { transform: translateX(var(--dx)); } }
@keyframes loader-popcorn-y {
0% { transform: translateY(0) scale(0.35); opacity: 0; animation-timing-function: cubic-bezier(0.33, 0.67, 0.67, 1); }
6% { opacity: 1; }
12% { transform: translateY(calc(var(--h) * 0.45)) scale(1.15); }
42% { transform: translateY(var(--h)) scale(1); animation-timing-function: cubic-bezier(0.33, 0, 0.67, 0.33); }
84% { transform: translateY(2px) scale(1); opacity: 1; }
92%, 100% { transform: translateY(6px) scale(1); opacity: 0; }
}
@keyframes loader-popcorn-spin { from { transform: rotate(0deg); } to { transform: rotate(var(--rot)); } }
@keyframes loader-popcorn-jiggle { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-1px); } }
.loader-popcorn { position: relative; width: 100px; height: 115px; }
.loader-popcorn svg { width: 100%; height: 100%; overflow: visible; }
.loader-popcorn__puff { fill: light-dark(#ebedf0, #e5e6e9); stroke: light-dark(#cbced3, #a7abb2); stroke-width: 0.8; }
.loader-popcorn__x { animation: loader-popcorn-x var(--dur) linear infinite var(--d); }
.loader-popcorn__y { opacity: 0; animation: loader-popcorn-y var(--dur) linear infinite var(--d); }
.loader-popcorn__spin { animation: loader-popcorn-spin var(--dur) linear infinite var(--d); }
.loader-popcorn__pile { animation: loader-popcorn-jiggle 0.45s ease-in-out infinite; }
.loader-popcorn__bucket { fill: light-dark(#cbced3, #393c41); }
.loader-popcorn__stripe { fill: light-dark(#5f626a, #a7abb2); }
.loader-popcorn__rim { fill: light-dark(#6d7179, #7d8087); }
@media (prefers-reduced-motion: reduce) {
.loader-popcorn__x, .loader-popcorn__y, .loader-popcorn__spin, .loader-popcorn__pile { animation: none; }
}
`;
// One popped kernel: a few overlapping puffs.
function Puff() {
return (
<>
<circle className="loader-popcorn__puff" cx="-2.6" cy="-1.2" r="2.5" />
<circle className="loader-popcorn__puff" cx="2.8" cy="-1.5" r="2.7" />
<circle className="loader-popcorn__puff" cx="0.6" cy="-3.4" r="2.3" />
<circle className="loader-popcorn__puff" cx="0" cy="0.2" r="3.1" />
</>
);
}
const KERNELS = [
{ x: 36, dx: -16, h: -34, rot: -320, dur: 1.5, d: 0 },
{ x: 48, dx: 12, h: -42, rot: 280, dur: 1.7, d: -0.55 },
{ x: 42, dx: 4, h: -28, rot: 200, dur: 1.4, d: -1.0 },
{ x: 54, dx: -8, h: -38, rot: -240, dur: 1.8, d: -0.3 },
{ x: 32, dx: 10, h: -30, rot: 300, dur: 1.6, d: -1.25 },
{ x: 50, dx: 16, h: -26, rot: -200, dur: 1.5, d: -0.8 },
];
// The heap showing over the rim.
const PILE = [
[22, 49, 0.9],
[30, 44, 1.1],
[40, 41, 1.15],
[50, 42, 1.1],
[60, 46, 1],
[27, 51, 0.8],
[36, 48, 1],
[46, 47, 1],
[56, 49, 0.9],
[64, 51, 0.8],
];
export function PopcornLoader() {
return (
<>
<style>
{css}
</style>
<div className="loader-popcorn" role="status" aria-label="Loading">
<svg viewBox="0 0 84 96" aria-hidden="true">
{KERNELS.map((k, i) => (
<g key={i} transform={`translate(${k.x} 44)`}>
<g className="loader-popcorn__x" style={{ '--dx': `${k.dx}px`, '--dur': `${k.dur}s`, '--d': `${k.d}s` } as CSSProperties}>
<g className="loader-popcorn__y" style={{ '--h': `${k.h}px`, '--dur': `${k.dur}s`, '--d': `${k.d}s` } as CSSProperties}>
<g className="loader-popcorn__spin" style={{ '--rot': `${k.rot}deg`, '--dur': `${k.dur}s`, '--d': `${k.d}s` } as CSSProperties}>
<Puff />
</g>
</g>
</g>
</g>
))}
<g className="loader-popcorn__pile">
{PILE.map(([x, y, s]) => (
<g key={`${x}-${y}`} transform={`translate(${x} ${y}) scale(${s})`}>
<Puff />
</g>
))}
</g>
<path className="loader-popcorn__bucket" d="M18 52H66L60 92H24Z" />
{[1, 3, 5].map((i) => (
<path key={i} className="loader-popcorn__stripe" d={`M${18 + 8 * i} 52H${26 + 8 * i}L${30 + 6 * i} 92H${24 + 6 * i}Z`} />
))}
<rect className="loader-popcorn__rim" x="15" y="50" width="54" height="4" rx="1.5" />
</svg>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<div class="loader-popcorn" role="status" aria-label="Loading">
<svg viewBox="0 0 84 96" aria-hidden="true">
<g transform="translate(36 44)">
<g class="loader-popcorn__x" style="--dx:-16px;--dur:1.5s;--d:0s">
<g class="loader-popcorn__y" style="--h:-34px;--dur:1.5s;--d:0s">
<g class="loader-popcorn__spin" style="--rot:-320deg;--dur:1.5s;--d:0s">
<circle class="loader-popcorn__puff" cx="-2.6" cy="-1.2" r="2.5"></circle>
<circle class="loader-popcorn__puff" cx="2.8" cy="-1.5" r="2.7"></circle>
<circle class="loader-popcorn__puff" cx="0.6" cy="-3.4" r="2.3"></circle>
<circle class="loader-popcorn__puff" cx="0" cy="0.2" r="3.1"></circle>
</g>
</g>
</g>
</g>
<g transform="translate(48 44)">
<g class="loader-popcorn__x" style="--dx:12px;--dur:1.7s;--d:-0.55s">
<g class="loader-popcorn__y" style="--h:-42px;--dur:1.7s;--d:-0.55s">
<g class="loader-popcorn__spin" style="--rot:280deg;--dur:1.7s;--d:-0.55s">
<circle class="loader-popcorn__puff" cx="-2.6" cy="-1.2" r="2.5"></circle>
<circle class="loader-popcorn__puff" cx="2.8" cy="-1.5" r="2.7"></circle>
<circle class="loader-popcorn__puff" cx="0.6" cy="-3.4" r="2.3"></circle>
<circle class="loader-popcorn__puff" cx="0" cy="0.2" r="3.1"></circle>
</g>
</g>
</g>
</g>
<g transform="translate(42 44)">
<g class="loader-popcorn__x" style="--dx:4px;--dur:1.4s;--d:-1s">
<g class="loader-popcorn__y" style="--h:-28px;--dur:1.4s;--d:-1s">
<g class="loader-popcorn__spin" style="--rot:200deg;--dur:1.4s;--d:-1s">
<circle class="loader-popcorn__puff" cx="-2.6" cy="-1.2" r="2.5"></circle>
<circle class="loader-popcorn__puff" cx="2.8" cy="-1.5" r="2.7"></circle>
<circle class="loader-popcorn__puff" cx="0.6" cy="-3.4" r="2.3"></circle>
<circle class="loader-popcorn__puff" cx="0" cy="0.2" r="3.1"></circle>
</g>
</g>
</g>
</g>
<g transform="translate(54 44)">
<g class="loader-popcorn__x" style="--dx:-8px;--dur:1.8s;--d:-0.3s">
<g class="loader-popcorn__y" style="--h:-38px;--dur:1.8s;--d:-0.3s">
<g class="loader-popcorn__spin" style="--rot:-240deg;--dur:1.8s;--d:-0.3s">
<circle class="loader-popcorn__puff" cx="-2.6" cy="-1.2" r="2.5"></circle>
<circle class="loader-popcorn__puff" cx="2.8" cy="-1.5" r="2.7"></circle>
<circle class="loader-popcorn__puff" cx="0.6" cy="-3.4" r="2.3"></circle>
<circle class="loader-popcorn__puff" cx="0" cy="0.2" r="3.1"></circle>
</g>
</g>
</g>
</g>
<g transform="translate(32 44)">
<g class="loader-popcorn__x" style="--dx:10px;--dur:1.6s;--d:-1.25s">
<g class="loader-popcorn__y" style="--h:-30px;--dur:1.6s;--d:-1.25s">
<g class="loader-popcorn__spin" style="--rot:300deg;--dur:1.6s;--d:-1.25s">
<circle class="loader-popcorn__puff" cx="-2.6" cy="-1.2" r="2.5"></circle>
<circle class="loader-popcorn__puff" cx="2.8" cy="-1.5" r="2.7"></circle>
<circle class="loader-popcorn__puff" cx="0.6" cy="-3.4" r="2.3"></circle>
<circle class="loader-popcorn__puff" cx="0" cy="0.2" r="3.1"></circle>
</g>
</g>
</g>
</g>
<g transform="translate(50 44)">
<g class="loader-popcorn__x" style="--dx:16px;--dur:1.5s;--d:-0.8s">
<g class="loader-popcorn__y" style="--h:-26px;--dur:1.5s;--d:-0.8s">
<g class="loader-popcorn__spin" style="--rot:-200deg;--dur:1.5s;--d:-0.8s">
<circle class="loader-popcorn__puff" cx="-2.6" cy="-1.2" r="2.5"></circle>
<circle class="loader-popcorn__puff" cx="2.8" cy="-1.5" r="2.7"></circle>
<circle class="loader-popcorn__puff" cx="0.6" cy="-3.4" r="2.3"></circle>
<circle class="loader-popcorn__puff" cx="0" cy="0.2" r="3.1"></circle>
</g>
</g>
</g>
</g>
<g class="loader-popcorn__pile">
<g transform="translate(22 49) scale(0.9)">
<circle class="loader-popcorn__puff" cx="-2.6" cy="-1.2" r="2.5"></circle>
<circle class="loader-popcorn__puff" cx="2.8" cy="-1.5" r="2.7"></circle>
<circle class="loader-popcorn__puff" cx="0.6" cy="-3.4" r="2.3"></circle>
<circle class="loader-popcorn__puff" cx="0" cy="0.2" r="3.1"></circle>
</g>
<g transform="translate(30 44) scale(1.1)">
<circle class="loader-popcorn__puff" cx="-2.6" cy="-1.2" r="2.5"></circle>
<circle class="loader-popcorn__puff" cx="2.8" cy="-1.5" r="2.7"></circle>
<circle class="loader-popcorn__puff" cx="0.6" cy="-3.4" r="2.3"></circle>
<circle class="loader-popcorn__puff" cx="0" cy="0.2" r="3.1"></circle>
</g>
<g transform="translate(40 41) scale(1.15)">
<circle class="loader-popcorn__puff" cx="-2.6" cy="-1.2" r="2.5"></circle>
<circle class="loader-popcorn__puff" cx="2.8" cy="-1.5" r="2.7"></circle>
<circle class="loader-popcorn__puff" cx="0.6" cy="-3.4" r="2.3"></circle>
<circle class="loader-popcorn__puff" cx="0" cy="0.2" r="3.1"></circle>
</g>
<g transform="translate(50 42) scale(1.1)">
<circle class="loader-popcorn__puff" cx="-2.6" cy="-1.2" r="2.5"></circle>
<circle class="loader-popcorn__puff" cx="2.8" cy="-1.5" r="2.7"></circle>
<circle class="loader-popcorn__puff" cx="0.6" cy="-3.4" r="2.3"></circle>
<circle class="loader-popcorn__puff" cx="0" cy="0.2" r="3.1"></circle>
</g>
<g transform="translate(60 46) scale(1)">
<circle class="loader-popcorn__puff" cx="-2.6" cy="-1.2" r="2.5"></circle>
<circle class="loader-popcorn__puff" cx="2.8" cy="-1.5" r="2.7"></circle>
<circle class="loader-popcorn__puff" cx="0.6" cy="-3.4" r="2.3"></circle>
<circle class="loader-popcorn__puff" cx="0" cy="0.2" r="3.1"></circle>
</g>
<g transform="translate(27 51) scale(0.8)">
<circle class="loader-popcorn__puff" cx="-2.6" cy="-1.2" r="2.5"></circle>
<circle class="loader-popcorn__puff" cx="2.8" cy="-1.5" r="2.7"></circle>
<circle class="loader-popcorn__puff" cx="0.6" cy="-3.4" r="2.3"></circle>
<circle class="loader-popcorn__puff" cx="0" cy="0.2" r="3.1"></circle>
</g>
<g transform="translate(36 48) scale(1)">
<circle class="loader-popcorn__puff" cx="-2.6" cy="-1.2" r="2.5"></circle>
<circle class="loader-popcorn__puff" cx="2.8" cy="-1.5" r="2.7"></circle>
<circle class="loader-popcorn__puff" cx="0.6" cy="-3.4" r="2.3"></circle>
<circle class="loader-popcorn__puff" cx="0" cy="0.2" r="3.1"></circle>
</g>
<g transform="translate(46 47) scale(1)">
<circle class="loader-popcorn__puff" cx="-2.6" cy="-1.2" r="2.5"></circle>
<circle class="loader-popcorn__puff" cx="2.8" cy="-1.5" r="2.7"></circle>
<circle class="loader-popcorn__puff" cx="0.6" cy="-3.4" r="2.3"></circle>
<circle class="loader-popcorn__puff" cx="0" cy="0.2" r="3.1"></circle>
</g>
<g transform="translate(56 49) scale(0.9)">
<circle class="loader-popcorn__puff" cx="-2.6" cy="-1.2" r="2.5"></circle>
<circle class="loader-popcorn__puff" cx="2.8" cy="-1.5" r="2.7"></circle>
<circle class="loader-popcorn__puff" cx="0.6" cy="-3.4" r="2.3"></circle>
<circle class="loader-popcorn__puff" cx="0" cy="0.2" r="3.1"></circle>
</g>
<g transform="translate(64 51) scale(0.8)">
<circle class="loader-popcorn__puff" cx="-2.6" cy="-1.2" r="2.5"></circle>
<circle class="loader-popcorn__puff" cx="2.8" cy="-1.5" r="2.7"></circle>
<circle class="loader-popcorn__puff" cx="0.6" cy="-3.4" r="2.3"></circle>
<circle class="loader-popcorn__puff" cx="0" cy="0.2" r="3.1"></circle>
</g>
</g>
<path class="loader-popcorn__bucket" d="M18 52H66L60 92H24Z"></path>
<path class="loader-popcorn__stripe" d="M26 52H34L36 92H30Z"></path>
<path class="loader-popcorn__stripe" d="M42 52H50L48 92H42Z"></path>
<path class="loader-popcorn__stripe" d="M58 52H66L60 92H54Z"></path>
<rect class="loader-popcorn__rim" x="15" y="50" width="54" height="4" rx="1.5"></rect>
</svg>
</div>CSS
/* Each kernel bursts out of the pile, puffs open on the way up and falls back in. */
@keyframes loader-popcorn-x { from { transform: translateX(0); } to { transform: translateX(var(--dx)); } }
@keyframes loader-popcorn-y {
0% { transform: translateY(0) scale(0.35); opacity: 0; animation-timing-function: cubic-bezier(0.33, 0.67, 0.67, 1); }
6% { opacity: 1; }
12% { transform: translateY(calc(var(--h) * 0.45)) scale(1.15); }
42% { transform: translateY(var(--h)) scale(1); animation-timing-function: cubic-bezier(0.33, 0, 0.67, 0.33); }
84% { transform: translateY(2px) scale(1); opacity: 1; }
92%, 100% { transform: translateY(6px) scale(1); opacity: 0; }
}
@keyframes loader-popcorn-spin { from { transform: rotate(0deg); } to { transform: rotate(var(--rot)); } }
@keyframes loader-popcorn-jiggle { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-1px); } }
.loader-popcorn { position: relative; width: 100px; height: 115px; }
.loader-popcorn svg { width: 100%; height: 100%; overflow: visible; }
.loader-popcorn__puff { fill: light-dark(#ebedf0, #e5e6e9); stroke: light-dark(#cbced3, #a7abb2); stroke-width: 0.8; }
.loader-popcorn__x { animation: loader-popcorn-x var(--dur) linear infinite var(--d); }
.loader-popcorn__y { opacity: 0; animation: loader-popcorn-y var(--dur) linear infinite var(--d); }
.loader-popcorn__spin { animation: loader-popcorn-spin var(--dur) linear infinite var(--d); }
.loader-popcorn__pile { animation: loader-popcorn-jiggle 0.45s ease-in-out infinite; }
.loader-popcorn__bucket { fill: light-dark(#cbced3, #393c41); }
.loader-popcorn__stripe { fill: light-dark(#5f626a, #a7abb2); }
.loader-popcorn__rim { fill: light-dark(#6d7179, #7d8087); }
@media (prefers-reduced-motion: reduce) {
.loader-popcorn__x, .loader-popcorn__y, .loader-popcorn__spin, .loader-popcorn__pile { animation: none; }
}Goal: Create a "Popcorn" loader.
Visual behavior: A striped cinema bucket of popcorn where kernels keep popping out of the pile, puffing open on the way up and tumbling back in.
Technique:
- Each kernel nests three groups: one moves it across at a steady speed, one up and down with quadratic easing so the flight is a true arc, and one spins it. Per-kernel --dx, --h, --rot and --dur make every pop different.
- Kernels start small and swell as they leave the pile, then sink back into it and fade.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the bucket sits still.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Popcorn" loader.
Visual behavior: A striped cinema bucket of popcorn where kernels keep popping out of the pile, puffing open on the way up and tumbling back in.
Technique:
- Each kernel nests three groups: one moves it across at a steady speed, one up and down with quadratic easing so the flight is a true arc, and one spins it. Per-kernel --dx, --h, --rot and --dur make every pop different.
- Kernels start small and swell as they leave the pile, then sink back into it and fade.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the bucket sits still.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
A striped cinema bucket of popcorn where kernels keep popping out of the pile, puffing open on the way up and tumbling back in.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- continuous, loop, delay stagger
- Techniques
- keyframes, css variables, svg
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the bucket sits still.