Window

No dependencies
Code format
React + Tailwind
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>
    </>
  );
}

Free to use in any project, commercial included. MIT-0, no attribution required. Licence

Install with shadcn

npx shadcn@latest add @movin/slider-window
Source on GitHub

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.

More sliders

See all
Windsock
Zipper
Balance
Bass