Focus Recession Stage
A page-level focus system — when any region enters focus/edit mode, the rest of the layout recedes as a whole: scaled down, dimmed, and blurred into a depth-of-field backdrop, springing back on Esc or an outside click.
npx shadcn@latest add https://webberui.com/r/focus-recession-stage.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<FocusRecessionStage />
Installation
npx shadcn@latest add https://webberui.com/r/focus-recession-stage.jsonOr, once registries are configured in components.json, install it as @webberui/focus-recession-stage.
Usage
import {
FocusRecessionStage,
FocusRegion,
} from "@/components/ui/focus-recession-stage";
<FocusRecessionStage className="grid grid-cols-2 gap-4">
<FocusRegion id="inbox" label="Focus Inbox">
<InboxCard />
</FocusRegion>
<FocusRegion id="calendar" label="Focus Calendar">
<CalendarCard />
</FocusRegion>
</FocusRecessionStage>;When a region contains interactive elements (inputs, multiple buttons), trigger it with FocusTrigger instead and turn off whole-region activation:
import { FocusRegion, FocusTrigger, useFocusRegion } from "@/components/ui/focus-recession-stage";
function NoteCard() {
const { focused } = useFocusRegion();
return (
<div>
{focused ? <textarea autoFocus /> : <p>Note preview…</p>}
<FocusTrigger>{focused ? "Done" : "Edit"}</FocusTrigger>
</div>
);
}
<FocusRegion id="note" activateOnPress={false}>
<NoteCard />
</FocusRegion>;Props
FocusRecessionStage
| Prop | Type | Default | Description |
|---|---|---|---|
focusedId | string | null | — | Controlled id of the focused region; null means nothing is focused. When omitted, the component manages it internally |
defaultFocusedId | string | null | null | Initial focused id in uncontrolled mode |
onFocusedChange | (id: string | null) => void | — | Fires when the focus target changes (including dismissal via Esc / outside click) |
variant | 'depth' | 'flat' | 'spotlight' | 'depth' | Depth preset; can be overridden by the parameters below |
recedeScale | number | per variant | Override: scale of receded regions |
recedeOpacity | number | per variant | Override: opacity of receded regions (0–1) |
recedeBlur | number | per variant | Override: blur radius of receded regions (px) |
focusScale | number | per variant | Override: scale-up of the focused region |
scrimOpacity | number | per variant | Override: scrim opacity (0–1) |
scrim | boolean | true | Whether a dark overlay is laid down on focus to unify the depth |
dismissOnEsc | boolean | true | Whether Esc dismisses focus |
dismissOnOutsideClick | boolean | true | Whether clicking outside the focused region dismisses focus |
children | React.ReactNode | — | Put FocusRegion children directly inside |
className | string | — | Appended to the stage container's class; use it to set up the grid/flex layout |
FocusRegion
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — (required) | Unique key: used to pair focus and to decide outside clicks |
activateOnPress | boolean | true | The whole region is the trigger (click / Enter / Space to focus); set to false when it holds interactive content or you use FocusTrigger |
disabled | boolean | false | Disables this region: it cannot take focus and has no button semantics |
label | string | — | Accessible name (aria-label) when the whole region is the trigger |
children | React.ReactNode | — | Region content; read the focus state through useFocusRegion() |
className | string | — | Appended to the region frame's class |
FocusTrigger
A native button placed inside a FocusRegion (with activateOnPress={false}); clicking it toggles between focused and dismissed. It accepts all native <button> attributes (except onClick, which the component takes over).
useFocusRegion()
Call it inside a FocusRegion subtree; it returns { id, focused, receded, focus, dismiss }, so you can conditionally show editing UI or expanded detail based on focused.
How it works
- Depth recession: when a region takes focus, it scales up and lifts above the overlay while staying perfectly sharp; every other region simultaneously shrinks, dims, and blurs into a depth-of-field backdrop, with a dark overlay laid over the top to unify the mood
- Three variants:
depthshrinks + blurs + dims (default),flatonly dims without displacement (low interference),spotlightis a hard spotlight (darker, blurrier, with a more pronounced lift on the focused region); every dimension can be fine-tuned with its own override prop - Spring recoil: displacement uses a spring (with a slight overshoot), and Esc or an outside click springs the whole thing back with the same spring; opacity and blur use a tween instead, so overshoot cannot cause blur flicker
- Controlled and uncontrolled: leave it entirely to the component, or wire it to your own state with
focusedId+onFocusedChange - Pure transform effects: scale, blur, and opacity never trigger reflow; the focused region lifts over its neighbours with
z-index, so the layout does not shift - Two ways to trigger: whole-region activation (
activateOnPress, good for display cards) orFocusTrigger(good for regions containing inputs or multiple buttons) - Dismissal: the listeners only mount after focus and only take effect on the next frame, so they never mistakenly catch the click that caused focus in the first place; every listener is cleaned up on dismissal and on unmount
Accessibility
- With whole-region activation, the region carries
role="button",tabIndex={0}, andaria-label, and supports Enter / Space to focus - Receded regions are marked
aria-hidden, moved out of the Tab order (tabIndex={-1}), and have pointer events disabled — they become pure backdrop - The state is exposed as
data-focus-state="focused | receded | idle"for easy custom styling hooks - When the user has "reduce motion" enabled at the system level, focus and recession switch instantly (no spring, no gradation) while the depth and focus features are fully preserved
Lasso Select Stage
Drag a lasso box across a grid; the items it catches lift and light up in sequence, and on release the count badge morphs into a floating batch action bar.
Roving Highlight System
One shared morphing highlight surface that travels between any registered elements across tabs, sidebars, and grids, with built-in roving tabindex keyboard navigation and a synchronized focus ring.