Lunar
React + Tailwind
const css = `
/* Two halves and a disc turning about the vertical axis: the disc's edge is the terminator.
Dark face first (crescent to quarter), lit face next (gibbous to full); the halves swap
sides at full, while the disc hides them, so the moon wanes the right way round. */
@keyframes loader-lunar-turn { to { transform: rotateY(360deg); } }
@keyframes loader-lunar-right { 0%, 49.9% { background: light-dark(#373a40, #f1f2f3); } 50%, 100% { background: light-dark(#ebedf0, #232529); } }
@keyframes loader-lunar-left { 0%, 49.9% { background: light-dark(#ebedf0, #232529); } 50%, 100% { background: light-dark(#373a40, #f1f2f3); } }
.loader-lunar { position: relative; width: 72px; height: 72px; }
.loader-lunar::before { content: ''; position: absolute; inset: -14px; border-radius: 50%; background: radial-gradient(circle, color-mix(in oklab, light-dark(#373a40, #f1f2f3) 14%, transparent) 40%, transparent 70%); }
.loader-lunar__moon { position: absolute; inset: 0; border-radius: 50%; overflow: hidden; box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); }
.loader-lunar__half { position: absolute; top: 0; bottom: 0; width: 50%; }
.loader-lunar__half--l { left: 0; animation: loader-lunar-left 6s steps(1) infinite; }
.loader-lunar__half--r { right: 0; animation: loader-lunar-right 6s steps(1) infinite; }
.loader-lunar__disc { position: absolute; inset: 0; transform-style: preserve-3d; animation: loader-lunar-turn 6s linear infinite; }
.loader-lunar__face { position: absolute; inset: 0; border-radius: 50%; backface-visibility: hidden; background: light-dark(#ebedf0, #232529); }
.loader-lunar__face--lit { transform: rotateY(180deg); background: light-dark(#373a40, #f1f2f3); }
/* Craters, faint on both sides so the surface reads through the phases. */
.loader-lunar__craters { position: absolute; inset: 0; border-radius: 50%; background: radial-gradient(circle at 34% 30%, color-mix(in oklab, light-dark(#6d7179, #7d8087) 35%, transparent) 0 6px, transparent 6.5px), radial-gradient(circle at 62% 58%, color-mix(in oklab, light-dark(#6d7179, #7d8087) 30%, transparent) 0 9px, transparent 9.5px), radial-gradient(circle at 40% 72%, color-mix(in oklab, light-dark(#6d7179, #7d8087) 30%, transparent) 0 4px, transparent 4.5px); pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
.loader-lunar__half, .loader-lunar__disc { animation: none; }
.loader-lunar__disc { transform: rotateY(60deg); }
.loader-lunar__half--r { background: light-dark(#373a40, #f1f2f3); }
.loader-lunar__half--l { background: light-dark(#ebedf0, #232529); }
}
`;
export function LunarLoader() {
return (
<>
<style>
{css}
</style>
<div className="loader-lunar" role="status" aria-label="Loading">
<div className="loader-lunar__moon" aria-hidden="true">
<span className="loader-lunar__half loader-lunar__half--l" />
<span className="loader-lunar__half loader-lunar__half--r" />
<span className="loader-lunar__disc">
<span className="loader-lunar__face" />
<span className="loader-lunar__face loader-lunar__face--lit" />
</span>
<span className="loader-lunar__craters" />
</div>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<div class="loader-lunar" role="status" aria-label="Loading">
<div class="loader-lunar__moon" aria-hidden="true">
<span class="loader-lunar__half loader-lunar__half--l"></span>
<span class="loader-lunar__half loader-lunar__half--r"></span>
<span class="loader-lunar__disc">
<span class="loader-lunar__face"></span>
<span class="loader-lunar__face loader-lunar__face--lit"></span>
</span>
<span class="loader-lunar__craters"></span>
</div>
</div>CSS
/* Two halves and a disc turning about the vertical axis: the disc's edge is the terminator.
Dark face first (crescent to quarter), lit face next (gibbous to full); the halves swap
sides at full, while the disc hides them, so the moon wanes the right way round. */
@keyframes loader-lunar-turn { to { transform: rotateY(360deg); } }
@keyframes loader-lunar-right { 0%, 49.9% { background: light-dark(#373a40, #f1f2f3); } 50%, 100% { background: light-dark(#ebedf0, #232529); } }
@keyframes loader-lunar-left { 0%, 49.9% { background: light-dark(#ebedf0, #232529); } 50%, 100% { background: light-dark(#373a40, #f1f2f3); } }
.loader-lunar { position: relative; width: 72px; height: 72px; }
.loader-lunar::before { content: ''; position: absolute; inset: -14px; border-radius: 50%; background: radial-gradient(circle, color-mix(in oklab, light-dark(#373a40, #f1f2f3) 14%, transparent) 40%, transparent 70%); }
.loader-lunar__moon { position: absolute; inset: 0; border-radius: 50%; overflow: hidden; box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); }
.loader-lunar__half { position: absolute; top: 0; bottom: 0; width: 50%; }
.loader-lunar__half--l { left: 0; animation: loader-lunar-left 6s steps(1) infinite; }
.loader-lunar__half--r { right: 0; animation: loader-lunar-right 6s steps(1) infinite; }
.loader-lunar__disc { position: absolute; inset: 0; transform-style: preserve-3d; animation: loader-lunar-turn 6s linear infinite; }
.loader-lunar__face { position: absolute; inset: 0; border-radius: 50%; backface-visibility: hidden; background: light-dark(#ebedf0, #232529); }
.loader-lunar__face--lit { transform: rotateY(180deg); background: light-dark(#373a40, #f1f2f3); }
/* Craters, faint on both sides so the surface reads through the phases. */
.loader-lunar__craters { position: absolute; inset: 0; border-radius: 50%; background: radial-gradient(circle at 34% 30%, color-mix(in oklab, light-dark(#6d7179, #7d8087) 35%, transparent) 0 6px, transparent 6.5px), radial-gradient(circle at 62% 58%, color-mix(in oklab, light-dark(#6d7179, #7d8087) 30%, transparent) 0 9px, transparent 9.5px), radial-gradient(circle at 40% 72%, color-mix(in oklab, light-dark(#6d7179, #7d8087) 30%, transparent) 0 4px, transparent 4.5px); pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
.loader-lunar__half, .loader-lunar__disc { animation: none; }
.loader-lunar__disc { transform: rotateY(60deg); }
.loader-lunar__half--r { background: light-dark(#373a40, #f1f2f3); }
.loader-lunar__half--l { background: light-dark(#ebedf0, #232529); }
}Goal: Create a "Lunar" loader.
Visual behavior: The moon running through its phases, new to full and back to new, with a true curved terminator and faint craters showing through.
Technique:
- A clipped circle holds two halves and a disc that turns on the Y axis; the disc's edge, seen in projection, is the curved terminator.
- The disc has a dark face and a lit face (backface-visibility: hidden), so its first half turn grows a crescent into a gibbous and the second shrinks it again.
- At full the two halves swap shades with steps(1) keyframes while the disc covers them, so the moon wanes from the correct side.
- A layer of radial gradients sits on top as craters, faint enough to read in every phase.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the moon holds still as a waxing gibbous.
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 "Lunar" loader.
Visual behavior: The moon running through its phases, new to full and back to new, with a true curved terminator and faint craters showing through.
Technique:
- A clipped circle holds two halves and a disc that turns on the Y axis; the disc's edge, seen in projection, is the curved terminator.
- The disc has a dark face and a lit face (backface-visibility: hidden), so its first half turn grows a crescent into a gibbous and the second shrinks it again.
- At full the two halves swap shades with steps(1) keyframes while the disc covers them, so the moon wanes from the correct side.
- A layer of radial gradients sits on top as craters, faint enough to read in every phase.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the moon holds still as a waxing gibbous.
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 moon running through its phases, new to full and back to new, with a true curved terminator and faint craters showing through.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- continuous, loop, 3d
- Techniques
- keyframes, preserve 3d, 3d transform
- 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 moon holds still as a waxing gibbous.