Focus
import { useRef } from 'react';
const css = `
.card-focus {
--f: 0.5;
--b-far: 3.5px;
--b-mid: 0px;
--b-near: 3.5px;
position: relative;
width: 230px;
height: 180px;
border-radius: 18px;
background: linear-gradient(light-dark(#ebedf0, #232529), light-dark(#fbfcfd, #121417));
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
overflow: hidden;
cursor: ns-resize;
}
.card-focus__layer { position: absolute; inset: 0; transition: filter 0.25s ease; }
/* Each layer's blur is its distance from the focus plane (set from script). */
.card-focus__far { filter: blur(var(--b-far)); }
.card-focus__mid { filter: blur(var(--b-mid)); }
.card-focus__near { filter: blur(var(--b-near)); }
.card-focus__far::before {
content: '';
position: absolute;
left: -10%;
right: -10%;
bottom: 54px;
height: 70px;
background: light-dark(#cbced3, #393c41);
clip-path: polygon(0 100%, 12% 40%, 26% 70%, 42% 10%, 58% 60%, 72% 25%, 88% 65%, 100% 35%, 100% 100%);
}
.card-focus__far::after {
content: '';
position: absolute;
left: 66%;
top: 22px;
width: 26px;
height: 26px;
border-radius: 50%;
background: light-dark(#50535b, #c6c8cd);
}
.card-focus__mid::before {
content: '';
position: absolute;
left: 34%;
bottom: 40px;
width: 64px;
height: 74px;
background: light-dark(#5f626a, #a7abb2);
clip-path: polygon(50% 0, 100% 100%, 0 100%);
}
.card-focus__mid::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 44px;
background: light-dark(#dbdde1, #2e3035);
}
.card-focus__near::before {
content: '';
position: absolute;
left: -6px;
bottom: -8px;
width: 70px;
height: 110px;
background: light-dark(#42454c, #e5e6e9);
clip-path: polygon(30% 100%, 0 20%, 22% 30%, 34% 0, 46% 36%, 70% 8%, 62% 50%, 100% 40%, 66% 100%);
}
.card-focus__near::after {
content: '';
position: absolute;
right: -10px;
bottom: -6px;
width: 54px;
height: 74px;
background: light-dark(#50535b, #c6c8cd);
clip-path: polygon(40% 100%, 10% 30%, 34% 40%, 50% 0, 60% 44%, 92% 22%, 72% 100%);
}
.card-focus__hud {
position: absolute;
left: 12px;
top: 10px;
display: flex;
gap: 6px;
align-items: center;
color: light-dark(#373a40, #f1f2f3);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 10px;
}
.card-focus__hud i { width: 6px; height: 6px; border-radius: 50%; background: light-dark(#373a40, #f1f2f3); }
.card-focus__scale { position: absolute; right: 10px; top: 14px; bottom: 14px; width: 2px; background: light-dark(#dbdde1, #2e3035); border-radius: 1px; }
.card-focus__scale span {
position: absolute;
left: -3px;
width: 8px;
height: 2px;
background: light-dark(#373a40, #f1f2f3);
top: calc((1 - var(--f)) * 100%);
transition: top 0.25s ease;
}
@media (prefers-reduced-motion: reduce) {
.card-focus__layer, .card-focus__scale span { transition: none; }
}
`;
export function FocusCard() {
const root = useRef<HTMLDivElement>(null);
const label = useRef<HTMLSpanElement>(null);
const set = (f: number) => {
const s = root.current!.style;
s.setProperty('--f', f.toFixed(3));
// Layers sit at depth 0 (far), 0.5 and 1 (near); blur by distance from the focus plane f.
s.setProperty('--b-far', `${(f * 7).toFixed(2)}px`);
s.setProperty('--b-mid', `${(Math.abs(f - 0.5) * 7).toFixed(2)}px`);
s.setProperty('--b-near', `${((1 - f) * 7).toFixed(2)}px`);
root.current!.setAttribute('aria-valuenow', String(Math.round(f * 100)));
// Focus distance readout: near the bottom is close, the top is far away.
const m = 0.4 + (1 - f) ** 2 * 60;
label.current!.textContent = f < 0.1 ? 'f/1.8 · ∞' : `f/1.8 · ${m < 10 ? m.toFixed(1) : Math.round(m)} m`;
};
return (
<>
<style>
{css}
</style>
<div
ref={root}
className="card-focus"
role="slider"
tabIndex={0}
aria-label="Focus distance"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={50}
onPointerMove={(e) => {
const r = root.current!.getBoundingClientRect();
set(Math.min(1, Math.max(0, (e.clientY - r.top) / r.height)));
}}
onKeyDown={(e) => {
const f = Number(root.current!.style.getPropertyValue('--f') || 0.5);
if (e.key === 'ArrowDown') set(Math.min(1, f + 0.1));
if (e.key === 'ArrowUp') set(Math.max(0, f - 0.1));
}}
>
<div className="card-focus__layer card-focus__far" aria-hidden="true" />
<div className="card-focus__layer card-focus__mid" aria-hidden="true" />
<div className="card-focus__layer card-focus__near" aria-hidden="true" />
<span className="card-focus__hud" aria-hidden="true">
<i />
<span ref={label}>f/1.8 · 15 m</span>
</span>
<span className="card-focus__scale" aria-hidden="true">
<span />
</span>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCardFocus(el) on the mounted element instead.
<div class="card-focus" role="slider" tabindex="0" aria-label="Focus distance" aria-valuemin="0" aria-valuemax="100" aria-valuenow="50">
<div class="card-focus__layer card-focus__far" aria-hidden="true"></div>
<div class="card-focus__layer card-focus__mid" aria-hidden="true"></div>
<div class="card-focus__layer card-focus__near" aria-hidden="true"></div>
<span class="card-focus__hud" aria-hidden="true">
<i></i>
<span>f/1.8 · 15 m</span>
</span>
<span class="card-focus__scale" aria-hidden="true">
<span></span>
</span>
</div>.card-focus {
--f: 0.5;
--b-far: 3.5px;
--b-mid: 0px;
--b-near: 3.5px;
position: relative;
width: 230px;
height: 180px;
border-radius: 18px;
background: linear-gradient(light-dark(#ebedf0, #232529), light-dark(#fbfcfd, #121417));
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
overflow: hidden;
cursor: ns-resize;
}
.card-focus__layer { position: absolute; inset: 0; transition: filter 0.25s ease; }
/* Each layer's blur is its distance from the focus plane (set from script). */
.card-focus__far { filter: blur(var(--b-far)); }
.card-focus__mid { filter: blur(var(--b-mid)); }
.card-focus__near { filter: blur(var(--b-near)); }
.card-focus__far::before {
content: '';
position: absolute;
left: -10%;
right: -10%;
bottom: 54px;
height: 70px;
background: light-dark(#cbced3, #393c41);
clip-path: polygon(0 100%, 12% 40%, 26% 70%, 42% 10%, 58% 60%, 72% 25%, 88% 65%, 100% 35%, 100% 100%);
}
.card-focus__far::after {
content: '';
position: absolute;
left: 66%;
top: 22px;
width: 26px;
height: 26px;
border-radius: 50%;
background: light-dark(#50535b, #c6c8cd);
}
.card-focus__mid::before {
content: '';
position: absolute;
left: 34%;
bottom: 40px;
width: 64px;
height: 74px;
background: light-dark(#5f626a, #a7abb2);
clip-path: polygon(50% 0, 100% 100%, 0 100%);
}
.card-focus__mid::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 44px;
background: light-dark(#dbdde1, #2e3035);
}
.card-focus__near::before {
content: '';
position: absolute;
left: -6px;
bottom: -8px;
width: 70px;
height: 110px;
background: light-dark(#42454c, #e5e6e9);
clip-path: polygon(30% 100%, 0 20%, 22% 30%, 34% 0, 46% 36%, 70% 8%, 62% 50%, 100% 40%, 66% 100%);
}
.card-focus__near::after {
content: '';
position: absolute;
right: -10px;
bottom: -6px;
width: 54px;
height: 74px;
background: light-dark(#50535b, #c6c8cd);
clip-path: polygon(40% 100%, 10% 30%, 34% 40%, 50% 0, 60% 44%, 92% 22%, 72% 100%);
}
.card-focus__hud {
position: absolute;
left: 12px;
top: 10px;
display: flex;
gap: 6px;
align-items: center;
color: light-dark(#373a40, #f1f2f3);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 10px;
}
.card-focus__hud i { width: 6px; height: 6px; border-radius: 50%; background: light-dark(#373a40, #f1f2f3); }
.card-focus__scale { position: absolute; right: 10px; top: 14px; bottom: 14px; width: 2px; background: light-dark(#dbdde1, #2e3035); border-radius: 1px; }
.card-focus__scale span {
position: absolute;
left: -3px;
width: 8px;
height: 2px;
background: light-dark(#373a40, #f1f2f3);
top: calc((1 - var(--f)) * 100%);
transition: top 0.25s ease;
}
@media (prefers-reduced-motion: reduce) {
.card-focus__layer, .card-focus__scale span { transition: none; }
}function initCardFocus(root) {
var label = root.querySelector('.card-focus__hud span');
if (!label) return;
var f = 0.5;
function set(v) {
f = v;
root.style.setProperty('--f', f.toFixed(3));
// Layers sit at depth 0 (far), 0.5 and 1 (near); blur by distance from the focus plane f.
root.style.setProperty('--b-far', (f * 7).toFixed(2) + 'px');
root.style.setProperty('--b-mid', (Math.abs(f - 0.5) * 7).toFixed(2) + 'px');
root.style.setProperty('--b-near', ((1 - f) * 7).toFixed(2) + 'px');
root.setAttribute('aria-valuenow', String(Math.round(f * 100)));
// Focus distance readout: the bottom is close, the top is far away.
var m = 0.4 + Math.pow(1 - f, 2) * 60;
label.textContent = f < 0.1 ? 'f/1.8 · ∞' : 'f/1.8 · ' + (m < 10 ? m.toFixed(1) : Math.round(m)) + ' m';
}
function move(e) {
var r = root.getBoundingClientRect();
set(Math.min(1, Math.max(0, (e.clientY - r.top) / r.height)));
}
function key(e) {
if (e.key === 'ArrowDown') set(Math.min(1, f + 0.1));
if (e.key === 'ArrowUp') set(Math.max(0, f - 0.1));
}
root.addEventListener('pointermove', move);
root.addEventListener('keydown', key);
return function () {
root.removeEventListener('pointermove', move);
root.removeEventListener('keydown', key);
};
}
document.querySelectorAll('.card-focus').forEach(initCardFocus);Goal: Create a "Focus" card.
Visual behavior: A scene in depth that you focus like a camera: move the pointer up and down and the focus pulls between the far mountains and the near grass, with a distance readout and a scale marker following.
Technique:
- Three full-size layers (far, mid, near) are drawn with clip-path shapes in pseudo-elements and each carries filter: blur(var(--b-far / mid / near)).
- The pointer's vertical position becomes a focus plane f from 0 to 1; each layer's blur is 7px times its distance from f (layers sit at depths 0, 0.5 and 1).
- The blur transitions over 0.25s so pulls feel like a lens; the readout converts f to a distance (infinity at the top) and a marker slides on a scale.
Accessibility: The card is a slider (role="slider", "Focus distance") with aria-valuenow; Up and Down arrows pull focus. The scene is aria-hidden. Under prefers-reduced-motion focus changes are instant.
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" card.
Visual behavior: A scene in depth that you focus like a camera: move the pointer up and down and the focus pulls between the far mountains and the near grass, with a distance readout and a scale marker following.
Technique:
- Three full-size layers (far, mid, near) are drawn with clip-path shapes in pseudo-elements and each carries filter: blur(var(--b-far / mid / near)).
- The pointer's vertical position becomes a focus plane f from 0 to 1; each layer's blur is 7px times its distance from f (layers sit at depths 0, 0.5 and 1).
- The blur transitions over 0.25s so pulls feel like a lens; the readout converts f to a distance (infinity at the top) and a marker slides on a scale.
Accessibility: The card is a slider (role="slider", "Focus distance") with aria-valuenow; Up and Down arrows pull focus. The scene is aria-hidden. Under prefers-reduced-motion focus changes are instant.
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 scene in depth that you focus like a camera: move the pointer up and down and the focus pulls between the far mountains and the near grass, with a distance readout and a scale marker following.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- mouse tracking, card, state change
- Techniques
- filter, css variables, event position, clip path
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
The card is a slider (role="slider", "Focus distance") with aria-valuenow; Up and Down arrows pull focus. The scene is aria-hidden. Under prefers-reduced-motion focus changes are instant.