> 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/internals.md).

# Plugin internals

Internal Alpine plugins for caching, theme-editor integration, prefetching, pointer effects, media players, and section updates.

The theme uses these plugins for shared behavior that does not belong to one component. They are internal and can change between versions.

## x-cache

Fills a shared cache store from JSON script tags, so section markup can hand data (products, settings) to Alpine without extra requests. Define resources with `$store.cache.addResource(name, {singleton, builder})` before the directives run.

| Directive / magic                                        | What it does                                                                      |
| -------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `x-cache.products`                                       | On a `<script>` tag. Parses the tag's JSON and upserts it into the named resource |
| `x-cache.products="expression"`                          | Evaluates the expression as the cache key and stores the JSON under it            |
| `$cache`                                                 | The store's resources: `$cache.products.get(id)`                                  |
| `resource.get(key)` / `.set(key, value)` / `.evict(key)` | Read, write, and remove single records                                            |
| `resource.populate(records)` / `.upsert(records)`        | Replace all records, or merge new ones in                                         |
| `$store.cache.reset()`                                   | Resets every resource                                                             |

A resource defined with `singleton: true` holds one record and drops the key argument: `resource.get()` / `.set(value)`. A `builder` function transforms each record on read.

## $waitForAnimations

Async magic that waits for the element's animations to start and then finish, so code can run after a transition without hard-coding its duration.

| Magic / option                      | What it does                                                                                                    |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `$waitForAnimations()`              | Resolves with the finished animations. Warns in development when none start within 50ms or finish within 2000ms |
| `{target}`                          | Waits on another element instead of `$el`                                                                       |
| `{subtree: true}`                   | Includes animations on descendants                                                                              |
| `{startTimeout}`                    | How long to wait for animations to start (`false` checks immediately)                                           |
| `{excludeSubtree: (el) => boolean}` | Skips animations inside matching elements                                                                       |
| `data-skip-animation-wait`          | Attribute form of the exclude filter, used by the default filter                                                |

## better-errors

Replaces Alpine's default error handler so expression errors stay debuggable.

| Behaviour       | What it does                                                                                 |
| --------------- | -------------------------------------------------------------------------------------------- |
| `console.error` | Logs the message together with the element and the failing expression                        |
| Rethrow         | Throws the error again in a timeout, so it still reaches `window.onerror` and error tracking |

## theme-editor

Tracks which section and block are selected in the theme editor and exposes them as magics. Every magic returns false or null outside the theme editor, so markup that binds to them is inert on the storefront.

| Magic / store                                   | What it does                                                                  |
| ----------------------------------------------- | ----------------------------------------------------------------------------- |
| `$isSelectedSection`                            | True when the section containing the element is selected                      |
| `$wasSelectedSection`                           | True when that section was the previously selected one                        |
| `$isSelectedBlock`                              | True when the element's block is the selected block                           |
| `$containsSelectedBlock`                        | True when the element's block is or contains the selected block               |
| `$isInSelectedBlockTree`                        | True when the element's block contains, is, or sits inside the selected block |
| `$wasSelectedBlock` / `$wasInSelectedBlockTree` | The same checks against the previous selection                                |
| `$selectedSection` / `$selectedBlock`           | Root element of the current selection, or null                                |
| `$prevSelectedSection` / `$prevSelectedBlock`   | Root element of the previous selection, or null                               |
| `$store.themeEditor.isInspectorActive`          | True while the editor's inspector is active                                   |

## x-screen

Runs expressions when named media queries match. The theme configures the query names at setup (`media({sm: '(max-width: 1023px)', …})`), and a `media` store keeps every match state live.

| Directive / magic          | What it does                                                  |
| -------------------------- | ------------------------------------------------------------- |
| `x-screen:sm="expression"` | Runs the expression each time the named query starts matching |
| `x-screen="expression"`    | Runs on every change of any query, with `$matches` in scope   |
| `$media.matches.sm`        | Live boolean per named query                                  |
| `$media.queries`           | The configured query strings                                  |

## x-prefetch

Prefetches pages with `<link rel="prefetch">` so a likely next navigation is instant. Each URL is fetched once per page, and nothing is fetched on a slow connection or when the visitor has data saver on. Prefetches wait for browser idle time, with a deadline so a busy main thread cannot put them off forever.

| Directive / magic                   | What it does                                                          |
| ----------------------------------- | --------------------------------------------------------------------- |
| `x-prefetch`                        | On an `<a>`. Prefetches the href when the link scrolls into view      |
| `x-prefetch="'/url'"`               | Non-anchor elements pass the URL as the expression                    |
| `.eager`                            | Prefetches on init instead of waiting for view                        |
| `.intent`                           | Also prefetches on the first mouseover or focus                       |
| `.manual`                           | No automatic prefetch; call `$prefetch` yourself                      |
| `.high` / `.low`                    | Fetch priority (defaults: high for eager and intent, low for in-view) |
| `$prefetch('/url', {priority, as})` | Prefetch by hand                                                      |

## x-async-link

Progressively enhances links to call an endpoint with `fetch` and show a pending state. Modified clicks (new tab, middle click) fall through to the browser. Dispatches `link:pending` when a request starts and `link:loaded` when it succeeds, each with the href as the event detail.

| Directive / magic                               | What it does                                                                            |
| ----------------------------------------------- | --------------------------------------------------------------------------------------- |
| `x-async-link="callback"`                       | On an `<a>`. Shorthand for `:context` plus `:link` on one element                       |
| `x-async-link:context="callback"`               | Holds the state. The callback receives the clicked href (`$href`) and returns a promise |
| `x-async-link:link`                             | An `<a>` inside the context. Intercepts plain left clicks                               |
| `$asyncLink.state`                              | `idle` or `pending`                                                                     |
| `$asyncLink.pendingHref`                        | The href of the in-flight link                                                          |
| `$asyncLink.errorMessage` / `.errorDescription` | From a 422 response body, or the status text                                            |
| `$asyncLink.reset`                              | Clears state and errors                                                                 |

## x-clipboard

Copies a string to the clipboard and tracks the result. Inside the theme editor's iframe the clipboard is blocked by policy, so the plugin simulates success there.

| Directive / magic                    | What it does                                                                               |
| ------------------------------------ | ------------------------------------------------------------------------------------------ |
| `x-clipboard`                        | The root. Holds the state                                                                  |
| `x-clipboard:target`                 | The element with the copy: an input's value, the directive expression, or the text content |
| `x-clipboard:button`                 | Copies on click and selects the target's text                                              |
| `$clipboard.save(text)`              | Copies an arbitrary string                                                                 |
| `$clipboard.state`                   | `initial`, `success` (clears after 2.5s), or `denied` (clears after 4s)                    |
| `$clipboard.isClipboardWriteAllowed` | Result of the permission check, or null before it resolves                                 |

## x-range

Manages a dual-range filter: two range sliders and two number inputs that stay in sync and never cross. When one value collides with the other, the other moves by one step of the input's `step` attribute, clamped to its limit.

| Directive / magic                           | What it does                                                                           |
| ------------------------------------------- | -------------------------------------------------------------------------------------- |
| `x-range`                                   | The root. Holds the shared min/max state                                               |
| `x-range:number-min` / `x-range:number-max` | Number inputs for the two values                                                       |
| `x-range:range-min` / `x-range:range-max`   | Range inputs for the two values                                                        |
| `.fill`                                     | On range inputs: shows the limit value when empty. Use when there are no number inputs |
| `$range.minLimit` / `.maxLimit`             | The limits read from the inputs' `min` / `max` attributes                              |
| `$range.minValue` / `.maxValue`             | The current values                                                                     |
| `$range.isDormant`                          | True while both values sit at their limits (or are unset)                              |

## x-cursor-position

Publishes where the pointer sits inside the element as the CSS custom properties `--cursor-x` and `--cursor-y`, each a percentage of the element's own size, so styles can follow the cursor without their own JavaScript. Values are driven by motion values, so a burst of pointer moves settles into one style write per frame. Does nothing on a touch-only device, so styles should carry a fallback: `var(--cursor-x, 50%)`.

| Directive           | What it does                                                                   |
| ------------------- | ------------------------------------------------------------------------------ |
| `x-cursor-position` | Writes `--cursor-x` / `--cursor-y` on the element while the pointer is over it |

## x-gradient-follow

Springs a gradient's centre toward the pointer as the CSS custom properties `--btn-gradient-x` and `--btn-gradient-y`, and drifts back to a resting point off the top-left corner (-10%) when the pointer leaves. Does nothing on a touch-only device, so declare the resting position as the CSS fallback: `var(--btn-gradient-x, -10%)`.

| Directive           | What it does                                                                   |
| ------------------- | ------------------------------------------------------------------------------ |
| `x-gradient-follow` | Writes the spring-eased `--btn-gradient-x` / `--btn-gradient-y` on the element |

## x-custom-cursor

A custom cursor that follows the mouse with spring physics, snaps to snap points, and supports named cursor variants. Every directive does nothing on a touch-only device; `$customCursor` still reads safely there.

| Directive / magic                                            | What it does                                                                                                                                 |
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `x-custom-cursor`                                            | The root. Tracks mouse enter, leave, and move                                                                                                |
| `x-custom-cursor:element`                                    | A cursor element, positioned with spring-animated transforms. An expression names it (`="plus"`); unnamed elements act as the default cursor |
| `.mass.<n>` / `.stiffness.<n>` / `.damping.<n>`              | Spring tuning on `:element`                                                                                                                  |
| `x-custom-cursor:snap-point`                                 | Snaps the cursor to this element's centre on hover                                                                                           |
| `data-custom-cursor="<name>"`                                | Shows the named cursor while hovering this descendant                                                                                        |
| `data-hide-custom-cursor`                                    | Hides all cursors while hovering this descendant                                                                                             |
| `data-active` / `data-hidden` / `data-edge` / `data-snapped` | Reactive attributes set on cursor elements for styling                                                                                       |
| `$customCursor.isActive` / `.isSnapped` / `.isHidden`        | The cursor's current state                                                                                                                   |
| `$customCursor.targetPosition`                               | Current target `{x, y}` in viewport coordinates                                                                                              |
| `$customCursor.snap(el)` / `.unsnap()`                       | Snap and unsnap by hand                                                                                                                      |

## x-grid-inspector

Measures a CSS grid's computed rows, columns, and bounding box and keeps the numbers fresh through resize and DOM changes, so an overlay can draw the grid. Development tooling only.

| Directive                 | What it does                                                                                                                    |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `x-grid-inspector`        | The root. Exposes `debugGridRows`, `debugGridCols`, and `debugGridBounds`, plus `observeSourceGrid()` / `unobserveSourceGrid()` |
| `x-grid-inspector:source` | The grid element to measure                                                                                                     |

## Utility magics

The `utils` plugin registers small standalone magics.

| Magic                                                           | What it does                                                       |
| --------------------------------------------------------------- | ------------------------------------------------------------------ |
| `$debounce(fn, ms)`                                             | Alpine's debounce                                                  |
| `$throttle(fn, ms)`                                             | Alpine's throttle                                                  |
| `$yieldToMain()`                                                | Resolves after yielding to the main thread, to break up long tasks |
| `$load.script(url)`                                             | Injects a script tag once and resolves when it loads               |
| `$cookie.write(name, value)` / `.read(name)` / `.destroy(name)` | Browser cookie access                                              |

## x-marquee-track (Analog)

Couples the marquee's position to the visitor's scroll exactly while scrolling, then coasts back to the loop's cruising speed when scrolling stops, the way a flywheel would. Sits on the same element as `x-marquee` and reads its internals.

| Directive / behaviour    | What it does                                                                   |
| ------------------------ | ------------------------------------------------------------------------------ |
| `x-marquee-track`        | Attaches the scroll drive to the marquee on the same element                   |
| `x-marquee.speed.-N`     | A negative marquee speed reverses the scroll drive, so paired marquees diverge |
| `prefers-reduced-motion` | The directive does nothing, since the marquee itself does not animate there    |

## x-before-after-slider-reveal (Analog)

Wraps `x-slider-reveal` for the before/after section: forwards its modifiers and enables the reveal's scroll drive only while the section is in view.

| Directive                      | What it does                                                                                                                            |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| `x-before-after-slider-reveal` | Binds `x-slider-reveal` with the same modifiers and toggles `$sliderReveal.enableScroll()` / `.disableScroll()` on section intersection |

## x-vimeo-video (Analog)

Loads and controls the Vimeo iframe player for a video element.

| Directive / state                                                  | What it does                                          |
| ------------------------------------------------------------------ | ----------------------------------------------------- |
| `x-vimeo-video="videoId"`                                          | Loads the player for the numeric Vimeo id             |
| `.lazy`                                                            | Waits for `loadPlayer()` instead of loading on init   |
| `.autoplay` / `.loop`                                              | Playback options passed to the player                 |
| `state`                                                            | `initial`, `loading`, `ready`, `playing`, or `paused` |
| `loadPlayer()` / `playVideo()` / `pauseVideo()` / `unloadPlayer()` | Player control methods on the component               |

## x-youtube-video (Analog)

Loads and controls the YouTube iframe player for a video element. Same surface as `x-vimeo-video` with a string video id.

| Directive / state                                                  | What it does                                          |
| ------------------------------------------------------------------ | ----------------------------------------------------- |
| `x-youtube-video="videoId"`                                        | Loads the player for the YouTube id                   |
| `.lazy`                                                            | Waits for `loadPlayer()` instead of loading on init   |
| `.autoplay` / `.loop`                                              | Playback options passed to the player                 |
| `state`                                                            | `initial`, `loading`, `ready`, `playing`, or `paused` |
| `loadPlayer()` / `playVideo()` / `pauseVideo()` / `unloadPlayer()` | Player control methods on the component               |

## x-shipping-progress (Analog)

Converts the free-shipping threshold after Liquid renders the cart. Liquid exposes the cart currency but not `Shopify.currency.rate`, so it cannot calculate the converted amount remaining, progress width, or success state.

The snippet adds `x-shipping-progress` only when the store and cart currencies differ and the threshold needs conversion. The directive uses [`$convertCurrency`](/reference/alpine/currency.md) for the threshold and `$formatCurrency` for the amount remaining.

| Surface                                                            | What it does                                                        |
| ------------------------------------------------------------------ | ------------------------------------------------------------------- |
| `x-shipping-progress`                                              | Converts the threshold and updates the bar after Alpine initializes |
| `data-limit`                                                       | The unconverted threshold in the store's major currency unit        |
| `data-cart-total`                                                  | The cart total in cents                                             |
| `data-shipping-amount`                                             | Receives the converted and formatted amount remaining               |
| `data-shipping-bar`                                                | Receives the converted `--limit-percent` value                      |
| `data-shipping-progress-message` / `data-shipping-success-message` | Show or hide after the converted comparison                         |

Cart updates morph new Liquid HTML into the page. A `MutationObserver` reapplies the conversion after each morph and disconnects during its own writes. The directive is registered in the theme bundle, but it observes the DOM only on a bar that needs conversion.

## x-eager-load-images (Analog)

On viewport intersection, upgrades lazy `<img>` descendants to `loading="eager"`. The first image, visible carousel slides, and anything passing `checkVisibility()` also get `fetchpriority="high"`. Intended for slider tracks where lazy-loading the visible slides delays paint as the track scrolls into view.

| Directive             | What it does                                                                   |
| --------------------- | ------------------------------------------------------------------------------ |
| `x-eager-load-images` | Upgrades descendant images once, during idle time after the element intersects |

## x-link-area (Analog)

Makes a container act as a clickable link area by delegating clicks to a real `<a>` descendant, so plain text and prices inside a card navigate too. The `<a>` keeps keyboard navigation, the context menu, and screen reader access. Clicks on interactive descendants and clicks made while text is selected are left alone; modified clicks and middle clicks open a new tab.

| Directive            | What it does                                                      |
| -------------------- | ----------------------------------------------------------------- |
| `x-link-area`        | The container. Delegates clicks to the first `a[href]` descendant |
| `x-link-area:anchor` | Marks a specific `<a>` as the target link                         |

## x-preloader (Analog)

Preloads section-rendering responses for descendant elements carrying `data-preload-url`, through `$sectionApi.load`, so a filter or pagination click renders instantly. Each URL loads once per page, and nothing loads on a slow connection or with data saver on.

| Directive                 | What it does                                                                                                     |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `x-preloader`             | `.intersect` and `.interact` together (the default when no trigger modifier is given)                            |
| `.intersect`              | Batch-preloads every URL at low priority once the section has intersected                                        |
| `.interact`               | Preloads a URL at high priority when its element is clicked, touched, focused, or hovered                        |
| `.attention`              | Below 768 px, preloads every URL at low priority once the root crosses the central 40% of the viewport           |
| `.all`                    | With `.interact`, preloads every URL; the directly interacted URL is high priority and the rest are low priority |
| `x-preloader="sectionId"` | The section to render for each URL (defaults to the element's own section)                                       |

When a directly interacted element also carries `data-preload-image-src`, `data-preload-image-srcset`, and `data-preload-image-sizes`, the preloader starts a high-priority decode before loading its Section API response. It also warms the first image found in a completed response. Product cards combine `.interact.attention.all`; see [Product card architecture](/developer-platform/product-cards.md).

## x-product-complementary (Analog)

Fetches complementary product recommendations from Shopify's product recommendations endpoint when the element first nears view, then renders them into it. The element is its own nested `x-section-api` controller and render region, so the request gets the controller's cache, idle scheduling, and latest-wins handling without touching the product section's state. The request names the enclosing section, since that is what the endpoint renders.

The element carries `data-morph-update="attributes"` and `data-product-id`. The section owns the attributes, so the id follows every product switch and a changed id refetches after `theme:section:update`. The controller owns the children, so a section update keeps the loaded cards. A block that has not entered view only takes the new id and fetches it when it enters view.

| Directive                 | What it does                                                                                                                                                                                                  |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `x-product-complementary` | Loads recommendations for `data-product-id` through the element's own controller during idle time after the element intersects, and again when the enclosing section's `theme:section:update` changes that id |
| `.limit.<n>`              | How many recommendations to request (default 1)                                                                                                                                                               |

## x-section-sibling-update (Analog)

Defers section updates after a product sibling change (`theme:sibling:change`) so a page full of sections does not re-render at once. A pending section updates when it nears the viewport (300px margin), when the user interacts, or one-by-one after a 2s timeout with a yield between each.

| Directive / behaviour      | What it does                                                                        |
| -------------------------- | ----------------------------------------------------------------------------------- |
| `x-section-sibling-update` | Marks the section for deferred sibling updates                                      |
| `no-sibling-autoupdate`    | Schema class opt-out: the event still fires, but the section handles its own update |

## x-slideshow-mobile-swipe (Analog)

Drives the slideshow's swipe transition on touch devices: exposes the pan progress as the CSS custom property `--slideshow-mobile-swipe-progress` and advances the slideshow when the swipe passes a quarter of the element's width. Does nothing unless `$media.matches.touch`.

| Directive                  | What it does                                                                                                               |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `x-slideshow-mobile-swipe` | Attaches `x-pan`, maps pan distance to the progress property, and calls `$slideshow.prev()` / `.next()` past the threshold |
