FocusScope
Manage focus behavior within elements accessibly.
A utility component for managing keyboard focus within a container. Controls focus trapping, auto-focus behavior, and focus cycling for accessible interactive components.
Note that this is a web-only component, on native it is a no-op.
- Trap focus within a container for modal-like behavior.
- Auto-focus on mount and return focus on unmount.
- Loop focus between first and last tabbable elements.
- Prevent reflows during animations with focusOnIdle.
Installation
FocusScope is already installed in @hanzo/gui, or you can install it
independently:
npm install @hanzogui/focus-scopeUsage
Wrap any content that needs focus management:
import { Button, FocusScope, XStack } from '@hanzo/gui'
export default () => (
<FocusScope loop trapped>
<XStack space="$4">
<Button>First</Button>
<Button>Second</Button>
<Button>Third</Button>
</XStack>
</FocusScope>
)Focus Trapping
Use trapped to prevent focus from escaping the scope:
import { Button, Dialog, FocusScope, XStack, YStack } from '@hanzo/gui'
export default () => (
<Dialog>
<Dialog.Trigger asChild>
<Button>Open Dialog</Button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
{/* key used by AnimatePresence to animate */}
<Dialog.Content key="content">
<FocusScope trapped>
<YStack space="$4">
<Dialog.Title>Focused Content</Dialog.Title>
<XStack space="$2">
<Button>Cancel</Button>
<Button>Confirm</Button>
</XStack>
</YStack>
</FocusScope>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
)Focus Looping
Enable loop to cycle focus between first and last elements:
import { Button, FocusScope, XStack } from '@hanzo/gui'
export default () => (
<FocusScope loop>
<XStack space="$4">
<Button>First</Button>
<Button>Second</Button>
<Button>Last</Button>
{/* Tab from "Last" goes to "First" */}
</XStack>
</FocusScope>
)Animation-Friendly Focusing
Use focusOnIdle to prevent reflows during animations:
import { Button, FocusScope, XStack } from '@hanzo/gui'
export default () => (
<FocusScope
focusOnIdle={true} // Wait for idle callback
// or focusOnIdle={200} // Wait 200ms
>
<XStack space="$4">
<Button>Animated</Button>
<Button>Content</Button>
</XStack>
</FocusScope>
)Advanced Control with FocusScopeController
Use the controller pattern for managing focus from parent components:
import { Button, FocusScope, XStack, YStack } from '@hanzo/gui'
import { useState } from 'react'
export default () => {
const [trapped, setTrapped] = useState(false)
return (
<YStack space="$4">
<Button onPress={() => setTrapped(!trapped)}>
{trapped ? 'Disable' : 'Enable'} Focus Trap
</Button>
<FocusScope.Controller trapped={trapped} loop>
<FocusScope>
<XStack space="$4">
<Button>Controlled</Button>
<Button>Focus</Button>
<Button>Behavior</Button>
</XStack>
</FocusScope>
</FocusScope.Controller>
</YStack>
)
}Function as Children
For advanced use cases, pass a function to get access to focus props:
import { FocusScope, View } from '@hanzo/gui'
export default () => (
<FocusScope loop>
{({ onKeyDown, tabIndex, ref }) => (
<View
ref={ref}
tabIndex={tabIndex}
onKeyDown={onKeyDown}
padding="$4"
borderWidth={1}
borderColor="$borderColor"
>
Custom focus container
</View>
)}
</FocusScope>
)API Reference
FocusScope
| Prop | Type | Default | Required |
|---|---|---|---|
| enabled | boolean | true | - |
| loop | boolean | false | - |
| trapped | boolean | false | - |
| focusOnIdle | boolean | number | { min?: number; max?: number } | false | - |
| onMountAutoFocus | (event: Event) => void | - | - |
| onUnmountAutoFocus | (event: Event) => void | - | - |
| forceUnmount | boolean | false | - |
| children | React.ReactNode | ((props: FocusProps) => React.ReactNode) | - | - |
FocusScope.Controller
Provides context-based control over FocusScope behavior:
| Prop | Type | Default | Required |
|---|---|---|---|
| enabled | boolean | - | - |
| loop | boolean | - | - |
| trapped | boolean | - | - |
| focusOnIdle | boolean | number | - | - |
| onMountAutoFocus | (event: Event) => void | - | - |
| onUnmountAutoFocus | (event: Event) => void | - | - |
| forceUnmount | boolean | - | - |
The FocusScope component automatically inherits props from the nearest FocusScope.Controller, with controller props taking precedence over direct props.
Usage in Other Components
Many Hanzo GUI components export FocusScope for advanced focus control:
import { Dialog, Popover, Select } from '@hanzo/gui'
// Available on:
<Dialog.FocusScope />
<Popover.FocusScope />
<Select.FocusScope />
// And more...Accessibility
FocusScope follows accessibility best practices:
- Manages
tabindexappropriately for focus flow - Respects user's reduced motion preferences
- Maintains focus visible indicators
- Provides proper ARIA support when used with other components
- Handles edge cases like disabled elements and hidden content
FocusScope is primarily designed for web platforms. On React Native, it renders children without focus management since native platforms handle focus differently.
Examples
Modal Focus Management
import { Button, Dialog, FocusScope, Input, XStack, YStack } from '@hanzo/gui'
export default () => (
<Dialog>
<Dialog.Trigger asChild>
<Button>Open Modal</Button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<FocusScope trapped loop focusOnIdle={100}>
<YStack space="$4" padding="$4">
<Dialog.Title>User Details</Dialog.Title>
<Input placeholder="Name" />
<Input placeholder="Email" />
<XStack space="$2">
<Dialog.Close asChild>
<Button variant="outlined">Cancel</Button>
</Dialog.Close>
<Button>Save</Button>
</XStack>
</YStack>
</FocusScope>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
)Custom Focus Container
import { Button, FocusScope, styled, XStack } from '@hanzo/gui'
const FocusContainer = styled(XStack, {
borderWidth: 2,
borderColor: 'transparent',
borderRadius: '$4',
padding: '$4',
variants: {
focused: {
true: {
borderColor: '$blue10',
shadowColor: '$blue10',
shadowRadius: 10,
shadowOpacity: 0.3,
},
},
},
})
export default () => (
<FocusScope loop>
{({ onKeyDown, tabIndex, ref }) => (
<FocusContainer
ref={ref}
tabIndex={tabIndex}
onKeyDown={onKeyDown}
space="$4"
focused
>
<Button>Action 1</Button>
<Button>Action 2</Button>
<Button>Action 3</Button>
</FocusContainer>
)}
</FocusScope>
)Last updated on