Skip to main content

Focus Boundaries

A focus boundary prevents focus from escaping a container when the user presses an arrow key. This is essential for modal dialogs, isolated panels, and any UI region that should trap focus within itself.

isFocusBoundary

Set isFocusBoundary: true on a container to block focus from leaving it in all directions.

const { ref, focusKey } = useFocusable({
focusKey: 'MODAL',
isFocusBoundary: true
});

When the user reaches the edge of the container and presses an arrow key, focus stays on the last focusable element in that direction rather than escaping to a sibling container.

focusBoundaryDirections

To block only specific directions, provide an array of direction strings:

const { ref, focusKey } = useFocusable({
focusKey: 'SIDEBAR',
isFocusBoundary: true,
focusBoundaryDirections: ['left'] // block left only
});

Valid direction values: 'up', 'down', 'left', 'right'.

Without focusBoundaryDirections, all directions are blocked when isFocusBoundary: true.


A modal should trap focus so the user cannot accidentally navigate to content behind it.

import React, { useEffect } from 'react';
import {
useFocusable,
FocusContext
} from '@noriginmedia/norigin-spatial-navigation-react';

interface ModalProps {
onConfirm: () => void;
onCancel: () => void;
}

function ConfirmButton({
label,
onPress
}: {
label: string;
onPress: () => void;
}) {
const { ref, focused } = useFocusable({
onEnterPress: () => onPress()
});

return (
<button
ref={ref}
style={{
padding: '12px 24px',
backgroundColor: focused ? '#0066cc' : '#444',
color: 'white',
border: 'none',
borderRadius: '4px'
}}
>
{label}
</button>
);
}

function Modal({ onConfirm, onCancel }: ModalProps) {
const { ref, focusKey, focusSelf } = useFocusable({
focusKey: 'MODAL',
isFocusBoundary: true, // trap focus inside the modal
trackChildren: true
});

// Focus the modal when it mounts
useEffect(() => {
focusSelf();
}, [focusSelf]);

return (
<div
style={{
position: 'fixed',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(0,0,0,0.7)'
}}
>
<FocusContext.Provider value={focusKey}>
<div
ref={ref}
style={{
backgroundColor: '#222',
padding: '40px',
borderRadius: '8px',
display: 'flex',
flexDirection: 'column',
gap: '24px'
}}
>
<p style={{ color: 'white', fontSize: '20px' }}>Are you sure?</p>
<div style={{ display: 'flex', gap: '16px' }}>
<ConfirmButton label="Confirm" onPress={onConfirm} />
<ConfirmButton label="Cancel" onPress={onCancel} />
</div>
</div>
</FocusContext.Provider>
</div>
);
}

When the modal mounts, it calls focusSelf() to claim focus. Because isFocusBoundary: true, pressing any arrow key keeps focus inside the modal. When the modal unmounts, autoRestoreFocus (enabled by default) returns focus to the previously focused component.


Partial Boundary Example

A sidebar that should never let focus escape to the left (useful when the sidebar is on the left edge of the screen):

const { ref, focusKey } = useFocusable({
focusKey: 'SIDEBAR',
isFocusBoundary: true,
focusBoundaryDirections: ['left']
});

Focus can still move right from the sidebar (into the main content area), but pressing left at the leftmost sidebar item does nothing.


Releasing a Boundary Programmatically

If you need to move focus out of a bounded container programmatically (e.g., the user presses a custom "escape" key), use setFocus or navigateByDirection from outside the boundary:

import { setFocus } from '@noriginmedia/norigin-spatial-navigation-core';

function handleEscape() {
// Boundaries only block spatial navigation from within;
// programmatic setFocus always works regardless of boundaries.
setFocus('MAIN_CONTENT');
}

Combining Boundaries with autoRestoreFocus

When a bounded container unmounts, autoRestoreFocus: true (the default) returns focus to a sensible location. This pair is ideal for modals and overlays:

const { ref, focusKey, focusSelf } = useFocusable({
focusKey: 'MODAL',
isFocusBoundary: true,
autoRestoreFocus: true // focus returns to the trigger button when modal closes
});