> For the complete documentation index, see [llms.txt](https://analog.groupthought.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://analog.groupthought.com/reference/alpine/state-structure.md).

# State & structure internals

Internal Alpine modules for per-section data, morphing, shared selection, overflow, focus, and deferred initialization.

These modules manage state shared by the theme's Alpine components. They are internal and can change between versions.

## section

Magics for the closest ancestor shopify-section element. `$sectionData` stores state in a global store keyed by section id, so it survives the theme editor re-rendering the section.

| Magic          | What it does                                   |
| -------------- | ---------------------------------------------- |
| `$sectionEl`   | The closest ancestor shopify-section element   |
| `$sectionId`   | Its id without the `shopify-section-` prefix   |
| `$sectionData` | Get and set per-section data in a global store |

## morph

The morph engine behind [x-section-api](/reference/alpine/section-api.md): wraps Alpine.morph with the theme's `data-morph-*` attribute handling (documented on that page) and captures the focused element before the morph so focus can be restored afterwards.

| Export / event                 | What it does                                                                                                                                                                                        |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `morph(rootEl, html, config?)` | Morphs `html` into `rootEl`, merging the passed config with the default attribute handling. The root element itself is never replaced, only its children                                            |
| `morph:sync-control`           | Bubbling event on a `data-morph-form-state="server"` control whose live state the morph changed, with `{previousValue, value}` in the detail. `morph:sync` remains a deprecated compatibility event |

A descendant with `data-morph-view-transition` uses an element-scoped transition. Its incoming and connected images decode before the new frame is exposed. Browsers without scoped transitions keep the old image painted until the replacement is ready; the morph never falls back to a document transition. Product cards use this only on their media region. See [Product card architecture](/developer-platform/product-cards.md).

## x-list-state

Tracks a selected index (or a Set of indices) for a list of registered elements. The hotspots use it under the scope name `hotspots`.

| Directive / magic                     | What it does                                                                                                                                                 |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `x-list-state`                        | Creates single-select state on the root                                                                                                                      |
| `x-list-state.multiple`               | A Set of selected indices instead of one index                                                                                                               |
| `x-list-state="{ selectedIndex: 0 }"` | Options object: `selectedIndex`, `selectedIndices`, `scope`                                                                                                  |
| `x-list-state.as.name`                | Exposes the state as `$name` instead of `$listState`                                                                                                         |
| `x-list-state:item`                   | Registers the element; unregisters on destroy                                                                                                                |
| `x-list-state:item.for.name`          | Registers into the scoped state `$name`                                                                                                                      |
| `$listState`                          | `selectedIndex` / `selectedIndices`, `totalItems`, `isSelected(i)`, `select(i)`, `deselect(i)`, `toggle(i)`, `indexOf(el)`, `register(el)`, `unregister(el)` |

## x-ensemble

Binds a slideshow, accordion, tabs, carousel, and hotspots to one selected index. Stepping any member steps them all. The index lives in `$sectionData`, so a theme editor reload keeps the place.

| Directive / magic                                                    | What it does                                                                                                                                      |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `x-ensemble`                                                         | The root. Holds the shared `$ensemble` state                                                                                                      |
| `x-ensemble.autoplay`                                                | The ensemble plays while in view and not focused                                                                                                  |
| `x-ensemble:slideshow` / `:tabs` / `:disclosure-group` / `:carousel` | Binds the underlying directive's index to the shared one; modifiers pass through                                                                  |
| `x-ensemble:hotspots`                                                | Binds an x-list-state scoped as `hotspots`                                                                                                        |
| `$ensemble`                                                          | `selectedIndex`, `totalSlides`, `isPlaying`, `autoplay`, `hasFocus`, `isInView`, `titleEl`, `members`, `registerMember(id, el)`, `getPeerIds(id)` |

A member nested inside another member opts out of the shared state and runs as its plain underlying directive. Focus or pointer-down anywhere in the root sets `hasFocus`, which pauses autoplay.

## x-overflow

Watches boundary elements with an IntersectionObserver and reports whether they are scrolled out of view, for edge masks and prev/next button states.

| Directive / magic                      | What it does                                                                                                  |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `x-overflow`                           | The root; acts as viewport and container unless overridden                                                    |
| `x-overflow.autoregister`              | Registers the container's first and last children as `start` and `end`; a MutationObserver keeps them current |
| `x-overflow.ratio`                     | Boundaries report visibility as 0.0–1.0 instead of booleans                                                   |
| `x-overflow:viewport`                  | The scrollable element                                                                                        |
| `x-overflow:container`                 | The element whose children `.autoregister` watches                                                            |
| `x-overflow:{name}`                    | Registers a named boundary element                                                                            |
| `$overflow.{name}`                     | True while the boundary is not fully visible (or the ratio)                                                   |
| `$overflow.registerBoundary(name, el)` | Registers a boundary from code                                                                                |
| `data-overflow-ignore`                 | Excludes a child from autoregistration                                                                        |

## page-layout

The `page` store tracks scroll and viewport state; `x-header-stack` stacks sticky header rows and feeds the header metrics into the store and body CSS variables. Scroll tracking starts on the page's first scroll since the defaults are already right at the top.

| Store / directive                                                              | What it does                                                                                                                                                                                                                                                                                                                                           |
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `$store.page.scrollY` / `.scrollProgress` / `.scrollDirection` / `.isScrolled` | Scroll position in px, progress 0–1, `'up'` / `'down'` / null                                                                                                                                                                                                                                                                                          |
| `$store.page.visualViewportHeight`                                             | window\.visualViewport height, debounced                                                                                                                                                                                                                                                                                                               |
| `$store.page.headerHeight` / `.headerBottom` / `.headerStickyOffset`           | Header stack metrics. Only `headerHeight` is mirrored to a body var, `--header-height`. Bind `--header-bottom` or `--header-sticky-offset` from the store on the element that needs it, since a custom property write on `body` recomputes the style of every element on the page                                                                      |
| `$store.page.headerScrollUpHeight`                                             | Height of every row that sticks while the page scrolls up: the `always` and `scroll` rows. Subtract it from a scroll target so a smooth scroll up lands below the header                                                                                                                                                                               |
| `x-header-stack`                                                               | The stack root; carries `data-header-stuck` while any row is stuck                                                                                                                                                                                                                                                                                     |
| `x-header-stack:header.always`                                                 | The row is always sticky at its stacked offset                                                                                                                                                                                                                                                                                                         |
| `x-header-stack:header.scroll`                                                 | Sticky only when scrolling up; hides when scrolling down                                                                                                                                                                                                                                                                                               |
| `$scrollIntoView(el = $el, { behavior })`                                      | Scrolls the page so the element sits in view below the rows that stick on a scroll up. The top aligns to the header line when it is hidden, when the element is too tall to fit below the header, or when the element spans the viewport. The bottom aligns to the viewport edge when the element sits below. An element already in view does not move |
| `theme:scroll:up` / `theme:scroll:down`                                        | Document events with the scroll position in the detail                                                                                                                                                                                                                                                                                                 |

## keyboard-active

Tracks whether the user is navigating with the keyboard: a Tab press that moves focus turns it on, a mousedown turns it off. `theme.liquid` binds `:data-keyboard-active="$isKeyboardActive"` on the body to gate focus rings.

| Magic               | What it does                                                                        |
| ------------------- | ----------------------------------------------------------------------------------- |
| `$isKeyboardActive` | True after a Tab press moves focus, until the next mousedown                        |
| `$hasKeyboardFocus` | True when keyboard navigation is active and the element is `document.activeElement` |

## x-focus-state

Runs an expression when the element gains and loses focus, instead of a `@focus`/`@blur` pair. The blur listener only binds after a focus, so an idle element costs one listener.

| Directive / magic                      | What it does                                   |
| -------------------------------------- | ---------------------------------------------- |
| `x-focus-state="hasFocus = $hasFocus"` | Runs the expression on focus and again on blur |
| `$hasFocus`                            | True between focus and blur                    |

## x-focusin-state

The same pattern for containment: runs an expression when focus enters the element and when it leaves. A focusout that lands on a descendant is ignored, so tabbing between children never flickers the state.

| Directive / magic                      | What it does                                                             |
| -------------------------------------- | ------------------------------------------------------------------------ |
| `x-focusin-state="open = $hasFocusIn"` | Runs the expression on focusin and on a focusout that leaves the element |
| `$hasFocusIn`                          | True while the element contains focus                                    |

## x-focus-match

Type-ahead focus for lists: typing a printable character focuses the next item whose text starts with it. Used in listboxes and menus.

| Directive / magic                            | What it does                                                     |
| -------------------------------------------- | ---------------------------------------------------------------- |
| `x-focus-match`                              | The root; handles the keydown                                    |
| `x-focus-match="expression"`                 | Supplies the focusable elements                                  |
| `x-focus-match:list`                         | The element whose focusables are searched (defaults to the root) |
| `$focusMatch.setFocusByFirstCharacter(char)` | The same jump, from code                                         |

## lazy-alpine

Defers Alpine initialization for marked subtrees: they are skipped during the initial tree walk and wake when scrolled near, interacted with, or after a fallback timeout. Interaction listeners run in the capture phase on the document, so a tap on a lazy root's own button initializes it before the click handler runs; a button whose `aria-controls` names the root (or anything inside it) wakes it too.

| Attribute                                     | What it does                                                                                   |
| --------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `data-lazy-alpine`                            | Marks a lazy root                                                                              |
| `data-lazy-alpine="desktop"`                  | Media-gated: inert while the condition (a named condition or a raw media query) does not match |
| `data-lazy-alpine-on="visible interact idle"` | Which wakes apply (these three are the default); `immediate` and `idle:2000` are also accepted |
| `data-lazy-alpine-trigger="selector"`         | Interactions inside the root's closest matching ancestor wake it                               |
| `data-lazy-alpine-module="name"`              | Imports the module before the root initializes                                                 |
| `data-alpine-loading`                         | Present while the root is still asleep; removed after init                                     |

## x-product-card (Analog)

Hover, focus, and quick-add state for a product card, exposed as `$productCard`.

| Directive / magic                                     | What it does                                                                                                                                                 |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `x-product-card`                                      | The root; tracks hover (with 150/200ms enter/leave delays), focus within, selection hover, and whether media may load (first touch, focus, or pointer enter) |
| `x-product-card:media`                                | Hover tracking for the media area                                                                                                                            |
| `x-product-card:quick-add-button` / `:quick-add-menu` | The quick-add variant menu; closes on mouse leave, outside click, or when the card leaves the viewport                                                       |
| `$productCard`                                        | `isActive`, `isHoveringMedia`, `isHoveringSelections`, `isQuickAddMenuActive`, `isQuickAddMenuVisible`, `canLoadMedia`                                       |

The menu stays visible while the product form is submitting, so the shopper sees the loading or error state before it closes. Product content is server-owned and updates through the card's Section API controller; `x-product-card` does not patch title, price, media, swatches, or actions.

## x-section-tabs (Analog)

Wraps x-tabs and stores `selectedIndex` in `$sectionData`, so the open tab survives theme editor section morphing.

| Directive        | What it does                                                                        |
| ---------------- | ----------------------------------------------------------------------------------- |
| `x-section-tabs` | x-tabs with its `selectedIndex` in `$sectionData`; modifiers pass through to x-tabs |

## x-section-disclosure-group (Analog)

Wraps x-disclosure-group and stores the expanded state in `$sectionData`, so open accordions survive theme editor section morphing.

| Directive                             | What it does                                              |
| ------------------------------------- | --------------------------------------------------------- |
| `x-section-disclosure-group`          | x-disclosure-group with `expandedIndex` in `$sectionData` |
| `x-section-disclosure-group.multiple` | Multi-select; stores `expandedIndices` (a Set) instead    |

## section-intersection-manager (Analog)

Watches every shopify-section and tracks when it counts as in view. While the user scrolls, a section must reach the middle 40% of the viewport or be fully visible; when scrolling stops, any visible part counts, so nothing on screen sits blank.

| Surface                                         | What it does                                                                                      |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `data-is-intersecting` / `data-has-intersected` | Set on the section element                                                                        |
| `$store.sections`                               | `intersecting` and `intersected` Sets, `isIntersecting(id)`, `hasIntersected(id)`                 |
| `theme:section:enter` / `theme:section:leave`   | Bubble from the section with `{sectionId}`; leave only fires when the section is fully off screen |

## x-section-related (Analog)

Related and recently-viewed product tabs that load their content through [x-section-api](/reference/alpine/section-api.md), each into its own morph root, so the two parallel requests never overwrite each other.

| Directive / magic                                            | What it does                                                                                             |
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
| `x-section-related="productId"`                              | The root; holds the update helpers and reloads on `theme:sibling-section:should-update`                  |
| `x-section-related:related-products-tab.limit.6`             | Loads Shopify product recommendations into the `related-products` morph root                             |
| `x-section-related:recent-products-tab.limit.10.threshold.3` | Loads recently viewed products through the search API into `recent-products`; hidden below the threshold |
| `$sectionRelated`                                            | `canShowRecentProducts`, `hasRecentProductsTab`                                                          |

## x-section-slideshow (Analog)

Wraps x-slideshow and stores `currentSlideIndex` in `$sectionData`, so the current slide survives theme editor section morphing.

| Directive             | What it does                                                                                      |
| --------------------- | ------------------------------------------------------------------------------------------------- |
| `x-section-slideshow` | x-slideshow with its `currentSlideIndex` in `$sectionData`; modifiers pass through to x-slideshow |

## shopify-model (Analog)

Extends x-shopify-media with a `model` value for 3D product models: loads Shopify's model-viewer-ui on demand and manages the play state.

| Surface                                         | What it does                                                                                               |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `x-shopify-media:model`                         | The component; `initModel()` loads model-viewer-ui                                                         |
| `state`                                         | `'initial'` / `'loading'` / `'ready'` / `'playing'` / `'paused'`, with `hasLoaded` and `isLoading` getters |
| `getPointerCoordinates()` / `togglePlayPause()` | A tap (under 10px of movement) toggles play/pause; a drag rotates the model without toggling               |
| `theme:model:loaded`                            | Dispatched when the viewer is ready                                                                        |
