Glin UI

Keyboard UX

Focus Management Patterns

Keep keyboard and assistive technology navigation predictable across modals, popovers, and long pages.

Focus rules

Check these on every interactive flow.

Keyboard reference

What users expect from common widgets.

Expected keyboard behavior
KeysExpected behaviorWhere
TabMove focus to the next interactive elementEverywhere
ShiftTabMove focus to the previous elementEverywhere
EscClose the overlay and return focus to its triggerDialog, sheet, popover
EnterActivate the focused button or linkButtons, links
SpaceActivate a button or toggle a checkbox or switchButtons, form controls
Arrow keysMove between itemsMenus, tabs, radio groups

Do and don't

Do

  • Use :focus-visible styles so keyboard users see the ring but mouse clicks do not flash it.

  • Send focus to the dialog title or first field when a modal opens.

Don't

  • Remove the outline without a replacement.

    button:focus { outline: none; }
  • Use positive tabindex values to reorder focus. Fix the DOM order instead.

The modal traps focus while open. This pattern returns focus to the trigger on close.

import { useRef } from "react"
import {
Button,
Modal,
ModalContent,
ModalHeader,
ModalTitle,
ModalDescription,
ModalTrigger
} from "@glinui/ui"
export function DeleteDialog() {
const triggerRef = useRef<HTMLButtonElement | null>(null)
return (
<Modal>
<ModalTrigger asChild>
<Button ref={triggerRef} variant="destructive">
Delete project
</Button>
</ModalTrigger>
<ModalContent
onCloseAutoFocus={(event) => {
event.preventDefault()
triggerRef.current?.focus()
}}
>
<ModalHeader>
<ModalTitle>Delete project</ModalTitle>
<ModalDescription>This action cannot be undone.</ModalDescription>
</ModalHeader>
</ModalContent>
</Modal>
)
}

Let keyboard users jump past repeated navigation.

<a
href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-50 focus:rounded-md focus:bg-background focus:px-3 focus:py-2"
>
Skip to content
</a>

Manual test pass