Spirit level
React + Tailwind
const css = `
/* Tips one way, then the other, then comes level and holds. */
@keyframes loader-level-tilt {
0%, 4% { transform: rotate(0deg); }
14%, 30% { transform: rotate(-7deg); }
44%, 60% { transform: rotate(6deg); }
72% { transform: rotate(-1deg); }
78%, 100% { transform: rotate(0deg); }
}
/* The bubble runs to the high end a beat behind the tilt, with a little overshoot. */
@keyframes loader-level-bubble {
0%, 8% { transform: translateX(0); }
22% { transform: translateX(13px); animation-timing-function: ease-out; }
25%, 36% { transform: translateX(11.5px); }
50% { transform: translateX(-13px); animation-timing-function: ease-out; }
53%, 64% { transform: translateX(-11.5px); }
80% { transform: translateX(1.5px); }
86%, 100% { transform: translateX(0); }
}
@keyframes loader-level-mark { 0%, 82% { stroke: light-dark(#5f626a, #a7abb2); } 86%, 97% { stroke: light-dark(#373a40, #f1f2f3); } 100% { stroke: light-dark(#5f626a, #a7abb2); } }
.loader-level { position: relative; width: 144px; height: 86px; }
.loader-level svg { width: 100%; height: 100%; overflow: visible; }
.loader-level__bar { transform-origin: 60px 46px; animation: loader-level-tilt 3.4s ease-in-out infinite; }
.loader-level__body { fill: light-dark(#cbced3, #393c41); stroke: light-dark(#6d7179, #7d8087); stroke-width: 1; }
.loader-level__cap { fill: light-dark(#6d7179, #7d8087); }
.loader-level__vial { fill: light-dark(#cbced3, #232529); stroke: light-dark(#5f626a, #a7abb2); stroke-width: 0.8; }
.loader-level__bubble { fill: light-dark(#fbfcfd, #e5e6e9); animation: loader-level-bubble 3.4s cubic-bezier(0.3, 1.25, 0.5, 1) infinite; }
.loader-level__mark { stroke: light-dark(#5f626a, #a7abb2); stroke-width: 1.2; animation: loader-level-mark 3.4s linear infinite; }
.loader-level__hole { fill: light-dark(#ebedf0, #232529); stroke: light-dark(#6d7179, #7d8087); stroke-width: 1; }
.loader-level__fulcrum { fill: light-dark(#cbced3, #393c41); }
.loader-level__ground { stroke: light-dark(#6d7179, #7d8087); stroke-width: 2.5; stroke-linecap: round; }
@media (prefers-reduced-motion: reduce) {
.loader-level__bar, .loader-level__bubble, .loader-level__mark { animation: none; }
}
`;
export function LevelLoader() {
return (
<>
<style>
{css}
</style>
<div className="loader-level" role="status" aria-label="Loading">
<svg viewBox="0 0 120 72" aria-hidden="true">
<path className="loader-level__fulcrum" d="M60 46L51 62H69Z" />
<path className="loader-level__ground" d="M34 62H86" />
<g className="loader-level__bar">
<rect className="loader-level__body" x="8" y="33" width="104" height="13" rx="3" />
<rect className="loader-level__cap" x="8" y="33" width="9" height="13" rx="2.5" />
<rect className="loader-level__cap" x="103" y="33" width="9" height="13" rx="2.5" />
<circle className="loader-level__hole" cx="27" cy="39.5" r="3" />
<circle className="loader-level__hole" cx="93" cy="39.5" r="3" />
<rect className="loader-level__vial" x="41" y="35.5" width="38" height="8" rx="4" />
<ellipse className="loader-level__bubble" cx="60" cy="39.5" rx="5" ry="2.6" />
<path className="loader-level__mark" d="M54 35.5V43.5M66 35.5V43.5" />
</g>
</svg>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<div class="loader-level" role="status" aria-label="Loading">
<svg viewBox="0 0 120 72" aria-hidden="true">
<path class="loader-level__fulcrum" d="M60 46L51 62H69Z"></path>
<path class="loader-level__ground" d="M34 62H86"></path>
<g class="loader-level__bar">
<rect class="loader-level__body" x="8" y="33" width="104" height="13" rx="3"></rect>
<rect class="loader-level__cap" x="8" y="33" width="9" height="13" rx="2.5"></rect>
<rect class="loader-level__cap" x="103" y="33" width="9" height="13" rx="2.5"></rect>
<circle class="loader-level__hole" cx="27" cy="39.5" r="3"></circle>
<circle class="loader-level__hole" cx="93" cy="39.5" r="3"></circle>
<rect class="loader-level__vial" x="41" y="35.5" width="38" height="8" rx="4"></rect>
<ellipse class="loader-level__bubble" cx="60" cy="39.5" rx="5" ry="2.6"></ellipse>
<path class="loader-level__mark" d="M54 35.5V43.5M66 35.5V43.5"></path>
</g>
</svg>
</div>CSS
/* Tips one way, then the other, then comes level and holds. */
@keyframes loader-level-tilt {
0%, 4% { transform: rotate(0deg); }
14%, 30% { transform: rotate(-7deg); }
44%, 60% { transform: rotate(6deg); }
72% { transform: rotate(-1deg); }
78%, 100% { transform: rotate(0deg); }
}
/* The bubble runs to the high end a beat behind the tilt, with a little overshoot. */
@keyframes loader-level-bubble {
0%, 8% { transform: translateX(0); }
22% { transform: translateX(13px); animation-timing-function: ease-out; }
25%, 36% { transform: translateX(11.5px); }
50% { transform: translateX(-13px); animation-timing-function: ease-out; }
53%, 64% { transform: translateX(-11.5px); }
80% { transform: translateX(1.5px); }
86%, 100% { transform: translateX(0); }
}
@keyframes loader-level-mark { 0%, 82% { stroke: light-dark(#5f626a, #a7abb2); } 86%, 97% { stroke: light-dark(#373a40, #f1f2f3); } 100% { stroke: light-dark(#5f626a, #a7abb2); } }
.loader-level { position: relative; width: 144px; height: 86px; }
.loader-level svg { width: 100%; height: 100%; overflow: visible; }
.loader-level__bar { transform-origin: 60px 46px; animation: loader-level-tilt 3.4s ease-in-out infinite; }
.loader-level__body { fill: light-dark(#cbced3, #393c41); stroke: light-dark(#6d7179, #7d8087); stroke-width: 1; }
.loader-level__cap { fill: light-dark(#6d7179, #7d8087); }
.loader-level__vial { fill: light-dark(#cbced3, #232529); stroke: light-dark(#5f626a, #a7abb2); stroke-width: 0.8; }
.loader-level__bubble { fill: light-dark(#fbfcfd, #e5e6e9); animation: loader-level-bubble 3.4s cubic-bezier(0.3, 1.25, 0.5, 1) infinite; }
.loader-level__mark { stroke: light-dark(#5f626a, #a7abb2); stroke-width: 1.2; animation: loader-level-mark 3.4s linear infinite; }
.loader-level__hole { fill: light-dark(#ebedf0, #232529); stroke: light-dark(#6d7179, #7d8087); stroke-width: 1; }
.loader-level__fulcrum { fill: light-dark(#cbced3, #393c41); }
.loader-level__ground { stroke: light-dark(#6d7179, #7d8087); stroke-width: 2.5; stroke-linecap: round; }
@media (prefers-reduced-motion: reduce) {
.loader-level__bar, .loader-level__bubble, .loader-level__mark { animation: none; }
}Goal: Create a "Spirit level" loader.
Visual behavior: A spirit level rocking on a fulcrum: the bubble runs to the high end each time it tips, then it comes level and the bubble settles between the marks.
Technique:
- The bar tilts one way, then the other, then comes level, on one keyframe. Inside it the bubble runs its own keyframe a beat behind the tilt with a small overshoot.
- The centre marks light while it holds level. The bubble stays light in both themes.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the level sits flat.
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 "Spirit level" loader.
Visual behavior: A spirit level rocking on a fulcrum: the bubble runs to the high end each time it tips, then it comes level and the bubble settles between the marks.
Technique:
- The bar tilts one way, then the other, then comes level, on one keyframe. Inside it the bubble runs its own keyframe a beat behind the tilt with a small overshoot.
- The centre marks light while it holds level. The bubble stays light in both themes.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the level sits flat.
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 spirit level rocking on a fulcrum: the bubble runs to the high end each time it tips, then it comes level and the bubble settles between the marks.
- Dependencies
- None
- Difficulty
- Beginner
- 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 level sits flat.