Skip to main content
Solid Primitives 2

Primitives for autofocusing HTML elements and trapping focus within a container

StageCategoryVersionLast UpdatedDemo
3Inputs1.0.0-next.4 (next)Aug 12, 2026Demo →
Terminal window
npm i @solid-primitives/focus@next

Primitives for autofocusing HTML elements and trapping focus within a container.

The native autofocus attribute only works on page load, which makes it incompatible with SolidJS. These primitives run on render, allowing autofocus on initial render as well as dynamically added components.

  • autofocus - Ref callback factory to autofocus an element on render.
  • createAutofocus - Reactive primitive to autofocus an element on render.
  • createFocusTrap - Traps focus inside a given DOM element.
  • createFocusRestore - Restores focus to the previously focused element, without trapping.
  • createFocusGroup - Imperatively moves focus between the focusable elements of a container.

autofocus

How to use it

autofocus is a ref callback factory. It uses the native autofocus attribute to determine whether to focus the element.

import { autofocus } from "@solid-primitives/focus";
<button ref={autofocus()} autofocus>
Autofocused
</button>;

To conditionally enable autofocus, control the autofocus attribute directly — the autofocus() ref only focuses when the attribute is present, so removing it is sufficient to opt out:

// Conditionally autofocus by toggling the attribute
<button ref={autofocus()} autofocus={shouldFocus()}>
Maybe Autofocused
</button>

Note: The enabled parameter was removed because it was redundant — the same effect is achieved by omitting the autofocus attribute. Previously, Solid directives always received an accessor argument whether you used it or not, which gave the impression an explicit toggle was necessary.

createAutofocus

createAutofocus reactively autofocuses an element passed in as a signal.

import { createAutofocus } from "@solid-primitives/focus";
// Using ref
let ref!: HTMLButtonElement;
createAutofocus(() => ref);
<button ref={ref}>Autofocused</button>;
// Using ref signal
const [ref, setRef] = createSignal<HTMLButtonElement>();
createAutofocus(ref);
<button ref={setRef}>Autofocused</button>;

createFocusTrap

createFocusTrap traps keyboard focus inside a given DOM element, cycling through focusable children on Tab / Shift+Tab. It uses a MutationObserver to stay up to date with DOM changes and restores focus to the previously focused element when deactivated.

Ported from solid-focus-trap by Jasmin Noetzli (GiyoMoon), adapted for Solid.js 2.0.

How to use it

import { createFocusTrap } from "@solid-primitives/focus";
const DialogContent: Component<{ open: boolean }> = props => {
const [contentRef, setContentRef] = createSignal<HTMLElement | null>(null);
createFocusTrap({
element: contentRef,
enabled: () => props.open,
});
return (
<Show when={props.open}>
<div ref={setContentRef}>
<button>Close</button>
<input />
</div>
</Show>
);
};

Props

PropTypeDefaultDescription
elementMaybeAccessor<HTMLElement|null>Element to trap focus within.
enabledMaybeAccessor<boolean>trueWhether the trap is active.
observeChangesMaybeAccessor<boolean>trueWatch for DOM mutations inside the container and refresh focusable elements.
initialFocusElementMaybeAccessor<HTMLElement|null>First focusable elementElement to focus when the trap activates.
restoreFocusMaybeAccessor<boolean>trueRestore focus to the previously focused element when the trap deactivates.
finalFocusElementMaybeAccessor<HTMLElement|null>Previously focused elementElement to focus when the trap deactivates.
onInitialFocus(event: Event) => voidCallback when focus moves into the trap. Call event.preventDefault() to cancel.
onFinalFocus(event: Event) => voidCallback when focus restores. Call event.preventDefault() to cancel.

Custom initial focus

const [contentRef, setContentRef] = createSignal<HTMLElement | null>(null);
const [inputRef, setInputRef] = createSignal<HTMLElement | null>(null);
createFocusTrap({
element: contentRef,
enabled: () => props.open,
initialFocusElement: inputRef,
});
return (
<Show when={props.open}>
<div ref={setContentRef}>
<button>Close</button>
<input ref={setInputRef} />
</div>
</Show>
);

Preventing focus moves

createFocusTrap({
element: contentRef,
onInitialFocus: event => {
event.preventDefault(); // focus won't move on activation
},
onFinalFocus: event => {
event.preventDefault(); // focus won't restore on deactivation
},
});

createFocusRestore

createFocusRestore saves the currently focused element while active and restores focus to it once deactivated — without trapping focus or managing tab order. Use it for non-modal surfaces (Popover, Tooltip, Menu) that should return focus to their trigger on close but must not intercept Tab navigation while open. For modal dialogs that need both behaviors, use createFocusTrap's restoreFocus option instead.

How to use it

import { createFocusRestore } from "@solid-primitives/focus";
const Popover: Component<{ open: boolean }> = props => {
createFocusRestore({ enabled: () => props.open });
return (
<Show when={props.open}>
<div role="dialog">...</div>
</Show>
);
};

Props

PropTypeDefaultDescription
enabledMaybeAccessor<boolean>trueWhether focus-restore is active.
elementMaybeAccessor<HTMLElement|null>document.bodyElement to dispatch the onFinalFocus event on.
finalFocusElementMaybeAccessor<HTMLElement|null>Previously focused elementElement to focus when deactivated.
onFinalFocus(event: Event) => voidCallback when focus restores. Call event.preventDefault() to cancel.

createFocusGroup

createFocusGroup creates a FocusGroup that moves focus between the focusable elements of a container — e.g. arrow-key navigation in a menu, listbox or toolbar. It walks the DOM with a TreeWalker, either restricting itself to tabbable elements or considering everything focusable. Keyboard navigation (arrow keys, Home/End, Tab) is enabled by default: the keydown listener is attached to the focus group ref automatically.

How to use it

import { createFocusGroup } from "@solid-primitives/focus";
const [ref, setRef] = createSignal<HTMLElement>();
// Keyboard navigation is attached to the ref automatically.
createFocusGroup(ref);
return (
<div ref={setRef} role="menu">
<button role="menuitem">One</button>
<button role="menuitem">Two</button>
<button role="menuitem">Three</button>
</div>
);

The returned group also exposes imperative methods for moving focus, e.g. inside a click handler:

const group = createFocusGroup(ref);
return <button onClick={() => group.focusNext()}>Next</button>;

FocusGroup

The object returned by createFocusGroup. Each method focuses its target and returns it (or undefined when there is nothing to move to). Methods accept an options object:

MethodDescription
focusNext()Moves focus to the next focusable/tabbable element.
focusPrevious()Moves focus to the previous focusable/tabbable element.
focusFirst()Moves focus to the first focusable/tabbable element.
focusLast()Moves focus to the last focusable/tabbable element.

Keyboard navigation

Keyboard navigation is enabled by default and can be disabled with the keyboardNavigation option. The keydown listener is attached to the focus group ref (removed when the ref changes or the group is disposed):

  • Arrow keys move focus between items, following orientation and textDirection. Home/End jump to the first/last item.
  • Tab/Shift+Tab move within the group when handleTab is enabled and focus is already inside it; at a boundary the browser takes over.
  • wrap: true loops around at the ends.
createFocusGroup(ref, () => ({
orientation: "horizontal",
wrap: true,
}));

Options

from, tabbable, wrap, and accept are traversal options: pass them per-method-call, or as defaults (second argument to createFocusGroup) that every call falls back to unless overridden.

const group = createFocusGroup(ref, () => ({ wrap: true, tabbable: true }));
group.focusNext({ tabbable: false }); // overrides the default for this call only
OptionTypeDefaultDescription
fromElementCurrently focusedElement to start searching from.
tabbablebooleanfalseOnly include tabbable elements (tabindex="-1" excluded).
wrapbooleanfalseWrap around when reaching the end of the container.
accept(node) => booleanCallback determining whether an element is eligible for focus.

orientation, textDirection, handleTab, and keyboardNavigation are group-level keyboard options. They only take effect via createFocusGroup's default options (second argument) — passing them to an individual focusNext()/focusPrevious()/focusFirst()/focusLast() call has no effect, since those methods only read the traversal options above from their own opts argument.

OptionTypeDefaultDescription
orientationMaybeAccessor<Orientation>"vertical"The orientation of the focus group ("vertical" or "horizontal").
textDirectionMaybeAccessor<TextDirection>"ltr"The text direction of the focus group ("ltr" or "rtl").
handleTabMaybeAccessor<boolean>trueWhether tab key presses should be handled.
keyboardNavigationMaybeAccessor<boolean>trueWhether the keydown listener is attached to the ref.

Credits

createFocusTrap is ported from solid-focus-trap, part of the corvu UI toolkit by Jasmin Noetzli (GiyoMoon). Licensed under the MIT License.

createFocusGroup is ported from kobalte's createFocusManager, which in turn is based on react-spectrum's FocusManager (Apache License 2.0, Copyright 2020 Adobe).

Changelog

See CHANGELOG.md

Solid Primitives 2High-quality reactive primitives for building applications in Solid2
Community
githubdiscord