Titouan Mathis

CTO at Studio Meta and ikko

What's new in @studiometa/ui 1.9.0

29/07/2026 in #studiometa-ui #release

@studiometa/ui 1.9.0 is out of beta, and it is the largest release the library has had in a while. It adds three full components, a handful of headless primitives, two Shopify Fetch adapters, a declarative analytics family and a brand-new ESLint plugin, all on top of @studiometa/js-toolkit 3.8.0. This note walks through the additions worth knowing about, with a few live demos along the way.

Availability

Everything below ships with @studiometa/ui v1.9.0. If you are on 1.8 or earlier, upgrade before reaching for any of it.

Table of content

New components

Dialog, and the end of Modal and Panel

Dialog is the headline addition. It wraps the native <dialog> element to build accessible modal and drawer patterns: opening with showModal() hands the focus trap, background inert, Esc-to-close and top-layer stacking to the platform, and the component adds scroll-locking and orchestration of its Transition / ViewTransition children on top.

The markup stays declarative: an Action button opens the dialog, the backdrop is one Transition child that fades, the box is another that scales, and a single Dialog drives both.

<button type="button" data-component="Action" data-on:click="Dialog(#modal)->target.open()">
  Open dialog
</button>

<dialog
  id="modal"
  data-component="Action Dialog"
  data-on:cancel.prevent="Dialog.close()"
  data-on:click="event.target === $el && Dialog.close()">
  <!-- a Transition backdrop and a Transition box -->
</dialog>

A drawer is the same component with the panel anchored to an edge and a ViewTransition child for the slide. Because Dialog covers both cases, Modal and Panel are now deprecated. They still work in 1.9.0, but plan on migrating to Dialog.

Carousel is a component family built on the new Indexable primitive: CarouselWrapper, CarouselItem, CarouselBtn and an optional CarouselDrag. It uses native CSS scroll-snap on touch devices and pointer drag (via CarouselDrag, mounted only on pointer: fine devices) elsewhere, so it stays light and accessible without hijacking the scroll.

Boundaries are configurable through the boundary option it inherits from Indexable: clamp (the default) stops at the ends and disables the prev/next controls there, while loop and bounce keep going. A --carousel-progress custom property on the root and a --carousel-item-active one on each item let you drive progress bars and active-slide styling from CSS alone, which is what the progress bar and the active ring in the demo above are wired to.

ViewTransition

ViewTransition brings the View Transitions API to the same enter() / leave() / toggle() interface as Transition, so Action can drive it declaratively. Every enter/leave call made in the same microtask is coalesced into a single document.startViewTransition(), which is what lets several elements animate together as one coordinated transition. When the API is unavailable, the state change just applies instantly.

The animation itself lives entirely in the ::view-transition-old() / ::view-transition-new() CSS. The component only names the snapshot and toggles the state.

New primitives

1.9.0 leans hard into small, headless building blocks you compose yourself.

Indexable

Indexable, with the withIndex decorator, gives you index navigation through goTo(), goNext(), goPrev(), a boundary option and an index event. It powers Carousel, and a total option (data-option-total="3") lets you use it straight from HTML without subclassing.

InView and InViewOnce

InView and InViewOnce emit directional in-view / out-of-view events as an element crosses the viewport, the Once variant firing a single time. Mount it alongside Action and a reveal-on-scroll is a one-liner, no custom class required:

<div
  data-component="Action InView"
  data-on:in-view="$el.classList.add('is-visible')"
  data-on:out-of-view="$el.classList.remove('is-visible')">

</div>

Timer and TimerProgress

Timer and TimerProgress are a headless countdown emitting bubbling lifecycle events, with a requestAnimationFrame-driven progress signal for smooth bars and dials. Give it a delay in seconds and react to timer-end from an Action on the same element:

<div
  data-component="Action Timer"
  data-option-delay="5"
  data-on:timer-end="$el.classList.add('is-done')">

</div>

ClickOutside

ClickOutside dispatches a click-outside event when a click lands outside its element, pairing with Action to dismiss menus and popovers declaratively. Put both on the panel and keep its trigger inside, so the click that opens it never counts as an outside click:

<div data-component="ClickOutside Action" data-on:click-outside="$el.classList.remove('is-open')">

</div>

DataScope

DataScope is a local boundary for the reactive Data components, so you can build patterns like tabs and scoped forms from primitives and drop two of them on a page without collisions. I covered it on its own in Scoped, reusable UI patterns with DataScope.

Talking to the server: Fetch

Fetch grew a src option and a no-argument fetch() call, so you can point a request at an explicit URL instead of relying on a form's action or a link's href. When both are set, the new src option takes precedence.

On top of that, two Shopify adapters land for the Section Rendering API:

  • FetchShopifyPartial: request one or more sections, with its partials option taking a comma-separated string (e.g. data-option-partials="cart-drawer,cart-icon").
  • FetchShopifySection: an adapter for the stable Section Rendering endpoint.

Declarative analytics: Track

The new Track family covers analytics tracking without hand-written listeners: Track fires events, TrackShopify and a dataLayer/GTM provider ship out of the box, and TrackContext lets ancestors contribute context that is merged into every event fired by their descendants.

Tooling and under the hood

  • @studiometa/eslint-plugin-ui: a new package of ESLint / Oxlint rules that help you discover and correctly use components from the library.
  • Reactivity: the Data components were refactored onto signals with globally-shared channels, and DataScope resolves scoped groups through withGroup's new getScope / getGroup options.
  • Dependencies: the library now builds on @studiometa/js-toolkit 3.8.0.
  • Fixes: Slider's SliderBtn / SliderCount / SliderDots / SliderProgress no longer crash when mounted before their parent, Figure / FigureVideo stop hanging forever when an image or poster fails to load, and Accordion no longer relies on the deprecated $parent accessor.