WebberUI

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.

Loading preview…
npx shadcn@latest add https://webberui.com/r/focus-recession-stage.json

Playground

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.json

Or, 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

PropTypeDefaultDescription
focusedIdstring | nullControlled id of the focused region; null means nothing is focused. When omitted, the component manages it internally
defaultFocusedIdstring | nullnullInitial focused id in uncontrolled mode
onFocusedChange(id: string | null) => voidFires 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
recedeScalenumberper variantOverride: scale of receded regions
recedeOpacitynumberper variantOverride: opacity of receded regions (0–1)
recedeBlurnumberper variantOverride: blur radius of receded regions (px)
focusScalenumberper variantOverride: scale-up of the focused region
scrimOpacitynumberper variantOverride: scrim opacity (0–1)
scrimbooleantrueWhether a dark overlay is laid down on focus to unify the depth
dismissOnEscbooleantrueWhether Esc dismisses focus
dismissOnOutsideClickbooleantrueWhether clicking outside the focused region dismisses focus
childrenReact.ReactNodePut FocusRegion children directly inside
classNamestringAppended to the stage container's class; use it to set up the grid/flex layout

FocusRegion

PropTypeDefaultDescription
idstring— (required)Unique key: used to pair focus and to decide outside clicks
activateOnPressbooleantrueThe whole region is the trigger (click / Enter / Space to focus); set to false when it holds interactive content or you use FocusTrigger
disabledbooleanfalseDisables this region: it cannot take focus and has no button semantics
labelstringAccessible name (aria-label) when the whole region is the trigger
childrenReact.ReactNodeRegion content; read the focus state through useFocusRegion()
classNamestringAppended 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: depth shrinks + blurs + dims (default), flat only dims without displacement (low interference), spotlight is 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) or FocusTrigger (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}, and aria-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

On this page