Tunnel
React + Tailwind
const css = `
@keyframes loader-tunnel-rush {
0% { transform: scale(0.04) rotate(0deg); opacity: 0; }
20% { opacity: 1; }
100% { transform: scale(1.5) rotate(90deg); opacity: 0; }
}
.loader-tunnel {
position: relative;
width: 104px;
height: 104px;
border-radius: 18px;
overflow: hidden;
background: radial-gradient(circle, light-dark(#fbfcfd, #121417) 0 20%, light-dark(#f3f4f7, #1a1c1f));
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
}
.loader-tunnel__frame {
position: absolute;
inset: 10px;
border: 1.5px solid light-dark(#42454c, #e5e6e9);
border-radius: 14px;
opacity: 0;
/* ease-in: frames creep out of the distance and rush past at the end, as perspective does. */
animation: loader-tunnel-rush 2.4s cubic-bezier(0.7, 0, 0.9, 0.6) var(--d) infinite;
}
.loader-tunnel__light {
position: absolute;
left: 50%;
top: 50%;
width: 6px;
height: 6px;
margin: -3px;
border-radius: 50%;
background: light-dark(#373a40, #f1f2f3);
box-shadow: 0 0 12px 4px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 45%, transparent);
}
@media (prefers-reduced-motion: reduce) { .loader-tunnel__frame { animation-duration: 9s; } }
`;
const FRAMES = 8;
export function TunnelLoader() {
return (
<>
<style>
{css}
</style>
<div className="loader-tunnel" role="status" aria-label="Loading">
<span className="loader-tunnel__light" aria-hidden="true" />
{Array.from({ length: FRAMES }, (_, i) => (
<span
key={i}
className="loader-tunnel__frame"
aria-hidden="true"
style={{ '--d': `${(-(i / FRAMES) * 2.4).toFixed(2)}s` } as React.CSSProperties}
/>
))}
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<div class="loader-tunnel" role="status" aria-label="Loading">
<span class="loader-tunnel__light" aria-hidden="true"></span>
<span class="loader-tunnel__frame" aria-hidden="true" style="--d:0.00s"></span>
<span class="loader-tunnel__frame" aria-hidden="true" style="--d:-0.30s"></span>
<span class="loader-tunnel__frame" aria-hidden="true" style="--d:-0.60s"></span>
<span class="loader-tunnel__frame" aria-hidden="true" style="--d:-0.90s"></span>
<span class="loader-tunnel__frame" aria-hidden="true" style="--d:-1.20s"></span>
<span class="loader-tunnel__frame" aria-hidden="true" style="--d:-1.50s"></span>
<span class="loader-tunnel__frame" aria-hidden="true" style="--d:-1.80s"></span>
<span class="loader-tunnel__frame" aria-hidden="true" style="--d:-2.10s"></span>
</div>CSS
@keyframes loader-tunnel-rush {
0% { transform: scale(0.04) rotate(0deg); opacity: 0; }
20% { opacity: 1; }
100% { transform: scale(1.5) rotate(90deg); opacity: 0; }
}
.loader-tunnel {
position: relative;
width: 104px;
height: 104px;
border-radius: 18px;
overflow: hidden;
background: radial-gradient(circle, light-dark(#fbfcfd, #121417) 0 20%, light-dark(#f3f4f7, #1a1c1f));
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
}
.loader-tunnel__frame {
position: absolute;
inset: 10px;
border: 1.5px solid light-dark(#42454c, #e5e6e9);
border-radius: 14px;
opacity: 0;
/* ease-in: frames creep out of the distance and rush past at the end, as perspective does. */
animation: loader-tunnel-rush 2.4s cubic-bezier(0.7, 0, 0.9, 0.6) var(--d) infinite;
}
.loader-tunnel__light {
position: absolute;
left: 50%;
top: 50%;
width: 6px;
height: 6px;
margin: -3px;
border-radius: 50%;
background: light-dark(#373a40, #f1f2f3);
box-shadow: 0 0 12px 4px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 45%, transparent);
}
@media (prefers-reduced-motion: reduce) { .loader-tunnel__frame { animation-duration: 9s; } }Goal: Create a "Tunnel" loader.
Visual behavior: Rounded frames rush out of a point of light at the centre, twisting as they come, like flying down a wormhole.
Technique:
- Eight rounded square outlines share one keyframe: scale(0.04) to scale(1.5) while turning 90deg and fading in, then out.
- Each frame has a negative delay of 1/8 of the cycle, so they are evenly spaced down the tunnel.
- A steep ease-in makes frames creep out of the distance and rush past at the end, which reads as perspective; a glowing dot marks the vanishing point.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the frames travel much more slowly.
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 "Tunnel" loader.
Visual behavior: Rounded frames rush out of a point of light at the centre, twisting as they come, like flying down a wormhole.
Technique:
- Eight rounded square outlines share one keyframe: scale(0.04) to scale(1.5) while turning 90deg and fading in, then out.
- Each frame has a negative delay of 1/8 of the cycle, so they are evenly spaced down the tunnel.
- A steep ease-in makes frames creep out of the distance and rush past at the end, which reads as perspective; a glowing dot marks the vanishing point.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the frames travel much more slowly.
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
Rounded frames rush out of a point of light at the centre, twisting as they come, like flying down a wormhole.
- Dependencies
- None
- Difficulty
- Beginner
- Triggers
- continuous, loop, 3d
- Techniques
- keyframes, css variables, radial gradient
- 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 frames travel much more slowly.