Window
import { useState } from 'react';
const css = `
@keyframes slider-window-flap { from { transform: rotate(-38deg); } to { transform: rotate(6deg); } }
@keyframes slider-window-wind { from { transform: translateX(30px); opacity: 0; } 40% { opacity: 0.8; } to { transform: translateX(-50px); opacity: 0; } }
.slider-window { display: grid; gap: 12px; width: 230px; }
.slider-window__door { position: relative; height: 128px; border-radius: 14px 30px 14px 14px; background: linear-gradient(light-dark(#dbdde1, #2e3035), light-dark(#ebedf0, #232529)); box-shadow: inset 0 1px 0 light-dark(#cbced3, #393c41), 0 12px 24px -14px light-dark(rgba(0, 0, 0, 0.21), rgba(0, 0, 0, 0.7)); }
.slider-window__door::after { content: ''; position: absolute; left: 26px; bottom: 26px; width: 34px; height: 7px; border-radius: 4px; background: light-dark(#6d7179, #7d8087); box-shadow: inset 0 1px 0 light-dark(#5f626a, #a7abb2); }
.slider-window__opening { position: absolute; left: 14px; right: 14px; top: 12px; height: 72px; overflow: hidden; background: light-dark(#fbfcfd, #121417); clip-path: polygon(0 100%, 0 12%, 6% 0, 72% 0, 100% 100%); }
.slider-window__wind { position: absolute; height: 2px; border-radius: 1px; background: light-dark(#5f626a, #a7abb2); opacity: 0; }
.slider-window[data-open] .slider-window__wind { animation: slider-window-wind 0.8s linear infinite; }
.slider-window__dog { position: absolute; left: 50px; bottom: -8px; width: 70px; height: 62px; transform: translateY(calc((1 - clamp(0, calc((var(--slider-window-v) - 0.35) * 3), 1)) * 64px)); transition: transform 0.5s cubic-bezier(0.3, 1.5, 0.5, 1); }
.slider-window__dog svg { width: 100%; height: 100%; overflow: visible; }
.slider-window__head { fill: light-dark(#5f626a, #a7abb2); }
.slider-window__snout { fill: light-dark(#50535b, #c6c8cd); }
.slider-window__dark { fill: light-dark(#fbfcfd, #121417); }
.slider-window__ear { fill: light-dark(#cbced3, #393c41); transform-box: fill-box; transform-origin: 80% 10%; }
.slider-window__ear--r { transform-origin: 20% 10%; }
.slider-window[data-breeze] .slider-window__ear { animation: slider-window-flap 0.16s ease-in-out infinite alternate; }
.slider-window[data-breeze] .slider-window__ear--r { animation-delay: -0.08s; }
.slider-window__tongue { fill: light-dark(#42454c, #e5e6e9); }
.slider-window__glass {
position: absolute;
inset: 0;
background: linear-gradient(115deg, color-mix(in oklab, light-dark(#50535b, #c6c8cd) 30%, transparent) 0 30%, color-mix(in oklab, light-dark(#42454c, #e5e6e9) 45%, transparent) 30% 36%, color-mix(in oklab, light-dark(#50535b, #c6c8cd) 30%, transparent) 36%);
box-shadow: inset 0 2px 0 light-dark(#5f626a, #a7abb2);
transform: translateY(calc(var(--slider-window-v) * 100%));
transition: transform 0.35s cubic-bezier(0.3, 1.1, 0.5, 1);
}
.slider-window__track { position: relative; height: 24px; }
.slider-window__rail { position: absolute; left: 0; right: 0; top: 10px; height: 4px; border-radius: 2px; background: light-dark(#ebedf0, #232529); }
.slider-window__fill { position: absolute; left: 0; top: 10px; height: 4px; border-radius: 2px; width: calc(9px + var(--slider-window-v) * (100% - 18px)); background: light-dark(#50535b, #c6c8cd); }
.slider-window__thumb { position: absolute; top: 3px; left: calc(var(--slider-window-v) * (100% - 18px)); width: 18px; height: 18px; border-radius: 50%; background: light-dark(#373a40, #f1f2f3); box-shadow: 0 2px 6px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.4)); }
.slider-window:has(.slider-window__input:focus-visible) .slider-window__thumb { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; }
.slider-window__input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; -webkit-appearance: none; appearance: none; background: none; }
.slider-window__input::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; }
.slider-window__input::-moz-range-thumb { width: 18px; height: 18px; border: 0; }
@media (prefers-reduced-motion: reduce) {
.slider-window * { animation: none !important; transition-duration: 0.01s !important; }
}
`;
export function WindowSlider() {
const [v, setV] = useState(20);
return (
<>
<style>
{css}
</style>
<div
className="slider-window"
data-open={v >= 50 ? '' : undefined}
data-breeze={v >= 75 ? '' : undefined}
style={{ '--slider-window-v': v / 100 } as React.CSSProperties}
>
<span className="slider-window__door" aria-hidden="true">
<span className="slider-window__opening">
<span className="slider-window__wind" style={{ left: 120, top: 18, width: 26 }} />
<span className="slider-window__wind" style={{ left: 140, top: 32, width: 18, animationDelay: '-0.3s' }} />
<span className="slider-window__wind" style={{ left: 110, top: 46, width: 30, animationDelay: '-0.55s' }} />
<span className="slider-window__dog">
<svg viewBox="0 0 70 62">
<path className="slider-window__ear" d="M14 10C4 12 0 28 6 36C10 30 14 22 20 16Z" />
<path className="slider-window__ear slider-window__ear--r" d="M56 10C66 12 70 28 64 36C60 30 56 22 50 16Z" />
<ellipse className="slider-window__head" cx="35" cy="30" rx="22" ry="24" />
<ellipse className="slider-window__snout" cx="35" cy="42" rx="12" ry="9" />
<ellipse className="slider-window__dark" cx="35" cy="37" rx="4.5" ry="3.2" />
<circle className="slider-window__dark" cx="26" cy="26" r="2.6" />
<circle className="slider-window__dark" cx="44" cy="26" r="2.6" />
<path className="slider-window__tongue" d="M31 48h8v6a4 4 0 0 1-8 0Z" />
</svg>
</span>
<span className="slider-window__glass" />
</span>
</span>
<div className="slider-window__track">
<span className="slider-window__rail" />
<span className="slider-window__fill" />
<span className="slider-window__thumb" />
<input className="slider-window__input" type="range" min={0} max={100} value={v} onChange={(e) => setV(+e.target.value)} aria-label="Window" aria-valuetext={`${v} percent open`} />
</div>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initSliderWindow(el) on the mounted element instead.
<div class="slider-window" style="--slider-window-v:0.2">
<span class="slider-window__door" aria-hidden="true">
<span class="slider-window__opening">
<span class="slider-window__wind" style="left:120px;top:18px;width:26px"></span>
<span class="slider-window__wind" style="left:140px;top:32px;width:18px;animation-delay:-0.3s"></span>
<span class="slider-window__wind" style="left:110px;top:46px;width:30px;animation-delay:-0.55s"></span>
<span class="slider-window__dog">
<svg viewBox="0 0 70 62">
<path class="slider-window__ear" d="M14 10C4 12 0 28 6 36C10 30 14 22 20 16Z"></path>
<path class="slider-window__ear slider-window__ear--r" d="M56 10C66 12 70 28 64 36C60 30 56 22 50 16Z"></path>
<ellipse class="slider-window__head" cx="35" cy="30" rx="22" ry="24"></ellipse>
<ellipse class="slider-window__snout" cx="35" cy="42" rx="12" ry="9"></ellipse>
<ellipse class="slider-window__dark" cx="35" cy="37" rx="4.5" ry="3.2"></ellipse>
<circle class="slider-window__dark" cx="26" cy="26" r="2.6"></circle>
<circle class="slider-window__dark" cx="44" cy="26" r="2.6"></circle>
<path class="slider-window__tongue" d="M31 48h8v6a4 4 0 0 1-8 0Z"></path>
</svg>
</span>
<span class="slider-window__glass"></span>
</span>
</span>
<div class="slider-window__track">
<span class="slider-window__rail"></span>
<span class="slider-window__fill"></span>
<span class="slider-window__thumb"></span>
<input class="slider-window__input" type="range" min="0" max="100" aria-label="Window" aria-valuetext="20 percent open" value="20"/>
</div>
</div>@keyframes slider-window-flap { from { transform: rotate(-38deg); } to { transform: rotate(6deg); } }
@keyframes slider-window-wind { from { transform: translateX(30px); opacity: 0; } 40% { opacity: 0.8; } to { transform: translateX(-50px); opacity: 0; } }
.slider-window { display: grid; gap: 12px; width: 230px; }
.slider-window__door { position: relative; height: 128px; border-radius: 14px 30px 14px 14px; background: linear-gradient(light-dark(#dbdde1, #2e3035), light-dark(#ebedf0, #232529)); box-shadow: inset 0 1px 0 light-dark(#cbced3, #393c41), 0 12px 24px -14px light-dark(rgba(0, 0, 0, 0.21), rgba(0, 0, 0, 0.7)); }
.slider-window__door::after { content: ''; position: absolute; left: 26px; bottom: 26px; width: 34px; height: 7px; border-radius: 4px; background: light-dark(#6d7179, #7d8087); box-shadow: inset 0 1px 0 light-dark(#5f626a, #a7abb2); }
.slider-window__opening { position: absolute; left: 14px; right: 14px; top: 12px; height: 72px; overflow: hidden; background: light-dark(#fbfcfd, #121417); clip-path: polygon(0 100%, 0 12%, 6% 0, 72% 0, 100% 100%); }
.slider-window__wind { position: absolute; height: 2px; border-radius: 1px; background: light-dark(#5f626a, #a7abb2); opacity: 0; }
.slider-window[data-open] .slider-window__wind { animation: slider-window-wind 0.8s linear infinite; }
.slider-window__dog { position: absolute; left: 50px; bottom: -8px; width: 70px; height: 62px; transform: translateY(calc((1 - clamp(0, calc((var(--slider-window-v) - 0.35) * 3), 1)) * 64px)); transition: transform 0.5s cubic-bezier(0.3, 1.5, 0.5, 1); }
.slider-window__dog svg { width: 100%; height: 100%; overflow: visible; }
.slider-window__head { fill: light-dark(#5f626a, #a7abb2); }
.slider-window__snout { fill: light-dark(#50535b, #c6c8cd); }
.slider-window__dark { fill: light-dark(#fbfcfd, #121417); }
.slider-window__ear { fill: light-dark(#cbced3, #393c41); transform-box: fill-box; transform-origin: 80% 10%; }
.slider-window__ear--r { transform-origin: 20% 10%; }
.slider-window[data-breeze] .slider-window__ear { animation: slider-window-flap 0.16s ease-in-out infinite alternate; }
.slider-window[data-breeze] .slider-window__ear--r { animation-delay: -0.08s; }
.slider-window__tongue { fill: light-dark(#42454c, #e5e6e9); }
.slider-window__glass {
position: absolute;
inset: 0;
background: linear-gradient(115deg, color-mix(in oklab, light-dark(#50535b, #c6c8cd) 30%, transparent) 0 30%, color-mix(in oklab, light-dark(#42454c, #e5e6e9) 45%, transparent) 30% 36%, color-mix(in oklab, light-dark(#50535b, #c6c8cd) 30%, transparent) 36%);
box-shadow: inset 0 2px 0 light-dark(#5f626a, #a7abb2);
transform: translateY(calc(var(--slider-window-v) * 100%));
transition: transform 0.35s cubic-bezier(0.3, 1.1, 0.5, 1);
}
.slider-window__track { position: relative; height: 24px; }
.slider-window__rail { position: absolute; left: 0; right: 0; top: 10px; height: 4px; border-radius: 2px; background: light-dark(#ebedf0, #232529); }
.slider-window__fill { position: absolute; left: 0; top: 10px; height: 4px; border-radius: 2px; width: calc(9px + var(--slider-window-v) * (100% - 18px)); background: light-dark(#50535b, #c6c8cd); }
.slider-window__thumb { position: absolute; top: 3px; left: calc(var(--slider-window-v) * (100% - 18px)); width: 18px; height: 18px; border-radius: 50%; background: light-dark(#373a40, #f1f2f3); box-shadow: 0 2px 6px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.4)); }
.slider-window:has(.slider-window__input:focus-visible) .slider-window__thumb { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; }
.slider-window__input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; -webkit-appearance: none; appearance: none; background: none; }
.slider-window__input::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; }
.slider-window__input::-moz-range-thumb { width: 18px; height: 18px; border: 0; }
@media (prefers-reduced-motion: reduce) {
.slider-window * { animation: none !important; transition-duration: 0.01s !important; }
}function initSliderWindow(root) {
var input = root.querySelector('.slider-window__input');
function update() {
var v = +input.value;
root.style.setProperty('--slider-window-v', v / 100);
// The wind starts halfway down; the ears flap from three quarters.
root.toggleAttribute('data-open', v >= 50);
root.toggleAttribute('data-breeze', v >= 75);
input.setAttribute('aria-valuetext', v + ' percent open');
}
input.addEventListener('input', update);
update();
return function () { input.removeEventListener('input', update); };
}
document.querySelectorAll('.slider-window').forEach(initSliderWindow);Goal: Create a "Window" slider.
Visual behavior: A car window slider: the glass rolls down, a dog pokes its head out past a third of the way, wind starts to stream by halfway and near the bottom its ears flap.
Technique:
- A real <input type="range"> lies over the drawing, transparent (opacity 0, appearance none), with its thumb hit area sized to the drawn thumb, so dragging, clicking the track and the arrow, Page Up/Down, Home and End keys all work natively. The drawing is aria-hidden.
- The value is written to one CSS custom property (0 to 1) on the root; the glass is translated down by it and the dog rises into the gap once it passes 35%.
- Two data attributes on the root, data-open from 50% and data-breeze from 75%, start the wind lines and the ear-flap keyframes, so the scene stays still while the window is closed.
- :has(input:focus-visible) outlines the drawn thumb, so keyboard focus is visible though the input itself is not.
Accessibility: A native range input with an aria-label and an aria-valuetext in words, so a screen reader announces the reading rather than a bare number; keyboard focus outlines the drawn thumb. Under prefers-reduced-motion the wind and the ears stop and the glass moves without easing.
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 "Window" slider.
Visual behavior: A car window slider: the glass rolls down, a dog pokes its head out past a third of the way, wind starts to stream by halfway and near the bottom its ears flap.
Technique:
- A real <input type="range"> lies over the drawing, transparent (opacity 0, appearance none), with its thumb hit area sized to the drawn thumb, so dragging, clicking the track and the arrow, Page Up/Down, Home and End keys all work natively. The drawing is aria-hidden.
- The value is written to one CSS custom property (0 to 1) on the root; the glass is translated down by it and the dog rises into the gap once it passes 35%.
- Two data attributes on the root, data-open from 50% and data-breeze from 75%, start the wind lines and the ear-flap keyframes, so the scene stays still while the window is closed.
- :has(input:focus-visible) outlines the drawn thumb, so keyboard focus is visible though the input itself is not.
Accessibility: A native range input with an aria-label and an aria-valuetext in words, so a screen reader announces the reading rather than a bare number; keyboard focus outlines the drawn thumb. Under prefers-reduced-motion the wind and the ears stop and the glass moves without easing.
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 car window slider: the glass rolls down, a dog pokes its head out past a third of the way, wind starts to stream by halfway and near the bottom its ears flap.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- state change, loop, continuous
- Techniques
- css variables, keyframes, svg
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A native range input with an aria-label and an aria-valuetext in words, so a screen reader announces the reading rather than a bare number; keyboard focus outlines the drawn thumb. Under prefers-reduced-motion the wind and the ears stop and the glass moves without easing.