Tape Measure
React + Tailwind
const T = 3;
const BLADE = 150;
const css = `
.loader-tape-measure { --shell: light-dark(#50535b, #393c41); --grip: light-dark(#373a40, #232529); --blade: light-dark(#ebedf0, #e5e6e9); --tick: light-dark(#373a40, #121417); --metal: light-dark(#6d7179, #a7abb2); position: relative; width: 230px; height: 128px; }
/* The case and everything it holds kick back together when the blade slams home. */
.loader-tape-measure__body { position: absolute; left: 2px; top: 22px; width: 80px; height: 80px; transform-origin: 40px 44px; animation: loader-tape-measure-kick ${T}s infinite; }
.loader-tape-measure__shell { position: absolute; inset: 0; z-index: 2; border-radius: 22px 22px 22px 14px; background: linear-gradient(150deg, color-mix(in oklab, var(--shell) 70%, light-dark(#373a40, #f1f2f3)), var(--shell) 45%, color-mix(in oklab, var(--shell) 75%, light-dark(#fbfcfd, #121417))); box-shadow: inset 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 25%, transparent), 0 8px 14px -6px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); }
.loader-tape-measure__shell::before { content: ''; position: absolute; inset: 9px 9px 11px; border-radius: 16px; background: repeating-linear-gradient(0deg, var(--grip) 0 2px, color-mix(in oklab, var(--grip) 80%, light-dark(#6d7179, #7d8087)) 2px 4px); }
.loader-tape-measure__badge { position: absolute; left: 50%; top: 46%; z-index: 1; display: grid; place-items: center; width: 30px; height: 30px; margin: -15px; border-radius: 50%; background: radial-gradient(circle at 40% 35%, color-mix(in oklab, var(--metal) 50%, light-dark(#373a40, #f1f2f3)), var(--metal) 70%); color: light-dark(#f3f4f7, #1a1c1f); font: 700 9px/1 'Arial Narrow', 'Helvetica Neue', sans-serif; letter-spacing: 0.04em; }
.loader-tape-measure__lock { position: absolute; left: 24px; top: -5px; width: 22px; height: 8px; border-radius: 3px 3px 0 0; background: var(--metal); }
/* The blade lives in a track that starts at the mouth; inside the case it is hidden. */
.loader-tape-measure__track { position: absolute; left: 74px; top: 58px; z-index: 1; width: ${BLADE}px; height: 14px; clip-path: inset(-14px -24px -14px 0); transform-origin: 0 50%; animation: loader-tape-measure-sag ${T}s infinite; }
.loader-tape-measure__blade { position: absolute; inset: 0; border-radius: 0 1px 1px 0; background: repeating-linear-gradient(90deg, var(--tick) 0 1.2px, transparent 1.2px 25px) 0 0 / 100% 7px no-repeat, repeating-linear-gradient(90deg, var(--tick) 0 0.8px, transparent 0.8px 5px) 0 0 / 100% 3.5px no-repeat, linear-gradient(transparent 85%, color-mix(in oklab, var(--tick) 25%, transparent) 85%), var(--blade); box-shadow: 0 2px 3px light-dark(rgba(0, 0, 0, 0.09), rgba(0, 0, 0, 0.3)); animation: loader-tape-measure-run ${T}s infinite; }
.loader-tape-measure__num { position: absolute; top: 5px; color: var(--tick); font: 700 7px/1 'Arial Narrow', 'Helvetica Neue', sans-serif; }
/* The hook: a bent tab that sits against the mouth when the blade is in. */
.loader-tape-measure__hook { position: absolute; right: -4px; top: -5px; width: 5px; height: 24px; border-radius: 1px 2px 2px 1px; background: linear-gradient(90deg, var(--metal), color-mix(in oklab, var(--metal) 65%, light-dark(#fbfcfd, #121417))); }
.loader-tape-measure__hook::before { content: ''; position: absolute; left: -2px; top: 9px; width: 3px; height: 3px; border-radius: 50%; background: color-mix(in oklab, var(--metal) 50%, light-dark(#fbfcfd, #121417)); }
@keyframes loader-tape-measure-run {
0%, 8% { transform: translateX(${-BLADE + 6}px); animation-timing-function: cubic-bezier(0.2, 0.7, 0.3, 1); }
42%, 68% { transform: translateX(0); animation-timing-function: cubic-bezier(0.65, 0, 0.95, 0.55); }
75%, 100% { transform: translateX(${-BLADE + 6}px); }
}
@keyframes loader-tape-measure-sag {
0%, 40% { transform: rotate(0deg); }
47% { transform: rotate(5deg); }
52% { transform: rotate(2.2deg); }
57% { transform: rotate(4.2deg); }
62%, 68% { transform: rotate(3.5deg); animation-timing-function: ease-in; }
75%, 100% { transform: rotate(0deg); }
}
@keyframes loader-tape-measure-kick {
0%, 75% { transform: none; }
78% { transform: translateX(-4px) rotate(-8deg); }
82% { transform: translateX(1px) rotate(3deg); }
86% { transform: rotate(-1.2deg); }
90%, 100% { transform: none; }
}
@media (prefers-reduced-motion: reduce) {
.loader-tape-measure *, .loader-tape-measure__body { animation: none !important; }
.loader-tape-measure__blade { transform: translateX(-40px); }
}
`;
export function Preview() {
return (
<>
<style href="kit-loaders-tape-measure" precedence="kit">
{css}
</style>
<div className="loader-tape-measure" role="status" aria-label="Loading">
<span className="loader-tape-measure__body" aria-hidden="true">
<span className="loader-tape-measure__lock" />
<span className="loader-tape-measure__track">
<span className="loader-tape-measure__blade">
{[1, 2, 3, 4, 5].map((n) => (
<span key={n} className="loader-tape-measure__num" style={{ left: n * 25 - 7 }}>
{n}
</span>
))}
<span className="loader-tape-measure__hook" />
</span>
</span>
<span className="loader-tape-measure__shell">
<span className="loader-tape-measure__badge">5 m</span>
</span>
</span>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<div class="loader-tape-measure" role="status" aria-label="Loading">
<span class="loader-tape-measure__body" aria-hidden="true">
<span class="loader-tape-measure__lock"></span>
<span class="loader-tape-measure__track">
<span class="loader-tape-measure__blade">
<span class="loader-tape-measure__num" style="left:18px">1</span>
<span class="loader-tape-measure__num" style="left:43px">2</span>
<span class="loader-tape-measure__num" style="left:68px">3</span>
<span class="loader-tape-measure__num" style="left:93px">4</span>
<span class="loader-tape-measure__num" style="left:118px">5</span>
<span class="loader-tape-measure__hook"></span>
</span>
</span>
<span class="loader-tape-measure__shell">
<span class="loader-tape-measure__badge">5 m</span>
</span>
</span>
</div>CSS
.loader-tape-measure { --shell: light-dark(#50535b, #393c41); --grip: light-dark(#373a40, #232529); --blade: light-dark(#ebedf0, #e5e6e9); --tick: light-dark(#373a40, #121417); --metal: light-dark(#6d7179, #a7abb2); position: relative; width: 230px; height: 128px; }
/* The case and everything it holds kick back together when the blade slams home. */
.loader-tape-measure__body { position: absolute; left: 2px; top: 22px; width: 80px; height: 80px; transform-origin: 40px 44px; animation: loader-tape-measure-kick 3s infinite; }
.loader-tape-measure__shell { position: absolute; inset: 0; z-index: 2; border-radius: 22px 22px 22px 14px; background: linear-gradient(150deg, color-mix(in oklab, var(--shell) 70%, light-dark(#373a40, #f1f2f3)), var(--shell) 45%, color-mix(in oklab, var(--shell) 75%, light-dark(#fbfcfd, #121417))); box-shadow: inset 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 25%, transparent), 0 8px 14px -6px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); }
.loader-tape-measure__shell::before { content: ''; position: absolute; inset: 9px 9px 11px; border-radius: 16px; background: repeating-linear-gradient(0deg, var(--grip) 0 2px, color-mix(in oklab, var(--grip) 80%, light-dark(#6d7179, #7d8087)) 2px 4px); }
.loader-tape-measure__badge { position: absolute; left: 50%; top: 46%; z-index: 1; display: grid; place-items: center; width: 30px; height: 30px; margin: -15px; border-radius: 50%; background: radial-gradient(circle at 40% 35%, color-mix(in oklab, var(--metal) 50%, light-dark(#373a40, #f1f2f3)), var(--metal) 70%); color: light-dark(#f3f4f7, #1a1c1f); font: 700 9px/1 'Arial Narrow', 'Helvetica Neue', sans-serif; letter-spacing: 0.04em; }
.loader-tape-measure__lock { position: absolute; left: 24px; top: -5px; width: 22px; height: 8px; border-radius: 3px 3px 0 0; background: var(--metal); }
/* The blade lives in a track that starts at the mouth; inside the case it is hidden. */
.loader-tape-measure__track { position: absolute; left: 74px; top: 58px; z-index: 1; width: 150px; height: 14px; clip-path: inset(-14px -24px -14px 0); transform-origin: 0 50%; animation: loader-tape-measure-sag 3s infinite; }
.loader-tape-measure__blade { position: absolute; inset: 0; border-radius: 0 1px 1px 0; background: repeating-linear-gradient(90deg, var(--tick) 0 1.2px, transparent 1.2px 25px) 0 0 / 100% 7px no-repeat, repeating-linear-gradient(90deg, var(--tick) 0 0.8px, transparent 0.8px 5px) 0 0 / 100% 3.5px no-repeat, linear-gradient(transparent 85%, color-mix(in oklab, var(--tick) 25%, transparent) 85%), var(--blade); box-shadow: 0 2px 3px light-dark(rgba(0, 0, 0, 0.09), rgba(0, 0, 0, 0.3)); animation: loader-tape-measure-run 3s infinite; }
.loader-tape-measure__num { position: absolute; top: 5px; color: var(--tick); font: 700 7px/1 'Arial Narrow', 'Helvetica Neue', sans-serif; }
/* The hook: a bent tab that sits against the mouth when the blade is in. */
.loader-tape-measure__hook { position: absolute; right: -4px; top: -5px; width: 5px; height: 24px; border-radius: 1px 2px 2px 1px; background: linear-gradient(90deg, var(--metal), color-mix(in oklab, var(--metal) 65%, light-dark(#fbfcfd, #121417))); }
.loader-tape-measure__hook::before { content: ''; position: absolute; left: -2px; top: 9px; width: 3px; height: 3px; border-radius: 50%; background: color-mix(in oklab, var(--metal) 50%, light-dark(#fbfcfd, #121417)); }
@keyframes loader-tape-measure-run {
0%, 8% { transform: translateX(-144px); animation-timing-function: cubic-bezier(0.2, 0.7, 0.3, 1); }
42%, 68% { transform: translateX(0); animation-timing-function: cubic-bezier(0.65, 0, 0.95, 0.55); }
75%, 100% { transform: translateX(-144px); }
}
@keyframes loader-tape-measure-sag {
0%, 40% { transform: rotate(0deg); }
47% { transform: rotate(5deg); }
52% { transform: rotate(2.2deg); }
57% { transform: rotate(4.2deg); }
62%, 68% { transform: rotate(3.5deg); animation-timing-function: ease-in; }
75%, 100% { transform: rotate(0deg); }
}
@keyframes loader-tape-measure-kick {
0%, 75% { transform: none; }
78% { transform: translateX(-4px) rotate(-8deg); }
82% { transform: translateX(1px) rotate(3deg); }
86% { transform: rotate(-1.2deg); }
90%, 100% { transform: none; }
}
@media (prefers-reduced-motion: reduce) {
.loader-tape-measure *, .loader-tape-measure__body { animation: none !important; }
.loader-tape-measure__blade { transform: translateX(-40px); }
}Goal: Create a "Tape Measure" loader.
Visual behavior: A tape measure runs its blade out, sags and wobbles at full length, then snaps back so the case jumps.
Technique:
- Pure CSS: every motion is a keyframe animation, so the plain HTML version needs no script.
- The blade slides out of a track clipped at the case mouth, so inside the case it is hidden and only the length that is out shows.
- At full length the track bends down about the mouth and settles, the way a long blade droops under its own weight.
- The return is fast and lands with a kick: the case jolts back and rocks before the next run.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside it is aria-hidden. Under prefers-reduced-motion the blade rests most of the way out.
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 "Tape Measure" loader.
Visual behavior: A tape measure runs its blade out, sags and wobbles at full length, then snaps back so the case jumps.
Technique:
- Pure CSS: every motion is a keyframe animation, so the plain HTML version needs no script.
- The blade slides out of a track clipped at the case mouth, so inside the case it is hidden and only the length that is out shows.
- At full length the track bends down about the mouth and settles, the way a long blade droops under its own weight.
- The return is fast and lands with a kick: the case jolts back and rocks before the next run.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside it is aria-hidden. Under prefers-reduced-motion the blade rests most of the way out.
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 tape measure runs its blade out, sags and wobbles at full length, then snaps back so the case jumps.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- continuous, loop, spring
- Techniques
- keyframes, clip path, 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 it is aria-hidden. Under prefers-reduced-motion the blade rests most of the way out.