Project
v0.2.0
Plugins, with docs, versions and search as the first three; named template roots; serving a site from your own backend with five adapters (Elysia, Fastify, Koa, Express, Hono) and a default 404 page; and 37 new components, from dialogs, menus and toasts to an AI chat stack.
Breaking changes#
Three changes need an edit in your config, your command line or your sidebar markup.
templates is now roots#
The two options are one, and roots[0] is the project root, which templates never moved.
Rename the key and keep your project directory first unless it already is:
-
Before:
defineConfig({ templates: ["."] }) - After:
defineConfig({ roots: ["."] })
A config that still says templates is a TypeScript error, not a silent fallback.
ResolvedConfig.templateRoots is gone with it; read ResolvedConfig.roots (the same list,
entries as { name?, dir }) and map dir if you needed the bare directories. See
Roots.
create-htmx-ui defaults to bun#
The scaffolder's --pm default is bun instead of npm. Pass
--pm npm (or pnpm, yarn) for the old behaviour. Since
bun create htmx-ui already ran Bun, this mostly changes what the scaffolder writes into a new project's
package.json scripts and lockfile.
Sidebar is a composable app sidebar#
The sidebar is rebuilt from parts: a layout, header, content, groups, menus, footer, rail and trigger, with variants, collapsible modes and a mobile panel. Rename the old classes:
.sidebar-linkbecomes.sidebar-menu-button, in a<li class="sidebar-menu-item">of a<ul class="sidebar-menu">..sidebar-labelbecomes.sidebar-group-label..sidebar-backdropis gone: wrap the sidebar and its main area in.sidebar-layout, which draws the backdrop.[data-sidebar-toggle]becomes[data-sidebar-trigger];[data-sidebar-close]is now for close buttons inside the panel.
For the old sticky column from lg, set data-collapsible="none" data-breakpoint="lg",
--sidebar-top: 3.5rem and --sidebar-width: 15rem. See
Sidebar for the full upgrade table.
Added#
Components#
37 new components, each with a docs page of its own. The components index now groups every component by category (forms, actions, navigation, overlays, feedback, layout, data, content and chat), so the sidebar, prev/next links and the agent outputs follow the same order.
- Checkbox: a native checkbox styled with the design tokens, in sizes, with disabled, invalid and indeterminate states.
- Radio group: native radios as cards or inline rows, so a choice is one control that submits with the form.
-
Select: a listbox in a popup opened from a button, styled like an input,
searchable and keyboard driven, falling back to the native
<select>without JavaScript. - Switch: an on/off setting that applies at once, drawn as a sliding track on a real checkbox, so it needs no JavaScript.
-
Dialog: a modal on the native
<dialog>, opened with invoker commands and closed by Escape, a click outside or any[data-dialog-close]. Scrolling content, a sticky footer, dialogs over dialogs, and htmx support: a dialog a request returns opens itself, andHX-Trigger: dialog:closecloses one from the server. -
Alert dialog: a dialog with
role="alertdialog"that waits for an answer, with a small size, media and destructive actions. - Attachment: files, images, video and audio with upload states (and a shimmer while in progress), three sizes, a vertical orientation and a scrolling, snapping group.
- Avatar: an image with a fallback, in sizes, with status badges, groups and a count, and as a menu trigger.
- Breadcrumb: custom separators, dropdowns, an ellipsis, and collapsing on its own when it doesn't fit.
-
Badge: ghost and link variants, solid status badges, any colour with
.badge-tone, and hover and focus styles on badges that are links. -
Tooltip: a short hint on hover or keyboard focus, on any side, shown by CSS
alone; a small behaviour adds
aria-describedbyand dismissing with Esc. -
Label:
.labelfor any control, around checkboxes, radios and switches, or as a.label-cardchoice card. It works as.field-labelinside a field. - Separator: a horizontal or vertical line, semantic or decorative.
-
Collapsible: a button anywhere inside shows and hides a panel; collapsibles
nest into file trees, and the same classes style
<details>with no JavaScript. - Combobox: an input with suggestions that filters as you type (ignoring case, accents and word order, with keywords), groups and separators, auto highlight, multiple picks as chips, a clear button, form states, a macro, and server-side search with htmx.
- Input OTP: a one-time code in slots over one real input, so pasting, SMS autofill and the phone's number pad just work; digit, alphanumeric or custom patterns, groups and separators, masking, sizes and states.
-
Kbd: keys and shortcuts with
.kbdand.kbd-group, at home in buttons, tooltips and input groups. -
Popover: a header and footer, explicit start alignment, and
data-popover-closebuttons for forms. -
Progress: a styled native
<progress>with a label, value and help text, sizes, colours, an indeterminate state and aprogress()macro. -
Pagination: page links with previous and next, first and last, icon-only buttons for data tables, a responsive mode, and a
pagination()macro that works out the page range with ellipses. - Item: media, a title, a description and actions, in variants and sizes, with icons, avatars, images, headers, groups, whole-item links and dropdowns.
-
Empty: empty states with an icon or avatars, a message and actions, in outline and muted variants, with an
empty()macro. - Menubar: a persistent row of menus with roving focus, typeahead, submenus, checkbox and radio items, shortcuts and a vertical orientation.
- Navigation menu: site navigation with links and triggers opening panels, from simple lists to mega menus, with hover delays and arrow keys.
- Hover card: a preview of what is behind a link, on hover or keyboard focus, on any side and alignment, flipping at the viewport edge, and lazy-loadable with htmx.
-
Sheet: a dialog attached to any edge of the screen, in sizes, with every dialog feature and a
sheet()macro. -
Drawer: a panel you swipe away in any of four directions, with a drag handle, nested drawers that stack behind the frontmost, and a responsive dialog from
md. -
Slider: one value, a range or several thumbs on native range inputs, vertical too, with form states, sizes,
<output>values and aslider()macro. - Skeleton: placeholders of any shape, text lines, pulse or shimmer, and macros for avatars, cards, lists, tables and forms.
-
Toast: stacking, swipeable notifications in five types with actions and promises, from script, markup, the
HX-Triggerheader or any htmx request. - Command: a command menu with filtering, keyboard navigation, groups, shortcuts, a ⌘K dialog and server-side search.
- Carousel: Embla Carousel, loaded on demand, with item sizes, spacing, a vertical orientation, options and plugins.
- Message: chat messages with avatars, headers, footers, alignment, variants, groups and attachments, plus reasoning, tool calls and typing for AI chat.
- Loader: eight pure-CSS loading indicators, from bouncing dots and waves to shimmering text, sized, timed and coloured with custom properties.
- Scroll button: a floating button that appears once the reader scrolls away from an edge, and jumps back to the bottom of a container or the top of the page.
- Reasoning: a collapsible for AI reasoning that opens while it streams and closes, with how long it took, when the stream ends.
- Chain of thought: steps on a connecting line, each with a status and details that fold away, for reasoning traces, tool calls and logs.
-
Prompt input: the text box of an AI chat: a growing textarea, tools, send and stop, and
@mention and/command menus. - Message scroller: scrolling for a streaming chat transcript: new questions rise to the top, replies grow beneath them, the reader's place survives layout changes and loaded history, and links jump to any message.
- Textarea: its own page, a character count, keyboard submit, sizes and states.
- Tabs: vertical tabs, disabled tabs, icons, and pill, outline and full-width styles.
-
Dropdown: checkbox and radio items, shortcuts, destructive items, submenus, typeahead, a
menu:changeevent and form fields for named items. -
Sidebar: variants, collapsing to icons with tooltips or off the screen, either side, Ctrl/⌘+B, a rail, a saved state in a cookie, a mobile panel and
--sidebar-*tokens.
Templates and data#
-
Named template roots:
rootsinhtmx-ui.config.tsis an ordered list of directories, each optionally named, and the first entry is the project root. A name is the prefix templates reach that directory by, so it can move without a single template changing. -
Names work everywhere a template name does —
extends,include,import,from,json(),svg(),glob()andasset()— because resolution moved into a loader Nunjucks uses for every template load. -
Deploying templates a server renders at runtime: copy the roots
next to
dist/and pointrootsat the copy.
Plugins#
-
Plugins: optional features as separate packages, listed under
pluginsinhtmx-ui.config.ts. A plugin can add template roots, globals, filters, a transform, routes, a fetch fallback, files written by the build and CLI commands; the project's own options always win. They work inhtmx-ui dev, builds, on Bun and Node, and behind every server adapter. -
The first three, which this website's docs now run on: htmx-ui-plugin-docs
(heading anchors, Markdown for every page, llms.txt, sitemaps,
docsNav()and page macros), htmx-ui-plugin-versions (archived docs versions, the version switcher and banner,htmx-ui versions:nameandhtmx-ui versions:archive) and htmx-ui-plugin-search (the Ctrl/⌘K palette). They release with the other packages, at the same version. -
Writing a plugin:
definePlugin(),editHtml()(a streaming HTML editor for transforms that runs on Bun and Node alike) andcontentType()fromhtmx-ui-engine, plugin commands listed byhtmx-ui --help, and anapifor plugins that build on each other.
Engine#
-
Serving a site from your own backend:
createSite()is the framework-free core of the dev servers, wrapped byhtmx-ui-engine/elysia(recommended),htmx-ui-engine/fastify,htmx-ui-engine/koa,htmx-ui-engine/expressandhtmx-ui-engine/hono. None of them imports its framework. Elysia, Fastify and Koa answer only what your own routes don't, whatever the order; Express and Hono are mounted last. Recipes cover Bun.serve, Deno, node:http, h3 and NestJS, and backends in other languages. -
site.render()renders a full page,site.fragment()renders any template as an htmx fragment, andsite.handle()answers a request from configroutes, the builtdist/, the config'sfetch, then a 404 page. Outside production it renders a page from its template when nothing is built yet, with thecontextyou pass. -
A default 404 page: a project without
pages/404.htmlgets htmx-ui's own, from the adapters,htmx-ui dev,htmx-ui previewand asdist/404.htmlin every build. Addpages/404.htmlto replace it. -
Request bodies a framework's body parser already read (
express.urlencoded(),express.json(), Fastify's JSON parser, Koa body parsers) reach the config'sroutesintact. - Code blocks highlight
dockerandnginx. -
render: truein the config checks at startup that the template roots and pages directory arrived, so a packaging mistake fails the boot instead of the first request. -
A cached runtime renderer:
render(path, roots, { cache: true })withwarm()to compile ahead of the first request (createSite()does both), andmarkup()for a global that returns HTML. -
debugin the config logs what answered each request, every render with its duration, what the template cache compiled, and builds:debug: truefor everything or a list of topics (requests,render,templates,build). Off by default.
Package#
-
htmx-uiexports the component APIs for script —toastand its types,registerCarouselPlugin,getCarousel,matchesHotkey,commandMatches,getMessageScroller,scrollToEdge,distanceFromEdgeandscrollTargetOf— plusqueryAll(the helper components use to include the root in their scan) and theThemetype. It now depends onembla-carouselfor the carousel. -
htmx-ui-engineexportscreateSite,Site,SiteOptions,normalizeRoots,tryLocate,locate,TemplateRoot,RootSpec,warm,markup,logger,TOPICSand the plugin writer's kit:definePlugin,editHtmlandcontentType. -
37 new icons, from
arrow-upandbelltofolder-plus,paperclip,shield-checkandusers. -
bun run version:set <x.y.z>names the docs version in development, dates it, and sets the version on every package at once.
Changed#
-
Components animate with shared motion tokens
(
--motion-duration-*,--motion-ease-*), which collapse to0sfor reduced motion. Popovers, dropdown and menubar menus, select and combobox lists fade and zoom in from their trigger and fade out, in CSS only; dialogs also rise slightly as they open. -
The dev watcher honours
.gitignoreand skipsnode_modules,dist,publicand.gitwhatever the roots say, so a root of.no longer adds a file watcher for every directory innode_modules. The trade-off: a new file under one of those directories shows up on the next refresh rather than immediately. - The dev server watches the roots you configured, so a template that moved is still watched.
- Dropdown items no longer spread their content apart: content is left-aligned and trailing badges, shortcuts and icons are pushed right. Hovering an item focuses it, and choosing one returns focus to the trigger.
-
.tabsis styled: a column with a gap between the list and the panels (side by side when vertical), in place of the margin on.tabs-panel. -
data-dialog-removewaits for the dialog's own transition before removing it, so sheets and drawers finish sliding out.
Fixed#
- Links in a
.field-descriptionare underlined again; the minified build dropped the underline. -
htmx-ui buildon Bun prints the bundler's own errors, such as an import that doesn't resolve, instead of only "Bundle failed". -
Icons in a badge are badge-sized again; the
size-4classicon()adds used to win over the badge's own icon size. -
assetVer()'s?ver=can no longer break a build or be dropped by the bundler. Neither Bun nor Vite can resolve an asset URL carrying a query string, so the version is parked in adata-verattribute before the bundler sees the page and moved onto the finished URL after it. -
.selectno longer restyles the select component's trigger as a native dropdown; the rule is scoped toselect.select. -
A field whose label sits in
.field-content— what a switch or a radio group produces — marks an invalid field the same way a top-level label does, instead of losing the danger colour.