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.

Features

  • 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 tamagui, or you can install it independently:

yarn add @tamagui/focus-scope

Usage

Wrap any content that needs focus management:

import { Button, FocusScope, XStack } from 'tamagui'
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 'tamagui'
export default () => (
<Dialog>
<Dialog.Trigger asChild>
<Button>Open Dialog</Button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.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 'tamagui'
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 'tamagui'
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 'tamagui'
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 'tamagui'
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

Props

  • enabled

    boolean

    Default: 

    true

    Whether focus management is enabled

  • loop

    boolean

    Default: 

    false

    When true, tabbing from last item will focus first tabbable and shift+tab from first item will focus last tabbable

  • trapped

    boolean

    Default: 

    false

    When true, focus cannot escape the focus scope via keyboard, pointer, or programmatic focus

  • focusOnIdle

    boolean | number

    Default: 

    false

    When true, waits for idle before focusing. When a number, waits that many ms. This prevents reflows during animations

  • onMountAutoFocus

    (event: Event) => void

    Event handler called when auto-focusing on mount. Can be prevented

  • onUnmountAutoFocus

    (event: Event) => void

    Event handler called when auto-focusing on unmount. Can be prevented

  • forceUnmount

    boolean

    Default: 

    false

    If unmount is animated, you want to force re-focus at start of animation not after

  • children

    React.ReactNode | ((props: FocusProps) => React.ReactNode)

    Content to apply focus management to, or function that receives focus props

  • FocusScope.Controller

    Provides context-based control over FocusScope behavior:

    Props

  • enabled

    boolean

    Override enabled state for all child FocusScope.Controller.Scope components

  • loop

    boolean

    Override loop state for all child FocusScope.Controller.Scope components

  • trapped

    boolean

    Override trapped state for all child FocusScope.Controller.Scope components

  • focusOnIdle

    boolean | number

    Override focusOnIdle behavior for all child FocusScope.Controller.Scope components

  • onMountAutoFocus

    (event: Event) => void

    Override onMountAutoFocus handler for all child FocusScope.Controller.Scope components

  • onUnmountAutoFocus

    (event: Event) => void

    Override onUnmountAutoFocus handler for all child FocusScope.Controller.Scope components

  • forceUnmount

    boolean

    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 Tamagui components export FocusScope for advanced focus control:

    import { Dialog, Popover, Select } from 'tamagui'
    // 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

    FocusScope is primarily designed for web platforms. On React Native, it renders children without focus management since native platforms handle focus differently.

    Examples

    import { Button, Dialog, FocusScope, Input, XStack, YStack } from 'tamagui'
    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 'tamagui'
    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>
    )