# How htmx Preserves Focus and Text Selection During DOM Swaps

> Learn how htmx maintains user focus and text selection during DOM swaps by intelligently capturing and restoring element states. Ensure seamless user experiences with this key feature.

- Repository: [Big Sky Software/htmx](https://github.com/bigskysoftware/htmx)
- Tags: internals
- Published: 2026-08-30

---

**htmx preserves focus and text selection by capturing `document.activeElement` and its `selectionStart`/`selectionEnd` values before swapping DOM content, then restoring those values to the matching element (by ID) after the swap completes.**

When htmx performs partial page updates via AJAX, it must prevent the user from losing their cursor position or focus state. According to the bigskysoftware/htmx source code, this is achieved through a sophisticated preservation mechanism that handles both standard focus restoration and the `hx-preserve` attribute for keeping entire elements intact.

## The Two-Step Preservation Process

The core implementation in [`src/htmx.js`](https://github.com/bigskysoftware/htmx/blob/main/src/htmx.js) uses a capture-and-restore pattern that executes around the swap operation.

### Step 1: Capturing Focus State Before the Swap

Just before parsing the response fragment, htmx records the currently focused element and its selection boundaries. This occurs immediately after the swap target is resolved but before any DOM manipulation begins.

```javascript
// src/htmx.js – preserve focus and selection
const activeElt = document.activeElement;
let selectionInfo = {
  elt: activeElt,
  // @ts-ignore – selectionStart may be undefined on non‑text inputs
  start: activeElt ? activeElt.selectionStart : null,
  // @ts-ignore – selectionEnd may be undefined on non‑text inputs
  end: activeElt ? activeElt.selectionEnd : null
};

```

The `selectionInfo` object stores the active element reference along with its `selectionStart` and `selectionEnd` properties. These cursor positions are only valid on text input elements, so the code defensively checks for their existence.

### Step 2: Restoring Focus After the Swap

Once the new content is inserted via `swapWithStyle` (part of the `swapOuterHTML` or `swapInnerHTML` routines), htmx attempts to locate the original element within the newly rendered DOM using its `id` attribute.

```javascript
// src/htmx.js – apply saved focus and selection information
if (selectionInfo.elt &&
    !bodyContains(selectionInfo.elt) &&            
    getRawAttribute(selectionInfo.elt, 'id')) {

  const newActiveElt = document.getElementById(
    getRawAttribute(selectionInfo.elt, 'id')
  );

  const focusOptions = {
    preventScroll: swapSpec.focusScroll !== undefined
                   ? !swapSpec.focusScroll
                   : !htmx.config.defaultFocusScroll
  };

  if (newActiveElt) {
    if (selectionInfo.start && newActiveElt.setSelectionRange) {
      try {
        newActiveElt.setSelectionRange(selectionInfo.start,
                                       selectionInfo.end);
      } catch (e) {
        // Some elements expose setSelectionRange but don’t support it
      }
    }
    newActiveElt.focus(focusOptions);
  }
}

```

The restoration only triggers if the original element was removed from the document (`!bodyContains`) and possesses an `id`. The `focusOptions` object respects the `focusScroll` swap specification or falls back to the global `htmx.config.defaultFocusScroll` setting.

## The hx-preserve Attribute for Complete Element Survival

For elements that must survive a swap entirely—including their internal state and child nodes—htmx provides the `hx-preserve` attribute. This mechanism functions differently than focus restoration; it physically moves the existing element out of the DOM before the swap and reinserts it afterward.

During the swap preparation phase, `handlePreservedElements(fragment)` identifies elements marked with `hx-preserve` and moves them to a hidden temporary container (`#--htmx-preserve-pantry--`). After the new fragment is inserted, `restorePreservedElements()` places the preserved nodes back into their original positions, replacing any freshly rendered duplicates.

```html
<div id="editor" hx-get="/update" hx-swap="outerHTML" hx-preserve>
  <textarea id="code">User edits remain here</textarea>
</div>

```

In this example, even when the outer div is replaced via `outerHTML`, the textarea and its exact cursor position remain untouched because the original node never leaves the document—it simply visits the pantry temporarily.

## Controlling Scroll Behavior on Focus Restoration

By default, restoring focus may trigger the browser to scroll the element into view. htmx allows fine-grained control over this behavior through both inline attributes and global configuration.

- **Per-element control**: Use `hx-focus-scroll="false"` to prevent scrolling for a specific swap
- **Global configuration**: Set `htmx.config.defaultFocusScroll` to `false` to disable scroll-on-focus application-wide

The `focusOptions.preventScroll` parameter passed to `element.focus()` is derived from these settings, ensuring consistent behavior across different swap types including out-of-band swaps.

## Practical Implementation Examples

### Example 1: Search Input with Live Results

```html
<input id="search" 
       hx-get="/results" 
       hx-trigger="keyup changed delay:500ms" 
       hx-target="#results" />

```

As the user types, htmx captures the caret position in `#search`, swaps the results container, and restores the cursor to the exact character position within the input field.

### Example 2: Preserving Complex Editor State

```html
<form hx-post="/save" hx-swap="outerHTML">
  <div hx-preserve>
    <textarea id="comment">Draft content...</textarea>
  </div>
  <button type="submit">Save</button>
</form>

```

The textarea maintains focus and selection even when the form receives a full `outerHTML` replacement after submission.

### Example 3: Disabling Focus Scroll

```html
<input id="username" 
       hx-get="/check-availability" 
       hx-swap="outerHTML" 
       hx-focus-scroll="false">

```

When the input is replaced with validation feedback, the browser will not scroll the page to bring the element into view, preserving the user's reading position.

## Summary

- **htmx captures focus state** immediately before DOM swaps by storing `document.activeElement` and its `selectionStart`/`selectionEnd` values in [`src/htmx.js`](https://github.com/bigskysoftware/htmx/blob/main/src/htmx.js).
- **Focus restoration requires ID attributes** because htmx locates the replacement element using `getElementById`, then reapplies selection ranges via `setSelectionRange`.
- **Scroll behavior is configurable** through the `focusScroll` swap option or `htmx.config.defaultFocusScroll`, passed as `preventScroll` in the focus options object.
- **The `hx-preserve` attribute** leverages `handlePreservedElements()` and `restorePreservedElements()` to move nodes to a hidden pantry (`#--htmx-preserve-pantry--`), keeping them entirely untouched during swaps.
- **Both mechanisms execute** after fragment insertion but before `afterSwap` events fire, ensuring consistent state for event handlers.

## Frequently Asked Questions

### Does htmx preserve focus without an ID on the element?

No. According to the source code in [`src/htmx.js`](https://github.com/bigskysoftware/htmx/blob/main/src/htmx.js), htmx only attempts to restore focus if `getRawAttribute(selectionInfo.elt, 'id')` returns a truthy value. The preservation mechanism relies on `document.getElementById` to locate the corresponding element in the new DOM fragment. Without an ID, the focus is lost after the swap.

### What happens if the preserved element doesn't support text selection?

The code safely handles non-text inputs. When capturing state, `selectionStart` and `selectionEnd` may be `null` or `undefined` for elements like buttons or checkboxes. During restoration, the code only calls `setSelectionRange` if `selectionInfo.start` exists and the new element supports the method. The `try...catch` block silently handles elements that expose `setSelectionRange` but do not implement it.

### How does hx-preserve differ from standard focus preservation?

Standard focus preservation captures metadata (element reference and cursor position) before the swap and attempts to restore it to a new DOM node. In contrast, `hx-preserve` physically moves the original DOM node to a temporary container (`#--htmx-preserve-pantry--`) before the swap occurs, then reinserts that exact same node afterward. This preserves internal state that cannot be captured, such as video playback position or complex editor states beyond simple text selection.

### Can I disable focus restoration entirely?

While there is no global toggle to disable focus preservation entirely in the core library, you can effectively prevent focus restoration by ensuring focused elements lack `id` attributes, though this is not recommended. For scroll behavior specifically, you can set `htmx.config.defaultFocusScroll = false` or use `hx-focus-scroll="false"` on individual elements to prevent the browser from scrolling when focus is restored.