Files
docs/apps/gui-docs/content/docs/components/focus-scope.mdx
zeekay a572ed7c00 feat: add gui-docs app for gui.hanzo.ai
Migrated 93 MDX pages from old Tamagui-based site to @hanzo/docs system:
- 13 intro pages (installation, props, styles, themes, etc.)
- 22 core pages (configuration, styled, variants, animations, etc.)
- 48 component pages (button, dialog, select, toast, etc.)
- 14 guide pages (next-js, expo, vite, webpack, metro, etc.)

Zero tamagui references. Uses @hanzo/docs-base-ui + @hanzo/docs-mdx.
2026-03-29 16:47:38 -07:00

393 lines
9.5 KiB
Plaintext

---
title: FocusScope
description: Manage focus behavior within elements accessibly.
name: focus-scope
component: FocusScope
package: focus-scope
---
<IntroParagraph marginTop={-5} marginBottom={30}>
A utility component for managing keyboard focus within a container. Controls focus
trapping, auto-focus behavior, and focus cycling for accessible interactive components.
</IntroParagraph>
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:
```bash
npm install @hanzogui/focus-scope
```
## Usage
Wrap any content that needs focus management:
```tsx
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:
```tsx
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:
```tsx
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:
```tsx
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:
```tsx
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:
```tsx
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
<PropsTable
data={[
{
name: 'enabled',
type: 'boolean',
default: 'true',
description: 'Whether focus management is enabled',
},
{
name: 'loop',
type: 'boolean',
default: 'false',
description:
'When true, tabbing from last item will focus first tabbable and shift+tab from first item will focus last tabbable',
},
{
name: 'trapped',
type: 'boolean',
default: 'false',
description:
'When true, focus cannot escape the focus scope via keyboard, pointer, or programmatic focus',
},
{
name: 'focusOnIdle',
type: 'boolean | number | { min?: number; max?: number }',
default: 'false',
description:
'When true, waits for idle before focusing using requestIdleCallback. When a number, waits that many ms. Object sets a lower and upper bound. Helps to prevent reflows during animations, as focusing inputs easily blocks main thread.',
},
{
name: 'onMountAutoFocus',
type: '(event: Event) => void',
description: 'Event handler called when auto-focusing on mount. Can be prevented',
},
{
name: 'onUnmountAutoFocus',
type: '(event: Event) => void',
description: 'Event handler called when auto-focusing on unmount. Can be prevented',
},
{
name: 'forceUnmount',
type: 'boolean',
default: 'false',
description:
'If unmount is animated, you want to force re-focus at start of animation not after',
},
{
name: 'children',
type: 'React.ReactNode | ((props: FocusProps) => React.ReactNode)',
description:
'Content to apply focus management to, or function that receives focus props',
},
]}
/>
### FocusScope.Controller
Provides context-based control over FocusScope behavior:
<PropsTable
data={[
{
name: 'enabled',
type: 'boolean',
description:
'Override enabled state for all child FocusScope.Controller.Scope components',
},
{
name: 'loop',
type: 'boolean',
description:
'Override loop state for all child FocusScope.Controller.Scope components',
},
{
name: 'trapped',
type: 'boolean',
description:
'Override trapped state for all child FocusScope.Controller.Scope components',
},
{
name: 'focusOnIdle',
type: 'boolean | number',
description:
'Override focusOnIdle behavior for all child FocusScope.Controller.Scope components',
},
{
name: 'onMountAutoFocus',
type: '(event: Event) => void',
description:
'Override onMountAutoFocus handler for all child FocusScope.Controller.Scope components',
},
{
name: 'onUnmountAutoFocus',
type: '(event: Event) => void',
description:
'Override onUnmountAutoFocus handler for all child FocusScope.Controller.Scope components',
},
{
name: 'forceUnmount',
type: 'boolean',
description:
'Override forceUnmount behavior for all child FocusScope.Controller.Scope components',
},
]}
/>
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:
```tsx
import { Dialog, Popover, Select } from '@hanzo/gui'
// Available on:
<Dialog.FocusScope />
<Popover.FocusScope />
<Select.FocusScope />
// And more...
```
## Accessibility
FocusScope follows accessibility best practices:
- Manages `tabindex` appropriately 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
<Notice>
FocusScope is primarily designed for web platforms. On React Native, it renders children
without focus management since native platforms handle focus differently.
</Notice>
## Examples
### Modal Focus Management
```tsx
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
```tsx
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>
)
```