Focus and Dismissal
Use focus and dismissal with the current published Askr packages.
@askrjs/ui0.0.13@askrjs/themes0.0.13
How to use focus and dismissal
Focus and Dismissal should use the standard themed surface unless the application is deliberately implementing its own design system on top of @askrjs/ui.
- Import the themed component from @askrjs/themes/components; use @askrjs/ui directly only when building a custom visual system.
- Keep focus and dismissal labels, state, and application actions visible at the call site instead of styling internal DOM nodes.
- Verify keyboard behavior, focus movement or return, disabled state, and both standard themes.
import { Button, Dialog, DialogContent, DialogTitle, DialogTrigger } from '@askrjs/themes/components';
<Dialog>
<DialogTrigger asChild><Button>Open settings</Button></DialogTrigger>
<DialogContent>
<DialogTitle>Project settings</DialogTitle>
<ProjectSettings />
</DialogContent>
</Dialog>Purpose
FocusScope and DismissableLayer are the two headless primitives every overlay in @askrjs/ui (Dialog, Popover, Dropdown, Menu, Tooltip, HoverCard, Menubar) is built on. FocusScope handles trapping and restoring keyboard focus; DismissableLayer handles closing on outside interaction. They're exported and usable directly if you're building a custom overlay that isn't already covered by the shipped components.
Install and import
Both come from the root package: `import { FocusScope, DismissableLayer } from '@askrjs/ui'`. There are no themed counterparts in `@askrjs/themes` — these are pure interaction primitives with no visual output of their own, so nothing needs to be imported from the themes package for them to work.
Live examples
Wrapping a custom panel in `<FocusScope trapped loop restoreFocus>` keeps Tab cycling inside the panel and returns focus to the trigger element on unmount. Wrapping the same panel in `<DismissableLayer onDismiss={() => setOpen(false)}>` closes it on Escape or a pointerdown outside its bounds, calling `onEscapeKeyDown`, `onPointerDownOutside`, or `onInteractOutside` individually if you need finer-grained control before the shared `onDismiss` fires.
Anatomy
Both are single-element primitives with `asChild` support (`FocusScopeAsChildProps`, `DismissableLayerAsChildProps`), so you can attach their behavior to an existing element rather than have them render an extra wrapper `<div>`. They compose — a Dialog's content is typically inside both a FocusScope and a DismissableLayer at once, which is how it traps Tab while also closing on outside click.
State model
Neither primitive owns open/closed state itself — that stays with the consumer (or the overlay component wrapping them, like DialogContent). FocusScope's props (`trapped`, `loop`, `restoreFocus`) and DismissableLayer's callbacks are configuration, not state; you mount and unmount them in response to state that lives elsewhere.
Keyboard and accessibility
FocusScope's contract lists three boolean features — `trapped`, `loop`, and `restoreFocus` — all enabled by their corresponding props. DismissableLayer's contract fixes its `INTERACTION_POLICY` as `'background-dismissable'` and its `DISMISS_EVENTS` as escape-key and outside-pointer; those two events are the entire dismissal surface, there's no configurable third trigger.
Styling and tokens
Both are behavior-only and render no visible chrome, so there's nothing for `@askrjs/themes` to style directly — any visual treatment (backdrop, panel border, shadow) belongs to whatever element you nest inside them, typically a themed Dialog or Popover content component.
API
`FocusScopeOwnProps`: `trapped?`, `loop?`, `restoreFocus?`, plus `id?` and `tabIndex?`. `DismissableLayerOwnProps`: `disabled?`, `disableOutsidePointerEvents?`, `onEscapeKeyDown?(event: KeyboardEvent)`, `onPointerDownOutside?(event: PointerEvent)`, `onInteractOutside?(event: Event)`, and `onDismiss?()`. Both accept `id` and forward a `ref` to the underlying `HTMLDivElement` when not used with `asChild`.
Edge cases
Setting `disableOutsidePointerEvents` on DismissableLayer blocks pointer events from reaching elements behind the layer entirely, not just from triggering dismissal — useful for modal-style overlays but wrong for a non-modal popover that should still let the page behind it scroll or hover. Nesting multiple DismissableLayers (e.g. a Select inside a Dialog) means an outside click on the outer layer's trigger can trigger both layers' dismissal logic if you don't stop propagation in the inner one's `onInteractOutside`.
Related pages
See the Dialog, Popover, Dropdown, and Tooltip component pages for how these primitives get composed into full overlays. See Interaction Policies for how to read the `FOCUS_SCOPE_A11Y_CONTRACT` and `DISMISSABLE_LAYER_A11Y_CONTRACT` constants.