Focus
import { useState } from 'react';
const css = `
@keyframes slider-focus-lock { 0% { transform: scale(1); } 40% { transform: scale(0.94); } 100% { transform: scale(1); } }
.slider-focus { display: grid; gap: 12px; width: 220px; }
.slider-focus__frame { position: relative; height: 110px; overflow: hidden; border-radius: 10px; background: light-dark(#ebedf0, #232529); }
.slider-focus__scene { display: block; width: 100%; height: 100%; filter: blur(calc((1 - var(--slider-focus-v)) * 7px)); transform: scale(1.06); transition: filter 0.15s; }
.slider-focus__far { fill: light-dark(#dbdde1, #2e3035); }
.slider-focus__mid { fill: light-dark(#cbced3, #393c41); }
.slider-focus__subject { fill: light-dark(#42454c, #e5e6e9); }
.slider-focus__detail { fill: light-dark(#dbdde1, #2e3035); }
.slider-focus__box { position: absolute; left: 50%; top: 50%; width: calc(56px + (1 - var(--slider-focus-v)) * 40px); height: calc(48px + (1 - var(--slider-focus-v)) * 30px); transform: translate(-50%, -50%); transition: width 0.2s, height 0.2s; }
.slider-focus[data-locked] .slider-focus__box { animation: slider-focus-lock 0.35s ease-out; }
.slider-focus__corner { position: absolute; width: 10px; height: 10px; border: 0 solid color-mix(in oklab, light-dark(#373a40, #f1f2f3) calc(40% + var(--slider-focus-v) * 60%), transparent); }
.slider-focus__corner:nth-child(1) { left: 0; top: 0; border-width: 2px 0 0 2px; }
.slider-focus__corner:nth-child(2) { right: 0; top: 0; border-width: 2px 2px 0 0; }
.slider-focus__corner:nth-child(3) { left: 0; bottom: 0; border-width: 0 0 2px 2px; }
.slider-focus__corner:nth-child(4) { right: 0; bottom: 0; border-width: 0 2px 2px 0; }
.slider-focus__af { position: absolute; left: 8px; top: 6px; display: flex; align-items: center; gap: 4px; color: light-dark(#50535b, #c6c8cd); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; }
.slider-focus__af::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: light-dark(#cbced3, #393c41); transition: background-color 0.2s, box-shadow 0.2s; }
.slider-focus[data-locked] .slider-focus__af { color: light-dark(#373a40, #f1f2f3); }
.slider-focus[data-locked] .slider-focus__af::before { background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 6px light-dark(#373a40, #f1f2f3); }
.slider-focus__track { position: relative; height: 24px; }
.slider-focus__rail { position: absolute; left: 0; right: 0; top: 8px; height: 8px; border-radius: 4px; background: repeating-linear-gradient(90deg, light-dark(#dbdde1, #2e3035) 0 2px, transparent 2px 5px); }
.slider-focus__thumb { position: absolute; top: 2px; left: calc(var(--slider-focus-v) * (100% - 14px)); width: 14px; height: 20px; border-radius: 4px; 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-focus:has(.slider-focus__input:focus-visible) .slider-focus__thumb { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; }
.slider-focus__input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; -webkit-appearance: none; appearance: none; background: none; }
.slider-focus__input::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 20px; }
.slider-focus__input::-moz-range-thumb { width: 14px; height: 20px; border: 0; }
@media (prefers-reduced-motion: reduce) {
.slider-focus * { transition-duration: 0.01s !important; animation: none !important; }
}
`;
export function FocusSlider() {
const [v, setV] = useState(40);
return (
<>
<style>
{css}
</style>
<div className="slider-focus" data-locked={v >= 96 ? '' : undefined} style={{ '--slider-focus-v': v / 100 } as React.CSSProperties}>
<div className="slider-focus__frame" aria-hidden="true">
<svg className="slider-focus__scene" viewBox="0 0 220 110" preserveAspectRatio="xMidYMid slice">
<path className="slider-focus__far" d="M0 70L50 40L90 60L140 30L220 64V110H0Z" />
<path className="slider-focus__mid" d="M0 88C60 76 140 80 220 86V110H0Z" />
<circle className="slider-focus__subject" cx="110" cy="58" r="18" />
<circle className="slider-focus__detail" cx="104" cy="54" r="3" />
<circle className="slider-focus__detail" cx="117" cy="54" r="3" />
<path className="slider-focus__detail" d="M102 64q8 7 16 0" />
</svg>
<span className="slider-focus__box">
<span className="slider-focus__corner" />
<span className="slider-focus__corner" />
<span className="slider-focus__corner" />
<span className="slider-focus__corner" />
</span>
<span className="slider-focus__af">AF</span>
</div>
<div className="slider-focus__track">
<span className="slider-focus__rail" />
<span className="slider-focus__thumb" />
<input className="slider-focus__input" type="range" min={0} max={100} value={v} onChange={(e) => setV(+e.target.value)} aria-label="Focus" />
</div>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initSliderFocus(el) on the mounted element instead.
<div class="slider-focus" style="--slider-focus-v:0.4">
<div class="slider-focus__frame" aria-hidden="true">
<svg class="slider-focus__scene" viewBox="0 0 220 110" preserveAspectRatio="xMidYMid slice">
<path class="slider-focus__far" d="M0 70L50 40L90 60L140 30L220 64V110H0Z"></path>
<path class="slider-focus__mid" d="M0 88C60 76 140 80 220 86V110H0Z"></path>
<circle class="slider-focus__subject" cx="110" cy="58" r="18"></circle>
<circle class="slider-focus__detail" cx="104" cy="54" r="3"></circle>
<circle class="slider-focus__detail" cx="117" cy="54" r="3"></circle>
<path class="slider-focus__detail" d="M102 64q8 7 16 0"></path>
</svg>
<span class="slider-focus__box">
<span class="slider-focus__corner"></span>
<span class="slider-focus__corner"></span>
<span class="slider-focus__corner"></span>
<span class="slider-focus__corner"></span>
</span>
<span class="slider-focus__af">AF</span>
</div>
<div class="slider-focus__track">
<span class="slider-focus__rail"></span>
<span class="slider-focus__thumb"></span>
<input class="slider-focus__input" type="range" min="0" max="100" aria-label="Focus" value="40"/>
</div>
</div>@keyframes slider-focus-lock { 0% { transform: scale(1); } 40% { transform: scale(0.94); } 100% { transform: scale(1); } }
.slider-focus { display: grid; gap: 12px; width: 220px; }
.slider-focus__frame { position: relative; height: 110px; overflow: hidden; border-radius: 10px; background: light-dark(#ebedf0, #232529); }
.slider-focus__scene { display: block; width: 100%; height: 100%; filter: blur(calc((1 - var(--slider-focus-v)) * 7px)); transform: scale(1.06); transition: filter 0.15s; }
.slider-focus__far { fill: light-dark(#dbdde1, #2e3035); }
.slider-focus__mid { fill: light-dark(#cbced3, #393c41); }
.slider-focus__subject { fill: light-dark(#42454c, #e5e6e9); }
.slider-focus__detail { fill: light-dark(#dbdde1, #2e3035); }
.slider-focus__box { position: absolute; left: 50%; top: 50%; width: calc(56px + (1 - var(--slider-focus-v)) * 40px); height: calc(48px + (1 - var(--slider-focus-v)) * 30px); transform: translate(-50%, -50%); transition: width 0.2s, height 0.2s; }
.slider-focus[data-locked] .slider-focus__box { animation: slider-focus-lock 0.35s ease-out; }
.slider-focus__corner { position: absolute; width: 10px; height: 10px; border: 0 solid color-mix(in oklab, light-dark(#373a40, #f1f2f3) calc(40% + var(--slider-focus-v) * 60%), transparent); }
.slider-focus__corner:nth-child(1) { left: 0; top: 0; border-width: 2px 0 0 2px; }
.slider-focus__corner:nth-child(2) { right: 0; top: 0; border-width: 2px 2px 0 0; }
.slider-focus__corner:nth-child(3) { left: 0; bottom: 0; border-width: 0 0 2px 2px; }
.slider-focus__corner:nth-child(4) { right: 0; bottom: 0; border-width: 0 2px 2px 0; }
.slider-focus__af { position: absolute; left: 8px; top: 6px; display: flex; align-items: center; gap: 4px; color: light-dark(#50535b, #c6c8cd); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; }
.slider-focus__af::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: light-dark(#cbced3, #393c41); transition: background-color 0.2s, box-shadow 0.2s; }
.slider-focus[data-locked] .slider-focus__af { color: light-dark(#373a40, #f1f2f3); }
.slider-focus[data-locked] .slider-focus__af::before { background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 6px light-dark(#373a40, #f1f2f3); }
.slider-focus__track { position: relative; height: 24px; }
.slider-focus__rail { position: absolute; left: 0; right: 0; top: 8px; height: 8px; border-radius: 4px; background: repeating-linear-gradient(90deg, light-dark(#dbdde1, #2e3035) 0 2px, transparent 2px 5px); }
.slider-focus__thumb { position: absolute; top: 2px; left: calc(var(--slider-focus-v) * (100% - 14px)); width: 14px; height: 20px; border-radius: 4px; 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-focus:has(.slider-focus__input:focus-visible) .slider-focus__thumb { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; }
.slider-focus__input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; -webkit-appearance: none; appearance: none; background: none; }
.slider-focus__input::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 20px; }
.slider-focus__input::-moz-range-thumb { width: 14px; height: 20px; border: 0; }
@media (prefers-reduced-motion: reduce) {
.slider-focus * { transition-duration: 0.01s !important; animation: none !important; }
}function initSliderFocus(root) {
var input = root.querySelector('.slider-focus__input');
function update() {
var v = +input.value;
root.style.setProperty('--slider-focus-v', v / 100);
// Sharp enough: the focus box locks on.
root.toggleAttribute('data-locked', v >= 96);
}
input.addEventListener('input', update);
update();
return function () { input.removeEventListener('input', update); };
}
document.querySelectorAll('.slider-focus').forEach(initSliderFocus);Goal: Create a "Focus" slider.
Visual behavior: A camera focus slider: the scene sharpens from a blur as you drag, the focus box closes in on the subject, and at sharp focus it locks with a flash and the AF light comes on.
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 scene blur is a filter: blur() that falls towards zero, and the focus box corners close in by it.
- A data-locked attribute from 96% plays the lock keyframes and lights the AF label.
- :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; keyboard focus outlines the drawn thumb. Under prefers-reduced-motion the scene and the box change without transitions and the lock does not flash.
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 "Focus" slider.
Visual behavior: A camera focus slider: the scene sharpens from a blur as you drag, the focus box closes in on the subject, and at sharp focus it locks with a flash and the AF light comes on.
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 scene blur is a filter: blur() that falls towards zero, and the focus box corners close in by it.
- A data-locked attribute from 96% plays the lock keyframes and lights the AF label.
- :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; keyboard focus outlines the drawn thumb. Under prefers-reduced-motion the scene and the box change without transitions and the lock does not flash.
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 camera focus slider: the scene sharpens from a blur as you drag, the focus box closes in on the subject, and at sharp focus it locks with a flash and the AF light comes on.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- state change, keyframe
- Techniques
- css variables, filter, keyframes
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A native range input with an aria-label; keyboard focus outlines the drawn thumb. Under prefers-reduced-motion the scene and the box change without transitions and the lock does not flash.