Telescope

No dependencies
Code format
React + Tailwind
import { useState } from 'react';

const css = `
  .slider-telescope { display: grid; gap: 12px; width: 240px; }
  .slider-telescope__row { display: flex; align-items: center; gap: 14px; }
  .slider-telescope__scope { position: relative; width: 122px; height: 100px; }
  .slider-telescope__arm { position: absolute; left: 6px; top: 38px; width: 120px; height: 22px; transform: rotate(-18deg); transform-origin: 20px 50%; }
  .slider-telescope__tube { position: absolute; top: 50%; border-radius: 3px; box-shadow: inset 0 1px 0 light-dark(#5f626a, #a7abb2); transition: transform 0.45s cubic-bezier(0.3, 1.3, 0.5, 1); }
  .slider-telescope__tube--3 { left: 46px; width: 40px; height: 10px; margin-top: -5px; background: light-dark(#6d7179, #7d8087); transform: translateX(calc(var(--slider-telescope-v) * 34px)); }
  .slider-telescope__tube--2 { left: 30px; width: 44px; height: 14px; margin-top: -7px; background: light-dark(#5f626a, #a7abb2); transform: translateX(calc(var(--slider-telescope-v) * 18px)); }
  .slider-telescope__tube--1 { left: 8px; width: 46px; height: 20px; margin-top: -10px; background: light-dark(#50535b, #c6c8cd); }
  .slider-telescope__eye { position: absolute; left: 0; top: 50%; width: 10px; height: 8px; margin-top: -4px; border-radius: 2px; background: light-dark(#cbced3, #393c41); }
  .slider-telescope__leg { position: absolute; top: 52px; left: 26px; width: 3px; height: 46px; border-radius: 2px; background: light-dark(#cbced3, #393c41); transform-origin: 50% 0; }
  .slider-telescope__view { position: relative; flex: none; width: 104px; height: 104px; overflow: hidden; border-radius: 50%; background: light-dark(#fbfcfd, #121417); box-shadow: inset 0 0 0 3px light-dark(#dbdde1, #2e3035), inset 0 0 18px 6px light-dark(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.6)); }
  .slider-telescope__moon {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 34px;
    height: 34px;
    margin: -17px;
    border-radius: 50%;
    background:
      radial-gradient(circle at 30% 35%, light-dark(#5f626a, #a7abb2) 0 3px, transparent 3.5px),
      radial-gradient(circle at 62% 60%, light-dark(#5f626a, #a7abb2) 0 4.5px, transparent 5px),
      radial-gradient(circle at 70% 28%, light-dark(#5f626a, #a7abb2) 0 2px, transparent 2.5px),
      radial-gradient(circle at 38% 72%, light-dark(#5f626a, #a7abb2) 0 2.5px, transparent 3px),
      radial-gradient(circle at 35% 30%, light-dark(#373a40, #f1f2f3), light-dark(#50535b, #c6c8cd));
    box-shadow: 0 0 14px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 35%, transparent);
    transform: scale(calc(1 + var(--slider-telescope-v) * 2.6));
    transition: transform 0.45s cubic-bezier(0.3, 1.3, 0.5, 1);
  }
  .slider-telescope__cross { position: absolute; inset: 0; background: linear-gradient(light-dark(#6d7179, #7d8087), light-dark(#6d7179, #7d8087)) 50% 50% / 1px 100% no-repeat, linear-gradient(light-dark(#6d7179, #7d8087), light-dark(#6d7179, #7d8087)) 50% 50% / 100% 1px no-repeat; opacity: 0.45; }
  .slider-telescope__mag { position: absolute; right: 50%; bottom: 10px; transform: translateX(50%); color: light-dark(#42454c, #e5e6e9); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 9px; }
  .slider-telescope__track { position: relative; height: 24px; }
  .slider-telescope__rail { position: absolute; left: 0; right: 0; top: 10px; height: 4px; border-radius: 2px; background: light-dark(#ebedf0, #232529); }
  .slider-telescope__fill { position: absolute; left: 0; top: 10px; height: 4px; border-radius: 2px; width: calc(9px + var(--slider-telescope-v) * (100% - 18px)); background: light-dark(#50535b, #c6c8cd); }
  .slider-telescope__thumb { position: absolute; top: 3px; left: calc(var(--slider-telescope-v) * (100% - 18px)); width: 18px; height: 18px; border-radius: 50%; background: light-dark(#373a40, #f1f2f3); box-shadow: inset 0 0 0 4px light-dark(#50535b, #c6c8cd), 0 2px 6px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.4)); }
  .slider-telescope:has(.slider-telescope__input:focus-visible) .slider-telescope__thumb { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; }
  .slider-telescope__input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; -webkit-appearance: none; appearance: none; background: none; }
  .slider-telescope__input::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; }
  .slider-telescope__input::-moz-range-thumb { width: 18px; height: 18px; border: 0; }
  @media (prefers-reduced-motion: reduce) {
    .slider-telescope * { transition-duration: 0.01s !important; }
  }
`;

export function TelescopeSlider() {
  const [v, setV] = useState(30);
  const mag = Math.round(10 + (v / 100) * 140);
  return (
    <>
      <style>
        {css}
      </style>
      <div className="slider-telescope" style={{ '--slider-telescope-v': v / 100 } as React.CSSProperties}>
        <div className="slider-telescope__row" aria-hidden="true">
          <span className="slider-telescope__scope">
            <span className="slider-telescope__leg" style={{ transform: 'rotate(22deg)' }} />
            <span className="slider-telescope__leg" style={{ transform: 'rotate(-14deg)' }} />
            <span className="slider-telescope__leg" style={{ transform: 'rotate(4deg)' }} />
            <span className="slider-telescope__arm">
              <span className="slider-telescope__tube slider-telescope__tube--3" />
              <span className="slider-telescope__tube slider-telescope__tube--2" />
              <span className="slider-telescope__tube slider-telescope__tube--1" />
              <span className="slider-telescope__eye" />
            </span>
          </span>
          <span className="slider-telescope__view">
            <span className="slider-telescope__moon" />
            <span className="slider-telescope__cross" />
            <span className="slider-telescope__mag">{mag}×</span>
          </span>
        </div>
        <div className="slider-telescope__track">
          <span className="slider-telescope__rail" />
          <span className="slider-telescope__fill" />
          <span className="slider-telescope__thumb" />
          <input className="slider-telescope__input" type="range" min={0} max={100} value={v} onChange={(e) => setV(+e.target.value)} aria-label="Zoom" aria-valuetext={`${mag} times`} />
        </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-telescope
Source on GitHub

About this component

A zoom slider as a telescope: the tubes slide out of each other and the moon in the eyepiece grows until its craters fill the view, with the magnification beside it.

Dependencies
None
Difficulty
Intermediate
Triggers
state change, continuous
Techniques
css variables, css transition, radial gradient
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 tubes and the moon change without easing.

More sliders

See all
Thermometer
Throttle
Tide
Tracking