Runway LoaderAn airplane taxis down a runway progress bar, nose tilting up and taking off at 100%.
WidgetsOnboardingLoading stateEmpty state
Switch to light
Refresh
Full screen
0%
Preparing for takeoff
Loading source…
Add to your project
One command adds this component to your project.
1
Run the following command. New project? Run npx shadcn@latest init first to set up Tailwind and path aliases.
npx shadcn@latest add @aicanvas/runway-loader
2
For dark mode, add the dark class to your <html> element:
Optional
<html class="dark">
Install with AI Canvas MCP
With AI Canvas MCP, your AI knows every component we ship. Ask for one inside Claude Code, Codex, or Cursor and it installs the component you pick. Works with any AI Canvas account, free or premium.
Runway Loader is a progress bar with an airplane taxiing along it: as the load advances the plane rolls forward, and at 100% the nose lifts and it takes off. Motion handles both the linear taxi and the rotation-on-liftoff so the whole sequence reads as one continuous gesture. It is a lighthearted choice for onboarding flows, file imports, deploy pipelines, or any wait that benefits from a small narrative payoff at the end.
Built with
MotionTailwind CSS
Frequently asked questions
Is Runway Loader free to use?
Yes. Runway Loader is part of the free AI Canvas library and is open source under the MIT license, so you can use it in personal and commercial projects.
How do I install Runway Loader?
One command: npx shadcn@latest add https://aicanvas.me/r/runway-loader.json, free with a free AI Canvas account. Or connect the AI Canvas MCP server and ask your AI editor to install Runway Loader for you. You can also copy the source straight from the Code tab, no account needed. It works in any React project with Tailwind CSS.
What is Runway Loader built with?
Runway Loader is built with React and TypeScript, using Motion and Tailwind CSS. It ships with both light and dark styling.
Where would I use Runway Loader?
Common uses include Onboarding, Loading state, and Empty state. Like every AI Canvas component, it is self-contained and drops into any React project.
Can I remix Runway Loader with AI?
Yes. Runway Loader ships with one comprehensive AI prompt written against the real source code. Open "Remix with AI" on this page to read and copy it into Claude, Cursor, ChatGPT, or any AI tool. Prompts are for remixing your own variation; for the exact component, install it with the one-command CLI.
One comprehensive prompt, written against the real source code. Works in Claude, Cursor, ChatGPT, or any AI tool you use.
This prompt is for remixing. Use it to build your own variation of Runway Loader. Results depend on the model you use, and no prompt in the world is 100% exact.
Want the exact component?
One command installs it, pixel-perfect. Copy, paste into your project, done.
npx shadcn@latest add @aicanvas/runway-loader
AI prompt for Runway Loader
Before building, verify your project has the following setup:
- React / Next.js
- TypeScript
- Tailwind CSS v4
If any are missing, set up via the shadcn CLI:
npx shadcn@latest init
---
Create a React client component named `RunwayLoader`: a playful progress indicator shaped like a runway. A top-down SVG airplane taxis left→right along a rounded horizontal track while a red fill grows behind it. When the plane nose touches the right wall it "takes off" (scales + slides off-screen + fades), the bar completes, then the whole thing resets and loops. Numeric % and label read out below. Supports light + dark themes.
Write this as a single self-contained React client component. Inline everything. Do not extract helper hooks, utility functions, or separate files. 'use client' at the top. No 'any' types. Define the Airplane SVG as a plain inner function component in the same file (not an exported helper).
Imports: useEffect, useRef, useState from 'react'; motion, AnimatePresence from 'framer-motion'.
Airplane({isDark}): <svg className="w-[52px] sm:w-[72px] h-auto" viewBox="0 0 72 46" fill="none" style={{filter: isDark ? 'drop-shadow(0 4px 12px rgba(0,0,0,0.75)) drop-shadow(0 2px 4px rgba(0,0,0,0.5))' : 'drop-shadow(0 2px 8px rgba(0,0,0,0.35)) drop-shadow(0 1px 3px rgba(0,0,0,0.2))'}}>
Colors (dark vs light): bodyTop '#ececec'/'#c8c8c8', bodyMid '#ffffff'/'#dedede', bodyBot '#d0d0d0'/'#b4b4b4', wingLight '#f8f8f8'/'#d8d8d8', wingDark '#bababa'/'#909090', tail '#c8c8c8'/'#a8a8a8', nose '#f4f4f4'/'#d8d8d8'.
defs: linearGradient id="rl-body" x1=0 y1=0 x2=0 y2=1 stops 0% bodyTop, 40% bodyMid, 100% bodyBot. "rl-wing-t" x1=0 y1=1 x2=1 y2=0 stops wingLight→wingDark. "rl-wing-b" x1=0 y1=0 x2=1 y2=1 stops wingLight→wingDark.
Paths:
- Top wing: <path d="M44,21 L28,21 L8,2 L18,0 Z" fill="url(#rl-wing-t)"/>
- Bottom wing: <path d="M44,25 L28,25 L8,44 L18,46 Z" fill="url(#rl-wing-b)"/>
- Tail stabilisers: <path d="M8,21 L2,14 L6,14 L12,21 Z" fill={tail}/> and <path d="M8,25 L2,32 L6,32 L12,25 Z" fill={tail}/>
- Fuselage: <ellipse cx=34 cy=23 rx=29 ry=7 fill="url(#rl-body)"/>
- Nose: <path d="M61,19.5 C66,21.5 67.5,23 67,23 C67.5,23 66,24.5 61,26.5 L58,23 Z" fill={nose}/>
- Cockpit window: <ellipse cx=57 cy=23 rx=4 ry=3 fill="#7ec8e8" opacity="0.9"/>
Type Phase = 'taxiing' | 'takeoff' | 'resetting'.
State/refs in RunwayLoader:
- containerRef<HTMLDivElement>
- [isDark, setIsDark] default true
- [progress, setProgress] number starting 0
- [phase, setPhase]<Phase> default 'taxiing'
- [cycle, setCycle] number 0
- rafRef number 0, startRef undefined, aliveRef true, disappearedRef false
- const WALL_PCT = 91
Theme effect: same closest [data-card-theme] / html fallback pattern. MutationObserver on both, cleanup disconnect.
Animation effect (runs once):
- aliveRef.current=true; disappearedRef.current=false; TAXI_MS=5500.
- tick(ts): if !aliveRef return. if !startRef set startRef=ts. t=min((ts-startRef)/TAXI_MS, 1). eased = t<=0.75 ? (t/0.75)*82 : 82 + (1 - (1-(t-0.75)/0.25)^2)*18. p=min(eased,100). setProgress(p). If p>=WALL_PCT && !disappearedRef: disappearedRef=true; setPhase('takeoff'). If p<100 raf=requestAnimationFrame(tick); else setTimeout 600ms: setPhase('resetting'), setTimeout 400ms: disappearedRef=false, startRef=undefined, setProgress(0), setPhase('taxiing'), setCycle(c=>c+1), raf=requestAnimationFrame(tick).
- Initial: rafRef=requestAnimationFrame(tick). Cleanup: aliveRef=false; cancelAnimationFrame(rafRef).
planePct = min(progress, WALL_PCT).
Theme-responsive track styles:
- trackBg dark: 'linear-gradient(to bottom, #404040, #252525)'; light: 'linear-gradient(to bottom, #bab7b2, #a09d98)'
- trackShadow dark: 'inset 0 4px 12px rgba(0,0,0,0.85), inset 0 -1px 3px rgba(255,255,255,0.04), 0 2px 8px rgba(0,0,0,0.5)'; light: 'inset 0 3px 8px rgba(0,0,0,0.22), inset 0 -1px 3px rgba(255,255,255,0.28), 0 2px 5px rgba(0,0,0,0.12)'
- dashColor dark 'rgba(255,255,255,0.22)' light 'rgba(255,255,255,0.42)'
JSX:
<div ref={containerRef} className="flex h-full w-full items-center justify-center" style={{background: isDark?'#110F0C':'#F5F1EA'}}>
<div className="flex w-full max-w-[440px] flex-col items-center gap-4 px-4 sm:gap-6 sm:px-6">
<div className="relative h-11 w-full overflow-visible rounded-full sm:h-14" style={{background:trackBg, boxShadow:trackShadow}}>
{/* Centreline dashes */}
<div className="pointer-events-none absolute top-1/2 -translate-y-px" style={{ left:18, right:18, height:2, backgroundImage: `repeating-linear-gradient(to right, ${dashColor} 0, ${dashColor} 10px, transparent 10px, transparent 24px)` }}/>
{/* Red fill on top */}
<div className="absolute inset-y-0 left-0 rounded-full" style={{ width: `${progress}%`, background:'linear-gradient(to right, #6b0000, #be1c1c 50%, #e03030)', boxShadow:'inset 0 1px 3px rgba(255,255,255,0.1)' }}/>
{/* Heat shimmer exhaust — only when taxiing & progress>30 */}
{phase === 'taxiing' && progress > 30 && (
<motion.div className="pointer-events-none absolute top-1/2 -translate-y-1/2 rounded-full" style={{ right: `calc(${100 - planePct}% + 26px)`, width: Math.min(((progress-30)/70)*56, 56), height: 22, background:'radial-gradient(ellipse at right, rgba(255,140,20,0.3), transparent)', filter:'blur(8px)' }} animate={{opacity:[0.3,0.75,0.2,0.65,0.3]}} transition={{duration:0.2, repeat:Infinity}}/>
)}
<AnimatePresence>
{phase !== 'resetting' && (
<div key={`plane-${cycle}`} className="pointer-events-none absolute top-1/2 z-20" style={{ left: `${planePct}%`, transform:'translateX(-50%) translateY(-50%)' }}>
{phase === 'taxiing'
? <Airplane isDark={isDark}/>
: <motion.div initial={{opacity:1,x:0,scale:1}} animate={{opacity:0,x:120,scale:1.8}} transition={{duration:0.7,ease:'easeIn'}}><Airplane isDark={isDark}/></motion.div>
}
</div>
)}
</AnimatePresence>
</div>
<div className="flex flex-col items-center gap-1">
<div className="flex items-baseline gap-1">
<span className="text-2xl font-bold tabular-nums sm:text-[28px]" style={{color: isDark?'#FAF7F2':'#1C1916'}}>{Math.round(progress)}</span>
<span className="text-sm font-semibold sm:text-base" style={{color:'#9E9890'}}>%</span>
</div>
<span className="text-[10px] font-semibold uppercase tracking-widest sm:text-[11px]" style={{color: isDark?'#736D65':'#9E9890'}}>
{phase === 'takeoff' ? 'Taking off!' : phase === 'resetting' ? '—' : 'Preparing for takeoff'}
</span>
</div>
</div>
</div>
## Typography
- Font: project default sans-serif
- Sizes: 10px, 11px, 14px, 16px, 24px, 28px
- Weights: 600, 700