Alpine.js x-resize: Responsive Components Without ResizeObserver Code
AI generated
x-data
Alpine
Alpine.js · x-resize · ResizeObserver · Hyvä
Alpine.js x-resize:
Responsive Components Without ResizeObserver Code

Viewport breakpoints are not enough: components need to react to their own size, not to the window width. x-resize brings the ResizeObserver into Alpine.js declaratively, without a single line of boilerplate code.

10 min read x-resize · ResizeObserver · Container Queries · Alpine.js Plugin Alpine.js 3.x · Hyvä Themes · Magento 2

1. The Problem With window.resize and Viewport Breakpoints

Responsive design is traditionally implemented through viewport breakpoints: when the browser window is narrower than 768 pixels, the layout switches. That works well for page structure and global layouts, but it fails for reusable components. A product card that shows four products side by side in the main column needs to show two in a narrower sidebar, even though the viewport is identical. The viewport tells the component nothing about its own available width.

The classic workaround was window.addEventListener('resize', handler). This approach has several problems: it reacts to window size changes, not element size changes. A component that grows or shrinks because of a sidebar toggle, an accordion, or a dynamic layout does not trigger a resize event. The handler has to query the element's width itself via getBoundingClientRect(), which forces a layout reflow. The result is fragile, hard to test code that requires special cases for every possible layout context.

The native ResizeObserver solves exactly this problem: it observes size changes of individual elements, regardless of the cause. But using it directly requires boilerplate: instantiate the observer, register it, observe during cleanup, and disconnect it when the component is destroyed. x-resize encapsulates this logic entirely in a single Alpine.js directive.

2. The Native ResizeObserver and Its Boilerplate

To understand what x-resize does under the hood, it helps to look at the native ResizeObserver. The browser API is conceptually simple: you create an observer with a callback, call observer.observe(element), and the callback fires whenever the element's size changes. The callback receives an array of ResizeObserverEntry objects with a contentRect that holds the new size.

The boilerplate effort in Alpine.js without x-resize is considerable: create and start the observer in x-init, react to data in x-effect or via $refs, and disconnect the observer again in the cleanup hook ($destroy or a lifecycle hook). If you forget to disconnect it, you get a memory leak, because the observer holds a reference to the element and it cannot be released by the garbage collector. x-resize handles this entire lifecycle fully automatically.


// WITHOUT x-resize: manual ResizeObserver boilerplate in Alpine.js
<div x-data="{
  width: 0,
  height: 0,
  observer: null,
  init() {
    this.observer = new ResizeObserver(([entry]) => {
      this.width = Math.round(entry.contentRect.width)
      this.height = Math.round(entry.contentRect.height)
    })
    this.observer.observe(this.$el)
  },
  destroy() {
    // Must disconnect manually, or you get a memory leak!
    if (this.observer) this.observer.disconnect()
  }
}">
  <p x-text="`Width: ${width}px, Height: ${height}px`"></p>
</div>

// WITH x-resize: zero boilerplate, lifecycle handled automatically
<div
  x-data="{ width: 0, height: 0 }"
  x-resize="width = $width; height = $height"
>
  <p x-text="`Width: ${width}px, Height: ${height}px`"></p>
</div>

3. The x-resize Plugin: Installation and Basic Principle

The x-resize plugin, like x-mask, is an official Alpine.js plugin and is installed the same way. It must be registered with Alpine.plugin(resize) before Alpine.start(). In Hyvä projects, the recommended approach is an npm install and inclusion in the central Tailwind build entrypoint. The plugin is minimal and does not add any meaningful bundle size.

The basic principle of x-resize is simple: the directive is placed on an element and given a JavaScript expression as its value. That expression runs every time the element's size changes. Two magic variables are available inside the expression: $width and $height, which hold the element's current width and height in pixels. These values can be written directly into Alpine data properties or used to compute breakpoints.

4. x-resize Basics: Reading width and height

The simplest use of x-resize is observing the current element size and storing it in Alpine data. x-resize="width = $width; height = $height" on a container element ensures that width and height in the Alpine data always match the current values. These values are reactive: any x-text, x-show, or x-bind that depends on them updates automatically.

One important detail: x-resize fires once initially when the component is initialized. That means the size is known right from the start, not only after the first manual resize. For components that need to render their layout correctly from the very first paint (not only after the first window resize), that is essential. The callback is also debounced, so that rapid, continuous size changes (for example when dragging the window edge) do not trigger it on every single frame.


// Basic x-resize usage: track element size reactively
<div
  x-data="{
    containerWidth: 0,
    get breakpoint() {
      if (this.containerWidth < 400) return 'xs'
      if (this.containerWidth < 640) return 'sm'
      if (this.containerWidth < 768) return 'md'
      return 'lg'
    },
    get columns() {
      return { xs: 1, sm: 2, md: 3, lg: 4 }[this.breakpoint]
    }
  }"
  x-resize="containerWidth = $width"
  class="w-full"
>
  <!-- Layout adapts to container width, not viewport width -->
  <p class="text-xs text-slate-500 mb-4">
    Container: <span x-text="containerWidth + 'px'"></span>
    · Breakpoint: <span x-text="breakpoint"></span>
    · Spalten: <span x-text="columns"></span>
  </p>
  <div :class="`grid gap-4 grid-cols-${columns}`">
    <template x-for="i in 8" :key="i">
      <div class="bg-teal-100 rounded p-4 text-center text-sm font-medium"
           x-text="`Produkt ${i}`"></div>
    </template>
  </div>
</div>

5. Component Breakpoints Without CSS Container Queries

CSS Container Queries are a modern browser feature that enables breakpoints at the container level, similar to what x-resize does for JavaScript logic. Both approaches are complementary, not competing. CSS Container Queries control pure CSS adjustments such as font sizes, spacing, and colors, while x-resize controls JavaScript logic and DOM structure: which component is displayed, how many columns a grid has, whether an accordion or tabs are used.

For teams that still need to target older browsers, or that need more complex JavaScript logic based on the container size, x-resize is the more reliable solution. The breakpoints are defined as computed getters in Alpine data, which makes them fully testable and inspectable. A getter such as get isWide() { return this.containerWidth >= 640 } is clearer than a CSS selector and can be reused in more complex decision trees.

6. Responsive Tables With x-resize

Tables are one of the hardest elements to handle in responsive design. A data table with eight columns cannot be rendered correctly at 320 pixels: horizontal scrolling is unsatisfying, and truncated text becomes unreadable. The standard pattern in modern web applications is converting the table into a card list on small screens, where each row becomes a card with the column label and value stacked underneath each other.

With x-resize, this switch can be controlled directly at the container level. When the width of the table container drops below a threshold, the Alpine component switches from table layout to card layout. That is more precise than a viewport breakpoint, because the table reacts correctly whether it is rendered in a wide main column or a narrow sidebar. The template contains both layouts, and x-show controls which one is active.


<!-- Responsive table: switches to card layout when container is narrow -->
<div
  x-data="{
    containerWidth: 0,
    get useCardLayout() { return this.containerWidth < 560 },
    orders: [
      { id: '2026-001', date: '10.05.2026', product: 'Alpine.js Kurs', amount: '49,00 €', status: 'Bezahlt' },
      { id: '2026-002', date: '09.05.2026', product: 'Hyvä Lizenz',    amount: '199,00 €', status: 'Offen' },
      { id: '2026-003', date: '08.05.2026', product: 'DevOps Paket',   amount: '349,00 €', status: 'Bezahlt' }
    ]
  }"
  x-resize="containerWidth = $width"
>
  <!-- Table layout (wide containers) -->
  <table x-show="!useCardLayout" class="w-full text-sm border-collapse">
    <thead class="bg-slate-100">
      <tr>
        <th class="text-left p-3 font-semibold">Bestell-Nr.</th>
        <th class="text-left p-3 font-semibold">Datum</th>
        <th class="text-left p-3 font-semibold">Produkt</th>
        <th class="text-left p-3 font-semibold">Betrag</th>
        <th class="text-left p-3 font-semibold">Status</th>
      </tr>
    </thead>
    <tbody>
      <template x-for="o in orders" :key="o.id">
        <tr class="border-t border-slate-200">
          <td class="p-3" x-text="o.id"></td>
          <td class="p-3" x-text="o.date"></td>
          <td class="p-3" x-text="o.product"></td>
          <td class="p-3 font-semibold" x-text="o.amount"></td>
          <td class="p-3"><span class="px-2 py-1 rounded text-xs font-bold bg-teal-100 text-teal-700" x-text="o.status"></span></td>
        </tr>
      </template>
    </tbody>
  </table>

  <!-- Card layout (narrow containers) -->
  <div x-show="useCardLayout" class="space-y-3">
    <template x-for="o in orders" :key="o.id">
      <div class="border border-slate-200 rounded-xl p-4 text-sm">
        <div class="flex justify-between mb-2">
          <span class="font-semibold" x-text="o.id"></span>
          <span class="px-2 py-1 rounded text-xs font-bold bg-teal-100 text-teal-700" x-text="o.status"></span>
        </div>
        <p class="text-slate-600" x-text="o.product"></p>
        <div class="flex justify-between mt-2 text-xs text-slate-500">
          <span x-text="o.date"></span>
          <span class="font-semibold text-slate-800" x-text="o.amount"></span>
        </div>
      </div>
    </template>
  </div>
</div>

A typical Hyvä layout has a main area and an optional sidebar. When the sidebar is shown, the main area becomes narrower, and a product list that previously had four columns needs to switch to three or two columns. A viewport breakpoint has no idea whether the sidebar is open or closed. x-resize knows, because it observes the current width of the container in real time.

The pattern is simple: the product list component has x-resize tracking the container width. A getter computes the number of columns. When the user shows or hides the sidebar via a button, the width of the product list container changes, x-resize fires, the column count is recalculated, and the grid adapts automatically, without the product list ever needing to know that a sidebar exists. That is true component encapsulation.

8. Performance: Using ResizeObserver Correctly

ResizeObserver callbacks fire synchronously before the browser's next paint. That is powerful, but it can cause performance problems if the callback itself causes DOM changes that trigger further size changes, a so called resize loop. Alpine.js and x-resize do not prevent this automatically. Developers need to make sure that the x-resize expression does not perform direct DOM manipulations that alter the observed element's size.

In practice, the most common case is unproblematic: x-resize only writes to Alpine data properties, and Alpine updates the DOM asynchronously through its own reactivity mechanism. That does not lead to synchronous resize loops. It becomes problematic if you set $el.style.height directly inside the x-resize expression, which can immediately trigger another resize event. The correct approach: write the size into data, and control layout through :class or :style bindings.

9. x-resize vs. CSS Container Queries vs. window.resize

All three approaches solve the problem of component dependent responsiveness, but at different levels and with different strengths. CSS Container Queries are a pure CSS solution, need no JavaScript, and are performant, they are suitable for any layout adjustment that can be expressed in CSS. x-resize is the JavaScript extension: it controls logical decisions, DOM structure, and Alpine state. window.resize is the approach of the past, which no longer has a place in modern Alpine.js projects.

Approach Reacts To JavaScript Needed? Strength
window.resize Window width Yes (lots of boilerplate) Outdated, imprecise
CSS Container Queries Container width No Pure CSS solution, performant
x-resize (Alpine) Element size Alpine.js (declarative) JS logic, DOM structure, no boilerplate
Native ResizeObserver Element size Yes (lots of boilerplate) Maximum control, high effort

The recommendation for Hyvä projects is a combined approach: CSS Container Queries for layout adjustments that can be expressed purely in CSS, and x-resize for anything involving Alpine.js state or DOM structure. Both can be active on the same element at the same time, they do not interfere with each other. window.resize no longer has a place in new components.

Mironsoft

Alpine.js · Hyvä Themes · Magento 2 Frontend Development

Components that work in every layout?

We build Hyvä components that respond to their own width, for product galleries, data tables, and filter bars that render correctly in every sidebar and every main area.

Container Awareness

Components that respond to their own width, not to the viewport

Responsive Tables

Automatic switching between table view and card layout depending on available width

Layout Refactoring

Replace window.resize listeners with x-resize: more maintainable, more correct, no boilerplate

10. Summary

Alpine.js x-resize is the declarative answer to one of the hardest challenges in responsive design: components that need to react to their own size, not to the viewport. The plugin fully encapsulates the native ResizeObserver, including lifecycle management, the initial call, and disconnecting on destroy. The result is components that react correctly in every sidebar configuration, every grid context, and every layout change, without window.resize handlers or manual observer management.

The most important difference from CSS Container Queries: x-resize controls JavaScript logic and DOM structure, not CSS properties. For breakpoints that only involve visual adjustments (font size, spacing, colors), Container Queries are the right choice. For breakpoints that change Alpine state, show and hide DOM elements, or calculate column counts, x-resize is indispensable. In modern Hyvä projects, both work together.

Alpine.js x-resize: The Essentials at a Glance

Magic Variables

$width and $height hold the current element size in pixels. Available inside the x-resize expression, which runs on every size change.

Automatic Lifecycle

x-resize starts on initialization, fires once initially, and disconnects the observer automatically when the component is destroyed. No manual cleanup needed.

No Resize Loops

Only change Alpine data inside the x-resize expression. No direct DOM manipulations that alter the element size, otherwise a resize loop can occur.

Combining With CSS

CSS Container Queries for visual adjustments, x-resize for JavaScript logic. Both can be active on the same element without interference.

11. FAQ: Alpine.js x-resize

1What is Alpine.js x-resize?
An official Alpine.js plugin that makes the native ResizeObserver available declaratively, with $width and $height as magic variables, no boilerplate.
2What are $width and $height?
Magic variables inside the x-resize expression: the current width and height of the element in pixels, as integers.
3Better than window.resize?
Yes. window.resize only reacts to window width. x-resize observes the element itself, including for sidebar toggles, accordion openings, and layout changes.
4Manual cleanup needed?
No. x-resize manages the lifecycle automatically. The observer is disconnected automatically on destroy.
5Combine x-resize with CSS Container Queries?
Yes, this is the recommended combination. CSS for visual adjustments, x-resize for JS logic and DOM structure. No interference.
6Avoid a resize loop?
Avoid direct DOM manipulations inside the x-resize expression. Only set Alpine data, control layout via :class/:style.
7When does x-resize fire first?
When the component initializes. The initial size is immediately available in the data, no first resize needed.
8Performance impact?
Minimal. ResizeObserver is optimized by the browser. x-resize debounces internally. Only an issue with resize loops caused by DOM manipulation.
9Multiple elements at once?
Yes. Every element with x-resize gets its own observer. Independent observation without interference.
10x-resize for adaptive navigation?
x-resize on the nav container, write width into a data property. Getter calculates horizontal bar or hamburger. Reacts to the container, not the viewport.