Pancake
React + Tailwind
const css = `
/* Two flips per cycle, so the pancake comes back the same side up. */
@keyframes loader-pancake-flip {
0% { transform: translate(0, 0) rotate(0deg) scale(1, 1); }
7% { transform: translate(0, 1px) rotate(0deg) scale(1.04, 0.85); }
12% { transform: translate(0, -6px) rotate(-10deg) scale(0.98, 1.08); animation-timing-function: cubic-bezier(0.33, 0.67, 0.67, 1); }
22% { transform: translate(3px, -40px) rotate(-95deg) scale(1, 1); animation-timing-function: cubic-bezier(0.33, 0, 0.67, 0.33); }
32% { transform: translate(0, 0) rotate(-180deg) scale(1, 1); }
34.5% { transform: translate(0, 1px) rotate(-180deg) scale(1.1, 0.72); }
38%, 50% { transform: translate(0, 0) rotate(-180deg) scale(1, 1); }
57% { transform: translate(0, 1px) rotate(-180deg) scale(1.04, 0.85); }
62% { transform: translate(0, -6px) rotate(-190deg) scale(0.98, 1.08); animation-timing-function: cubic-bezier(0.33, 0.67, 0.67, 1); }
72% { transform: translate(3px, -40px) rotate(-275deg) scale(1, 1); animation-timing-function: cubic-bezier(0.33, 0, 0.67, 0.33); }
82% { transform: translate(0, 0) rotate(-360deg) scale(1, 1); }
84.5% { transform: translate(0, 1px) rotate(-360deg) scale(1.1, 0.72); }
88%, 100% { transform: translate(0, 0) rotate(-360deg) scale(1, 1); }
}
/* The pan dips, flicks up to throw, then gives a little to catch. */
@keyframes loader-pancake-pan {
0% { transform: rotate(0deg); }
14% { transform: rotate(-5deg); }
24% { transform: rotate(8deg); animation-timing-function: ease-out; }
38%, 58% { transform: rotate(0deg); }
66% { transform: rotate(-3deg); }
76% { transform: rotate(0.6deg); }
84%, 100% { transform: rotate(0deg); }
}
@keyframes loader-pancake-steam { 0% { stroke-dashoffset: 1; opacity: 0; } 30% { opacity: 0.7; } 100% { stroke-dashoffset: -1; opacity: 0; } }
.loader-pancake { position: relative; width: 134px; height: 100px; }
.loader-pancake svg { width: 100%; height: 100%; overflow: visible; }
.loader-pancake__pan { transform-origin: 102px 60px; animation: loader-pancake-pan 1.6s ease-in-out infinite; }
.loader-pancake__cake { transform-origin: 42px 54px; animation: loader-pancake-flip 3.2s ease-in-out infinite; }
.loader-pancake__top { fill: light-dark(#50535b, #c6c8cd); }
.loader-pancake__under { fill: light-dark(#cbced3, #7d8087); }
.loader-pancake__body { fill: light-dark(#cbced3, #393c41); stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.2; stroke-linejoin: round; }
.loader-pancake__rim { stroke: light-dark(#5f626a, #a7abb2); stroke-width: 2.5; stroke-linecap: round; }
.loader-pancake__handle { fill: none; stroke: light-dark(#6d7179, #7d8087); stroke-width: 4.5; stroke-linecap: round; }
.loader-pancake__grip { fill: none; stroke: light-dark(#dbdde1, #2e3035); stroke-width: 6.5; stroke-linecap: round; }
.loader-pancake__steam { fill: none; stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.6; stroke-linecap: round; stroke-dasharray: 1; animation: loader-pancake-steam 2.4s ease-in-out infinite; }
.loader-pancake__steam + .loader-pancake__steam { animation-delay: -1.2s; }
@media (prefers-reduced-motion: reduce) {
.loader-pancake__pan, .loader-pancake__cake, .loader-pancake__steam { animation: none; }
}
`;
export function PancakeLoader() {
return (
<>
<style>
{css}
</style>
<div className="loader-pancake" role="status" aria-label="Loading">
<svg viewBox="0 0 112 84" aria-hidden="true">
<path className="loader-pancake__steam" pathLength={1} d="M32 44Q28 36 32 30Q36 24 32 16" />
<path className="loader-pancake__steam" pathLength={1} d="M52 44Q48 36 52 30Q56 24 52 16" />
<g className="loader-pancake__pan">
<g className="loader-pancake__cake">
<rect className="loader-pancake__under" x="22" y="51" width="40" height="6" rx="3" />
<rect className="loader-pancake__top" x="22" y="51" width="40" height="3.2" rx="1.6" />
</g>
<path className="loader-pancake__body" d="M14 54H70L66 64Q65 67 61 67H23Q19 67 18 64Z" />
<path className="loader-pancake__rim" d="M13 54H71" />
<path className="loader-pancake__handle" d="M70 57L86 59" />
<path className="loader-pancake__grip" d="M86 59L104 61" />
</g>
</svg>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<div class="loader-pancake" role="status" aria-label="Loading">
<svg viewBox="0 0 112 84" aria-hidden="true">
<path class="loader-pancake__steam" pathLength="1" d="M32 44Q28 36 32 30Q36 24 32 16"></path>
<path class="loader-pancake__steam" pathLength="1" d="M52 44Q48 36 52 30Q56 24 52 16"></path>
<g class="loader-pancake__pan">
<g class="loader-pancake__cake">
<rect class="loader-pancake__under" x="22" y="51" width="40" height="6" rx="3"></rect>
<rect class="loader-pancake__top" x="22" y="51" width="40" height="3.2" rx="1.6"></rect>
</g>
<path class="loader-pancake__body" d="M14 54H70L66 64Q65 67 61 67H23Q19 67 18 64Z"></path>
<path class="loader-pancake__rim" d="M13 54H71"></path>
<path class="loader-pancake__handle" d="M70 57L86 59"></path>
<path class="loader-pancake__grip" d="M86 59L104 61"></path>
</g>
</svg>
</div>CSS
/* Two flips per cycle, so the pancake comes back the same side up. */
@keyframes loader-pancake-flip {
0% { transform: translate(0, 0) rotate(0deg) scale(1, 1); }
7% { transform: translate(0, 1px) rotate(0deg) scale(1.04, 0.85); }
12% { transform: translate(0, -6px) rotate(-10deg) scale(0.98, 1.08); animation-timing-function: cubic-bezier(0.33, 0.67, 0.67, 1); }
22% { transform: translate(3px, -40px) rotate(-95deg) scale(1, 1); animation-timing-function: cubic-bezier(0.33, 0, 0.67, 0.33); }
32% { transform: translate(0, 0) rotate(-180deg) scale(1, 1); }
34.5% { transform: translate(0, 1px) rotate(-180deg) scale(1.1, 0.72); }
38%, 50% { transform: translate(0, 0) rotate(-180deg) scale(1, 1); }
57% { transform: translate(0, 1px) rotate(-180deg) scale(1.04, 0.85); }
62% { transform: translate(0, -6px) rotate(-190deg) scale(0.98, 1.08); animation-timing-function: cubic-bezier(0.33, 0.67, 0.67, 1); }
72% { transform: translate(3px, -40px) rotate(-275deg) scale(1, 1); animation-timing-function: cubic-bezier(0.33, 0, 0.67, 0.33); }
82% { transform: translate(0, 0) rotate(-360deg) scale(1, 1); }
84.5% { transform: translate(0, 1px) rotate(-360deg) scale(1.1, 0.72); }
88%, 100% { transform: translate(0, 0) rotate(-360deg) scale(1, 1); }
}
/* The pan dips, flicks up to throw, then gives a little to catch. */
@keyframes loader-pancake-pan {
0% { transform: rotate(0deg); }
14% { transform: rotate(-5deg); }
24% { transform: rotate(8deg); animation-timing-function: ease-out; }
38%, 58% { transform: rotate(0deg); }
66% { transform: rotate(-3deg); }
76% { transform: rotate(0.6deg); }
84%, 100% { transform: rotate(0deg); }
}
@keyframes loader-pancake-steam { 0% { stroke-dashoffset: 1; opacity: 0; } 30% { opacity: 0.7; } 100% { stroke-dashoffset: -1; opacity: 0; } }
.loader-pancake { position: relative; width: 134px; height: 100px; }
.loader-pancake svg { width: 100%; height: 100%; overflow: visible; }
.loader-pancake__pan { transform-origin: 102px 60px; animation: loader-pancake-pan 1.6s ease-in-out infinite; }
.loader-pancake__cake { transform-origin: 42px 54px; animation: loader-pancake-flip 3.2s ease-in-out infinite; }
.loader-pancake__top { fill: light-dark(#50535b, #c6c8cd); }
.loader-pancake__under { fill: light-dark(#cbced3, #7d8087); }
.loader-pancake__body { fill: light-dark(#cbced3, #393c41); stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.2; stroke-linejoin: round; }
.loader-pancake__rim { stroke: light-dark(#5f626a, #a7abb2); stroke-width: 2.5; stroke-linecap: round; }
.loader-pancake__handle { fill: none; stroke: light-dark(#6d7179, #7d8087); stroke-width: 4.5; stroke-linecap: round; }
.loader-pancake__grip { fill: none; stroke: light-dark(#dbdde1, #2e3035); stroke-width: 6.5; stroke-linecap: round; }
.loader-pancake__steam { fill: none; stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.6; stroke-linecap: round; stroke-dasharray: 1; animation: loader-pancake-steam 2.4s ease-in-out infinite; }
.loader-pancake__steam + .loader-pancake__steam { animation-delay: -1.2s; }
@media (prefers-reduced-motion: reduce) {
.loader-pancake__pan, .loader-pancake__cake, .loader-pancake__steam { animation: none; }
}Goal: Create a "Pancake" loader.
Visual behavior: A frying pan dips and flicks, the pancake flips over in the air and lands back with a squash, again and again.
Technique:
- The pancake is two-toned and lives inside the pan group, so it rides the pan as it dips and flicks. Its keyframe holds two full flips, rising on ease-out and falling on ease-in, so it comes back the same side up each cycle.
- The pan runs a keyframe half as long about the wrist: a dip, a flick that throws and a little give to catch.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the pancake rests in the pan.
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 "Pancake" loader.
Visual behavior: A frying pan dips and flicks, the pancake flips over in the air and lands back with a squash, again and again.
Technique:
- The pancake is two-toned and lives inside the pan group, so it rides the pan as it dips and flicks. Its keyframe holds two full flips, rising on ease-out and falling on ease-in, so it comes back the same side up each cycle.
- The pan runs a keyframe half as long about the wrist: a dip, a flick that throws and a little give to catch.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the pancake rests in the pan.
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 frying pan dips and flicks, the pancake flips over in the air and lands back with a squash, again and again.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- continuous, loop, spring
- Techniques
- keyframes, svg, transform only
- 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 pancake rests in the pan.