Skip to content

Utilities

Standalone focus, navigation item, scroll locking, and key mapping utilities from @diffgazer/keys.

Standalone focus, navigation item, scroll locking, and key mapping utilities. Most work independently of KeyboardProvider; package-only utilities should be imported from @diffgazer/keys.

Info:

For the complete API reference, see the @diffgazer/keys documentation.


useFocusTrap

Traps Tab/Shift+Tab focus within a container. Works independently of KeyboardProvider.

tsx
import { useFocusTrap } from "@diffgazer/keys"
// or in copy mode:
// import { useFocusTrap } from "@/hooks/use-focus-trap"

function Dialog({ open }: { open: boolean }) {
  const contentRef = useRef<HTMLDivElement>(null)
  const closeRef = useRef<HTMLButtonElement>(null)

  useFocusTrap(contentRef, {
    initialFocus: closeRef,
    restoreFocus: true,
    enabled: open,
  })

  if (!open) return null
  return (
    <div ref={contentRef} role="dialog">
      <p>Are you sure?</p>
      <button ref={closeRef}>Close</button>
    </div>
  )
}

@diffgazer/ui's DialogContent and CommandPaletteContent compose this focus-trap behavior through their shared dialog shell.


useFocusRestore

Captures the currently focused element before a temporary surface opens and restores it when that surface closes. This is used by @diffgazer/ui overlay primitives so nested dialogs and command palettes restore focus in close order.

tsx
import { useEffect, useRef, useState } from "react"
import { useFocusRestore } from "@diffgazer/keys"
// or in copy mode:
// import { useFocusRestore } from "@/hooks/use-focus-restore"

function TemporaryPanel() {
  const [open, setOpen] = useState(false)
  const panelRef = useRef<HTMLDivElement>(null)
  const focusRestore = useFocusRestore({ restoreOnUnmount: true })

  useEffect(() => {
    if (open) panelRef.current?.focus()
  }, [open])

  function openPanel() {
    focusRestore.capture()
    setOpen(true)
  }

  function closePanel() {
    setOpen(false)
    focusRestore.restore()
  }

  return open ? (
    <div ref={panelRef} role="dialog" tabIndex={-1}>
      <button onClick={closePanel}>Close</button>
    </div>
  ) : (
    <button onClick={openPanel}>Open</button>
  )
}

Use this for app-owned overlays or temporary focus zones. Prefer the built-in behavior on Dialog and CommandPalette when using those primitives directly.


useScrollLock

Reference-counted scroll lock. Sets overflow: hidden on an element, restores the original value when all locks are released.

tsx
import { useScrollLock } from "@diffgazer/keys"
// or in copy mode:
// import { useScrollLock } from "@/hooks/use-scroll-lock"

// Lock document.body
useScrollLock()

// Lock a specific element, conditionally
const panelRef = useRef<HTMLDivElement>(null)
useScrollLock({ target: panelRef, enabled: isOpen })

@diffgazer/ui's DialogContent and CommandPaletteContent own their background scroll lock through this hook, pointing it at the overlay's own ownerDocument.body while it is open and modal. The reference counting is what lets a stack of them hold one lock, and the hook adds the scrollbar's width to padding-right so hiding the scrollbar does not shift the page. Call it directly for app-owned overlays or custom scroll containers.


Package-mode helpers for component authors who need the same DOM item discovery contract used by useNavigation, NavigationList, RadioGroup, and related primitives.

tsx
import {
  getNavigationItemProps,
  getNavigationItems,
  focusNavigationItem,
} from "@diffgazer/keys"

function Option({ value, children }: { value: string; children: React.ReactNode }) {
  return (
    <button type="button" {...getNavigationItemProps("option", value)}>
      {children}
    </button>
  )
}

Use getNavigationItemProps(type, value) instead of inventing a local selector contract. It writes data-diffgazer-navigation-item and data-value, which keeps app-built primitives compatible with the same focus and navigation logic as the registry components.


keys()

Utility to map multiple hotkeys to the same handler. Requires the full @diffgazer/keys package (not available in copy mode).

tsx
import { useKey, keys } from "@diffgazer/keys"

useKey({
  ...keys(["j", "ArrowDown"], () => move(1)),
  ...keys(["k", "ArrowUp"], () => move(-1)),
  "/": () => focusSearch(),
  Escape: () => clearSearch(),
})