Announcing Winduum 3
Winduum 3 is a native-first release focused on one goal: use more of the browser and ship less JavaScript.
The web platform can now handle much of the behavior that component libraries used to recreate themselves. Dialogs can be opened with Invoker Commands, popovers can live in the top layer through the Popover API and anchor themselves with CSS Anchor Positioning, accordions can be exclusive through the native name attribute on <details>, and CSS Scroll Snap can drive carousels and drawers. Winduum 3 adopts those capabilities directly and adds only the small enhancement layer that is still useful.
The result is a leaner core, simpler markup and components that preserve useful behavior even before JavaScript loads — or when it does not load at all.
Upgrading from Winduum 2?
Start with the Winduum v3 migration guide, which covers the core and winduum-stimulus together. Once that migration works, projects replacing Stimulus with Winduum Elements can continue with the winduum-elements migration guide.
Winduum Elements, Built on Webuum
The main addition in this release is winduum-elements: a set of ready-to-use Custom Elements for Winduum components.
Winduum Elements is built on Webuum, a tiny framework for progressively enhancing server-rendered websites with native Custom Elements. Webuum has no virtual DOM, hydration layer, router or application runtime. It keeps component behavior close to the HTML and relies on browser APIs wherever the platform already has an answer.
Read the Webuum v0.x announcement for the ideas behind that foundation.
Install the next release and register only the elements your project needs:
npm install winduum@next winduum-elements@nextimport { Drawer } from 'winduum-elements/components/drawer/index.js'
import { Popover } from 'winduum-elements/components/popover/index.js'
customElements.define('x-drawer', Drawer, { extends: 'dialog' })
customElements.define('x-popover', Popover)This gives server-rendered projects a frameworkless default integration while keeping winduum-stimulus, winduum-vue and winduum-react available for projects that already use those stacks.
The Browser Is the Component Runtime
Winduum 3 replaces custom behavior with native HTML and CSS wherever possible.
| Component | Native foundation | JavaScript in Winduum 3 |
|---|---|---|
| Dialog | <dialog>, Invoker Commands, closedby | No controller or custom open/close API; only an optional compatibility side effect |
| Details | <details>, <summary>, name, CSS transitions | None for standard details and accordions; an optional helper syncs the checkbox variant |
| Popover | Popover API, Invoker Commands, CSS Anchor Positioning, interestfor | None in modern browsers; a small positioning fallback is available |
| Drawer | <dialog>, Invoker Commands, scroll snap, scroll-driven animations, observers | Reduced to the enhancement layer for command handling, snap state, swipe dismissal and fallbacks |
| Marquee | CSS motion path, sibling-index() and animations | None |
Dialog Without a JavaScript Controller
The Dialog component no longer needs its old showDialog, closeDialog and defaultOptions APIs. A button now opens and closes the native <dialog> declaratively with Invoker Commands (command and commandfor), while closedby controls light dismiss.
<button class="x-button" command="show-modal" commandfor="dialogBasic">Show dialog</button>
<dialog class="x-dialog" id="dialogBasic" closedby="any">
<form class="x-dialog-content" method="dialog">
<div class="x-heading">Example dialog</div>
<br>
<div class="x-text">
<p>You can close this dialog with Esc, clicking outside, or by form submit</p>
</div>
<br>
<button class="x-button">Close dialog</button>
</form>
</dialog>For programmatic control, use the native HTMLDialogElement.showModal() and HTMLDialogElement.close() methods. The optional side-effect script only fills gaps for browsers without closedby support and keeps the scrollbar-width CSS property up to date. See the complete Dialog documentation.
Details and Accordions in Native HTML
The Details component no longer uses showDetails, closeDetails or their JavaScript animation options. Open and close transitions are now handled in CSS, while exclusive accordions use the native name attribute on related <details> elements.
<details class="x-details group bg-body-secondary rounded-md">
<summary class="flex items-center gap-2 text-primary p-4">
<span class="x-title">Show more</span>
<svg class="size-4 group-open:-scale-y-100 transition-transform" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
</summary>
<div class="p-4">
Details content
</div>
</details>Standard details and accordion patterns need no JavaScript. The synchronous toggleDetails helper remains only for the optional pattern that keeps a checkbox inside <summary> in sync with the open state. See the complete Details documentation.
Popover and Anchor Positioning
The Popover component has been rewritten around the native Popover API, Invoker Commands and CSS Anchor Positioning. The browser owns top-layer placement, light dismiss, Escape handling and the open state.
<button class="x-button" command="toggle-popover" commandfor="popoverExample">Popover</button>
<div class="x-popover bottom my-2" popover id="popoverExample">
<div class="x-popover-content shadow dark:bg-body-secondary p-2 w-32 flex flex-col">
<button class="x-button ghosted accent-main justify-start w-full">Item 1</button>
<button class="x-button ghosted accent-main justify-start w-full">Item 2</button>
</div>
</div>The old showPopover, hidePopover, togglePopover and computePopover helpers are no longer part of the Winduum API — showPopover(), hidePopover() and togglePopover() now belong to the native platform. computePositionPopover and autoUpdatePopover remain as focused Floating UI fallbacks for browsers without CSS Anchor Positioning.
The former Tooltip component is deprecated in favor of the x-popover tooltip variant. Its directional variants now use the explicit tooltip-top, tooltip-bottom, tooltip-left and tooltip-right names. See the complete Popover documentation.
A Much Smaller Drawer Layer
The Drawer component still supports touch-driven dismissal, but the browser now does most of the work. Native Invoker Commands open and close its dialog, CSS Scroll Snap defines the open and closed positions, a scroll-driven animation drives the backdrop and an Intersection Observer tracks settled state. This follows the same platform-first principles described in the Chrome team's navigation drawer guidance: the user's finger controls the native scroller instead of a JavaScript transform tween.
<button class="x-button" command="show-modal" commandfor="drawerNoScriptElement">Show drawer</button>
<dialog class="x-drawer noscript:starting:-translate-x-full noscript:not-open:-translate-x-full noscript:duration-500" is="x-drawer" id="drawerNoScriptElement" closedby="any">
<div class="x-drawer-scroller snap-x snap-mandatory noscript:after:content-none">
<nav class="x-drawer-content" data-x-drawer-part="content">
Drawer content
<button class="x-button muted" command="request-close" commandfor="drawerNoScriptElement">Close drawer</button>
</nav>
</div>
</dialog><script setup lang="ts">
import { ref, useId } from 'vue'
import { Drawer, DrawerContent, DrawerScroller } from '@/components/drawer'
import { Button } from '@/components/button'
const drawerId = useId()
const contentElement = ref<HTMLElement>()
</script>
<template>
<Button command="show-modal" :commandfor="drawerId">Show drawer</Button>
<Drawer :id="drawerId" closedby="any" class="noscript:starting:-translate-x-full noscript:not-open:-translate-x-full noscript:duration-500" :refs="{ contentElement }">
<DrawerScroller class="snap-x snap-mandatory noscript:after:content-none">
<DrawerContent ref="contentElement" as="nav">
Drawer content
<Button class="muted" command="request-close" :commandfor="drawerId">
Close drawer
</Button>
</DrawerContent>
</DrawerScroller>
</Drawer>
</template>import { useId, useRef } from 'react'
import { Drawer, DrawerContent, DrawerScroller } from '@/components/drawer'
import { Button } from '@/components/button'
export function Example() {
const drawerId = useId()
const contentElement = useRef<HTMLElement>(null)
return (
<>
<Button command="show-modal" commandfor={drawerId}>Show drawer</Button>
<Drawer id={drawerId} closedby="any" className="noscript:starting:-translate-x-full noscript:not-open:-translate-x-full noscript:duration-500" refs={{ contentElement }}>
<DrawerScroller className="snap-x snap-mandatory noscript:after:content-none">
<DrawerContent ref={contentElement} as="nav">
Drawer content
<Button className="muted" command="request-close" commandfor={drawerId}>
Close drawer
</Button>
</DrawerContent>
</DrawerScroller>
</Drawer>
</>
)
}The v3 rewrite adds placement-based helpers, modal and non-modal patterns, a responsive dialog variant and two CSS-first fallbacks:
noscriptkeeps an animated drawer usable when JavaScript is disabled.nosnapremoves the scroll-snap behavior and works without JavaScript.dialogchanges from a bottom drawer to a centered dialog at a container breakpoint.left,right,topandbottomplacements share the same API.
See the complete Drawer documentation, including every placement and the dialog, noscript and nosnap examples.
CSS-only Marquee
The new Marquee component creates an infinite loop with a CSS motion path, sibling-count() and sibling-index(). It has no JavaScript runtime and pauses when keyboard focus moves inside the component.
The --x-marquee-sibling-index and --x-marquee-sibling-count properties provide a declarative fallback until sibling-index() and sibling-count() are available in every target browser. See the Marquee docs for the complete HTML, Vue and React examples.
Native-first Components Beyond the Headliners
The same direction continues across the rest of the library.
Experimental Carousel
The goal of Carousel Experimental was to mirror the CSS Overflow Module Level 5 carousel API in JavaScript. Scroll buttons, markers and the active snap target would use ordinary elements, making patterns such as Chrome's Horizontal List available without relying on the still-unsupported ::scroll-button() and ::scroll-marker pseudo-elements.
That compatibility layer is not ready yet because the required Scroll Snap behavior is not consistent across browsers. The scrollsnapchange event is still unavailable in Firefox and Safari, while Safari's scrollBy() can fail in combination with scroll snapping (WebKit #270064, WebKit #316084). The component therefore remains experimental; use the stable Carousel when broad browser support is required.
Integrations and Component APIs
winduum-vueandwinduum-reacthave been updated for Winduum 3 and now include the previously missing components, including the interactive components.winduum-stimulusv3 adopts the same Winduum 3 markup and APIs for projects that want to stay on Stimulus.- Text is split into the small base
x-textcomponent and thex-text contentvariant for rich CMS or WYSIWYG output. - Form now imports
validateFieldfrom the standalone Field module. The default selector is.x-field,validateSelectorsis renamed tovalidateSelector, and validation options are smaller. - Toast auto-hides after 7.5 seconds by default and removes its element after the exit transition. The new
toasterObserveropens and closes a Toaster popover as its contents change. tel-country-codeadds styles for country-code select controls.- Typography, headings, labels, selects, the container width and responsive container padding have updated defaults.
Tailwind CSS Utilities That Match the Platform
Several utilities and variants first shipped experimentally between Winduum 2.2.20 and 2.2.28. Winduum 3 makes them part of the documented workflow. Together they answer the question posed in Do you still need Framer Motion? in a deliberately pragmatic way: start with CSS when the browser already owns the interaction or timeline, then add JavaScript only when the effect needs runtime physics, gesture data or orchestration.
The documented workflow now includes:
- Link utilities and Interest variants.
- New
ease-anticipate,ease-springandease-emphasizedeasing curves. - Grid area utilities.
- Animation Timeline and Animation Trigger utilities with composable
enter/exitkeyframes and theanimation-trigger-enterfallback. - Scroll State variants.
- The experimental
timelineTriggerpolyfill andsupportsInterestForfeature check. - Position utilities.
Expressive easing without a runtime
Winduum now includes three easing tokens beyond the familiar ease-in, ease-out and ease-in-out curves:
ease-anticipatebriefly moves against the expected direction before accelerating forward.ease-springuses the CSSlinear()easing function to encode a sampled spring-like curve.ease-emphasizedaccelerates quickly and settles smoothly. It is also the default timing function foranimation-trigger-enter.
Tailwind CSS ease-* utilities apply the curve directly to transition-timing-function and expose it through --tw-ease. Keyframe and scroll-driven animations can reuse the same token, but must connect it to animation-timing-function explicitly:
<button class="transition-transform duration-700 ease-spring hover:scale-110">
Add to cart
</button><div class="transition-transform duration-500 ease-anticipate hover:-translate-y-2">
Hover me
</div><div
class="animation animation-[enter] from-translate-y-4 ease-emphasized
[animation-timing-function:var(--tw-ease)]"
>
Content
</div>ease-spring is intentionally a predefined curve, not a physics engine. It cannot inherit the release velocity of a drag gesture, change its duration from spring parameters or preserve velocity when interrupted. Motion still makes sense for those cases; CSS is the smaller option when a consistent spring-like transition is enough.
Scroll-linked animation with animation-timeline
CSS scroll-driven animations replace the usual scroll listener and per-frame progress calculation with a browser-owned timeline. Winduum exposes both kinds through its Animation Timeline utilities:
animation-timeline-scroll,-xand-yusescroll()so animation progress follows a scroll container.animation-timeline-viewusesview()so progress follows an element as it passes through the viewport.animation-range-*limits the part of that timeline in which the animation runs.- Named
scroll-timeline-*,view-timeline-*andtimeline-scope-*utilities connect a timeline to an animation elsewhere in the component.
Approaching Baseline
Scroll-linked animations are close to becoming Baseline across the major browser engines. The current scroll-timeline support table lists Firefox 155 as its first supported release, joining the existing Chromium and Safari implementations.
<article
class="animation animation-[enter] from-translate-y-8
animation-timeline-view animation-range-[entry_0%_cover_40%]"
>
Content reveals as it enters the viewport.
</article><div class="scroll-timeline-x-[--gallery] overflow-x-auto">
<ul class="flex w-max">...</ul>
<div
class="animation animation-[enter] from-scale-75 origin-left
animation-timeline-[--gallery]"
></div>
</div>This is scroll-linked motion: its progress continuously follows scroll position. For transform and opacity, the browser can keep the animation on its optimized rendering path, while the markup still exposes the underlying animation-timeline model.
Scroll-triggered animation with animation-trigger
Scroll-triggered motion is different: a normal time-based animation should start once when an element reaches a threshold. Winduum targets the emerging animation-trigger and timeline-trigger properties for that job.
The animation-trigger-enter utility selects the composable enter keyframes, while from-* and to-* classes define their opacity, translation, scale and rotation. timeline-trigger-enter supplies a view trigger with a default 20% entry threshold and a play-once action:
<h2
class="animation-trigger-enter from-translate-y-4 timeline-trigger-enter
[--tw-animation-duration:1s] [--tw-timeline-trigger-entry:30%]"
>
Our key partners
</h2>The default duration is 1.75s, the default easing is ease-emphasized, and the animation uses fill-mode: both. Winduum disables it for prefers-reduced-motion and print automatically.
The properties are still experimental, so the native path is progressive enhancement rather than a hard dependency. In a browser without timeline-trigger, Winduum's fallback keeps animation-trigger-enter paused until a data-enter state is present. The opt-in timelineTrigger polyfill supplies that state with IntersectionObserver; supported browsers need no observer or trigger JavaScript at all. See the complete Animation Trigger documentation.
Scroll state as a CSS variant
Scroll-state container queries let the browser expose state that previously required scroll listeners, measurements and classes toggled from JavaScript. After setting container-type: scroll-state, Winduum's Scroll State variants can react to four groups:
scrollable-*shows whether more content is available in a direction — useful for edge fades and navigation controls.scrolled-*shows whether the container has moved away from an edge — useful for separators and scroll affordances.snapped-*identifies the currently snapped item — useful for native carousels and galleries.stuck-*identifies when a sticky element is actually stuck — useful for changing a header's shadow or size only after it reaches an edge.
<header class="sticky top-0 [container-type:scroll-state]">
<nav class="stuck-top:shadow-md">
Navigation
</nav>
</header><div class="relative overflow-x-auto [container-type:scroll-state]">
<ul class="flex w-max">...</ul>
<span class="hidden scrollable-right:block">More content →</span>
</div><ul class="flex overflow-x-auto snap-x snap-mandatory">
<li class="snap-center [container-type:scroll-state]">
<img class="scale-75 transition-transform snapped-x:scale-100" src="image.jpg" alt="">
</li>
</ul>Unsupported browsers ignore the conditional styles, so the content and native scrolling remain usable. The variants are best used for visual enhancement rather than as the only way to expose essential state.
Interaction, positioning and layout primitives
The rest of this group follows the same rule: expose what the browser already knows instead of rebuilding it in a component script.
- Link scopes hover and focus transitions to real interactive elements using
:any-link, enabled buttons and button roles. - Interest turns the declarative
interestforrelationship intointerest-source:andinterest-target:variants for tooltips and hover cards, with an optional fallback selected bysupportsInterestFor. - Position composes CSS Anchor Positioning,
position-areaand fallback flipping for popovers instead of recalculating coordinates on every layout change. - Grid area makes named or overlapping grid areas composable, including
grid-area-fullfor stacking content without absolute positioning.
<button interestfor="tooltip" class="interest-source:text-accent">
Show tooltip
</button>
<div id="tooltip" popover="hint" class="opacity-0 interest-target:opacity-100">
Tooltip content
</div><button popovertarget="menu" class="[anchor-name:--menu]">Open menu</button>
<div id="menu" popover class="bottom-start [position-anchor:--menu]">
Menu content
</div><figure class="grid">
<img class="grid-area-full" src="image.jpg" alt="">
<figcaption class="grid-area-full self-end">Caption</figcaption>
</figure>The deprecated --radius token now resolves to the base xs radius (0.125rem) instead of --radius-xl. Use the named, calculated radius variables in new code.
Progressive Enhancement, Including the Fallbacks
Adopting new browser APIs does not mean shipping every polyfill to every browser. Winduum 3 adds a winduum/polyfill entry point and individual support checks so fallbacks can be loaded only where they are needed.
import {
supportsAnimationTimeline,
supportsInterestFor,
supportsScrollInitialTarget,
supportsScrollSnapEvents,
supportsTimelineTrigger,
} from 'winduum/supports'
if (!supportsInterestFor) {
await import('interestfor/src/interestfor.js')
}
if (!supportsTimelineTrigger) {
await import('winduum/src/polyfills/timelineTrigger.js')
}Winduum Elements also exposes Webuum's feature checks for Invoker Commands and customized built-in elements:
import { supportsCommand, supportsIs } from 'webuum/supports'
if (!supportsCommand) {
const { apply } = await import('invokers-polyfill/fn')
apply()
}
if (!supportsIs()) {
await import('@webreflection/custom-elements-builtin')
}The remaining checks — including animation timelines, scroll initial targets, scroll-snap events, anchor positioning and anchored containers — make it possible to choose a project-specific fallback rather than paying for a global compatibility layer. See the Polyfills documentation for the recommended setup.
Migrating to Winduum 3
Winduum 3 removes and renames JavaScript APIs, changes some component markup and updates several defaults. Existing projects should treat it as a deliberate migration rather than a dependency-only upgrade.
- Follow the Winduum v3 migration guide to upgrade core styles, component markup, native APIs and
winduum-stimuluscontrollers together. - Follow the winduum-elements migration guide only after that migration works when replacing the migrated Stimulus integration with Custom Elements.
- Review the full Winduum changelog for the release-by-release details.
Winduum 3 is not a rewrite of what a component library looks like. It is a rewrite of who should own the behavior. Whenever the browser can provide the semantics, focus handling, top layer, scrolling or animation, Winduum now lets the browser do it — and keeps its own JavaScript focused on progressive enhancement.