Focus Trap
Trap focus within a specified container.
Motivation
Focus trapping is essential for modal interfaces and other interactive elements that require user attention.
The FocusTrap
component helps maintain accessibility by ensuring keyboard focus remains within a
designated container until explicitly released.
Examples
import { useState } from 'react'
import { FocusTrap } from '../focus-trap'
export const Basic = () => {
const [trapped, setTrapped] = useState(false)
return (
<>
<button onClick={() => setTrapped(true)}>Start Trap</button>
<FocusTrap returnFocusOnDeactivate={false} disabled={!trapped}>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '1rem',
paddingBlock: '1rem',
}}
>
<input type="text" placeholder="input" />
<textarea placeholder="textarea" />
<button onClick={() => setTrapped(false)}>End Trap</button>
</div>
</FocusTrap>
</>
)
}
import { FocusTrap } from '@ark-ui/solid/focus-trap'
import { createSignal } from 'solid-js'
export const Basic = () => {
const [trapped, setTrapped] = createSignal(false)
return (
<>
<button onClick={() => setTrapped(true)}>Start Trap</button>
<FocusTrap returnFocusOnDeactivate={false} disabled={!trapped()}>
<div
style={{
display: 'flex',
'flex-direction': 'column',
gap: '1rem',
'padding-block': '1rem',
}}
>
<input type="text" placeholder="input" />
<textarea placeholder="textarea" />
<button onClick={() => setTrapped(false)}>End Trap</button>
</div>
</FocusTrap>
</>
)
}
<script setup lang="ts">
import { FocusTrap } from '@ark-ui/vue/focus-trap'
import { ref } from 'vue'
const trapped = ref(false)
</script>
<template>
<button @click="trapped = true">Start Trap</button>
<FocusTrap :return-focus-on-deactivate="false" :disabled="!trapped">
<div style="display: flex; flex-direction: column; gap: 1rem; padding-block: 1rem">
<input type="text" placeholder="input" />
<textarea placeholder="textarea" />
<button @click="trapped = false">End Trap</button>
</div>
</FocusTrap>
</template>
Autofocus
The focus trap respects elements with the autofocus
attribute.
import { useRef, useState } from 'react'
import { FocusTrap } from '../focus-trap'
export const Autofocus = () => {
const [trapped, setTrapped] = useState(false)
const toggle = () => setTrapped((c) => !c)
const buttonRef = useRef<HTMLButtonElement | null>(null)
const getButtonNode = () => {
const node = buttonRef.current
if (!node) throw new Error('Button not found')
return node
}
return (
<div>
<button ref={buttonRef} onClick={toggle}>
{trapped ? 'End Trap' : 'Start Trap'}
</button>
{trapped && (
<FocusTrap disabled={!trapped} setReturnFocus={getButtonNode}>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '1rem',
paddingBlock: '1rem',
}}
>
<input type="text" placeholder="Regular input" />
{/* biome-ignore lint/a11y/noAutofocus: <explanation> */}
<input type="text" placeholder="Autofocused input" autoFocus />
<button onClick={() => setTrapped(false)}>End Trap</button>
</div>
</FocusTrap>
)}
</div>
)
}
import { FocusTrap } from '@ark-ui/solid/focus-trap'
import { Show, createSignal } from 'solid-js'
export const Autofocus = () => {
const [trapped, setTrapped] = createSignal(false)
let buttonRef!: HTMLButtonElement
return (
<div>
<button ref={buttonRef} onClick={() => setTrapped((v) => !v)}>
{trapped() ? 'End Trap' : 'Start Trap'}
</button>
<Show when={trapped()}>
<FocusTrap disabled={!trapped()} setReturnFocus={buttonRef}>
<div
style={{
display: 'flex',
'flex-direction': 'column',
gap: '1rem',
'padding-block': '1rem',
}}
>
<input type="text" placeholder="Regular input" />
<input type="text" placeholder="Autofocused input" autofocus />
<button onClick={() => setTrapped(false)}>End Trap</button>
</div>
</FocusTrap>
</Show>
</div>
)
}
<script setup lang="ts">
import { FocusTrap } from '@ark-ui/vue/focus-trap'
import { ref } from 'vue'
const trapped = ref(false)
const buttonRef = ref<HTMLButtonElement>()
</script>
<template>
<div>
<button ref="buttonRef" @click="trapped = !trapped">
{{ trapped ? 'End Trap' : 'Start Trap' }}
</button>
<FocusTrap v-if="trapped" :disabled="!trapped" :set-return-focus="buttonRef">
<div style="display: flex; flex-direction: column; gap: 1rem; padding-block: 1rem">
<input type="text" placeholder="Regular input" />
<input type="text" placeholder="Autofocused input" autofocus />
<button @click="trapped = false">End Trap</button>
</div>
</FocusTrap>
</div>
</template>
Initial Focus
Use the initialFocus
prop to set the element that should receive initial focus when the trap is
activated.
import { useRef, useState } from 'react'
import { FocusTrap } from '../focus-trap'
export const InitialFocus = () => {
const [trapped, setTrapped] = useState(false)
const toggle = () => setTrapped((c) => !c)
const inputRef = useRef<HTMLInputElement>(null)
return (
<div>
<button onClick={toggle}>{trapped ? 'End Trap' : 'Start Trap'}</button>
<FocusTrap disabled={!trapped} initialFocus={() => inputRef.current}>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '1rem',
paddingBlock: '1rem',
}}
>
<input type="text" placeholder="First input" />
<input ref={inputRef} type="text" placeholder="Second input (initial focus)" />
<textarea placeholder="textarea" />
<button onClick={() => setTrapped(false)}>End Trap</button>
</div>
</FocusTrap>
</div>
)
}
import { FocusTrap } from '@ark-ui/solid/focus-trap'
import { createSignal } from 'solid-js'
export const InitialFocus = () => {
const [trapped, setTrapped] = createSignal(false)
const toggle = () => setTrapped((v) => !v)
let inputRef!: HTMLInputElement
return (
<div>
<button onClick={toggle}>{trapped() ? 'End Trap' : 'Start Trap'}</button>
<FocusTrap disabled={!trapped()} initialFocus={() => inputRef}>
<div
style={{
display: 'flex',
'flex-direction': 'column',
gap: '1rem',
'padding-block': '1rem',
}}
>
<input type="text" placeholder="First input" />
<input ref={inputRef} type="text" placeholder="Second input (initial focus)" />
<textarea placeholder="textarea" />
<button onClick={toggle}>End Trap</button>
</div>
</FocusTrap>
</div>
)
}
<script setup lang="ts">
import { FocusTrap } from '@ark-ui/vue/focus-trap'
import { ref } from 'vue'
const trapped = ref(false)
const inputRef = ref<HTMLInputElement>()
const toggle = () => {
trapped.value = !trapped.value
}
</script>
<template>
<div>
<button @click="toggle">{{ trapped ? 'End Trap' : 'Start Trap' }}</button>
<FocusTrap :disabled="!trapped" :initial-focus="() => inputRef">
<div style="display: flex; flex-direction: column; gap: 1rem; padding-block: 1rem">
<input type="text" placeholder="First input" />
<input ref="inputRef" type="text" placeholder="Second input (initial focus)" />
<textarea placeholder="textarea" />
<button @click="trapped = false">End Trap</button>
</div>
</FocusTrap>
</div>
</template>