Dive
import { useState, type CSSProperties, type KeyboardEvent } from 'react';
const MAX = 40; // metres
const TORCH = 25;
const TOP = 12; // where 0 m sits on the gauge
const SPAN = 172; // gauge length, 0 to 40 m
const css = `
.slider-dive { --deep: light-dark(#373a40, #121417); display: grid; grid-template-columns: 150px 1fr; gap: 14px; width: 230px; height: 196px; }
.slider-dive__sea { position: relative; overflow: hidden; border-radius: 12px; box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); }
/* Everything in the water dims with depth; the torch beam is drawn over it, undimmed. */
.slider-dive__scene { position: absolute; inset: 0; background: linear-gradient(light-dark(#dbdde1, #393c41), light-dark(#5f626a, #1a1c1f) 70%, var(--deep)); filter: brightness(calc(1 - var(--slider-dive-v) * 0.6)); transition: filter 0.3s; }
.slider-dive__surface { position: absolute; left: -20px; right: -20px; top: 4px; height: 6px; background: radial-gradient(10px 4px at 10px 0, transparent 9px, color-mix(in oklab, light-dark(#373a40, #f1f2f3) 35%, transparent) 9.5px 10.5px, transparent 11px) 0 0 / 20px 6px repeat-x; animation: slider-dive-swell 2.4s linear infinite; }
.slider-dive__ray { position: absolute; top: -10px; width: 22px; height: 150px; transform-origin: 50% 0; background: linear-gradient(color-mix(in oklab, light-dark(#373a40, #f1f2f3) 30%, transparent), transparent); opacity: calc(0.75 - var(--slider-dive-v) * 0.7); animation: slider-dive-ray 5s ease-in-out infinite alternate; }
.slider-dive__fish { position: absolute; left: 0; top: 74px; width: 46px; height: 18px; animation: slider-dive-school 7s ease-in-out infinite alternate; }
.slider-dive__fish i { position: absolute; width: 9px; height: 4px; border-radius: 50%; background: light-dark(#50535b, #c6c8cd); }
.slider-dive__fish i::after { content: ''; position: absolute; left: -3px; top: 0; border: 2px solid transparent; border-right: 3px solid light-dark(#50535b, #c6c8cd); }
.slider-dive__bed { position: absolute; left: -10px; right: -10px; bottom: -14px; height: 34px; border-radius: 50% 50% 0 0 / 70% 70% 0 0; background: light-dark(#6d7179, #2e3035); }
.slider-dive__weed { position: absolute; bottom: 10px; width: 3px; height: 26px; border-radius: 2px; transform-origin: 50% 100%; background: light-dark(#5f626a, #393c41); animation: slider-dive-weed 3s ease-in-out infinite alternate; }
/* The diver, head down, kicking. */
.slider-dive__diver { position: absolute; left: 38px; top: 0; width: 64px; height: 30px; translate: 0 calc(10px + var(--slider-dive-v) * 118px); transition: translate 0.45s cubic-bezier(0.3, 1.1, 0.5, 1); }
.slider-dive__diver svg { width: 100%; height: 100%; overflow: visible; rotate: 28deg; }
.slider-dive__body { fill: light-dark(#42454c, #e5e6e9); }
.slider-dive__tank { fill: light-dark(#5f626a, #a7abb2); }
.slider-dive__limb { fill: light-dark(#50535b, #c6c8cd); }
.slider-dive__arm { stroke: light-dark(#42454c, #e5e6e9); stroke-width: 3.5; stroke-linecap: round; }
.slider-dive__torch { fill: light-dark(#5f626a, #a7abb2); }
.slider-dive__mask { fill: light-dark(#fbfcfd, #121417); stroke: light-dark(#5f626a, #a7abb2); stroke-width: 1; }
.slider-dive__reg { fill: light-dark(#6d7179, #7d8087); }
.slider-dive__fin { fill: light-dark(#50535b, #c6c8cd); transform-box: fill-box; transform-origin: 100% 50%; animation: slider-dive-kick 0.7s ease-in-out infinite alternate; }
.slider-dive__fin--b { animation-delay: -0.35s; }
.slider-dive__bubble { position: absolute; left: 50px; top: 14px; width: 4px; height: 4px; border-radius: 50%; box-shadow: inset 0 0 0 1px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 70%, transparent); animation: slider-dive-bubble 1.8s ease-in infinite; }
.slider-dive__beam { position: absolute; left: 92px; top: 0; width: 70px; height: 60px; translate: 0 calc(38px + var(--slider-dive-v) * 118px); transform-origin: 0 0; rotate: 34deg; clip-path: polygon(0 0, 100% -40%, 100% 140%); background: linear-gradient(90deg, color-mix(in oklab, light-dark(#373a40, #f1f2f3) 55%, transparent), transparent); opacity: 0; transition: opacity 0.35s, translate 0.45s cubic-bezier(0.3, 1.1, 0.5, 1); }
.slider-dive[data-torch] .slider-dive__beam { opacity: 1; animation: slider-dive-torch 0.4s steps(1); }
/* The gauge: depth runs down, the handle is a tag with the reading. */
.slider-dive__gauge { position: relative; }
.slider-dive__line { position: absolute; left: 6px; top: ${TOP}px; height: ${SPAN}px; width: 2px; border-radius: 1px; background: light-dark(#dbdde1, #2e3035); }
.slider-dive__line::after { content: ''; position: absolute; left: 0; top: 0; width: 100%; height: calc(var(--slider-dive-v) * 100%); border-radius: inherit; background: light-dark(#50535b, #c6c8cd); }
.slider-dive__tick { position: absolute; left: 3px; width: 8px; height: 1px; background: light-dark(#6d7179, #7d8087); }
.slider-dive__tick b { position: absolute; left: 13px; top: -6px; color: light-dark(#6d7179, #7d8087); font-size: 9px; font-weight: 600; font-variant-numeric: tabular-nums; }
.slider-dive__tag { position: absolute; left: 15px; top: calc(${TOP}px + var(--slider-dive-v) * ${SPAN}px); translate: 0 -50%; padding: 3px 6px; border-radius: 5px; background: light-dark(#373a40, #f1f2f3); color: light-dark(#f3f4f7, #1a1c1f); font-size: 11px; font-weight: 700; white-space: nowrap; font-variant-numeric: tabular-nums; box-shadow: 0 2px 6px light-dark(rgba(0, 0, 0, 0.14), rgba(0, 0, 0, 0.5)); }
.slider-dive__tag::before { content: ''; position: absolute; left: -5px; top: 50%; translate: 0 -50%; border: 5px solid transparent; border-left: 0; border-right-color: light-dark(#373a40, #f1f2f3); }
.slider-dive:has(.slider-dive__input:focus-visible) .slider-dive__tag { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; }
/* A horizontal range turned a quarter: 0 m at the top, deeper downward. */
.slider-dive__input { position: absolute; left: 50%; top: ${TOP + SPAN / 2}px; width: ${SPAN + 24}px; height: 64px; margin: 0; translate: -50% -50%; rotate: 90deg; opacity: 0; cursor: ns-resize; -webkit-appearance: none; appearance: none; background: none; }
.slider-dive__input::-webkit-slider-thumb { -webkit-appearance: none; width: 24px; height: 64px; }
.slider-dive__input::-moz-range-thumb { width: 24px; height: 64px; border: 0; }
@keyframes slider-dive-swell { to { transform: translateX(20px); } }
@keyframes slider-dive-ray { from { transform: rotate(14deg); } to { transform: rotate(22deg); } }
@keyframes slider-dive-school { from { transform: translateX(10px); } to { transform: translateX(84px); } }
@keyframes slider-dive-weed { from { transform: rotate(-9deg); } to { transform: rotate(9deg); } }
@keyframes slider-dive-kick { from { transform: rotate(-16deg); } to { transform: rotate(16deg); } }
@keyframes slider-dive-bubble { from { transform: translate(0, 0) scale(0.6); opacity: 0.9; } to { transform: translate(-6px, -38px) scale(1.2); opacity: 0; } }
@keyframes slider-dive-torch { 0% { opacity: 0.4; } 30% { opacity: 0; } 60% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
.slider-dive *, .slider-dive *::after { animation: none !important; transition: none !important; }
}
`;
export function Preview() {
const [d, setD] = useState(14);
// Depth grows downward, so the down arrow should take you deeper.
const onKey = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
e.preventDefault();
setD((x) => Math.min(MAX, Math.max(0, x + (e.key === 'ArrowDown' ? 1 : -1))));
};
return (
<>
<style href="kit-sliders-dive" precedence="kit">
{css}
</style>
<div className="slider-dive" data-torch={d >= TORCH ? '' : undefined} style={{ '--slider-dive-v': (d / MAX).toFixed(3) } as CSSProperties}>
<div className="slider-dive__sea" aria-hidden="true">
<span className="slider-dive__scene">
<span className="slider-dive__ray" style={{ left: 22 }} />
<span className="slider-dive__ray" style={{ left: 70, animationDelay: '-2s' }} />
<span className="slider-dive__ray" style={{ left: 112, animationDelay: '-3.5s' }} />
<span className="slider-dive__surface" />
<span className="slider-dive__fish">
{[
[0, 2],
[14, 0],
[10, 9],
[26, 6],
].map(([x, y]) => (
<i key={`${x}-${y}`} style={{ left: x + 6, top: y }} />
))}
</span>
<span className="slider-dive__bed" />
<span className="slider-dive__weed" style={{ left: 18 }} />
<span className="slider-dive__weed" style={{ left: 26, height: 18, animationDelay: '-1.4s' }} />
<span className="slider-dive__weed" style={{ left: 126, height: 22, animationDelay: '-0.7s' }} />
<span className="slider-dive__diver">
<svg viewBox="0 0 64 30">
<path className="slider-dive__fin" d="M7 10.5 L-7 5 L-5 13.5 Z" />
<path className="slider-dive__fin slider-dive__fin--b" d="M7 18.5 L-5 15.5 L-7 24 Z" />
<rect className="slider-dive__limb" x="5" y="9.5" width="19" height="4" rx="2" />
<rect className="slider-dive__limb" x="5" y="16.5" width="19" height="4" rx="2" />
<rect className="slider-dive__tank" x="22" y="2.5" width="19" height="7" rx="3.5" />
<rect className="slider-dive__body" x="20" y="8" width="25" height="14" rx="7" />
<path className="slider-dive__arm" d="M41 18 L56 24" />
<rect className="slider-dive__torch" x="55" y="21.5" width="7" height="4" rx="1.5" transform="rotate(22 58 23.5)" />
<circle className="slider-dive__body" cx="50" cy="13.5" r="6.5" />
<rect className="slider-dive__mask" x="51" y="9.5" width="6.5" height="5.5" rx="2" />
<circle className="slider-dive__reg" cx="54.5" cy="18" r="1.8" />
</svg>
<i className="slider-dive__bubble" />
<i className="slider-dive__bubble" style={{ animationDelay: '-0.6s', left: 46, top: 16 }} />
<i className="slider-dive__bubble" style={{ animationDelay: '-1.2s', left: 53, top: 13 }} />
</span>
</span>
<span className="slider-dive__beam" />
</div>
<div className="slider-dive__gauge">
<span className="slider-dive__line" aria-hidden="true" />
{[0, 5, 10, 15, 20, 25, 30, 35, 40].map((m) => (
<span key={m} className="slider-dive__tick" style={{ top: TOP + (m / MAX) * SPAN, width: m % 10 ? 5 : 8 }} aria-hidden="true">
{m % 10 === 0 && <b>{m}</b>}
</span>
))}
<span className="slider-dive__tag" aria-hidden="true">
{d} m
</span>
<input className="slider-dive__input" type="range" min={0} max={MAX} value={d} onChange={(e) => setD(+e.target.value)} onKeyDown={onKey} aria-label="Depth" aria-valuetext={`${d} metres`} aria-orientation="vertical" />
</div>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initSliderDive(el) on the mounted element instead.
<div class="slider-dive" style="--slider-dive-v:0.350">
<div class="slider-dive__sea" aria-hidden="true">
<span class="slider-dive__scene">
<span class="slider-dive__ray" style="left:22px"></span>
<span class="slider-dive__ray" style="left:70px;animation-delay:-2s"></span>
<span class="slider-dive__ray" style="left:112px;animation-delay:-3.5s"></span>
<span class="slider-dive__surface"></span>
<span class="slider-dive__fish">
<i style="left:6px;top:2px"></i>
<i style="left:20px;top:0"></i>
<i style="left:16px;top:9px"></i>
<i style="left:32px;top:6px"></i>
</span>
<span class="slider-dive__bed"></span>
<span class="slider-dive__weed" style="left:18px"></span>
<span class="slider-dive__weed" style="left:26px;height:18px;animation-delay:-1.4s"></span>
<span class="slider-dive__weed" style="left:126px;height:22px;animation-delay:-0.7s"></span>
<span class="slider-dive__diver">
<svg viewBox="0 0 64 30">
<path class="slider-dive__fin" d="M7 10.5 L-7 5 L-5 13.5 Z"></path>
<path class="slider-dive__fin slider-dive__fin--b" d="M7 18.5 L-5 15.5 L-7 24 Z"></path>
<rect class="slider-dive__limb" x="5" y="9.5" width="19" height="4" rx="2"></rect>
<rect class="slider-dive__limb" x="5" y="16.5" width="19" height="4" rx="2"></rect>
<rect class="slider-dive__tank" x="22" y="2.5" width="19" height="7" rx="3.5"></rect>
<rect class="slider-dive__body" x="20" y="8" width="25" height="14" rx="7"></rect>
<path class="slider-dive__arm" d="M41 18 L56 24"></path>
<rect class="slider-dive__torch" x="55" y="21.5" width="7" height="4" rx="1.5" transform="rotate(22 58 23.5)"></rect>
<circle class="slider-dive__body" cx="50" cy="13.5" r="6.5"></circle>
<rect class="slider-dive__mask" x="51" y="9.5" width="6.5" height="5.5" rx="2"></rect>
<circle class="slider-dive__reg" cx="54.5" cy="18" r="1.8"></circle>
</svg>
<i class="slider-dive__bubble"></i>
<i class="slider-dive__bubble" style="animation-delay:-0.6s;left:46px;top:16px"></i>
<i class="slider-dive__bubble" style="animation-delay:-1.2s;left:53px;top:13px"></i>
</span>
</span>
<span class="slider-dive__beam"></span>
</div>
<div class="slider-dive__gauge">
<span class="slider-dive__line" aria-hidden="true"></span>
<span class="slider-dive__tick" style="top:12px;width:8px" aria-hidden="true">
<b>0</b>
</span>
<span class="slider-dive__tick" style="top:33.5px;width:5px" aria-hidden="true"></span>
<span class="slider-dive__tick" style="top:55px;width:8px" aria-hidden="true">
<b>10</b>
</span>
<span class="slider-dive__tick" style="top:76.5px;width:5px" aria-hidden="true"></span>
<span class="slider-dive__tick" style="top:98px;width:8px" aria-hidden="true">
<b>20</b>
</span>
<span class="slider-dive__tick" style="top:119.5px;width:5px" aria-hidden="true"></span>
<span class="slider-dive__tick" style="top:141px;width:8px" aria-hidden="true">
<b>30</b>
</span>
<span class="slider-dive__tick" style="top:162.5px;width:5px" aria-hidden="true"></span>
<span class="slider-dive__tick" style="top:184px;width:8px" aria-hidden="true">
<b>40</b>
</span>
<span class="slider-dive__tag" aria-hidden="true">14 m</span>
<input class="slider-dive__input" type="range" min="0" max="40" aria-label="Depth" aria-valuetext="14 metres" aria-orientation="vertical" value="14"/>
</div>
</div>.slider-dive { --deep: light-dark(#373a40, #121417); display: grid; grid-template-columns: 150px 1fr; gap: 14px; width: 230px; height: 196px; }
.slider-dive__sea { position: relative; overflow: hidden; border-radius: 12px; box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); }
/* Everything in the water dims with depth; the torch beam is drawn over it, undimmed. */
.slider-dive__scene { position: absolute; inset: 0; background: linear-gradient(light-dark(#dbdde1, #393c41), light-dark(#5f626a, #1a1c1f) 70%, var(--deep)); filter: brightness(calc(1 - var(--slider-dive-v) * 0.6)); transition: filter 0.3s; }
.slider-dive__surface { position: absolute; left: -20px; right: -20px; top: 4px; height: 6px; background: radial-gradient(10px 4px at 10px 0, transparent 9px, color-mix(in oklab, light-dark(#373a40, #f1f2f3) 35%, transparent) 9.5px 10.5px, transparent 11px) 0 0 / 20px 6px repeat-x; animation: slider-dive-swell 2.4s linear infinite; }
.slider-dive__ray { position: absolute; top: -10px; width: 22px; height: 150px; transform-origin: 50% 0; background: linear-gradient(color-mix(in oklab, light-dark(#373a40, #f1f2f3) 30%, transparent), transparent); opacity: calc(0.75 - var(--slider-dive-v) * 0.7); animation: slider-dive-ray 5s ease-in-out infinite alternate; }
.slider-dive__fish { position: absolute; left: 0; top: 74px; width: 46px; height: 18px; animation: slider-dive-school 7s ease-in-out infinite alternate; }
.slider-dive__fish i { position: absolute; width: 9px; height: 4px; border-radius: 50%; background: light-dark(#50535b, #c6c8cd); }
.slider-dive__fish i::after { content: ''; position: absolute; left: -3px; top: 0; border: 2px solid transparent; border-right: 3px solid light-dark(#50535b, #c6c8cd); }
.slider-dive__bed { position: absolute; left: -10px; right: -10px; bottom: -14px; height: 34px; border-radius: 50% 50% 0 0 / 70% 70% 0 0; background: light-dark(#6d7179, #2e3035); }
.slider-dive__weed { position: absolute; bottom: 10px; width: 3px; height: 26px; border-radius: 2px; transform-origin: 50% 100%; background: light-dark(#5f626a, #393c41); animation: slider-dive-weed 3s ease-in-out infinite alternate; }
/* The diver, head down, kicking. */
.slider-dive__diver { position: absolute; left: 38px; top: 0; width: 64px; height: 30px; translate: 0 calc(10px + var(--slider-dive-v) * 118px); transition: translate 0.45s cubic-bezier(0.3, 1.1, 0.5, 1); }
.slider-dive__diver svg { width: 100%; height: 100%; overflow: visible; rotate: 28deg; }
.slider-dive__body { fill: light-dark(#42454c, #e5e6e9); }
.slider-dive__tank { fill: light-dark(#5f626a, #a7abb2); }
.slider-dive__limb { fill: light-dark(#50535b, #c6c8cd); }
.slider-dive__arm { stroke: light-dark(#42454c, #e5e6e9); stroke-width: 3.5; stroke-linecap: round; }
.slider-dive__torch { fill: light-dark(#5f626a, #a7abb2); }
.slider-dive__mask { fill: light-dark(#fbfcfd, #121417); stroke: light-dark(#5f626a, #a7abb2); stroke-width: 1; }
.slider-dive__reg { fill: light-dark(#6d7179, #7d8087); }
.slider-dive__fin { fill: light-dark(#50535b, #c6c8cd); transform-box: fill-box; transform-origin: 100% 50%; animation: slider-dive-kick 0.7s ease-in-out infinite alternate; }
.slider-dive__fin--b { animation-delay: -0.35s; }
.slider-dive__bubble { position: absolute; left: 50px; top: 14px; width: 4px; height: 4px; border-radius: 50%; box-shadow: inset 0 0 0 1px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 70%, transparent); animation: slider-dive-bubble 1.8s ease-in infinite; }
.slider-dive__beam { position: absolute; left: 92px; top: 0; width: 70px; height: 60px; translate: 0 calc(38px + var(--slider-dive-v) * 118px); transform-origin: 0 0; rotate: 34deg; clip-path: polygon(0 0, 100% -40%, 100% 140%); background: linear-gradient(90deg, color-mix(in oklab, light-dark(#373a40, #f1f2f3) 55%, transparent), transparent); opacity: 0; transition: opacity 0.35s, translate 0.45s cubic-bezier(0.3, 1.1, 0.5, 1); }
.slider-dive[data-torch] .slider-dive__beam { opacity: 1; animation: slider-dive-torch 0.4s steps(1); }
/* The gauge: depth runs down, the handle is a tag with the reading. */
.slider-dive__gauge { position: relative; }
.slider-dive__line { position: absolute; left: 6px; top: 12px; height: 172px; width: 2px; border-radius: 1px; background: light-dark(#dbdde1, #2e3035); }
.slider-dive__line::after { content: ''; position: absolute; left: 0; top: 0; width: 100%; height: calc(var(--slider-dive-v) * 100%); border-radius: inherit; background: light-dark(#50535b, #c6c8cd); }
.slider-dive__tick { position: absolute; left: 3px; width: 8px; height: 1px; background: light-dark(#6d7179, #7d8087); }
.slider-dive__tick b { position: absolute; left: 13px; top: -6px; color: light-dark(#6d7179, #7d8087); font-size: 9px; font-weight: 600; font-variant-numeric: tabular-nums; }
.slider-dive__tag { position: absolute; left: 15px; top: calc(12px + var(--slider-dive-v) * 172px); translate: 0 -50%; padding: 3px 6px; border-radius: 5px; background: light-dark(#373a40, #f1f2f3); color: light-dark(#f3f4f7, #1a1c1f); font-size: 11px; font-weight: 700; white-space: nowrap; font-variant-numeric: tabular-nums; box-shadow: 0 2px 6px light-dark(rgba(0, 0, 0, 0.14), rgba(0, 0, 0, 0.5)); }
.slider-dive__tag::before { content: ''; position: absolute; left: -5px; top: 50%; translate: 0 -50%; border: 5px solid transparent; border-left: 0; border-right-color: light-dark(#373a40, #f1f2f3); }
.slider-dive:has(.slider-dive__input:focus-visible) .slider-dive__tag { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; }
/* A horizontal range turned a quarter: 0 m at the top, deeper downward. */
.slider-dive__input { position: absolute; left: 50%; top: 98px; width: 196px; height: 64px; margin: 0; translate: -50% -50%; rotate: 90deg; opacity: 0; cursor: ns-resize; -webkit-appearance: none; appearance: none; background: none; }
.slider-dive__input::-webkit-slider-thumb { -webkit-appearance: none; width: 24px; height: 64px; }
.slider-dive__input::-moz-range-thumb { width: 24px; height: 64px; border: 0; }
@keyframes slider-dive-swell { to { transform: translateX(20px); } }
@keyframes slider-dive-ray { from { transform: rotate(14deg); } to { transform: rotate(22deg); } }
@keyframes slider-dive-school { from { transform: translateX(10px); } to { transform: translateX(84px); } }
@keyframes slider-dive-weed { from { transform: rotate(-9deg); } to { transform: rotate(9deg); } }
@keyframes slider-dive-kick { from { transform: rotate(-16deg); } to { transform: rotate(16deg); } }
@keyframes slider-dive-bubble { from { transform: translate(0, 0) scale(0.6); opacity: 0.9; } to { transform: translate(-6px, -38px) scale(1.2); opacity: 0; } }
@keyframes slider-dive-torch { 0% { opacity: 0.4; } 30% { opacity: 0; } 60% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
.slider-dive *, .slider-dive *::after { animation: none !important; transition: none !important; }
}function initSliderDive(root) {
var input = root.querySelector('.slider-dive__input');
var tag = root.querySelector('.slider-dive__tag');
var max = +input.max;
function update() {
var d = +input.value;
root.style.setProperty('--slider-dive-v', (d / max).toFixed(3));
// Past 25 metres the diver switches the torch on.
root.toggleAttribute('data-torch', d >= 25);
tag.textContent = d + ' m';
input.setAttribute('aria-valuetext', d + ' metres');
}
// Depth grows downward, so the down arrow should take you deeper.
function key(e) {
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
e.preventDefault();
input.value = Math.min(max, Math.max(0, +input.value + (e.key === 'ArrowDown' ? 1 : -1)));
update();
}
input.addEventListener('input', update);
input.addEventListener('keydown', key);
update();
return function () {
input.removeEventListener('input', update);
input.removeEventListener('keydown', key);
};
}
document.querySelectorAll('.slider-dive').forEach(initSliderDive);Goal: Create a "Dive" slider.
Visual behavior: A depth gauge beside the sea: take the diver down and the water darkens, the light rays fade, and past 25 metres the torch comes on.
Technique:
- One real <input type="range"> turned a quarter lies over the gauge, 0 m at the top. Depth grows downward, so a keydown handler makes the down arrow go deeper and the up arrow shallower.
- The whole scene sits under one brightness filter that dims with depth. The torch beam is drawn over it, outside the filter, so it stays bright.
- The light rays fade out as the diver goes down, and past 25 metres the beam flickers on.
- The diver kicks, bubbles rise from the regulator, fish drift and the weed sways, all as small loops.
Accessibility: A native range input named "Depth" with an aria-valuetext in words, so a screen reader announces the reading rather than a bare number; keyboard focus outlines the drawn handle. The drawing is aria-hidden. Under prefers-reduced-motion the loops stop and the diver 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 "Dive" slider.
Visual behavior: A depth gauge beside the sea: take the diver down and the water darkens, the light rays fade, and past 25 metres the torch comes on.
Technique:
- One real <input type="range"> turned a quarter lies over the gauge, 0 m at the top. Depth grows downward, so a keydown handler makes the down arrow go deeper and the up arrow shallower.
- The whole scene sits under one brightness filter that dims with depth. The torch beam is drawn over it, outside the filter, so it stays bright.
- The light rays fade out as the diver goes down, and past 25 metres the beam flickers on.
- The diver kicks, bubbles rise from the regulator, fish drift and the weed sways, all as small loops.
Accessibility: A native range input named "Depth" with an aria-valuetext in words, so a screen reader announces the reading rather than a bare number; keyboard focus outlines the drawn handle. The drawing is aria-hidden. Under prefers-reduced-motion the loops stop and the diver 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 depth gauge beside the sea: take the diver down and the water darkens, the light rays fade, and past 25 metres the torch comes on.
- Dependencies
- None
- Difficulty
- Advanced
- Triggers
- state change, continuous, ambient
- Techniques
- css variables, filter, keyframes, svg
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A native range input named "Depth" with an aria-valuetext in words, so a screen reader announces the reading rather than a bare number; keyboard focus outlines the drawn handle. The drawing is aria-hidden. Under prefers-reduced-motion the loops stop and the diver moves without easing.