Sundial
React + Tailwind
const TICKS = Array.from({ length: 13 }, (_, i) => -90 + i * 15);
const R = 52;
const css = `
/* Morning to evening: the shadow swings from one side to the other, shortest at noon. */
@keyframes loader-sundial-shadow { 0% { transform: rotate(-80deg) scaleY(1.05); } 50% { transform: rotate(0deg) scaleY(0.42); } 100% { transform: rotate(80deg) scaleY(1.05); } }
@keyframes loader-sundial-sun { 0% { offset-distance: 0%; opacity: 0; } 8% { opacity: 1; } 92% { opacity: 1; } 100% { offset-distance: 100%; opacity: 0; } }
@keyframes loader-sundial-fade { 0%, 4% { opacity: 0; } 12%, 88% { opacity: 1; } 96%, 100% { opacity: 0; } }
.loader-sundial { position: relative; width: 140px; height: 96px; }
.loader-sundial svg { position: absolute; left: 0; bottom: 0; width: 140px; height: 60px; overflow: visible; }
.loader-sundial__plate { fill: light-dark(#cbced3, #393c41); stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.2; }
.loader-sundial__tick { stroke: light-dark(#50535b, #c6c8cd); stroke-width: 1.4; stroke-linecap: round; }
.loader-sundial__shadow { stroke: light-dark(#fbfcfd, #121417); stroke-width: 7; stroke-linecap: round; transform-origin: 70px 30px; animation: loader-sundial-shadow 4s ease-in-out infinite, loader-sundial-fade 4s linear infinite; opacity: 0.9; }
.loader-sundial__gnomon { fill: light-dark(#42454c, #e5e6e9); }
.loader-sundial__sun { position: absolute; left: 0; top: 0; width: 12px; height: 12px; margin: -6px; border-radius: 50%; background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 12px 4px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 35%, transparent); offset-path: path('M14 46C34 0 106 0 126 46'); offset-rotate: 0deg; animation: loader-sundial-sun 4s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.loader-sundial__shadow, .loader-sundial__sun { animation: none; }
.loader-sundial__sun { offset-distance: 50%; }
.loader-sundial__shadow { transform: scaleY(0.42); }
}
`;
export function SundialLoader() {
return (
<>
<style>
{css}
</style>
<div className="loader-sundial" role="status" aria-label="Loading">
<span className="loader-sundial__sun" aria-hidden="true" />
<svg viewBox="0 0 140 60" aria-hidden="true">
<ellipse className="loader-sundial__plate" cx="70" cy="30" rx="64" ry="24" />
{/* The dial is a circle seen at a slant: draw it flat, then squash it to the plate's ellipse. */}
<g transform="translate(70 30) scale(1 0.375) translate(-70 -30)">
{TICKS.map((a) => {
const t = (a * Math.PI) / 180;
return (
<line
key={a}
className="loader-sundial__tick"
x1={(70 + Math.sin(t) * (R - 8)).toFixed(1)}
y1={(30 + Math.cos(t) * (R - 8)).toFixed(1)}
x2={(70 + Math.sin(t) * R).toFixed(1)}
y2={(30 + Math.cos(t) * R).toFixed(1)}
/>
);
})}
<line className="loader-sundial__shadow" x1="70" y1="30" x2="70" y2={30 + R - 12} />
</g>
<path className="loader-sundial__gnomon" d="M70 31L70 12L82 31Z" />
</svg>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<div class="loader-sundial" role="status" aria-label="Loading">
<span class="loader-sundial__sun" aria-hidden="true"></span>
<svg viewBox="0 0 140 60" aria-hidden="true">
<ellipse class="loader-sundial__plate" cx="70" cy="30" rx="64" ry="24"></ellipse>
<g transform="translate(70 30) scale(1 0.375) translate(-70 -30)">
<line class="loader-sundial__tick" x1="26.0" y1="30.0" x2="18.0" y2="30.0"></line>
<line class="loader-sundial__tick" x1="27.5" y1="41.4" x2="19.8" y2="43.5"></line>
<line class="loader-sundial__tick" x1="31.9" y1="52.0" x2="25.0" y2="56.0"></line>
<line class="loader-sundial__tick" x1="38.9" y1="61.1" x2="33.2" y2="66.8"></line>
<line class="loader-sundial__tick" x1="48.0" y1="68.1" x2="44.0" y2="75.0"></line>
<line class="loader-sundial__tick" x1="58.6" y1="72.5" x2="56.5" y2="80.2"></line>
<line class="loader-sundial__tick" x1="70.0" y1="74.0" x2="70.0" y2="82.0"></line>
<line class="loader-sundial__tick" x1="81.4" y1="72.5" x2="83.5" y2="80.2"></line>
<line class="loader-sundial__tick" x1="92.0" y1="68.1" x2="96.0" y2="75.0"></line>
<line class="loader-sundial__tick" x1="101.1" y1="61.1" x2="106.8" y2="66.8"></line>
<line class="loader-sundial__tick" x1="108.1" y1="52.0" x2="115.0" y2="56.0"></line>
<line class="loader-sundial__tick" x1="112.5" y1="41.4" x2="120.2" y2="43.5"></line>
<line class="loader-sundial__tick" x1="114.0" y1="30.0" x2="122.0" y2="30.0"></line>
<line class="loader-sundial__shadow" x1="70" y1="30" x2="70" y2="70"></line>
</g>
<path class="loader-sundial__gnomon" d="M70 31L70 12L82 31Z"></path>
</svg>
</div>CSS
/* Morning to evening: the shadow swings from one side to the other, shortest at noon. */
@keyframes loader-sundial-shadow { 0% { transform: rotate(-80deg) scaleY(1.05); } 50% { transform: rotate(0deg) scaleY(0.42); } 100% { transform: rotate(80deg) scaleY(1.05); } }
@keyframes loader-sundial-sun { 0% { offset-distance: 0%; opacity: 0; } 8% { opacity: 1; } 92% { opacity: 1; } 100% { offset-distance: 100%; opacity: 0; } }
@keyframes loader-sundial-fade { 0%, 4% { opacity: 0; } 12%, 88% { opacity: 1; } 96%, 100% { opacity: 0; } }
.loader-sundial { position: relative; width: 140px; height: 96px; }
.loader-sundial svg { position: absolute; left: 0; bottom: 0; width: 140px; height: 60px; overflow: visible; }
.loader-sundial__plate { fill: light-dark(#cbced3, #393c41); stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.2; }
.loader-sundial__tick { stroke: light-dark(#50535b, #c6c8cd); stroke-width: 1.4; stroke-linecap: round; }
.loader-sundial__shadow { stroke: light-dark(#fbfcfd, #121417); stroke-width: 7; stroke-linecap: round; transform-origin: 70px 30px; animation: loader-sundial-shadow 4s ease-in-out infinite, loader-sundial-fade 4s linear infinite; opacity: 0.9; }
.loader-sundial__gnomon { fill: light-dark(#42454c, #e5e6e9); }
.loader-sundial__sun { position: absolute; left: 0; top: 0; width: 12px; height: 12px; margin: -6px; border-radius: 50%; background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 12px 4px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 35%, transparent); offset-path: path('M14 46C34 0 106 0 126 46'); offset-rotate: 0deg; animation: loader-sundial-sun 4s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.loader-sundial__shadow, .loader-sundial__sun { animation: none; }
.loader-sundial__sun { offset-distance: 50%; }
.loader-sundial__shadow { transform: scaleY(0.42); }
}Goal: Create a "Sundial" loader.
Visual behavior: The sun arcs over a sundial and the gnomon's shadow sweeps across the hours, long in the morning, short at noon and long again by evening.
Technique:
- The hour ticks and the shadow are drawn as if on a flat circle, then squashed into the plate's ellipse with one SVG transform, so the dial reads in perspective.
- The shadow line rotates from -80deg to 80deg and scales on Y, shortest at noon.
- The sun rides an arc with offset-path on the same timing, and both fade at dawn and dusk.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the sun stays at noon with a short shadow.
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 "Sundial" loader.
Visual behavior: The sun arcs over a sundial and the gnomon's shadow sweeps across the hours, long in the morning, short at noon and long again by evening.
Technique:
- The hour ticks and the shadow are drawn as if on a flat circle, then squashed into the plate's ellipse with one SVG transform, so the dial reads in perspective.
- The shadow line rotates from -80deg to 80deg and scales on Y, shortest at noon.
- The sun rides an arc with offset-path on the same timing, and both fade at dawn and dusk.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the sun stays at noon with a short shadow.
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
The sun arcs over a sundial and the gnomon's shadow sweeps across the hours, long in the morning, short at noon and long again by evening.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- continuous, loop
- Techniques
- svg, keyframes, 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 sun stays at noon with a short shadow.