{
  "name": "HTMX UI",
  "description": "Tailwind CSS components for htmx apps. Plain HTML, themeable with CSS variables, dark mode built in.",
  "url": "https://susonwaiba.github.io/htmx-ui",
  "version": "0.3",
  "pages": [
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs.md",
      "title": "Introduction",
      "description": "HTMX UI is a set of Tailwind CSS components for apps where the server renders HTML and htmx swaps it into the page.",
      "section": "Getting started",
      "headings": [
        {
          "level": 2,
          "id": "what",
          "text": "What it is"
        },
        {
          "level": 2,
          "id": "example",
          "text": "A quick look"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "Working with htmx"
        },
        {
          "level": 2,
          "id": "upgrading",
          "text": "Upgrading"
        },
        {
          "level": 2,
          "id": "stack",
          "text": "What it's built on"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "Installation Add the package to a new or existing Bun project. Theming Override design tokens or build a custom theme. Dark mode How dark mode works and how to add a switcher. Components Browse every component with live examples. Upgrading One command rewrites what a release renamed."
        },
        {
          "id": "what",
          "title": "What it is",
          "text": "Most component libraries assume a JavaScript framework owns the page. HTMX UI assumes the opposite: your server returns HTML, htmx swaps it in, and components need to look right and keep working when they arrive inside a fragment. So every component is: Markup and classes. A button is <button class=\"btn btn-primary\">. Any server language can render it. Styled with Tailwind v4. Component classes live in @layer components, so a utility class on the same element always wins. Themed with CSS variables. Colours and radius are variables like --primary and --radius, exposed to Tailwind as bg-primary, text-muted-foreground and so on. Behaviour only where needed. Things like copy buttons and the mobile sidebar are tiny TypeScript modules that re-run after every htmx swap."
        },
        {
          "id": "example",
          "title": "A quick look",
          "text": "Components compose like plain HTML. This card uses the card, badge and button components together: html <div class=\"card w-full max-w-sm\"> <div class=\"card-header pb-6\"> <div class=\"flex items-center justify-between\"> <h3 class=\"card-title\">Deploy preview</h3> <span class=\"badge badge-success badge-dot\">Ready</span> </div> <p class=\"card-description\">Built from main in 42 seconds.</p> </div> <div class=\"card-footer pt-6\"> <button class=\"btn btn-primary btn-sm\">Open preview</button> <button class=\"btn btn-ghost btn-sm\">View logs</button> </div> </div>"
        },
        {
          "id": "htmx",
          "title": "Working with htmx",
          "text": "The entry script calls initComponents(document) once the page loads, and again with each newly inserted element on the htmx:after:process event. Each initialiser only touches elements it hasn't seen yet (it marks them with data-init), so a fragment's components come alive without anything running twice. app.ts import \"htmx.org\"; import { initComponents } from \"htmx-ui\"; document.addEventListener(\"DOMContentLoaded\", () => initComponents(document)); document.addEventListener(\"htmx:after:process\", (e) => initComponents(e.target ?? document)); Why not htmx:after:swap? In htmx 4, htmx:after:swap is dispatched on the element that made the request, so its target is not the new content. htmx:after:process fires on each element htmx inserts, which is exactly what needs initialising."
        },
        {
          "id": "upgrading",
          "title": "Upgrading",
          "text": "Before 1.0 a minor release can rename a class or change a component's markup, and every one of them is listed under Breaking changes in the changelog. You don't make those edits by hand: htmx-ui-upgrade rewrites your templates, scripts, stylesheets and config to the release you are moving to, then prints the places it left for you to check. It refuses to run on uncommitted changes, so an upgrade is one diff to review — or to undo. See Upgrading."
        },
        {
          "id": "stack",
          "title": "What it's built on",
          "text": "htmx 4 for requests and swaps. Tailwind CSS v4 for styling and tokens. Bun for bundling, the dev server and tests. Nunjucks macros (optional) for less repetitive markup."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/installation",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/installation",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/installation.md",
      "title": "Installation",
      "description": "Start a new HTMX UI site with one command, or add the components to an app that already uses Tailwind CSS v4 and htmx.",
      "section": "Getting started",
      "headings": [
        {
          "level": 2,
          "id": "packages",
          "text": "Packages"
        },
        {
          "level": 2,
          "id": "requirements",
          "text": "Requirements"
        },
        {
          "level": 2,
          "id": "new-project",
          "text": "New project"
        },
        {
          "level": 2,
          "id": "engine-by-hand",
          "text": "Add the engine to a project"
        },
        {
          "level": 3,
          "id": "1-install",
          "text": "1. Install"
        },
        {
          "level": 3,
          "id": "2-add-the-scripts",
          "text": "2. Add the scripts"
        },
        {
          "level": 3,
          "id": "3-import-the-styles",
          "text": "3. Import the styles"
        },
        {
          "level": 3,
          "id": "4-initialise-components",
          "text": "4. Initialise components"
        },
        {
          "level": 3,
          "id": "5-add-a-layout-and-a-page",
          "text": "5. Add a layout and a page"
        },
        {
          "level": 2,
          "id": "existing-project",
          "text": "Existing project"
        },
        {
          "level": 3,
          "id": "already-using-the-dark-variant",
          "text": "Already using the dark: variant?"
        },
        {
          "level": 2,
          "id": "upgrading",
          "text": "Upgrading later"
        },
        {
          "level": 2,
          "id": "exports",
          "text": "What the package exports"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "Commands are shown for bun, npm, pnpm and yarn. Pick yours once; every command block on the site remembers the choice."
        },
        {
          "id": "packages",
          "title": "Packages",
          "text": "Package What it is htmx-ui The components: CSS classes, design tokens, dark mode and small behaviours. Works with any bundler. htmx-ui-engine Builds a site from Nunjucks pages: htmx-ui dev, build and preview, on Bun or Node. See Engine. create-htmx-ui Scaffolds a new site that uses both. htmx-ui-upgrade Not installed with the project: run it when you update, and it rewrites what the release changed. See Upgrading."
        },
        {
          "id": "requirements",
          "title": "Requirements",
          "text": "Dependency Version Why Tailwind CSS 4.x Component styles use @apply and @theme. htmx 4.x Fragment swaps; components initialise inside new content. Bun or Node Bun 1.2.3+ / Node 22.12+ For the engine. With your own bundler, any that handles TypeScript and CSS imports works."
        },
        {
          "id": "new-project",
          "title": "New project",
          "text": "Create a site, install it and start the dev server: bun create htmx-ui my-site cd my-site bun install bun run dev npm create htmx-ui my-site cd my-site npm install npm run dev pnpm create htmx-ui my-site cd my-site pnpm install pnpm run dev yarn create htmx-ui my-site cd my-site yarn install yarn run dev You get pages/ (one file per route), a layout with a theme toggle, Tailwind with the library's styles, an htmx button talking to a mock endpoint, and dev, build and preview scripts. A site created with bun runs on Bun; with npm, pnpm or yarn it runs on Node, with Vite. Open http://localhost:3000 and edit pages/index.html."
        },
        {
          "id": "engine-by-hand",
          "title": "Add the engine to a project",
          "text": ""
        },
        {
          "id": "1-install",
          "title": "1. Install",
          "text": "bun add htmx-ui htmx.org bun add -d htmx-ui-engine tailwindcss npm install htmx-ui htmx.org npm install -D htmx-ui-engine tailwindcss pnpm add htmx-ui htmx.org pnpm add -D htmx-ui-engine tailwindcss yarn add htmx-ui htmx.org yarn add -D htmx-ui-engine tailwindcss Then the Tailwind integration for the runtime you'll build with. On Bun: Terminal bun add -d bun-plugin-tailwind On Node (npm, pnpm or yarn), Vite and its Tailwind plugin: bun add -d vite @tailwindcss/vite npm install -D vite @tailwindcss/vite pnpm add -D vite @tailwindcss/vite yarn add -D vite @tailwindcss/vite"
        },
        {
          "id": "2-add-the-scripts",
          "title": "2. Add the scripts",
          "text": "package.json { \"scripts\": { \"dev\": \"htmx-ui dev\", \"build\": \"htmx-ui build\", \"preview\": \"htmx-ui preview\" } }"
        },
        {
          "id": "3-import-the-styles",
          "title": "3. Import the styles",
          "text": "Import Tailwind first, then the library. The @source line tells Tailwind where your own markup lives. styles.css @import \"tailwindcss\"; @import \"htmx-ui/styles.css\"; @source \"./\";"
        },
        {
          "id": "4-initialise-components",
          "title": "4. Initialise components",
          "text": "app.ts import \"htmx.org\"; import \"./styles.css\"; import { initComponents } from \"htmx-ui\"; import { initTheme } from \"htmx-ui/theme\"; document.addEventListener(\"DOMContentLoaded\", () => { initTheme(); initComponents(document); }); // htmx 4 fires htmx:after:process on each newly inserted element document.addEventListener(\"htmx:after:process\", (e) => { initComponents((e.target as ParentNode) ?? document); });"
        },
        {
          "id": "5-add-a-layout-and-a-page",
          "title": "5. Add a layout and a page",
          "text": "asset() makes the script path relative to whichever page is rendering, so the bundler finds it from any depth. layouts/base.html <!doctype html> <html lang=\"en\"> <head> <meta charset=\"utf-8\" /> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" /> <title>{% block title %}My site{% endblock %}</title> <script type=\"module\" src=\"{{ asset('app.ts') }}\"></script> </head> <body class=\"bg-background text-foreground\"> {% block content %}{% endblock %} </body> </html> pages/index.html {% extends \"layouts/base.html\" %} {% block content %}<button class=\"btn btn-primary\">It works</button>{% endblock %} bun run dev npm run dev pnpm run dev yarn run dev"
        },
        {
          "id": "existing-project",
          "title": "Existing project",
          "text": "If the app already has Tailwind v4, htmx and a bundler, it only needs the components: bun add htmx-ui npm install htmx-ui pnpm add htmx-ui yarn add htmx-ui Add @import \"htmx-ui/styles.css\"; directly after @import \"tailwindcss\";. Call initComponents() on load and on htmx:after:process, as in step 4 above. Clashing variable names The library defines CSS variables such as --primary, --border and --radius on :root, and Tailwind colours with the same names. If your app already defines these, the definition loaded last wins. Rename yours, or set them to the values you want (see Theming)."
        },
        {
          "id": "already-using-the-dark-variant",
          "title": "Already using the dark: variant?",
          "text": "The library switches Tailwind's dark: variant from the OS media query to a .dark class on <html>. If your app relies on the media-query behaviour, read Dark mode before upgrading."
        },
        {
          "id": "upgrading",
          "title": "Upgrading later",
          "text": "A release before 1.0 can rename a class or change a component's markup — each one listed under Breaking changes in the changelog. You don't have to apply them by hand: htmx-ui-upgrade rewrites your templates, scripts, stylesheets and config to the release you are moving to, and prints the places it left for you to check. It refuses to run on uncommitted changes, so an upgrade is one diff to review: bunx htmx-ui-upgrade@latest npx htmx-ui-upgrade@latest pnpm dlx htmx-ui-upgrade@latest yarn dlx htmx-ui-upgrade@latest Then install the new versions and build. What it changes, its options and how to check the result are on Upgrading."
        },
        {
          "id": "exports",
          "title": "What the package exports",
          "text": "JavaScript ships as ES modules with TypeScript types. htmx.org and tailwindcss are peer dependencies, so your app controls their versions. Import Contents htmx-ui initComponents(root): wires up every component's behaviour under root. htmx-ui/styles.css Design tokens, the dark variant, .prose (Tailwind Typography, installed with the package), syntax-highlighting colours and every component's styles. htmx-ui/theme initTheme() and getTheme() for the light/dark switcher. htmx-ui/icons/<name>.svg The SVG icons, for your own templates or <img> tags. htmx-ui/components/<name>/<name>.html Nunjucks macros (icon, code, alert, spinner). The engine resolves them by name: {% from \"components/icon/icon.html\" import icon %}."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/engine",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/engine",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/engine.md",
      "title": "Engine",
      "description": "htmx-ui-engine builds a site from Nunjucks pages: file-based routing, Tailwind, a dev server with hot reload and mock htmx endpoints, and static builds, on Bun or Node.",
      "section": "Getting started",
      "headings": [
        {
          "level": 2,
          "id": "cli",
          "text": "Commands"
        },
        {
          "level": 2,
          "id": "layout",
          "text": "Project layout"
        },
        {
          "level": 2,
          "id": "config",
          "text": "Configuration"
        },
        {
          "level": 2,
          "id": "roots",
          "text": "Roots"
        },
        {
          "level": 3,
          "id": "roots-names",
          "text": "Naming a directory"
        },
        {
          "level": 3,
          "id": "roots-dev-prod",
          "text": "Moving a directory between dev and production"
        },
        {
          "level": 3,
          "id": "roots-watch",
          "text": "Watching"
        },
        {
          "level": 2,
          "id": "mock-endpoints",
          "text": "Mock htmx endpoints"
        },
        {
          "level": 2,
          "id": "debug",
          "text": "Debugging"
        },
        {
          "level": 2,
          "id": "extending",
          "text": "Plugins"
        },
        {
          "level": 2,
          "id": "servers",
          "text": "Serving it from a backend"
        },
        {
          "level": 2,
          "id": "bundler-plugins",
          "text": "Use the bundler plugins directly"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "htmx-ui-engine is the build tool behind bun create htmx-ui and this website. It turns a folder of HTML pages into a static site: each page is rendered with Nunjucks, then bundled (scripts and styles hashed and minified, Tailwind compiled from every class the page uses). The same engine runs on two runtimes: Runtime Dev server Build Tailwind Bun Bun.serve with HMR Bun.build bun-plugin-tailwind Node Vite with HMR Vite @tailwindcss/vite Both render pages with the same code, so a site builds the same way on either."
        },
        {
          "id": "cli",
          "title": "Commands",
          "text": "package.json { \"scripts\": { \"dev\": \"htmx-ui dev\", \"build\": \"htmx-ui build\", \"preview\": \"htmx-ui preview\" } } Command / option Description htmx-ui dev Dev server at http://localhost:3000 with hot reload: scripts and styles update in place; pages reload when a layout, partial, macro or data file changes. htmx-ui build Builds every page into dist/ (same paths as pages/), with scripts, styles and images in dist/assets/, content-hashed and shared by every page that uses them. Copies public/ next to it. htmx-ui preview Serves dist/ with the same clean URLs (/about for about.html). --port <n> Port for dev and preview. $PORT works too. --root <dir> Project root, if not the current directory. --bun, --node Pick the runtime (also HTMX_UI_RUNTIME=bun|node). The runtime follows whatever started the command: bun run dev and bunx htmx-ui run on Bun; npm run, pnpm, yarn and npx run on Node. Install the Tailwind integration for the one you use: bun add -d htmx-ui-engine tailwindcss bun-plugin-tailwind npm install -D htmx-ui-engine tailwindcss vite @tailwindcss/vite"
        },
        {
          "id": "layout",
          "title": "Project layout",
          "text": "my-site/ pages/index.html -> / pages/about.html -> /about pages/docs/index.html -> /docs pages/docs/setup.html -> /docs/setup layouts/ partials/ shared templates data/*.json read from templates with json() public/ served at / and copied to dist/ as-is app.ts styles.css client entry and stylesheet htmx-ui.config.ts optional Pages, layouts and partials are covered in Templates & data. With htmx-ui installed, its macros and icons resolve by name, after your own files: {% from \"components/icon/icon.html\" import icon %}. Link files in public/ with a root-absolute URL (href=\"/favicon.svg\"); they keep their names. Link everything else with asset(), so the bundler can hash it."
        },
        {
          "id": "config",
          "title": "Configuration",
          "text": "Every option is optional, and a project without htmx-ui.config.ts uses the defaults. htmx-ui.config.ts import { defineConfig } from \"htmx-ui-engine\"; export default defineConfig({ roots: [\".\"], pages: \"pages\", outDir: \"dist\", publicDir: \"public\", url: \"https://example.com\", port: 3000, }); Option Description pages Routes directory. Default \"pages\". roots Where templates, data, icons and assets are found, searched in order. A directory, or one with a name. Default [\".\"]; htmx-ui's src/ is added last (ui: false turns that off). See Roots below. render This deployment renders templates at runtime. createSite() then checks at startup that the roots and pages are really there, and says what to copy if not. Default false. outDir Build output. Default \"dist\". publicDir Copied as-is. Default \"public\"; false to disable. url Public site URL, the origin template global. $SITE_URL overrides it. port Dev and preview port. Default 3000; $PORT overrides it. debug Log what the engine is doing: requests, renders, template compiles and builds. true, or a list of topics. Off by default (see below). globals, filters Extra Nunjucks globals and filters u2014 your own template helpers, on top of the built-in ones. transform(html, page) Post-process every rendered page, in dev and build. routes Request handlers for mock endpoints (see below). fetch(req) Fallback for anything else; return null for a 404. build.minify, build.sourcemap, build.define Production bundling options. build.done(ctx) Runs after a build with { outDir, pages }, e.g. to write a sitemap. vite Node only: extra Vite config, merged over the engine's. plugins Optional features, such as docs, versions and search: [docs(), search()]. See Plugins below. globals and filters are where a project adds its own template helpers; see custom globals and filters."
        },
        {
          "id": "roots",
          "title": "Roots",
          "text": "roots is every directory templates, data, icons and assets are found in, searched in order. It defaults to [\\\".\\\"], the project, which is why nothing has to configure it to get started. The first entry is the project root: pages, outDir, publicDir and asset() URLs are all measured from it, and it is the Vite root and Bun's working directory."
        },
        {
          "id": "roots-names",
          "title": "Naming a directory",
          "text": "An entry can carry a name, which is the prefix templates reach that directory by. Give one to a directory that is likely to move, and the move stops reaching your templates — rename the directory and the name still resolves it: htmx-ui.config.ts export default defineConfig({ roots: [ \".\", { name: \"layouts\", dir: \"new-layouts-2026\" }, { name: \"acme\", dir: \"../acme-ui\" }, ], }); The layout in new-layouts-2026/ is still reached as layouts/, and every page keeps saying so: pages/index.html {% extends \"layouts/base.html\" %}{% endblock %} A name is a single directory name, not a path, and no two roots may share one — both are refused at startup rather than resolved by luck of the order. The name works everywhere a template name does: extends, include, import, {% from %}, json(), svg(), glob() and asset(). Names are part of the template namespace name takes that directory's own files. With { name: \"layouts\", dir: \"new-layouts-2026\" }, a page asks for layouts/base.html, never layouts/new-layouts-2026/base.html. If two roots are inside one another, list the more specific one first: it is searched first, and it wins."
        },
        {
          "id": "roots-dev-prod",
          "title": "Moving a directory between dev and production",
          "text": "A deployment that renders fragments needs its templates on disk, and htmx-ui build does not copy them. That is a change to roots and nothing else — the templates are not edited, and no reference to them moves: htmx-ui.config.ts in the deployment export default defineConfig({ render: true, roots: [\".\", \"dist/_templates\"], // \".\" first: the first root is the project root pages: \"dist/_templates/pages\", }); See static pages, dynamic fragments for the copy step, and render for the startup check that says what is missing."
        },
        {
          "id": "roots-watch",
          "title": "Watching",
          "text": "The dev server watches the roots and nothing else, and always skips node_modules/, dist/ and public/. A project whose roots are its own directories therefore never walks its dependencies to look for templates that are not there. The trade is that a change under a skipped directory no longer reloads the page: a new public asset appears on the next refresh rather than immediately. On Bun it also watches the packages linked into the project (workspace packages, file: and link: dependencies) for stylesheets, since one that a Tailwind stylesheet @imports is invisible to Bun's own hot reloading. And because Bun only reliably notices edits under the directory it runs from, htmx-ui dev runs the server from the directory that holds the project and its linked packages, so editing a linked package's scripts updates the page too. templates is the older name for the same list of directories and still works, but it does not move the project root and cannot carry names — prefer roots."
        },
        {
          "id": "mock-endpoints",
          "title": "Mock htmx endpoints",
          "text": "While you build the front end, routes answers hx-get and hx-post with HTML fragments, so no backend is needed. Handlers take a web Request and return a Response, with Bun.serve's route syntax, on either runtime: htmx-ui.config.ts import { defineConfig } from \"htmx-ui-engine\"; const html = (body: string) => new Response(body, { headers: { \"Content-Type\": \"text/html\" } }); export default defineConfig({ routes: { \"/api/users/:id\": (req) => html(`<p>User ${req.params.id}</p>`), \"/api/search\": async (req) => { const q = new URL(req.url).searchParams.get(\"q\") ?? \"\"; return html(`<li>Results for ${q}</li>`); }, }, }); Dev server and your own server htmx-ui build is static, so a static host never sees these routes. Mount the site on a backend with a server adapter and they are served there too, by your own process, so leave mocks out of production."
        },
        {
          "id": "debug",
          "title": "Debugging",
          "text": "Set debug in htmx-ui.config.ts and the engine logs what it is doing: which requests arrive, what answers them, how long every render took, and which templates it had to compile. It is off by default, so it costs nothing in production. htmx-ui.config.ts import { defineConfig } from \"htmx-ui-engine\"; export default defineConfig({ debug: true, }); Every request is logged twice: once when it arrives, once when it is answered, with where the answer came from. That pair is the point. A request whose first line never gets a second one is the request still in flight — which is what a page that never finishes loading looks like from the server: Terminal [htmx-ui] GET /api/team/7 [htmx-ui] GET /api/team/7 200 via route /api/team/:id 0.0ms [htmx-ui] GET /nope [htmx-ui] GET /nope 404 via 404.html 0.2ms Where the answer came from is one of: a pattern from routes, dist for a built file, page for a page rendered from its template in development, fetch for the config's fallback, then a 404: 404.html from the build, pages/404.html in development, or default 404 for htmx-ui's own page. Narrow the output to what you are looking at by listing topics instead: requests, render, templates and build. htmx-ui.config.ts export default defineConfig({ debug: [\"requests\", \"render\"], }); render reports every site.render() and site.fragment() with its output size and duration. templates reports the template cache filling up: one line per template as it compiles, then the warm-up that createSite() does at startup, so nothing compiles on your first request. Any step over a second is also marked SLOW. Terminal [htmx-ui] compiled pages/docs/components/button.html [htmx-ui] warm: compiled 56 of 56 templates from 42 files in 61.2ms [htmx-ui] render /docs/components/button done 8.4KB 10.6ms cached=true [htmx-ui] fragment partials/row.html done 0.2KB 0.4ms"
        },
        {
          "id": "extending",
          "title": "Plugins",
          "text": "Features only some sites need are plugins: separate packages listed in plugins, which add templates, routes, build output and commands. The official ones give a site docs with Markdown and llms.txt, versioned docs and search: htmx-ui.config.ts import { defineConfig } from \"htmx-ui-engine\"; import docs from \"htmx-ui-plugin-docs\"; import search from \"htmx-ui-plugin-search\"; export default defineConfig({ plugins: [docs(), search()], }); See Plugins, and Writing a plugin to make your own."
        },
        {
          "id": "servers",
          "title": "Serving it from a backend",
          "text": "A built site is just files, so any server can host it. To put the site and your own API in one process, the engine has adapters for Elysia, Fastify, Koa, Express and Hono, and createSite() for anything else. The same templates also render the fragments your API returns to htmx: server.ts import { createSite } from \"htmx-ui-engine\"; const site = await createSite(); Bun.serve({ fetch: (request) => site.handle(request) }); // pages, assets, routes, a 404 page See Server frameworks."
        },
        {
          "id": "bundler-plugins",
          "title": "Use the bundler plugins directly",
          "text": "Already have a build script or a Vite config? Use the engine's Bun or Vite plugin and skip the CLI: import { htmxUi } from \"htmx-ui-engine/bun\"; import tailwind from \"bun-plugin-tailwind\"; await Bun.build({ entrypoints: [\"./pages/index.html\", \"./pages/about.html\"], root: \"./pages\", outdir: \"./dist\", plugins: [await htmxUi(), tailwind], // htmx-ui first, so Tailwind sees rendered markup }); import { htmxUi } from \"htmx-ui-engine/vite\"; import tailwindcss from \"@tailwindcss/vite\"; export default { plugins: [htmxUi(), tailwindcss()], }; The package root, htmx-ui-engine, exports the runtime-independent core for your own tooling: defineConfig, loadConfig, render, renderPage, findPages, routeFor and highlight, plus definePlugin, editHtml and contentType for writing plugins."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/theming",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/theming",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/theming.md",
      "title": "Theming",
      "description": "Every colour and corner in HTMX UI comes from a CSS variable. Built-in themes restyle the library in one import; override the variables to go further, and scope them to restyle part of a page.",
      "section": "Customization",
      "headings": [
        {
          "level": 2,
          "id": "how-it-works",
          "text": "How it works"
        },
        {
          "level": 2,
          "id": "tokens",
          "text": "Tokens"
        },
        {
          "level": 2,
          "id": "override",
          "text": "Overriding tokens"
        },
        {
          "level": 2,
          "id": "built-in-themes",
          "text": "Built-in themes"
        },
        {
          "level": 3,
          "id": "switching-themes",
          "text": "Switching themes"
        },
        {
          "level": 2,
          "id": "custom-themes",
          "text": "Custom themes"
        },
        {
          "level": 2,
          "id": "scrollbars",
          "text": "Scrollbars"
        },
        {
          "level": 2,
          "id": "motion",
          "text": "Motion"
        },
        {
          "level": 2,
          "id": "new-tokens",
          "text": "Adding your own tokens"
        },
        {
          "level": 2,
          "id": "components",
          "text": "Customising components"
        },
        {
          "level": 2,
          "id": "in-your-markup",
          "text": "Using tokens in your own markup"
        }
      ],
      "sections": [
        {
          "id": "how-it-works",
          "title": "How it works",
          "text": "Theming happens in two layers, both in htmx-ui/styles.css: Variables. Plain custom properties on :root hold the actual values, with a second set under :root.dark. Tailwind colours. A @theme inline block maps each variable to a Tailwind colour, so --primary becomes bg-primary, text-primary, border-primary/30 and so on. htmx-ui/styles.css (excerpt) :root { --radius: 0.5rem; --primary: var(--color-indigo-600); --primary-foreground: var(--color-white); /* … */ } :root.dark { --primary: var(--color-indigo-500); /* … */ } @theme inline { --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); /* … */ } Because the mapping is inline, the generated utilities read the variable at runtime: .bg-primary compiles to background-color: var(--primary). Change the variable anywhere (globally, under .dark, or on one element) and everything using it updates. No rebuild needed."
        },
        {
          "id": "tokens",
          "title": "Tokens",
          "text": "Components only use these tokens. Swatches show the current theme; toggle dark mode in the header to compare. Variable Utility Used for Light Dark --background bg-background Page background white zinc-950 --foreground text-foreground Default text zinc-950 zinc-50 --surface bg-surface Cards, alerts, raised panels white zinc-900 --muted bg-muted Subtle fills: code, table heads zinc-100 zinc-800 --muted-foreground text-muted-foreground Secondary text zinc-500 zinc-400 --accent bg-accent Hover and selected backgrounds zinc-100 zinc-800 --accent-foreground text-accent-foreground Text on accent zinc-900 zinc-50 --border border-border Borders and dividers (the default border colour) zinc-200 zinc-800 --ring outline-ring Focus rings indigo-500 indigo-400 --primary bg-primary Brand colour: primary buttons, links indigo-600 indigo-500 --primary-foreground text-primary-foreground Text on primary white white --secondary bg-secondary Secondary buttons and badges zinc-100 zinc-800 --secondary-foreground text-secondary-foreground Text on secondary zinc-900 zinc-50 --info text-info Info alerts and badges sky-600 sky-400 --success text-success Success alerts and badges emerald-600 emerald-400 --warning text-warning Warning alerts and badges amber-600 amber-400 --danger bg-danger Destructive buttons, error alerts red-600 red-500 --danger-foreground text-danger-foreground Text on danger white white --sidebar bg-sidebar Sidebar background zinc-50 zinc-900 --sidebar-foreground text-sidebar-foreground Sidebar text and icons --foreground --foreground --sidebar-primary bg-sidebar-primary Accent fills in the sidebar, such as a logo tile --primary --primary --sidebar-primary-foreground text-sidebar-primary-foreground Text on sidebar primary --primary-foreground --primary-foreground --sidebar-accent bg-sidebar-accent Hovered and current sidebar items --accent --accent --sidebar-accent-foreground text-sidebar-accent-foreground Text on sidebar accent --accent-foreground --accent-foreground --si"
        },
        {
          "id": "override",
          "title": "Overriding tokens",
          "text": "Redefine variables after importing the library. Set both the light and dark values for anything that should differ between them. src/styles.css @import \"tailwindcss\"; @import \"htmx-ui/styles.css\"; :root { --primary: oklch(0.6 0.2 30); /* coral */ --primary-foreground: white; --radius: 0.75rem; } :root.dark { --primary: oklch(0.7 0.17 30); } Selector specificity The library declares its tokens on :root and :root.dark inside @layer base. Unlayered CSS beats layered CSS, so overrides written outside any layer (as above) always win, whatever the import order."
        },
        {
          "id": "built-in-themes",
          "title": "Built-in themes",
          "text": "The package ships six ready-made themes under htmx-ui/themes/: aurora, ember, lagoon, dusk, jade and onyx. They are opt-in and take two steps: import the files you want, then set data-theme on <html> for the whole site or on any element for one part of a page. Projects that don't import a theme ship none of its CSS, and importing without the attribute styles nothing — so only what you use ships, and only where you ask for it. Each one in detail — preview, exact palette, a button that applies it to this site — is on the Themes page. src/styles.css @import \"tailwindcss\"; @import \"htmx-ui/styles.css\"; /* One, or several — an attribute picks which applies where. */ @import \"htmx-ui/themes/aurora.css\"; @import \"htmx-ui/themes/onyx.css\"; html <!-- Whole site <!-- Whole site --> <html lang=\"en\" data-theme=\"aurora\"> <!-- Or one section <!-- Or one section --> <section data-theme=\"dusk\">…</section>"
        },
        {
          "id": "switching-themes",
          "title": "Switching themes",
          "text": "The options are buttons carrying data-theme-palette — this site's header is one, built on a dropdown so it works from the keyboard. initTheme() (already running if you followed the install guide) wires the clicks: the chosen palette lands on <html> as data-theme, the option marks itself checked, and the choice is remembered in localStorage under palette. getPalette() and setPalette(name) are exported for switching from your own code. The theme-swatch dot paints itself with the palette in effect — give a swatch its own data-theme to show one specific palette instead. html <div class=\"dropdown\" data-dropdown> <button type=\"button\" class=\"btn btn-ghost btn-icon\" data-dropdown-trigger aria-haspopup=\"menu\" aria-expanded=\"false\" aria-label=\"Switch color theme\"> <span class=\"theme-swatch\" aria-hidden=\"true\"></span> </button> <div class=\"dropdown-menu w-44\" role=\"menu\" hidden> <p class=\"dropdown-label\">Color theme</p> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemradio\" aria-checked=\"true\" data-theme-palette=\"aurora\"> <span class=\"theme-swatch\" data-theme=\"aurora\" aria-hidden=\"true\"></span>Aurora </button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemradio\" aria-checked=\"false\" data-theme-palette=\"ember\"> <span class=\"theme-swatch\" data-theme=\"ember\" aria-hidden=\"true\"></span>Ember </button> <!-- … one per theme you imported <!-- … one per theme you imported --> </div> </div> Like the light/dark switch, apply the stored choice before first paint with a tiny inline script in the layout's <head>, reading the same keys as htmx-ui/theme: html <script> const palette = localStorage.getItem(\"palette\"); if (palette) document.documentElement.dataset.theme = palette; </script> Both opt-in steps still hold: a picker can only switch to what the project imported, and it writes the same data-theme attribute the markup uses."
        },
        {
          "id": "custom-themes",
          "title": "Custom themes",
          "text": "A theme is just a selector that redefines tokens — what the built-in files are. Scope one to an attribute and you can apply it to a whole page or a single section. These previews use the same markup, with a different data-theme on each wrapper: html <div class=\"grid w-full gap-4 sm:grid-cols-3\"> <div class=\"card\"> <div class=\"card-header\"> <div class=\"flex items-center justify-between\"> <h3 class=\"card-title capitalize\">default</h3> <span class=\"badge badge-primary\">Pro</span> </div> <p class=\"card-description\">Same markup, different tokens.</p> </div> <div class=\"card-footer pt-6\"> <button class=\"btn btn-primary btn-sm\">Upgrade</button> <button class=\"btn btn-outline btn-sm\">Later</button> </div> </div> <div data-theme=\"rose\" class=\"card\"> <div class=\"card-header\"> <div class=\"flex items-center justify-between\"> <h3 class=\"card-title capitalize\">rose</h3> <span class=\"badge badge-primary\">Pro</span> </div> <p class=\"card-description\">Same markup, different tokens.</p> </div> <div class=\"card-footer pt-6\"> <button class=\"btn btn-primary btn-sm\">Upgrade</button> <button class=\"btn btn-outline btn-sm\">Later</button> </div> </div> <div data-theme=\"emerald\" class=\"card\"> <div class=\"card-header\"> <div class=\"flex items-center justify-between\"> <h3 class=\"card-title capitalize\">emerald</h3> <span class=\"badge badge-primary\">Pro</span> </div> <p class=\"card-description\">Same markup, different tokens.</p> </div> <div class=\"card-footer pt-6\"> <button class=\"btn btn-primary btn-sm\">Upgrade</button> <button class=\"btn btn-outline btn-sm\">Later</button> </div> </div> </div> src/styles.css [data-theme=\"rose\"] { --primary: var(--color-rose-600); --ring: var(--color-rose-500); --radius: 1rem; } .dark [data-theme=\"rose\"] { --primary: var(--color-rose-500); } html <!-- Whole page <!-- Whole page --> <html data-theme=\"rose\"> <!-- Or one section <!-- Or one section --> <section data-theme=\"rose\">…</section>"
        },
        {
          "id": "scrollbars",
          "title": "Scrollbars",
          "text": "The library styles every scrollbar to match: thin, with a transparent track and a thumb tinted from --foreground, so it blends into any theme and follows dark mode. Browsers without scrollbar-color (Safari) get the same look through ::-webkit-scrollbar. src/styles.css :root { --scrollbar-thumb: color-mix(in oklab, var(--primary) 30%, transparent); } /* Native scrollbars for one element */ .native-scrollbars { scrollbar-color: auto; scrollbar-width: auto; }"
        },
        {
          "id": "motion",
          "title": "Motion",
          "text": "Components animate with a small set of duration and easing tokens, so the whole library moves the same way and one override retimes it. With prefers-reduced-motion: reduce every duration is 0s: panels still open and close, without moving. Token Default Used for --motion-duration-fast 100ms Small state changes, panels and tooltips leaving --motion-duration-normal 150ms Popovers, menus, lists, tooltips, hover cards --motion-duration-moderate 200ms Dialogs opening --motion-duration-slow 300ms Sheets --motion-duration-slower 400ms Drawers --motion-ease-out quick start, soft landing Things appearing, responding and leaving --motion-ease-in-out symmetric Things moving across the screen --motion-ease-drawer long glide Panels sliding from an edge --motion-ease-spring about 8% overshoot Playful moves; give it 200ms or more src/styles.css /* A calmer library: everything a little slower */ :root { --motion-duration-normal: 200ms; --motion-duration-moderate: 260ms; } Use them in your own markup with Tailwind's variable shorthand: html <div class=\"transition-opacity duration-(--motion-duration-normal) ease-(--motion-ease-out)\">…</div>"
        },
        {
          "id": "new-tokens",
          "title": "Adding your own tokens",
          "text": "Follow the same two steps to add a colour that Tailwind utilities can use: src/styles.css :root { --brand-gradient-from: var(--color-fuchsia-500); } :root.dark { --brand-gradient-from: var(--color-fuchsia-400); } @theme inline { --color-brand-from: var(--brand-gradient-from); } Now from-brand-from, text-brand-from and the rest are available, and they follow dark mode."
        },
        {
          "id": "components",
          "title": "Customising components",
          "text": "Component classes live in @layer components, which sits below utilities. You can restyle them two ways: One element: add utilities. <button class=\"btn btn-primary rounded-full px-8\"> gets a pill shape because utilities win. Everywhere: extend the class in your own @layer components block. src/styles.css @layer components { .btn { @apply font-semibold tracking-tight; } .card { @apply shadow-md; } }"
        },
        {
          "id": "in-your-markup",
          "title": "Using tokens in your own markup",
          "text": "Use the token utilities instead of raw palette colours, and your own UI themes and switches modes with the library: html <aside class=\"rounded-(--radius) border border-border bg-surface p-4 text-muted-foreground\"> Follows the theme and dark mode automatically. </aside>"
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/themes",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/themes",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/themes.md",
      "title": "Themes",
      "description": "The six built-in themes of HTMX UI — aurora, ember, lagoon, dusk, jade and onyx — with live previews, the exact values each file sets and a one-click apply for this site.",
      "section": "Customization",
      "headings": [
        {
          "level": 2,
          "id": "aurora",
          "text": "aurora"
        },
        {
          "level": 2,
          "id": "ember",
          "text": "ember"
        },
        {
          "level": 2,
          "id": "lagoon",
          "text": "lagoon"
        },
        {
          "level": 2,
          "id": "dusk",
          "text": "dusk"
        },
        {
          "level": 2,
          "id": "jade",
          "text": "jade"
        },
        {
          "level": 2,
          "id": "onyx",
          "text": "onyx"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "Six ready-made themes ship under htmx-ui/themes/. Using one takes the same two steps as on the Theming page — import the file, set data-theme — so this page is the catalogue instead: what each one looks like, every value it sets, and a button that puts it on this site. src/styles.css @import \"tailwindcss\"; @import \"htmx-ui/styles.css\"; /* One, or several — an attribute picks which applies where. */ @import \"htmx-ui/themes/aurora.css\"; @import \"htmx-ui/themes/onyx.css\"; html <!-- Whole site <!-- Whole site --> <html lang=\"en\" data-theme=\"aurora\"> <!-- Or one section <!-- Or one section --> <section data-theme=\"dusk\">…</section> Applying one is live: the buttons below — and the picker in this site's header — are plain data-theme-palette buttons wired by initTheme() (Switching themes). Each writes the site-wide data-theme and remembers the choice. Dark mode is independent, so flip that too and every theme shows its dark side."
        },
        {
          "id": "aurora",
          "title": "aurora",
          "text": "Violet → indigo → cyan on a cool, blue-black dark — and the theme this site itself runs on. 0.75rem corners. html <div data-theme=\"aurora\" class=\"w-full max-w-xl rounded-[calc(var(--radius)+4px)] bg-background p-4\"> <div class=\"card\"> <div class=\"card-header\"> <div class=\"flex items-center justify-between gap-2\"> <h3 class=\"card-title capitalize\">aurora</h3> <span class=\"badge badge-primary\">Pro</span> </div> <p class=\"card-description\"> Violet buttons and <code class=\"text-gradient font-semibold\">gradient headlines</code> on the library's plain white; near-black dark with a violet glow. </p> </div> <div class=\"card-footer pt-6\"> <button type=\"button\" class=\"btn btn-primary btn-sm\" data-theme-palette=\"aurora\" aria-pressed=\"false\">Apply aurora</button> </div> </div> </div> Token Light Dark --primary violet-600 violet-500 --primary-foreground white white --ring violet-500 violet-400 --brand-gradient violet-600 → indigo-600 45% → cyan-600 violet-300 → indigo-300 45% → cyan-300 --background white (default) #0a0a0f --surface white (default) #12121b --border zinc-200 (default) #262633 --radius 0.75rem 0.75rem Ships as @import \"htmx-ui/themes/aurora.css\";, applied by data-theme=\"aurora\"; tokens not listed keep their library value."
        },
        {
          "id": "ember",
          "title": "ember",
          "text": "Amber → orange → rose on warm paper, ember-lit near-black when the lights go out. 0.625rem corners. html <div data-theme=\"ember\" class=\"w-full max-w-xl rounded-[calc(var(--radius)+4px)] bg-background p-4\"> <div class=\"card\"> <div class=\"card-header\"> <div class=\"flex items-center justify-between gap-2\"> <h3 class=\"card-title capitalize\">ember</h3> <span class=\"badge badge-primary\">Pro</span> </div> <p class=\"card-description\"> Orange buttons on warm paper (#fffbf7), <code class=\"text-gradient font-semibold\">gradient headlines</code>, 0.625rem corners; coals in the dark. </p> </div> <div class=\"card-footer pt-6\"> <button type=\"button\" class=\"btn btn-primary btn-sm\" data-theme-palette=\"ember\" aria-pressed=\"false\">Apply ember</button> </div> </div> </div> Token Light Dark --primary orange-600 orange-400 --primary-foreground white #20120a --ring orange-500 orange-300 --brand-gradient amber-600 → orange-600 45% → rose-600 amber-300 → orange-300 45% → rose-300 --background #fffbf7 #150e0a --surface white (default) #1f1510 --border zinc-200 (default) #382a21 --radius 0.625rem 0.625rem Ships as @import \"htmx-ui/themes/ember.css\";, applied by data-theme=\"ember\"; tokens not listed keep their library value."
        },
        {
          "id": "lagoon",
          "title": "lagoon",
          "text": "Teal → cyan → sky on ice-clear light, deep-sea dark for the other side. The roundest-at-1rem corner of the set. html <div data-theme=\"lagoon\" class=\"w-full max-w-xl rounded-[calc(var(--radius)+4px)] bg-background p-4\"> <div class=\"card\"> <div class=\"card-header\"> <div class=\"flex items-center justify-between gap-2\"> <h3 class=\"card-title capitalize\">lagoon</h3> <span class=\"badge badge-primary\">Pro</span> </div> <p class=\"card-description\"> Teal buttons on ice-clear light (#f7fdfd), <code class=\"text-gradient font-semibold\">gradient headlines</code>, 1rem corners; underwater dark. </p> </div> <div class=\"card-footer pt-6\"> <button type=\"button\" class=\"btn btn-primary btn-sm\" data-theme-palette=\"lagoon\" aria-pressed=\"false\">Apply lagoon</button> </div> </div> </div> Token Light Dark --primary teal-600 teal-400 --primary-foreground white #04191c --ring cyan-500 cyan-400 --brand-gradient teal-600 → cyan-600 45% → sky-600 teal-300 → cyan-300 45% → sky-300 --background #f7fdfd #04141a --surface white (default) #0a222b --border zinc-200 (default) #1c3d48 --radius 1rem 1rem Ships as @import \"htmx-ui/themes/lagoon.css\";, applied by data-theme=\"lagoon\"; tokens not listed keep their library value."
        },
        {
          "id": "dusk",
          "title": "dusk",
          "text": "Pink → fuchsia → purple twilight, with the roundest corners in the set (1.25rem). html <div data-theme=\"dusk\" class=\"w-full max-w-xl rounded-[calc(var(--radius)+4px)] bg-background p-4\"> <div class=\"card\"> <div class=\"card-header\"> <div class=\"flex items-center justify-between gap-2\"> <h3 class=\"card-title capitalize\">dusk</h3> <span class=\"badge badge-primary\">Pro</span> </div> <p class=\"card-description\"> Fuchsia buttons on a lilac haze (#fdf9ff), <code class=\"text-gradient font-semibold\">gradient headlines</code>, extra-round corners; nightfall dark. </p> </div> <div class=\"card-footer pt-6\"> <button type=\"button\" class=\"btn btn-primary btn-sm\" data-theme-palette=\"dusk\" aria-pressed=\"false\">Apply dusk</button> </div> </div> </div> Token Light Dark --primary fuchsia-600 fuchsia-400 --primary-foreground white #22082a --ring fuchsia-500 fuchsia-300 --brand-gradient pink-600 → fuchsia-600 45% → purple-600 pink-300 → fuchsia-300 45% → purple-300 --background #fdf9ff #120a16 --surface white (default) #1c1122 --border zinc-200 (default) #332340 --radius 1.25rem 1.25rem Ships as @import \"htmx-ui/themes/dusk.css\";, applied by data-theme=\"dusk\"; tokens not listed keep their library value."
        },
        {
          "id": "jade",
          "title": "jade",
          "text": "Emerald → teal → lime: fresh mint in light, deep moss after rain in dark. The tightest colour corners at 0.5rem. html <div data-theme=\"jade\" class=\"w-full max-w-xl rounded-[calc(var(--radius)+4px)] bg-background p-4\"> <div class=\"card\"> <div class=\"card-header\"> <div class=\"flex items-center justify-between gap-2\"> <h3 class=\"card-title capitalize\">jade</h3> <span class=\"badge badge-primary\">Pro</span> </div> <p class=\"card-description\"> Emerald buttons on barely-there mint (#f7fdf9), <code class=\"text-gradient font-semibold\">gradient headlines</code>, 0.5rem corners; moss-dark. </p> </div> <div class=\"card-footer pt-6\"> <button type=\"button\" class=\"btn btn-primary btn-sm\" data-theme-palette=\"jade\" aria-pressed=\"false\">Apply jade</button> </div> </div> </div> Token Light Dark --primary emerald-600 emerald-400 --primary-foreground white #06251a --ring emerald-500 emerald-300 --brand-gradient emerald-600 → teal-600 45% → lime-600 emerald-300 → teal-300 45% → lime-400 --background #f7fdf9 #07140f --surface white (default) #0d211a --border zinc-200 (default) #1e3a2e --radius 0.5rem 0.5rem Ships as @import \"htmx-ui/themes/jade.css\";, applied by data-theme=\"jade\"; tokens not listed keep their library value."
        },
        {
          "id": "onyx",
          "title": "onyx",
          "text": "Graphite on paper, silver on true black — the monochrome counterpoint to the colourful five, with crisp 0.375rem corners. html <div data-theme=\"onyx\" class=\"w-full max-w-xl rounded-[calc(var(--radius)+4px)] bg-background p-4\"> <div class=\"card\"> <div class=\"card-header\"> <div class=\"flex items-center justify-between gap-2\"> <h3 class=\"card-title capitalize\">onyx</h3> <span class=\"badge badge-primary\">Pro</span> </div> <p class=\"card-description\"> Near-black buttons on graphite white (#fafafa), <code class=\"text-gradient font-semibold\">gradient headlines</code>, crisp corners; true-black dark with a silver button. </p> </div> <div class=\"card-footer pt-6\"> <button type=\"button\" class=\"btn btn-primary btn-sm\" data-theme-palette=\"onyx\" aria-pressed=\"false\">Apply onyx</button> </div> </div> </div> Token Light Dark --primary zinc-950 zinc-100 --primary-foreground white zinc-950 --ring zinc-700 zinc-400 --brand-gradient zinc-600 → zinc-950 45% → zinc-700 zinc-400 → zinc-100 45% → zinc-300 --background #fafafa #0a0a0a --surface white (default) #141414 --border zinc-200 (default) #2a2a2a --radius 0.375rem 0.375rem Ships as @import \"htmx-ui/themes/onyx.css\";, applied by data-theme=\"onyx\"; tokens not listed keep their library value. All six redefine the same small set — --radius, --primary, --primary-foreground, --ring and --brand-gradient — plus a tinted light --background where it suits, and in dark mode a brighter --primary, a new --brand-gradient and the --background / --surface / --border trio. Every other token keeps the library's value, so components never shift. Each file is a few hundred bytes: import all the ones you might switch between and the picker will offer exactly those."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/dark-mode",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/dark-mode",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/dark-mode.md",
      "title": "Dark mode",
      "description": "Light and dark are two sets of token values, switched by a .dark class on <html>. The theme switcher follows the OS until the user picks, and never flashes the wrong theme.",
      "section": "Customization",
      "headings": [
        {
          "level": 2,
          "id": "how-it-works",
          "text": "How it works"
        },
        {
          "level": 2,
          "id": "switcher",
          "text": "Theme switcher"
        },
        {
          "level": 3,
          "id": "following-the-os",
          "text": "Following the OS"
        },
        {
          "level": 2,
          "id": "no-flash",
          "text": "Avoiding a flash of the wrong theme"
        },
        {
          "level": 2,
          "id": "dark-variant",
          "text": "Writing dark styles"
        },
        {
          "level": 2,
          "id": "force",
          "text": "Forcing a theme"
        },
        {
          "level": 2,
          "id": "custom-switcher",
          "text": "Building your own switcher"
        }
      ],
      "sections": [
        {
          "id": "how-it-works",
          "title": "How it works",
          "text": "A class, not a media query. Tailwind v4's dark: variant normally follows prefers-color-scheme, which a button can't change. The library redefines it to match a .dark class instead. Tokens swap. :root.dark redefines every colour variable, so components don't need dark: classes at all. Native widgets follow. color-scheme switches too, so scrollbars and form controls match. htmx-ui/styles.css (excerpt) @custom-variant dark (&:where(.dark, .dark *)); @layer base { :root { color-scheme: light; --background: var(--color-white); /* … */ } :root.dark { color-scheme: dark; --background: var(--color-zinc-950); /* … */ } }"
        },
        {
          "id": "switcher",
          "title": "Theme switcher",
          "text": "Any element with data-theme-toggle becomes a toggle. Try this one; it's the same toggle as the one in the header. html <button type=\"button\" class=\"btn btn-outline\" data-theme-toggle aria-pressed=\"false\"> <span class=\"dark:hidden\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"4\"/><path d=\"M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41\"/></svg></span> <span class=\"hidden dark:inline\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\"/></svg></span> Theme: <span data-theme-label>light</span> </button> When clicked, initTheme() from htmx-ui/theme: flips the .dark class on <html>, saves the choice to localStorage under the key theme, sets aria-pressed=\"true\" on every toggle while dark is active, and writes the current theme (light or dark) into any [data-theme-label] inside a toggle. src/app.ts import { initTheme } from \"htmx-ui/theme\"; document.addEventListener(\"DOMContentLoaded\", () => initTheme());"
        },
        {
          "id": "following-the-os",
          "title": "Following the OS",
          "text": "Until the user clicks a toggle, the theme follows prefers-color-scheme and updates live when the OS changes. After a click, the stored choice wins. To go back to following the OS, clear it: ts localStorage.removeItem(\"theme\");"
        },
        {
          "id": "no-flash",
          "title": "Avoiding a flash of the wrong theme",
          "text": "Module scripts run after the first paint, so on a dark-mode visit the page would briefly render light. Put this tiny inline script in <head>, before your stylesheet and app script, so the class is set before anything is drawn: <head> <script> (() => { const stored = localStorage.getItem(\"theme\"); const dark = stored ? stored === \"dark\" : matchMedia(\"(prefers-color-scheme: dark)\").matches; document.documentElement.classList.toggle(\"dark\", dark); })(); </script> Keep it in sync This script repeats the logic in initTheme(). If you change the storage key or the class name, change both."
        },
        {
          "id": "dark-variant",
          "title": "Writing dark styles",
          "text": "Prefer tokens: bg-surface and text-muted-foreground already adapt. For one-off tweaks, use the dark: variant as usual: html <div class=\"rounded-(--radius) bg-amber-100 px-4 py-3 text-sm text-amber-900 dark:bg-amber-400/10 dark:text-amber-300\"> Amber in light, a soft glow in dark. </div>"
        },
        {
          "id": "force",
          "title": "Forcing a theme",
          "text": "To keep part of the page dark (a code sample, a hero) whatever the global theme, add the dark class to that element. Everything inside picks up the dark variant. Note that :root.dark tokens only apply at the root, so set the tokens you need on the element too: css .force-dark { --background: var(--color-zinc-950); --foreground: var(--color-zinc-50); --border: var(--color-zinc-800); color-scheme: dark; } html <section class=\"dark force-dark bg-background text-foreground\">…</section>"
        },
        {
          "id": "custom-switcher",
          "title": "Building your own switcher",
          "text": "getTheme() returns the active theme. A light/dark/system menu, for example, only needs to set or remove the stored value and toggle the class: ts import { getTheme } from \"htmx-ui/theme\"; function setTheme(choice: \"light\" | \"dark\" | \"system\") { if (choice === \"system\") localStorage.removeItem(\"theme\"); else localStorage.setItem(\"theme\", choice); const dark = choice === \"system\" ? matchMedia(\"(prefers-color-scheme: dark)\").matches : choice === \"dark\"; document.documentElement.classList.toggle(\"dark\", dark); } console.log(getTheme()); // \"light\" | \"dark\""
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/templates",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/templates",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/templates.md",
      "title": "Templates & data",
      "description": "Pages are Nunjucks templates rendered at build time. They can read JSON files, inline SVGs and list files, so most content needs no API server.",
      "section": "Guides",
      "headings": [
        {
          "level": 2,
          "id": "pages",
          "text": "Pages and layouts"
        },
        {
          "level": 2,
          "id": "json",
          "text": "JSON data"
        },
        {
          "level": 3,
          "id": "json-in-the-browser-too",
          "text": "JSON in the browser too"
        },
        {
          "level": 2,
          "id": "svg",
          "text": "SVG and icons"
        },
        {
          "level": 2,
          "id": "glob",
          "text": "Listing files"
        },
        {
          "level": 2,
          "id": "assets",
          "text": "Assets"
        },
        {
          "level": 2,
          "id": "custom",
          "text": "Custom globals and filters"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "Optional The components are plain HTML and CSS, and work with any server or template language. This page covers the Nunjucks templates of htmx-ui-engine, which this site is built with too. Macros are new The package's Nunjucks macros (htmx-ui/components/*.html) need the engine's template helpers (svg(), json(), | dedent, | highlight). Their parameters may still change in a minor release before 1.0; each change is listed in the changelog."
        },
        {
          "id": "pages",
          "title": "Pages and layouts",
          "text": "Every .html file in pages/ is a route (docs/theming.html is /docs/theming). The engine renders it with Nunjucks before the bundler (Bun's or Vite) sees it, so pages can extend layouts, include partials and call macros. Template names resolve from the project root, then from the htmx-ui package (its components/ and icons/): pages/pricing.html {% extends \"layouts/site.html\" %} {% from \"components/alert/alert.html\" import alert %} {% block title %}Pricing · HTMX UI{% endblock %} {% block content %} {% call alert(\"Launch offer\", variant=\"success\") %}20% off the first year.{% endcall %} {% endblock %}"
        },
        {
          "id": "json",
          "title": "JSON data",
          "text": "json(path) reads a JSON file at build time. Keep repeated content (navigation, pricing tiers, team members, release notes) in one file and render it anywhere, with no API and no client-side fetching. {% set releases = json(\"data/changelog.json\").releases %} <ul> {% for r in releases %} <li><a href=\"/docs/changelog/{{ r.version }}\">v{{ r.version }}</a>, {{ r.date or \"Unreleased\" }}</li> {% endfor %} </ul> { \"releases\": [ { \"version\": \"0.2.0\", \"summary\": \"Named template roots…\" }, { \"version\": \"0.1.0\", \"date\": \"2026-10-02\", \"summary\": \"First release…\" } ] } That template, live, reading this site's real file: v0.3.0, 2026-10-08 v0.2.0, 2026-10-06 v0.1.0, 2026-10-02 This site's data files: File (under site/) Description data/site.json Site name, description and URL (used for sitemaps). data/docs-nav.json Docs navigation: sidebar, prev/next, components index, page order for agents. data/changelog.json Releases, newest first. data/versions.json Docs versions; the latest is rendered live, older ones are archived."
        },
        {
          "id": "json-in-the-browser-too",
          "title": "JSON in the browser too",
          "text": "The same files can be imported by client code. Bun and Vite bundle them as plain objects: ts import nav from \"./data/docs-nav.json\"; console.log(nav.sections.length);"
        },
        {
          "id": "svg",
          "title": "SVG and icons",
          "text": "svg(path, attrs) inlines an SVG file and sets attributes on its root, and the icon macro builds on it. Inlined SVG inherits colour and costs no extra requests. jinja {{ svg(\"icons/sun.svg\", { class: \"size-5 text-primary\", \"aria-hidden\": \"true\" }) }}"
        },
        {
          "id": "glob",
          "title": "Listing files",
          "text": "glob(pattern) returns matching paths across the template roots, sorted. The icon gallery is generated this way, from the package's icons/: jinja {% for path in glob(\"icons/*.svg\") %}…{% endfor %}"
        },
        {
          "id": "assets",
          "title": "Assets",
          "text": "asset(path) turns a root-relative path into one relative to the page being rendered, so a layout's <script src> works from any depth. The bundler resolves it and names the built file after its content. jinja <script type=\"module\" src=\"{{ asset('app.ts') }}\"></script> assetVer(path) is the same URL with ?ver= appended: the project's own version from package.json, plus a random suffix per dev session (?ver=1.4.0-k3f9 in dev). Use it for assets whose name doesn't change with their content, so a browser or proxy never serves a stale copy. jinja <link rel=\"stylesheet\" href=\"{{ assetVer('app.css') }}\"> A project with no version in package.json gets assetVer() == asset() in a build, and a random one in dev. It makes no difference for most files: both Bun and Vite name built assets after their content, and Bun's dev server does too, so their URLs always change with the file. It matters where a URL stays the same — Vite's dev server serves /app.css un-hashed, and public/ files keep their names in dist/."
        },
        {
          "id": "custom",
          "title": "Custom globals and filters",
          "text": "The helpers above are the ones every site needs; whatever is specific to yours goes in globals and filters in htmx-ui.config.ts. Both are plain JavaScript, registered before every render, so a helper of your own is available wherever a built-in one is: layouts, partials, and macros imported without with context. htmx-ui.config.ts import { defineConfig, markup } from \"htmx-ui-engine\"; export default defineConfig({ globals: { // Any value you can compute at render time… siteName: \"Acme\", // …or a function, called with whatever the template passes. docsUrl: (path: string) => `https://docs.acme.com${path}`, badge: (label: string) => markup(`<span class=\"badge\">${label}</span>`), }, filters: { slug: (text: unknown) => String(text).toLowerCase().replace(/\\s+/g, \"-\"), }, }); jinja <a href=\"{{ docsUrl('/api') }}\">{{ siteName }} API</a> <span>{{ badge(\"new\") }}</span> {{ \"Acme Support\" | slug }} What you return is escaped like any other value, so a global that emits HTML wraps it in markup() — the same way svg() and | highlight return markup. An unknown name fails the build rather than rendering blank, which catches a typo in a helper's name too. Rendering a page yourself takes the same options: site.render() and the exported render() both accept globals and filters, alongside the context values a request supplies."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Function / filter Description json(path) Parsed JSON file. Paths are relative to a template root (the project, then htmx-ui); first match wins. svg(path, attrs) Inline SVG with attributes set on the root <svg>. glob(pattern) Sorted list of root-relative paths matching the pattern. asset(path) Root-relative path rewritten relative to the current page (for <script src>, <link href>). assetVer(path) asset(path) with ?ver=<package.json version>, plus a random suffix in dev. url The current page's route, e.g. /docs/templates. origin The public site URL: url in htmx-ui.config.ts, or $SITE_URL. | dedent Remove shared indentation. | highlight(lang) Syntax-highlight a string at build time. All of them work inside imported macros too, without with context. Add your own with globals and filters in the config, as above."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/preserve-scroll",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/preserve-scroll",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/preserve-scroll.md",
      "title": "Scroll position",
      "description": "Two htmx extensions: preserve-scroll keeps the scroll position of marked containers when htmx swaps them, and sidebar-active-scroll also keeps a sidebar's current page in view, on a fresh page load too.",
      "section": "Guides",
      "headings": [
        {
          "level": 2,
          "id": "setup",
          "text": "Setup"
        },
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "how",
          "text": "How it works"
        },
        {
          "level": 2,
          "id": "sidebar",
          "text": "Sidebars: sidebar-active-scroll"
        },
        {
          "level": 2,
          "id": "api",
          "text": "API"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "When htmx swaps content it inserts new elements, and a new element starts scrolled to the top. That is right for most content, but not for a sidebar the reader has scrolled to the current page, a log refreshed by polling, or a table re-rendered after a filter. Mark those containers with hx-preserve-scroll and they come back where they were. html <div class=\"w-full\"> <div id=\"scroll-demo\"> <div class=\"grid gap-4 sm:grid-cols-2\"> <div class=\"space-y-2\"> <p class=\"text-sm font-medium\">With hx-preserve-scroll</p> <ol class=\"h-48 space-y-1 overflow-y-auto rounded-(--radius) border border-border bg-surface p-3 text-sm\" hx-preserve-scroll=\"demo-list\" tabindex=\"0\" aria-label=\"Kept list\"> <li class=\"flex justify-between gap-4\"><span>Item 1</span><span class=\"text-muted-foreground tabular-nums\">page load</span></li> <li class=\"flex justify-between gap-4\"><span>Item 2</span><span class=\"text-muted-foreground tabular-nums\">page load</span></li> <li class=\"flex justify-between gap-4\"><span>Item 3</span><span class=\"text-muted-foreground tabular-nums\">page load</span></li> <li class=\"flex justify-between gap-4\"><span>Item 4</span><span class=\"text-muted-foreground tabular-nums\">page load</span></li> <li class=\"flex justify-between gap-4\"><span>Item 5</span><span class=\"text-muted-foreground tabular-nums\">page load</span></li> <li class=\"flex justify-between gap-4\"><span>Item 6</span><span class=\"text-muted-foreground tabular-nums\">page load</span></li> <li class=\"flex justify-between gap-4\"><span>Item 7</span><span class=\"text-muted-foreground tabular-nums\">page load</span></li> <li class=\"flex justify-between gap-4\"><span>Item 8</span><span class=\"text-muted-foreground tabular-nums\">page load</span></li> <li class=\"flex justify-between gap-4\"><span>Item 9</span><span class=\"text-muted-foreground tabular-nums\">page load</span></li> <li class=\"flex justify-between gap-4\"><span>Item 10</span><span class=\"text-muted-foreground tabular-nums\">page load</span></li> <li class=\"flex just"
        },
        {
          "id": "setup",
          "title": "Setup",
          "text": "Register the extension once, after htmx has loaded: app.ts import \"htmx.org\"; import { registerPreserveScroll } from \"htmx-ui\"; registerPreserveScroll(); // uses window.htmx; or registerPreserveScroll(htmx) Like every htmx 4 extension it applies to the whole page once registered; there is no hx-ext. If you restrict extensions with htmx.config.extensions, add preserve-scroll to the list. Registering twice is harmless."
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "Put hx-preserve-scroll on the element that scrolls (the one with overflow: auto), and give it a key: html <div class=\"h-96 overflow-y-auto\" hx-preserve-scroll=\"results\">…</div> <!-- An empty attribute uses the element's id as the key <!-- An empty attribute uses the element's id as the key --> <div id=\"log\" class=\"h-80 overflow-y-auto\" hx-preserve-scroll>…</div> The key is what connects the old element with the new one. Use as many containers on a page as you like, each with its own key. An element with no key and no id is ignored. Repeated keys are matched in document order: the first old element with the first new one, and so on. A list of horizontally scrolling rows can share one key, as the two rows in the demo above do. The swap can replace the container itself (outerHTML), its parent, or the whole body in a boosted navigation. It works with OOB swaps and partials too. hx-preserve-scroll=\"false\" opts an element out, for a fragment that should start at the top even though it has the same id."
        },
        {
          "id": "how",
          "title": "How it works",
          "text": "Before each swap (htmx:before:swap) it records the scroll position of every marked element in the document. New content is scrolled to the recorded position at htmx:before:settle, before the browser paints it, so it never flashes at the top. A last pass at htmx:finally:swap catches content from every task. Elements the swap didn't replace are left alone, so a container the reader scrolls while the request is in flight keeps the reader's position. Morph swaps keep the element and therefore its scroll already. Nothing is stored: positions are kept only for the duration of a swap. Content that grows after the swap Positions are applied once, when the content is inserted. If images or lazy content above the position load later and change its height, give them a fixed size (width and height on images) so the position still lands on the same content."
        },
        {
          "id": "sidebar",
          "title": "Sidebars: sidebar-active-scroll",
          "text": "Keeping the position is not enough for a navigation sidebar. Open a link in a new tab, or reload, and there is no position to keep: the sidebar starts at the top, and the current page may be far below. A link in the page itself can lead to a page whose sidebar entry is out of view. sidebar-active-scroll handles both: Fresh page load (new tab, reload, a sidebar new to the page): the active item is centred in the sidebar. After a swap: the old position comes back, as with hx-preserve-scroll. If the new active item is then out of view, the sidebar scrolls just enough to show it, with a neighbouring item's height to spare. Clicking a link that is already visible doesn't move anything. A hidden sidebar (an off-canvas panel with display: none) is centred the first time it gets a size. Only the sidebar scrolls; the page never jumps. app.ts import { registerSidebarActiveScroll } from \"htmx-ui\"; registerSidebarActiveScroll(); html <div class=\"sidebar-content\" hx-sidebar-active-scroll=\"docs-nav\"> … <a class=\"sidebar-menu-button\" href=\"/docs/theming\" aria-current=\"page\">Theming</a> … </div> Put hx-sidebar-active-scroll on the element that scrolls. Its value is the key, with the same rules as hx-preserve-scroll (empty: the id; \"false\": off). The active item is the first [aria-current=\"page\"] or [data-active] inside, as the Sidebar component marks it. hx-sidebar-active-item=\"<selector>\" picks something else. Use one extension per container. This site's docs sidebar uses sidebar-active-scroll."
        },
        {
          "id": "api",
          "title": "API",
          "text": "Exported from htmx-ui: registerPreserveScroll(htmx?): registers the extension as preserve-scroll. snapshotScroll(root?) and restoreScroll(snapshot, root?): the same capture and restore, for DOM you replace yourself. registerSidebarActiveScroll(htmx?): registers sidebar-active-scroll and centres the active item of sidebars already on the page. revealActive(container, mode?): scrolls a container so its active item shows, \"nearest\" (the default: only if it is out of view) or \"center\". revealAllActive(root?, mode?) does the same for every [hx-sidebar-active-scroll]; call it after changing aria-current yourself. ts import { restoreScroll, snapshotScroll } from \"htmx-ui\"; const snapshot = snapshotScroll(); panel.innerHTML = html; restoreScroll(snapshot);"
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/ai-agents",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/ai-agents",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/ai-agents.md",
      "title": "AI agents",
      "description": "Every docs page is published as Markdown, with an llms.txt index and JSON sitemaps, so coding agents can read the docs as specs.",
      "section": "Guides",
      "headings": [
        {
          "level": 2,
          "id": "markdown",
          "text": "Markdown pages"
        },
        {
          "level": 2,
          "id": "indexes",
          "text": "Indexes"
        },
        {
          "level": 2,
          "id": "prompts",
          "text": "Prompts"
        },
        {
          "level": 3,
          "id": "prompt-llms-txt",
          "text": "Use the docs for one task"
        },
        {
          "level": 3,
          "id": "prompt-specific-pages",
          "text": "Point at specific pages"
        },
        {
          "level": 2,
          "id": "skill",
          "text": "Make it a skill"
        },
        {
          "level": 2,
          "id": "how",
          "text": "How it's generated"
        }
      ],
      "sections": [
        {
          "id": "markdown",
          "title": "Markdown pages",
          "text": "Add .md to any docs URL to get the page as Markdown: /docs/components/button.md, /docs.md. Each file starts with frontmatter (title, description, URL, section). Live examples become their source code, and reference tables become Markdown tables. The Copy Markdown button at the top of every page copies it for pasting into a chat. /docs/components/badge.md --- title: \"Badge\" description: \"A small label for status, counts or categories.\" url: \"/docs/components/badge\" section: \"Components\" --- # Badge A small label for status, counts or categories. ```html <span class=\"badge badge-primary\">New</span> ```"
        },
        {
          "id": "indexes",
          "title": "Indexes",
          "text": "Each opens in a new tab. URL Contents /llms.txt llms.txt index (llmstxt.org): every docs page as a Markdown link with its description, grouped by section. Start here. /llms-full.txt Every docs page's Markdown in one file, for loading the whole library into context. /sitemap.json Every page with URL, Markdown URL, title, description, section, heading outline and section text, plus the list of docs versions. The index for site search. /docs/sitemap.json The same for the latest docs only. Each older version has its own, e.g. /docs/v0.1/sitemap.json. /docs/versions.json Every docs version with its URL prefix, its sitemap and the pages it contains. /sitemap.xml Standard XML sitemap for search engines. /robots.txt Allows crawling and points at the sitemap. /sitemap.json (excerpt) { \"name\": \"HTMX UI\", \"url\": \"https://example.com\", \"version\": \"0.1\", \"versions\": [ { \"id\": \"0.1\", \"label\": \"v0.1\", \"path\": \"/docs\", \"latest\": true, \"sitemap\": \"/docs/sitemap.json\" } ], \"pages\": [ { \"url\": \"/docs/components/tabs\", \"markdown\": \"/docs/components/tabs.md\", \"title\": \"Tabs\", \"section\": \"Components\", \"headings\": [{ \"level\": 2, \"id\": \"markup\", \"text\": \"Markup\" }], \"sections\": [{ \"id\": \"markup\", \"title\": \"Markup\", \"text\": \"Each [data-tab] button shows the [data-tab-panel]…\" }] } ] } Every sections[].id is a heading anchor, so url + \"#\" + id opens that exact section. This site's own search (Ctrl K) works this way, and an MCP server can list pages from these files and link straight to the right place."
        },
        {
          "id": "prompts",
          "title": "Prompts",
          "text": "These work with any agent that can fetch URLs (Claude, ChatGPT, Cursor, Copilot and others). Copy, then replace the task."
        },
        {
          "id": "prompt-llms-txt",
          "title": "Use the docs for one task",
          "text": "Prompt Read https://susonwaiba.github.io/htmx-ui/llms.txt. It indexes the htmx-ui docs; every link is a Markdown page. Fetch the pages relevant to the task below and follow their markup and class names exactly. Use htmx-ui's design tokens (bg-primary, text-muted-foreground, border-border) instead of raw colours. Task: build a settings page with tabs for Profile and Billing, and a danger-zone card with a delete button."
        },
        {
          "id": "prompt-specific-pages",
          "title": "Point at specific pages",
          "text": "Prompt Build a settings page using htmx-ui. Follow these specs exactly: https://susonwaiba.github.io/htmx-ui/docs/components/card.md https://susonwaiba.github.io/htmx-ui/docs/components/tabs.md"
        },
        {
          "id": "skill",
          "title": "Make it a skill",
          "text": "Save the lookup steps as a skill so your agent uses the docs automatically, every time you work on UI, without pasting URLs again. Ask your agent to create it: Prompt Create a skill named \"htmx-ui\" for this project (for Claude Code: .claude/skills/htmx-ui/SKILL.md). It should trigger whenever I build or change UI with htmx-ui components, classes, theming or dark mode. The skill must tell you to: 1. Fetch https://susonwaiba.github.io/htmx-ui/llms.txt to find the relevant docs pages. 2. Fetch those pages as Markdown (https://susonwaiba.github.io/htmx-ui/docs/<page>.md) and follow their markup exactly. 3. Check the htmx-ui version in package.json; if it's older than the latest docs, read https://susonwaiba.github.io/htmx-ui/docs/versions.json and use that version's pages instead. 4. Prefer design tokens over raw Tailwind colours, and call initComponents() for new htmx content. Keep the skill short and link to the docs rather than copying them. Or create the file yourself: .claude/skills/htmx-ui/SKILL.md --- name: htmx-ui description: Build UI with the htmx-ui component library (Tailwind CSS v4 + htmx). Use when adding or changing buttons, cards, alerts, tabs, dropdowns, tables, prose, theming or dark mode in this project. --- # htmx-ui The docs are written for agents. Don't guess class names; look them up. 1. Fetch the index: https://susonwaiba.github.io/htmx-ui/llms.txt 2. Fetch the pages you need as Markdown, e.g. https://susonwaiba.github.io/htmx-ui/docs/components/tabs.md 3. Match the docs to the installed version: read \"htmx-ui\" in package.json. If it's older than the latest docs, use the matching version from https://susonwaiba.github.io/htmx-ui/docs/versions.json (pages live under its \"path\"). 4. Search for a topic in https://susonwaiba.github.io/htmx-ui/sitemap.json: `pages[].sections[]` hold the text of every section. Rules: - Copy markup and class names from the docs exactly. - Use design tokens (bg-primary, text-muted-foreground, border-border), not raw pale"
        },
        {
          "id": "how",
          "title": "How it's generated",
          "text": "Nothing is written by hand. The docs plugin (htmx-ui-plugin-docs, in the site's site/htmx-ui.config.ts) renders every page at build time, converts the article body to Markdown, and writes the indexes. The dev server generates the same files on request. Add it to your own site to publish the same files for your docs. Page order follows the docs navigation in site/data/docs-nav.json. Absolute URLs (and the URLs in the prompts above) use the SITE_URL environment variable, falling back to url in site/data/site.json. Every docs heading gets an id and a # link, so sections can be linked directly. Add data-md-skip to an element to leave it out of the Markdown and the search text (purely visual blocks). Terminal SITE_URL=https://htmx-ui.example bun run build Write for both readers Agents read exactly what people read, so a clear page works for both. Show the full markup in every example, say what each class does in the reference table, and describe behaviour in words, not just in a live demo."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/plugins",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/plugins",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/plugins.md",
      "title": "Plugins",
      "description": "Optional features on top of htmx-ui-engine: install a plugin, list it in your config, and its templates, routes, build output and commands join your site.",
      "section": "Plugins",
      "headings": [
        {
          "level": 2,
          "id": "official",
          "text": "Official plugins"
        },
        {
          "level": 2,
          "id": "using",
          "text": "Using a plugin"
        },
        {
          "level": 3,
          "id": "conditional",
          "text": "Only in some environments"
        },
        {
          "level": 2,
          "id": "order",
          "text": "Who wins"
        },
        {
          "level": 2,
          "id": "overriding",
          "text": "Overriding a plugin's templates"
        },
        {
          "level": 2,
          "id": "commands",
          "text": "Plugin commands"
        },
        {
          "level": 2,
          "id": "writing",
          "text": "Writing your own"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "Most sites are a handful of pages, and the engine keeps them small. Features only some sites need, such as a docs section, versioned docs or site search, are plugins: separate packages you install and list in htmx-ui.config.ts. A site that doesn't use one doesn't install it or load it, and nothing in its build changes. A plugin can add everything a config can: templates and macros, template globals and filters, a transform over every page, request handlers, files written by the build, and CLI commands. They work the same in htmx-ui dev, htmx-ui build, on Bun and on Node, and behind every server adapter."
        },
        {
          "id": "official",
          "title": "Official plugins",
          "text": "Plugin Package Adds Docs htmx-ui-plugin-docs Heading anchors, Markdown for every docs page, llms.txt, JSON and XML sitemaps, navigation and page macros. Versions htmx-ui-plugin-versions Older releases' docs kept online as frozen builds, a version switcher, an old-version banner, and commands to name and archive versions. Search htmx-ui-plugin-search A Ctrl/⌘K command palette that fuzzy-searches pages and sections. They work alone and better together: search reads the sitemap the docs plugin publishes, and with the versions plugin, readers of old docs search that version and the sitemaps describe every version. This website uses all three."
        },
        {
          "id": "using",
          "title": "Using a plugin",
          "text": "Install it, then list it under plugins: bun add htmx-ui-plugin-docs npm install htmx-ui-plugin-docs pnpm add htmx-ui-plugin-docs yarn add htmx-ui-plugin-docs htmx-ui.config.ts import { defineConfig } from \"htmx-ui-engine\"; import docs from \"htmx-ui-plugin-docs\"; export default defineConfig({ url: \"https://example.com\", plugins: [docs({ name: \"Acme\", description: \"Acme's docs.\" })], }); Each plugin is a function: call it, with options if it takes any, and the result goes in the list. Restart htmx-ui dev after changing the list. A plugin that has a browser side or styles (the search plugin, for one) says so in its docs; those are two lines you add yourself, so you decide where they go: app.ts import { initSearch } from \"htmx-ui-plugin-search/client\"; document.addEventListener(\"DOMContentLoaded\", () => { initSearch(); }); styles.css @import \"tailwindcss\"; @import \"htmx-ui/styles.css\"; @import \"htmx-ui-plugin-docs/styles.css\"; A plugin's stylesheet also tells Tailwind to scan the plugin's templates, so the utilities they use are generated on Node too, where Vite doesn't look inside node_modules."
        },
        {
          "id": "conditional",
          "title": "Only in some environments",
          "text": "plugins skips false, null and undefined, and flattens nested lists, so a plugin can be switched on by a condition: htmx-ui.config.ts const preview = process.env.PREVIEW === \"1\"; export default defineConfig({ plugins: [docs(), preview && search()], });"
        },
        {
          "id": "order",
          "title": "Who wins",
          "text": "Your own config always has the last word. Between plugins, the order of the list decides. Hook Description routes Merged. Your route on the same pattern replaces a plugin's; a later plugin's replaces an earlier one's. globals, filters Merged the same way: yours win, then later plugins. fetch Yours runs first; then each plugin's in order, until one answers. transform Each plugin's in order, then yours, so yours sees their output. build.done Each plugin's in order, then yours. Templates Your project's roots first, then each plugin's, then htmx-ui's. A file of yours with the same name as a plugin's template replaces it. Commands A plugin can't replace dev, build or preview, and two plugins can't add the same command: the config refuses to load."
        },
        {
          "id": "overriding",
          "title": "Overriding a plugin's templates",
          "text": "Plugins keep their templates in a directory named after themselves, such as search/macros.html, and their template root is searched after yours. To change one, copy it into your project under the same name and edit it: your copy wins, everywhere it is used. my-site/ search/macros.html your own search() replaces the plugin's pages/ layouts/ ... Keep the data attributes A plugin's browser code finds its markup by data-* attributes (data-search-dialog, data-version-switcher, ...). Change the classes and the layout as you like; keep the attributes its docs list."
        },
        {
          "id": "commands",
          "title": "Plugin commands",
          "text": "A plugin can add commands to the CLI. htmx-ui --help lists the ones your plugins add: htmx-ui --help ... Plugin commands: versions:name <x.y.z> name the docs version in development (versions) versions:archive [--skip-build] build, freeze the latest docs version, start a new next (versions) They run on the same runtime as the rest of the CLI, so add them to package.json scripts like any other: package.json { \"scripts\": { \"docs:archive\": \"htmx-ui versions:archive\" } }"
        },
        {
          "id": "writing",
          "title": "Writing your own",
          "text": "A plugin is an object with a name and the hooks it needs, so a project can keep one in its own repository before it is worth publishing. See Writing a plugin."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/plugins/docs",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/plugins/docs",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/plugins/docs.md",
      "title": "Docs plugin",
      "description": "htmx-ui-plugin-docs: a docs section people and AI agents can both read. Heading anchors, Markdown for every page, llms.txt, sitemaps, navigation and page macros.",
      "section": "Plugins",
      "headings": [
        {
          "level": 2,
          "id": "install",
          "text": "Install"
        },
        {
          "level": 2,
          "id": "layout",
          "text": "The docs layout"
        },
        {
          "level": 2,
          "id": "navigation",
          "text": "Navigation"
        },
        {
          "level": 2,
          "id": "anchors",
          "text": "Heading anchors"
        },
        {
          "level": 2,
          "id": "outputs",
          "text": "What it publishes"
        },
        {
          "level": 2,
          "id": "macros",
          "text": "Macros"
        },
        {
          "level": 2,
          "id": "options",
          "text": "Options"
        },
        {
          "level": 2,
          "id": "api",
          "text": "For other plugins"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "htmx-ui-plugin-docs turns the pages under /docs into documentation: every section linkable, every page also published as Markdown, and indexes for search engines, site search and AI agents. One navigation file drives the sidebar, previous/next links and the order of everything it publishes. This website's docs are built with it; see AI agents for what the output looks like."
        },
        {
          "id": "install",
          "title": "Install",
          "text": "bun add htmx-ui-plugin-docs npm install htmx-ui-plugin-docs pnpm add htmx-ui-plugin-docs yarn add htmx-ui-plugin-docs htmx-ui.config.ts import { defineConfig } from \"htmx-ui-engine\"; import docs from \"htmx-ui-plugin-docs\"; export default defineConfig({ url: \"https://example.com\", // absolute URLs in the sitemaps; $SITE_URL overrides it plugins: [docs({ name: \"Acme\", description: \"Acme's docs.\" })], }); styles.css @import \"tailwindcss\"; @import \"htmx-ui/styles.css\"; @import \"htmx-ui-plugin-docs/styles.css\"; There is no browser module to add: the \"Copy Markdown\" button is a clipboard button that fetches the page's .md, so htmx-ui's initComponents() runs it. (htmx-ui-plugin-docs/client still exports initMarkdownCopy(), for pages built with the plugin's older markup.)"
        },
        {
          "id": "layout",
          "title": "The docs layout",
          "text": "The plugin reads pages the way they are rendered, so it needs three things from your docs layout: an <article> whose first h1 is the page title, the page body inside <div data-docs-content>, and a <meta name=\"description\">. An .eyebrow in the article names the page's section. layouts/docs.html {% extends \"layouts/base.html\" %} {% from \"docs/macros.html\" import markdown_actions %} {% set nav = docsNav(url) %} {% block content %} <aside> {% for group in nav.sections %} <p>{{ group.title }}</p> {% for item in group.items %} <a href=\"{{ item.href }}\"{% if item.href == nav.active %} aria-current=\"page\"{% endif %}>{{ item.title }}</a> {% endfor %} {% endfor %} </aside> <article> <p class=\"eyebrow\">{{ nav.section }}</p> <h1>{{ title }}</h1> {{ markdown_actions(url) }} <div data-docs-content class=\"prose\">{% block docs %}{% endblock %}</div> {% if nav.prev %}<a href=\"{{ nav.prev.href }}\">{{ nav.prev.title }}</a>{% endif %} {% if nav.next %}<a href=\"{{ nav.next.href }}\">{{ nav.next.title }}</a>{% endif %} </article> {% endblock %}"
        },
        {
          "id": "navigation",
          "title": "Navigation",
          "text": "One file lists the docs in order, in sections. It is found through the template roots, like json(): data/docs-nav.json { \"sections\": [ { \"title\": \"Getting started\", \"items\": [ { \"title\": \"Introduction\", \"href\": \"/docs\" }, { \"title\": \"Installation\", \"href\": \"/docs/installation\", \"description\": \"Set it up.\" } ] } ] } docsNav(url) returns where a page sits in it: Field Description sections The file's sections, for the sidebar. pages Every entry, in order. current The page's own entry, or null when it is not listed. active href of the closest entry: the page's own, or the longest one its URL starts with. /docs/servers/express marks /docs/servers active when it has no entry of its own, so a section can have pages of its own. section Title of the active entry's section (\"Docs\" when none), for the eyebrow. prev, next The entries before and after the page's own, or null. Entries can carry anything else you need (this site marks components with \"component\": true to list them on the components page). Pages missing from the file still get Markdown, and sort with their closest listed parent."
        },
        {
          "id": "anchors",
          "title": "Heading anchors",
          "text": "Every h2 and h3 in [data-docs-content] gets an id from its text and a # link, styled by htmx-ui's .heading-anchor. An explicit id is kept, so give one to a heading whose wording may change while links to it must not. Headings inside links, .not-prose component markup, .demo previews and [data-md-skip] are not sections and are left alone. The HTML, the Markdown and the sitemap all see the same ids."
        },
        {
          "id": "outputs",
          "title": "What it publishes",
          "text": "URL Description <route>.md Each docs page as Markdown with frontmatter: /docs.md, /docs/setup.md. Demos become their source, tables Markdown tables, alerts blockquotes. /llms.txt llmstxt.org index: every docs page as a Markdown link with its description, grouped by section. /llms-full.txt Every docs page's Markdown in one file. /sitemap.json Every page with title, description, section, outline and section text. The index site search reads. /docs/sitemap.json The same for the docs only. /sitemap.xml For search engines. The 404 page is left out of both sitemaps. /robots.txt Points crawlers at the sitemap. A robots.txt in public/ is used instead. htmx-ui dev and the server adapters generate them on request; htmx-ui build writes them to dist/. Mark purely visual blocks data-md-skip to leave them out of the Markdown and the search text."
        },
        {
          "id": "macros",
          "title": "Macros",
          "text": "jinja {% from \"docs/macros.html\" import demo, classes, markdown_actions %} {% call demo() %}<button class=\"btn btn-primary\">Save</button>{% endcall %} {{ classes([[\"btn\", \"The base class.\"], [\"btn-primary\", \"The main action.\"]]) }} {{ markdown_actions(url) }} Macro Description demo(class, source, isolate) A live preview of the call body with its source underneath. In Markdown it becomes just the source. classes(rows, heading) A two-column reference table of [name, description] rows. markdown_actions(page_url) A \"Copy Markdown\" clipboard button and a \"View .md\" link for the page."
        },
        {
          "id": "options",
          "title": "Options",
          "text": "Option Description prefix Route prefix of the docs. Default \"/docs\". nav The navigation file. Default \"data/docs-nav.json\"; false for none. name, description The site, for llms.txt and sitemap.json. anchors Heading ids and # links. Default true. robots Publish /robots.txt. Default true. Set the site URL for production builds Sitemaps and llms.txt carry absolute URLs from url in the config, or $SITE_URL. The build warns when neither is set."
        },
        {
          "id": "api",
          "title": "For other plugins",
          "text": "Its api renders and converts pages for plugins that build on it: ts const docs = config.plugins.find((p) => p.name === \"docs\")?.api as DocsApi | undefined; for (const page of docs?.pages() ?? []) console.log(page.url, page.title, page.sections.length); pages() renders every page (with title, description, section, headings and sections), markdown(page) converts one, and isDocs(url) tells docs pages from the rest. The converters and generators are exported too: pageMarkdown, pageMeta, addHeadingAnchors, sitemapJson, llmsTxt."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/plugins/versions",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/plugins/versions",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/plugins/versions.md",
      "title": "Versions plugin",
      "description": "htmx-ui-plugin-versions: keep every release's docs online as a frozen build, with a version switcher, an old-version banner and commands to name and archive versions.",
      "section": "Plugins",
      "headings": [
        {
          "level": 2,
          "id": "install",
          "text": "Install"
        },
        {
          "level": 2,
          "id": "templates",
          "text": "The switcher and the banner"
        },
        {
          "level": 2,
          "id": "releasing",
          "text": "Releasing a version"
        },
        {
          "level": 2,
          "id": "urls",
          "text": "URLs"
        },
        {
          "level": 2,
          "id": "options",
          "text": "Options"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "htmx-ui-plugin-versions keeps the docs of every release online. The version being written renders live at /docs, as usual. Each older one is a frozen build in archive/v<id>/, served at /docs/v<id>/: its pages, scripts and styles exactly as they were released, so later changes to components or layouts can never break them. This website's versions work this way."
        },
        {
          "id": "install",
          "title": "Install",
          "text": "bun add htmx-ui-plugin-versions npm install htmx-ui-plugin-versions pnpm add htmx-ui-plugin-versions yarn add htmx-ui-plugin-versions htmx-ui.config.ts import { defineConfig } from \"htmx-ui-engine\"; import docs from \"htmx-ui-plugin-docs\"; import versions from \"htmx-ui-plugin-versions\"; export default defineConfig({ plugins: [docs(), versions()], }); List the versions. The one being written has no number yet; it is next until it is released: data/versions.json { \"latest\": \"next\", \"versions\": [{ \"id\": \"next\", \"label\": \"next\" }] } styles.css @import \"tailwindcss\"; @import \"htmx-ui/styles.css\"; @import \"htmx-ui-plugin-versions/styles.css\"; app.ts import { initVersions } from \"htmx-ui-plugin-versions/client\"; document.addEventListener(\"DOMContentLoaded\", () => initVersions()); It works without the docs plugin, but with it each archived version also keeps its Markdown and its own sitemap, the sitemaps list every version, and search follows the version being read."
        },
        {
          "id": "templates",
          "title": "The switcher and the banner",
          "text": "layouts/docs.html {% from \"versions/macros.html\" import version_switcher, version_banner %} <aside> {{ version_switcher(class=\"mb-6 w-full\", links=[{ title: \"Changelog\", href: \"/docs/changelog\" }]) }} </aside> <article> {{ version_banner() }} <h1>{{ title }}</h1> </article> Template Description version_switcher(class, links) A Dropdown listing every version. links are extra entries under a separator: [{ title, href }]. At runtime the client points each version at the same page when it exists there, and adds versions released after the page was built. version_banner() Where an archived page's \"you're viewing an old version\" notice goes, linking to the same page in the latest version. Empty on the latest docs. docsVersions() The versions file as templates see it: latest, current, label, manifest and versions (id, label, released, path, latest). Archiving bakes the banner into every frozen page, so it shows without JavaScript. The client only refreshes it, so an old version learns about releases that came after it."
        },
        {
          "id": "releasing",
          "title": "Releasing a version",
          "text": "The plugin adds two CLI commands. Give them scripts, so they run on the runtime you build with: package.json { \"scripts\": { \"docs:name\": \"htmx-ui versions:name\", \"docs:archive\": \"htmx-ui versions:archive\" } } bun run docs:name 1.2.0 bun run docs:archive npm run docs:name 1.2.0 npm run docs:archive pnpm run docs:name 1.2.0 pnpm run docs:archive yarn run docs:name 1.2.0 yarn run docs:archive versions:name <x.y.z> numbers next and dates it today. Docs are versioned per minor before 1.0 (0.2.0 becomes 0.2) and per major after (1.2.0 becomes 1); pass 0.2 or 1 to choose yourself. It refuses when the latest version already has a number. versions:archive builds the site, freezes the latest version's built docs into archive/v<id>/ and opens a new next. Links between docs pages are rewritten into the snapshot, assets move to _assets/, pages get noindex so old versions don't compete with the latest in search results, and every <body> gets hx-boost=\"false\" so nothing on a frozen page boosts the live site's code into it. --skip-build archives the dist/ you already have. Commit archive/ and data/versions.json. Never edit the archive by hand Snapshots are built output. Fix an old version by rebuilding it from its release and archiving it again. Patch releases update the current docs in place rather than getting a version of their own."
        },
        {
          "id": "urls",
          "title": "URLs",
          "text": "URL Description /docs/... The latest version, rendered from pages/docs. /docs/v<id>/... An archived version, from archive/v<id>/. Also /docs/v<id>.md and each page's .md with the docs plugin. /docs/versions.json The manifest: every version with its path, release date, sitemap and the pages it has. The version in development never reaches a URL: it is always served at /docs, and only archived versions get a /v<id> prefix. Links that must point across versions carry data-version-link, which archiving leaves alone, and hx-boost=\"false\": every version ships its own theme and scripts, so switching versions must load the target page fresh instead of swapping the body under this version's stylesheet. Archiving applies the same rule to the whole snapshot, by setting hx-boost=\"false\" on its <body>. A frozen page ships its own theme, stylesheet and scripts, so no link inside it — its logo, footer, sidebar or a link in the prose — may be boosted into the current site, and no current page may be swapped into it: every navigation out of a snapshot is a full page load."
        },
        {
          "id": "options",
          "title": "Options",
          "text": "Option Description prefix Route prefix of the docs, the same as the docs plugin's. Default \"/docs\". file The versions file, relative to the project root. Default \"data/versions.json\". archive Where frozen versions are kept, relative to the project root. Default \"archive\". The functions behind the commands are exported for release scripts of your own: nameVersion, startNext, archiveDocs, versionId, readVersionsFile and writeVersionsFile. This repository's bun run version:set uses them to name the docs version and set its package versions in one step."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/plugins/search",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/plugins/search",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/plugins/search.md",
      "title": "Search plugin",
      "description": "htmx-ui-plugin-search: a Ctrl/⌘K command palette that fuzzy-searches every page and section, with no server or search service.",
      "section": "Plugins",
      "headings": [
        {
          "level": 2,
          "id": "install",
          "text": "Install"
        },
        {
          "level": 2,
          "id": "macro",
          "text": "search()"
        },
        {
          "level": 2,
          "id": "matching",
          "text": "How it matches"
        },
        {
          "level": 2,
          "id": "versions",
          "text": "With versioned docs"
        },
        {
          "level": 2,
          "id": "index",
          "text": "Your own index"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "htmx-ui-plugin-search adds a search button and a command palette. It opens with Ctrl/⌘ K or /, matches pages and the sections inside them as you type, and opens the page or jumps straight to the section. There is no server and no search service: it reads one JSON index, once, the first time it opens (or as soon as the pointer reaches the button). Try it in this site's header."
        },
        {
          "id": "install",
          "title": "Install",
          "text": "The index is the sitemap.json the docs plugin publishes, so install both: bun add htmx-ui-plugin-search htmx-ui-plugin-docs npm install htmx-ui-plugin-search htmx-ui-plugin-docs pnpm add htmx-ui-plugin-search htmx-ui-plugin-docs yarn add htmx-ui-plugin-search htmx-ui-plugin-docs htmx-ui.config.ts import { defineConfig } from \"htmx-ui-engine\"; import docs from \"htmx-ui-plugin-docs\"; import search from \"htmx-ui-plugin-search\"; export default defineConfig({ plugins: [docs(), search()], }); styles.css @import \"tailwindcss\"; @import \"htmx-ui/styles.css\"; @import \"htmx-ui-plugin-search/styles.css\"; app.ts import { initSearch } from \"htmx-ui-plugin-search/client\"; document.addEventListener(\"DOMContentLoaded\", () => initSearch()); Put the button and the palette in your header: partials/header.html {% from \"search/macros.html\" import search %} <header> ... {{ search() }} </header>"
        },
        {
          "id": "macro",
          "title": "search()",
          "text": "Argument Description src The index to search. Default \"/sitemap.json\". label The button's text. Default \"Search docs…\". placeholder The input's placeholder. It renders a wide button with the shortcut on larger screens, an icon button on small ones, and one <dialog>. Every element the client needs carries a data-search-* attribute; override the template to change the markup and keep those."
        },
        {
          "id": "matching",
          "title": "How it matches",
          "text": "Every page is one record and every section of a page (the text under an h2 or h3) is another, so a hit can open page#section. Each word of the query must match somewhere in a record. A word scores by how it matches, best first: the exact word, the start of a word, anywhere in a word, its letters in order, then one typo; and by where, the title above a heading above the text. Results are grouped by page, best page first. Key Description ↑ ↓ Move through the results. Enter Open the result. Ctrl/⌘ + Enter opens it in a new tab. Esc Close. So does clicking outside the palette."
        },
        {
          "id": "versions",
          "title": "With versioned docs",
          "text": "With the versions plugin, a reader of an archived version searches that version's own sitemap, so results stay in the docs they are reading. The palette's footer says which version it is searching."
        },
        {
          "id": "index",
          "title": "Your own index",
          "text": "Any JSON in the sitemap format works, so a blog or a site without the docs plugin can generate one from its build.done hook and pass it as src: /search.json { \"pages\": [ { \"url\": \"/blog/hello\", \"title\": \"Hello\", \"description\": \"The first post.\", \"section\": \"Blog\", \"sections\": [{ \"id\": \"why\", \"title\": \"Why\", \"text\": \"Plain text under the heading…\" }] } ] } The matcher is exported for a search UI of your own: buildIndex(sitemap) turns a sitemap into records, and searchIndex(records, query, limit) returns ranked results with a highlighted snippet."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/plugins/writing",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/plugins/writing",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/plugins/writing.md",
      "title": "Writing a plugin",
      "description": "Build an htmx-ui plugin: the hooks, templates, page edits with editHtml(), routes and build output, CLI commands, browser code, testing and publishing.",
      "section": "Plugins",
      "headings": [
        {
          "level": 2,
          "id": "example",
          "text": "A complete example"
        },
        {
          "level": 2,
          "id": "hooks",
          "text": "Hooks"
        },
        {
          "level": 2,
          "id": "runtime",
          "text": "Runs on Bun and Node"
        },
        {
          "level": 2,
          "id": "edit-html",
          "text": "Editing pages: editHtml()"
        },
        {
          "level": 2,
          "id": "templates",
          "text": "Templates"
        },
        {
          "level": 2,
          "id": "commands",
          "text": "Commands"
        },
        {
          "level": 2,
          "id": "other-plugins",
          "text": "Working with other plugins"
        },
        {
          "level": 2,
          "id": "browser",
          "text": "Browser code and styles"
        },
        {
          "level": 2,
          "id": "testing",
          "text": "Testing"
        },
        {
          "level": 2,
          "id": "publishing",
          "text": "Publishing"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "A plugin is an object with a name and the hooks it needs. Most hooks are config options you already know (globals, transform, routes, fetch, build.done), so anything a config can do, a plugin can package up. The engine merges every plugin into the config once, when it loads, and every runtime and adapter reads that merged config: a plugin never needs to know whether it is running under htmx-ui dev, a build, Vite or a server. Start in your own project. A plugin can live in a file next to htmx-ui.config.ts and be imported like any module; publish it once another site wants it."
        },
        {
          "id": "example",
          "title": "A complete example",
          "text": "This plugin publishes an Atom feed of the pages under /blog. It uses most hooks: a template with a <link> macro, a global, a transform, a route for development, a file for the build, and a command. plugins/feed/index.ts import { definePlugin, editHtml, pagesOf, renderPage, type ResolvedConfig } from \"htmx-ui-engine\"; import { writeFile } from \"node:fs/promises\"; import { join } from \"node:path\"; import { fileURLToPath } from \"node:url\"; export interface FeedOptions { /** Pages under this prefix are posts. */ prefix?: string; title: string; } export function feed({ prefix = \"/blog\", title }: FeedOptions) { let config: ResolvedConfig; /** Every post, newest first, from its <meta name=\"date\"> and <h1>. */ function posts() { return pagesOf(config) .filter((p) => p.url.startsWith(prefix + \"/\")) .map((p) => { let date = \"\"; let heading = \"\"; let inH1 = false; editHtml(renderPage(config, p.file), { element(el) { if (el.matches('meta[name=\"date\"]')) date = el.getAttribute(\"content\") ?? \"\"; if (el.tagName === \"h1\") { inH1 = true; el.onEndTag(() => void (inH1 = false)); } }, text(chunk) { if (inH1) heading += chunk; }, }); return { url: config.origin + p.url, title: heading.trim(), date }; }) .sort((a, b) => b.date.localeCompare(a.date)); } function xml() { const items = posts().map((p) => `<entry><title>${p.title}</title><link href=\"${p.url}\"/><updated>${p.date}</updated></entry>`); return `<?xml version=\"1.0\" encoding=\"utf-8\"?>\\n<feed xmlns=\"http://www.w3.org/2005/Atom\"><title>${title}</title>${items.join(\"\")}</feed>\\n`; } return definePlugin({ name: \"feed\", // templates/feed/link.html, reached as \"feed/link.html\" roots: [fileURLToPath(new URL(\"./templates\", import.meta.url))], configResolved(c) { config = c; }, globals: { feedUrl: \"/feed.xml\" }, // Posts get an id on their <article>, so other pages can link to them transform(html, page) { if (!page.url.startsWith(prefix + \"/\")) return html; return editHtml(html, { element(el) { if (el.tagName === \"article\" && !el.hasAttrib"
        },
        {
          "id": "hooks",
          "title": "Hooks",
          "text": "Every hook is optional except name. Hook Description name Unique. Other plugins find this one by it, and a config listing two plugins with one name refuses to load. roots Template roots, searched after the project's own and before htmx-ui's. Absolute, or relative to the project root. globals, filters Nunjucks globals and filters. The project's own of the same name win. transform(html, page) Post-process every rendered page, in dev, builds and servers. page is { file, url }. Runs before the project's own transform. routes Request handlers in Bun.serve route syntax (\"/feed.xml\", \"/api/:id\", \"/files/*\"), served by htmx-ui dev and by server adapters. Not part of a static build. fetch(req) Fallback for requests nothing else answered; return null to pass. Runs after the project's own fetch. build.done(ctx) After a production build, with { config, outDir, pages }: write files the static site needs. Runs before the project's own. commands CLI commands by name: { \"feed:list\": { description, usage, run(ctx) } }. configResolved(config) Called once the config is resolved, before anything renders, serves or builds. Keep the config for the other hooks. api Anything the plugin offers other plugins. A route serves, a build writes routes answer only where a server runs. For a file the static site needs too, like the feed above, generate it in a route for development and write the same thing in build.done. Both are cheap to keep in sync when one function makes the content."
        },
        {
          "id": "runtime",
          "title": "Runs on Bun and Node",
          "text": "Plugin code runs inside the config, and on Node the config is loaded by Vite. So a plugin uses node: APIs (node:fs, node:path) rather than Bun.file or Bun.write, and never HTMLRewriter, which Node does not have. Read the config in configResolved, not at module scope: the same module may be loaded for several configs. Globals and filters are synchronous, like Nunjucks itself: read files with readFileSync there. Routes, fetch, build.done and commands may be async. Find your own files from import.meta.url (new URL(\"./templates\", import.meta.url)), never from the working directory."
        },
        {
          "id": "edit-html",
          "title": "Editing pages: editHtml()",
          "text": "editHtml(html, handlers) from htmx-ui-engine is a small streaming HTML editor for transforms. It behaves like Bun's HTMLRewriter where they overlap (text and attribute values arrive as written, entities not decoded; setAttribute escapes only \"), and it leaves every tag you don't touch byte for byte, so an edit never reformats a page. ts import { editHtml } from \"htmx-ui-engine\"; const html = editHtml(page, { element(el) { if (el.matches(\"a[href^='http']\") && !el.closest(\"nav\")) el.setAttribute(\"rel\", \"noopener\"); if (el.matches(\"h2\") && el.closest(\"[data-docs-content]\")) el.append(' <a href=\"#top\">↑</a>'); }, text(chunk, parents) { // each run of text, with the elements open around it }, }); Element Description tagName, parents The lowercased tag name; the open ancestors, outermost first. getAttribute, hasAttribute, setAttribute, removeAttribute Attributes, as written. matches(selector), closest(selector) Tag, #id, .class and [attr] tests (=, ~=, ^=, $=, *=), comma-separated. No combinators: closest() walks the ancestors. before, prepend, append, after (html) Insert markup around or inside the element. replace(html), remove() Replace or drop the whole element. onEndTag(fn) Run when the element closes: its text has been seen by then."
        },
        {
          "id": "templates",
          "title": "Templates",
          "text": "Keep a plugin's templates in a directory named after it (templates/feed/link.html, reached as feed/link.html), so they can't collide with a project's own or another plugin's. Prefer macros to partials: a macro takes arguments, and a project calls it where it wants. Plugin templates can use htmx-ui's components ({% from \"components/icon/icon.html\" import icon %}), since htmx-ui's src/ is searched after them. A project overrides a template by having a file with the same name, so treat the names as your public API, and document the data-* attributes your browser code relies on."
        },
        {
          "id": "commands",
          "title": "Commands",
          "text": "run gets a context and returns an exit code (nothing means 0). Throw to fail with a message. Context Description config The resolved config, plugins included. args Positional arguments after the command: htmx-ui versions:name 0.2.0 gives [\"0.2.0\"]. options Options as parsed: --force gives { force: true }, --to=x gives { to: \"x\" }. build() Build the site the way htmx-ui build does, on the runtime the CLI is running on. Resolves to whether it succeeded. Name commands <plugin>:<verb>. A plugin can't replace dev, build or preview, and two plugins can't share a command."
        },
        {
          "id": "other-plugins",
          "title": "Working with other plugins",
          "text": "A plugin finds another by name in config.plugins and uses its api. Look it up when you need it, not in configResolved: the other plugin may come later in the list. Treat it as optional and do something sensible without it, as the official plugins do with each other: ts const docs = config.plugins.find((p) => p.name === \"docs\")?.api as { pages(): { url: string; title: string }[] } | undefined; const titles = docs ? docs.pages().map((p) => p.title) : [];"
        },
        {
          "id": "browser",
          "title": "Browser code and styles",
          "text": "The config side of a plugin never reaches the browser. Ship browser code as a separate entry (your-plugin/client) that the project imports in its app.ts, and styles as a stylesheet it imports after htmx-ui's: src/client.ts import { queryAll } from \"htmx-ui\"; export function initFeedLinks(root: ParentNode = document) { queryAll(root, \"[data-feed-link]:not([data-init])\").forEach((el) => { el.dataset.init = \"\"; // ... }); } src/styles.css /* Tailwind must see the utilities in the templates: Vite skips node_modules */ @source \"./\"; @layer components { .feed-link { @apply inline-flex items-center gap-1 text-muted-foreground; } } Follow htmx-ui's component contract: take a root, find elements with queryAll (it includes root), and mark each with data-init, so the function can also run on content htmx swaps in. Build styles from htmx-ui's tokens (bg-primary, text-muted-foreground) so they follow the theme and dark mode."
        },
        {
          "id": "testing",
          "title": "Testing",
          "text": "Resolve a config with your plugin over a temporary project, then render pages with renderPage and send requests through createSite, which answers like every adapter does: index.test.ts import { expect, test } from \"bun:test\"; import { createSite, renderPage, resolveConfig } from \"htmx-ui-engine\"; import { mkdtemp, mkdir, writeFile } from \"node:fs/promises\"; import { tmpdir } from \"node:os\"; import { join } from \"node:path\"; import feed from \"./index\"; test(\"publishes the feed\", async () => { const root = await mkdtemp(join(tmpdir(), \"feed-\")); await mkdir(join(root, \"pages/blog\"), { recursive: true }); await writeFile(join(root, \"pages/blog/hello.html\"), '<head><meta name=\"date\" content=\"2026-01-02\"></head><article><h1>Hello</h1></article>'); const config = resolveConfig({ url: \"https://x.test\", plugins: [feed({ title: \"Blog\" })] }, root); expect(renderPage(config, join(root, \"pages/blog/hello.html\"))).toContain('<article id=\"post\">'); const site = await createSite({ config }); const res = await site.handle(new Request(\"http://localhost/feed.xml\")); expect(await res.text()).toContain(\"<title>Hello</title>\"); }); config.user.build.done({ config, outDir, pages }) runs the build hooks without a build, and config.plugins.find(...).commands holds the commands to call directly."
        },
        {
          "id": "publishing",
          "title": "Publishing",
          "text": "Name it htmx-ui-plugin-<name> and add the keyword htmx-ui-plugin, so it can be found. Default-export a function that returns the plugin, taking an options object. Make htmx-ui-engine (and htmx-ui, when you use its components) peer dependencies. Ship compiled JavaScript for Node and for browser bundlers, which don't use the bun condition, and the TypeScript source for Bun. Templates and CSS ship as they are. package.json { \"name\": \"htmx-ui-plugin-feed\", \"type\": \"module\", \"keywords\": [\"htmx-ui\", \"htmx-ui-plugin\"], \"sideEffects\": [\"**/*.css\"], \"exports\": { \".\": { \"bun\": \"./src/index.ts\", \"types\": \"./lib/index.d.ts\", \"default\": \"./lib/index.js\" }, \"./client\": { \"bun\": \"./src/client.ts\", \"types\": \"./lib/client.d.ts\", \"default\": \"./lib/client.js\" }, \"./styles.css\": \"./src/styles.css\" }, \"files\": [\"lib\", \"src\"], \"peerDependencies\": { \"htmx-ui\": \"^0.2.0\", \"htmx-ui-engine\": \"^0.2.0\" } } A compiled lib/index.js sits beside src/, so point at templates through the package root and both find them: new URL(\"../src/templates\", import.meta.url). The official plugins in the repository are complete examples: plugin-search is the smallest, templates and a browser module; plugin-versions has commands."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/servers",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/servers",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/servers.md",
      "title": "Server frameworks",
      "description": "Serve a built htmx-ui site from your own backend: Elysia, Fastify, Koa, Express, Hono, Bun.serve, Deno, or a backend in another language.",
      "section": "Servers",
      "headings": [
        {
          "level": 2,
          "id": "pick",
          "text": "Pick your server"
        },
        {
          "level": 2,
          "id": "quick-start",
          "text": "Quick start"
        },
        {
          "level": 2,
          "id": "request-order",
          "text": "How a request is answered"
        },
        {
          "level": 3,
          "id": "not-found",
          "text": "The 404 page"
        },
        {
          "level": 3,
          "id": "ordering",
          "text": "Where to mount it"
        },
        {
          "level": 2,
          "id": "dev-prod",
          "text": "Development and production"
        },
        {
          "level": 2,
          "id": "node-config",
          "text": "Loading htmx-ui.config.ts"
        },
        {
          "level": 2,
          "id": "next",
          "text": "Next"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "A built htmx-ui site is just files: pages at their routes, hashed scripts and styles, and whatever you put in public/. Any server can host them. htmx-ui-engine adds adapters that put those files and your own API in one process: your routes answer what they claim, and htmx-ui answers the rest with the built site. The same templates also render the HTML fragments your API returns to htmx."
        },
        {
          "id": "pick",
          "title": "Pick your server",
          "text": "Server Use Order Runs on Elysia (recommended) htmx-ui-engine/elysia Any Bun, Node Fastify htmx-ui-engine/fastify Any Node, Bun Koa htmx-ui-engine/koa Any Node, Bun Express htmx-ui-engine/express Last Node, Bun Hono htmx-ui-engine/hono Last Node, Bun, Deno Bun.serve createSite() — Bun Deno, node:http, h3, NestJS createSite() or an adapter — Deno, Node, Bun Django, Rails, Laravel, Go… The built files — Any Order is where the adapter has to go relative to your own routes. \"Any\" means it only answers what no route of yours matched, wherever you register it. \"Last\" means it is ordinary middleware that answers every request it sees, so it has to come after your routes. The adapters use files on disk, so edge runtimes without a filesystem (Cloudflare Workers and the like) are not supported."
        },
        {
          "id": "quick-start",
          "title": "Quick start",
          "text": "Add the engine and your framework as dependencies. Your server imports the engine at runtime, so a production install that skips dev dependencies would leave it out. bun add htmx-ui-engine elysia npm install htmx-ui-engine elysia pnpm add htmx-ui-engine elysia yarn add htmx-ui-engine elysia Mount the adapter next to your own routes: import { Elysia } from \"elysia\"; import { htmxUi } from \"htmx-ui-engine/elysia\"; new Elysia() .get(\"/api/ping\", () => ({ ok: true })) .use(htmxUi()) // everything else is the built site .listen(3000); import Fastify from \"fastify\"; import { htmxUi } from \"htmx-ui-engine/fastify\"; const app = Fastify(); app.get(\"/api/ping\", async () => ({ ok: true })); app.register(htmxUi()); // everything else is the built site await app.listen({ port: 3000 }); import Koa from \"koa\"; import { htmxUi } from \"htmx-ui-engine/koa\"; const app = new Koa(); app.use(htmxUi()); // answers only what nothing else did app.use(async (ctx, next) => { if (ctx.path === \"/api/ping\") ctx.body = { ok: true }; else await next(); }); app.listen(3000); import express from \"express\"; import { htmxUi } from \"htmx-ui-engine/express\"; const app = express(); app.get(\"/api/ping\", (req, res) => res.json({ ok: true })); app.use(htmxUi()); // last: everything else is the built site app.listen(3000); import { Hono } from \"hono\"; import { htmxUi } from \"htmx-ui-engine/hono\"; const app = new Hono(); app.get(\"/api/ping\", (c) => c.json({ ok: true })); app.use(htmxUi()); // last: everything else is the built site export default app; import { createSite } from \"htmx-ui-engine\"; const site = await createSite(); Bun.serve({ port: 3000, routes: { \"/api/ping\": () => Response.json({ ok: true }) }, fetch: (request) => site.handle(request), // everything else is the built site }); Then build the site and start your server: package.json { \"scripts\": { \"dev\": \"htmx-ui dev\", \"build\": \"htmx-ui build\", \"start\": \"NODE_ENV=production bun server.ts\" } } On Node, start is NODE_ENV=production node server.ts "
        },
        {
          "id": "request-order",
          "title": "How a request is answered",
          "text": "Your framework sees each request first, so its routes and middleware keep the paths they claim. Whatever is left goes to the adapter, which tries in this order: The config's routes, the same ones htmx-ui dev serves (see Mock htmx endpoints). They answer in production too, so keep mocks out of production. dist/: a page at its route, a hashed asset, or a public/ file. Development only: the page template for that route, rendered on the spot. See Development and production. The config's fetch fallback. A 404 page. debug: [\"requests\"] in htmx-ui.config.ts logs which step answered each request and how long it took. See Debugging."
        },
        {
          "id": "not-found",
          "title": "The 404 page",
          "text": "Every path ends with an answer, so a request never falls through to your framework's own 404. What it gets, in order: dist/404.html, built from your pages/404.html, with your layout and styles. In development, before anything is built: pages/404.html, rendered on the spot. htmx-ui's default: a small, self-contained page that follows the visitor's light or dark preference. To use your own, add pages/404.html. It is an ordinary page that extends your layout, and it is served with a 404 status. A build with no pages/404.html writes the default to dist/404.html, so a static host serves the same page. htmx-ui dev and htmx-ui preview answer the same way. pages/404.html {% extends \"layouts/base.html\" %} {% block content %} <h1 class=\"h1\">Page not found</h1> <p class=\"muted\">Try the <a class=\"link\" href=\"/\">home page</a>.</p> {% endblock %} htmx swaps error responses htmx 4 swaps a 404 response like any other. If an hx-get asks for a path nothing answers, the whole 404 page lands in its target. Fix the path, or tell that element not to swap a 404: hx-status:404=\"swap:none\"."
        },
        {
          "id": "ordering",
          "title": "Where to mount it",
          "text": "Elysia registers a catch-all route, and Elysia's router always prefers a specific route. Fastify becomes the not-found handler. Koa runs the rest of the stack first and only answers when nothing else did. For these three, registration order doesn't matter. Express and Hono run middleware in order, and the adapter answers everything it sees. Register it after your own routes and middleware, or they will never be reached."
        },
        {
          "id": "dev-prod",
          "title": "Development and production",
          "text": "NODE_ENV decides one thing: whether pages may be rendered from their templates. Not production: a route dist/ has no file for is rendered from its template, so a server you start before building still shows every page. Templates are re-read on every render, so edits show on the next request; pages you add are found without a restart; and public/ is served as it is, before a build copies it. The scripts and stylesheets a rendered page links ({{ asset('app.ts') }}) are bundled from source when the browser asks for them, Tailwind included, and kept in memory until a file changes; then open pages reload. Nothing is written to dist/. dist/ still answers first, so an old build hides your template edits. Rebuild, or delete dist/ while you work. production: pages come only from dist/, so run htmx-ui build first. Fragments and site.render() still render at request time, and a page rendered that way links the bundles the build made for the same scripts and stylesheets, from the manifest the build writes (dist/.htmx-ui/manifest.json). See Deploying for what that needs on the server."
        },
        {
          "id": "node-config",
          "title": "Loading htmx-ui.config.ts",
          "text": "The adapters read htmx-ui.config.ts from the working directory at startup. Bun and Deno import TypeScript directly, and so does Node from 22.18. On an older Node, name the file htmx-ui.config.js (or .mjs), or pass the config yourself: htmxUi({ config: { routes } })."
        },
        {
          "id": "next",
          "title": "Next",
          "text": "Pages & fragments: render the per-user part of a page with site.fragment() and site.render(), handle forms and errors. Deploying: what to ship, cache headers, Docker and reverse proxies. createSite() API: every option and method."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/servers/rendering",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/servers/rendering",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/servers/rendering.md",
      "title": "Pages & fragments",
      "description": "Render the per-user part of a page as an htmx fragment from your server, render whole pages per request, and handle forms, htmx headers and errors.",
      "section": "Servers",
      "headings": [
        {
          "level": 2,
          "id": "per-user",
          "text": "When the HTML depends on who is asking"
        },
        {
          "level": 2,
          "id": "fragments",
          "text": "Fragments"
        },
        {
          "level": 2,
          "id": "render-per-request",
          "text": "Render a page per request"
        },
        {
          "level": 2,
          "id": "htmx-requests",
          "text": "One route, fragment or page"
        },
        {
          "level": 2,
          "id": "forms",
          "text": "Forms"
        },
        {
          "level": 2,
          "id": "errors",
          "text": "Errors"
        },
        {
          "level": 2,
          "id": "assets",
          "text": "Asset URLs in fragments"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "A page in pages/ is rendered once, by htmx-ui build, and served as a file. Every visitor gets the same bytes, which any cache can keep. Anything that depends on who is asking can't be in that file. It belongs in a fragment: a small piece of HTML your server renders per request, and htmx swaps into the page. Fragments are templates, like pages, so they use the same macros, components and data. Create one site and use it both to render fragments and to serve the build: import { Elysia } from \"elysia\"; import { createSite } from \"htmx-ui-engine\"; import { htmxUi } from \"htmx-ui-engine/elysia\"; const site = await createSite(); const html = (body: string, status = 200) => new Response(body, { status, headers: { \"Content-Type\": \"text/html; charset=utf-8\" } }); const app = new Elysia() // your routes, returning html(site.fragment(...)) .use(htmxUi({ site })) .listen(3000); import Fastify from \"fastify\"; import { createSite } from \"htmx-ui-engine\"; import { htmxUi } from \"htmx-ui-engine/fastify\"; const site = await createSite(); const app = Fastify(); // your routes, replying with reply.type(\"text/html\").send(site.fragment(...)) app.register(htmxUi({ site })); await app.listen({ port: 3000 }); import Koa from \"koa\"; import Router from \"@koa/router\"; import { createSite } from \"htmx-ui-engine\"; import { htmxUi } from \"htmx-ui-engine/koa\"; const site = await createSite(); const app = new Koa(); const router = new Router(); // your routes, setting ctx.type = \"html\" and ctx.body = site.fragment(...) app.use(htmxUi({ site })); app.use(router.routes()); app.listen(3000); import express from \"express\"; import { createSite } from \"htmx-ui-engine\"; import { htmxUi } from \"htmx-ui-engine/express\"; const site = await createSite(); const app = express(); // your routes, sending res.type(\"html\").send(site.fragment(...)) app.use(htmxUi({ site })); // last app.listen(3000); import { Hono } from \"hono\"; import { createSite } from \"htmx-ui-engine\"; import { htmxUi } from \"htmx-ui-engine/hon"
        },
        {
          "id": "per-user",
          "title": "When the HTML depends on who is asking",
          "text": "At build time there is no request, so there is no user. Here is the piece that usually needs one: a sign-in link for a visitor, a name and a sign-out button for a member. partials/user-menu.html {% if user %} <span class=\"text-sm\">{{ user.name }}</span> <form method=\"post\" action=\"/logout\"><button class=\"btn btn-sm\">Sign out</button></form> {% else %} <a class=\"btn btn-sm\" href=\"/login\">Sign in</a> {% endif %} The built page keeps a placeholder where the menu goes and asks for it as soon as it loads. Without hx-trigger=\"load\", a <div> only sends its request when clicked: layouts/base.html <div hx-get=\"/api/user-menu\" hx-trigger=\"load\" hx-swap=\"outerHTML\"> <a class=\"btn btn-sm\" href=\"/login\">Sign in</a> </div> Your server renders the partial for the current request: app.get(\"/api/user-menu\", ({ request }) => html(site.fragment(\"partials/user-menu.html\", { user: currentUser(request) })), ); app.get(\"/api/user-menu\", async (request, reply) => reply.type(\"text/html\").send(site.fragment(\"partials/user-menu.html\", { user: currentUser(request) })), ); router.get(\"/api/user-menu\", (ctx) => { ctx.type = \"html\"; ctx.body = site.fragment(\"partials/user-menu.html\", { user: currentUser(ctx) }); }); app.get(\"/api/user-menu\", (req, res) => { res.type(\"html\").send(site.fragment(\"partials/user-menu.html\", { user: currentUser(req) })); }); app.get(\"/api/user-menu\", (c) => c.html(site.fragment(\"partials/user-menu.html\", { user: currentUser(c) }))); The page stays a static file anyone can cache, the fragment is small, and the markup lives in one template. Use this pattern for a header, a nav, a sidebar, or any list of the visitor's own rows. Only the render needs the user Your framework's middleware sees the request first, so currentUser() can use a cookie, a session or a token, whatever you already have. Templates get the user as a context value. They never read the request themselves."
        },
        {
          "id": "fragments",
          "title": "Fragments",
          "text": "site.fragment(path, context) renders any template (a partial, a macro wrapper, a whole page you want in a modal) with the config's globals plus context. It adds no document shell and doesn't run the config's transform(). It returns a string, so send it with an HTML content type. Elysia, for one, sends a bare string as text/plain. partials/invoice-row.html <tr id=\"invoice-{{ invoice.id }}\"> <td>{{ invoice.number }}</td> <td>{{ invoice.total }}</td> <td> <button class=\"btn btn-sm\" hx-post=\"/api/invoices/{{ invoice.id }}/refund\" hx-target=\"#invoice-{{ invoice.id }}\" hx-swap=\"outerHTML\"> Refund </button> </td> </tr> app.post(\"/api/invoices/:id/refund\", ({ params }) => { const invoice = refund(params.id); return html(site.fragment(\"partials/invoice-row.html\", { invoice })); }); app.post<{ Params: { id: string } }>(\"/api/invoices/:id/refund\", async (request, reply) => { const invoice = refund(request.params.id); return reply.type(\"text/html\").send(site.fragment(\"partials/invoice-row.html\", { invoice })); }); router.post(\"/api/invoices/:id/refund\", (ctx) => { const invoice = refund(ctx.params.id); ctx.type = \"html\"; ctx.body = site.fragment(\"partials/invoice-row.html\", { invoice }); }); app.post(\"/api/invoices/:id/refund\", (req, res) => { const invoice = refund(req.params.id); res.type(\"html\").send(site.fragment(\"partials/invoice-row.html\", { invoice })); }); app.post(\"/api/invoices/:id/refund\", (c) => { const invoice = refund(c.req.param(\"id\")); return c.html(site.fragment(\"partials/invoice-row.html\", { invoice })); });"
        },
        {
          "id": "render-per-request",
          "title": "Render a page per request",
          "text": "site.render(url, context) renders the page at a route, with its layout and the config's transform(), just as the build does but with your context added. Use it when most of the page depends on the request. When only a corner does, a static page plus a fragment is cheaper and caches better. server.ts (Elysia) app.get(\"/dashboard\", ({ request }) => html(site.render(\"/dashboard\", { user: currentUser(request) }))); The page's scripts and stylesheets work as they do in a built page: in development they are bundled from source on request, and in production they are the build's bundles (see Development and production). Register the route yourself: a page in dist/ is only served when no route of yours claims the path. render() throws for a route with no page. Pass values in the context, not in config globals, so one visitor's data never reaches another's render."
        },
        {
          "id": "htmx-requests",
          "title": "One route, fragment or page",
          "text": "htmx marks its requests with HX-Request: true. A route can use that to answer htmx with a fragment and a direct visit (a bookmark, a reload, a link opened in a new tab) with the whole page: server.ts (Elysia) app.get(\"/invoices\", ({ request }) => { const invoices = listInvoices(new URL(request.url).searchParams); return request.headers.get(\"HX-Request\") ? html(site.fragment(\"partials/invoice-table.html\", { invoices })) : html(site.render(\"/invoices\", { invoices })); }); If you also cache that route, send Vary: HX-Request, so the fragment and the page are cached separately. Response headers steer htmx from the server: Header Effect HX-Redirect Full navigation to a URL, e.g. after sign-in. HX-Refresh true reloads the page. HX-Trigger Fires events on the client, e.g. to refresh a list elsewhere on the page. HX-Retarget, HX-Reswap Swap somewhere else, or another way, than the element asked for. HX-Push-Url, HX-Replace-Url Update the address bar."
        },
        {
          "id": "forms",
          "title": "Forms",
          "text": "A form with hx-post sends its fields as application/x-www-form-urlencoded, the same as without htmx. Read them however your framework does: a body parser, request.formData(), c.req.parseBody(). Answer with the fragment to swap in. When validation fails, re-render the form with its errors and a 422. htmx 4 swaps error responses too, so the errors appear where the form was: partials/invite-form.html <form hx-post=\"/api/invites\" hx-swap=\"outerHTML\" class=\"space-y-4\"> <div class=\"field\"> <label class=\"field-label\" for=\"email\">Email</label> <input class=\"input\" id=\"email\" name=\"email\" type=\"email\" value=\"{{ values.email }}\" required {% if errors.email %}aria-invalid=\"true\" aria-describedby=\"email-error\"{% endif %}> {% if errors.email %}<div class=\"field-error\" id=\"email-error\">{{ errors.email }}</div>{% endif %} </div> <button class=\"btn btn-primary\">Send invite</button> </form> server.ts (Elysia) app.post(\"/api/invites\", async ({ request }) => { const values = Object.fromEntries(await request.formData()); const errors = validateInvite(values); if (errors) return html(site.fragment(\"partials/invite-form.html\", { values, errors }), 422); await sendInvite(values.email); return html(site.fragment(\"partials/invite-sent.html\", { email: values.email })); }); The config's routes take a web Request, so they call request.formData() on every adapter. When a body parser (express.urlencoded(), Fastify's JSON parser, @koa/bodyparser) has already read the body, the adapter rebuilds it from what was parsed."
        },
        {
          "id": "errors",
          "title": "Errors",
          "text": "render() and fragment() throw when a template is missing or fails to render. The error reaches your framework's error handling like any other: Express's error middleware (the adapter calls next(error)), a try/catch middleware in Koa, Fastify's setErrorHandler(), Elysia's onError and Hono's app.onError. To answer htmx with something it can swap, render an error fragment there: server.ts (Elysia) app.onError(({ error, request }) => { console.error(error); if (request.headers.get(\"HX-Request\")) { return html(site.fragment(\"partials/error.html\", { message: \"Something went wrong. Try again.\" }), 500); } }); Set debug: [\"render\"] in htmx-ui.config.ts to log every render with its duration; see Debugging."
        },
        {
          "id": "assets",
          "title": "Asset URLs in fragments",
          "text": "A fragment rarely needs a URL. svg() and the icon() macro inline their markup, so there is no file to fetch. When one does link a project file, asset() returns a path from the project root (/images/logo.png), which your server only serves if that file is in dist/. Put it in public/, which the build copies as-is, or map URLs yourself with options.asset. Built pages already carry hashed URLs, so this only affects render() and fragment(). Scripts and stylesheets in a page render() returns are the exception: their source links are swapped for bundles (from source in development, the build's in production). In production that needs a page the build has that links the same files, which every page using your layout does. A script only some renders link (say, behind {% if user.admin %}) needs a built page that links it too; otherwise the server says which one it could not find and leaves the source link."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/servers/deploying",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/servers/deploying",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/servers/deploying.md",
      "title": "Deploying",
      "description": "Ship an htmx-ui site with your server: what to copy, NODE_ENV, keeping mock routes out of production, cache headers, Docker and reverse proxies.",
      "section": "Servers",
      "headings": [
        {
          "level": 2,
          "id": "shapes",
          "text": "Three shapes"
        },
        {
          "level": 3,
          "id": "static-only",
          "text": "1. Static only"
        },
        {
          "level": 3,
          "id": "static-and-dynamic",
          "text": "2. Static pages, dynamic fragments"
        },
        {
          "level": 3,
          "id": "all-dynamic",
          "text": "3. Everything from your server"
        },
        {
          "level": 2,
          "id": "node-env",
          "text": "NODE_ENV=production"
        },
        {
          "level": 2,
          "id": "mock-routes",
          "text": "Keep mock routes out of production"
        },
        {
          "level": 2,
          "id": "caching",
          "text": "Cache headers and compression"
        },
        {
          "level": 2,
          "id": "docker",
          "text": "Docker"
        },
        {
          "level": 2,
          "id": "reverse-proxy",
          "text": "Behind a reverse proxy"
        },
        {
          "level": 2,
          "id": "checklist",
          "text": "Checklist"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "The adapters serve files, so deploying is whatever your server already does: build during the release, then run the server with NODE_ENV=production. The one real decision is which routes your server answers. That decides what you have to ship next to dist/."
        },
        {
          "id": "shapes",
          "title": "Three shapes",
          "text": ""
        },
        {
          "id": "static-only",
          "title": "1. Static only",
          "text": "Every page is the same for everyone and htmx talks to an API on another host. Run htmx-ui build and put dist/ on a static host, a CDN or an object store. You need no server and no adapter. The config's routes and fetch are not part of the build, so a static host never answers them. dist/404.html is your 404 page, or htmx-ui's default."
        },
        {
          "id": "static-and-dynamic",
          "title": "2. Static pages, dynamic fragments",
          "text": "The usual shape. Pages come from dist/, and your server renders the per-visitor parts as fragments. A fragment is a template rendered at request time, so the server needs the Nunjucks templates as well as dist/. htmx-ui build doesn't copy them: it writes built pages and hashed assets, nothing else. The simplest deployment ships the project as it is, source and dist/ together, and needs no configuration: the defaults already point at pages/ and the project root. To ship less, copy the template directories into the deployment and point the config at the copy: Terminal htmx-ui build mkdir -p dist/_templates cp -r layouts partials pages data dist/_templates/ Copy every directory a template reaches at render time: macros/ and components/ too if your templates import from them, and whatever json() and svg() read. The build's dist/.htmx-ui/manifest.json ships with dist/: it is how a page site.render() returns links the built scripts and stylesheets instead of their sources. htmx-ui.config.ts import { defineConfig } from \"htmx-ui-engine\"; export default defineConfig({ // This deployment renders templates at runtime: check at startup that they arrived. render: true, // \".\" stays the project root; the copy is searched second. roots: [\".\", \"dist/_templates\"], pages: \"dist/_templates/pages\", }); With render: true, a packaging mistake stops the server at startup instead of causing a 500 later. createSite() checks the template roots and the pages directory, and if they are missing the error says what to copy. Without it, a deployment that forgot its templates starts normally and fails on the first htmx request."
        },
        {
          "id": "all-dynamic",
          "title": "3. Everything from your server",
          "text": "Your server renders each page per request with site.render(). This is shape 2 plus your own page routes. Ship the templates, set render, and register a route for each page you render, since a route you don't register is still served from dist/. Caching is then up to you: a page rendered per visitor must not be cached as if it were the same for everyone. server.ts (Elysia) app.get(\"/dashboard\", ({ request }) => html(site.render(\"/dashboard\", { user: currentUser(request) })));"
        },
        {
          "id": "node-env",
          "title": "NODE_ENV=production",
          "text": "Outside production, the adapter renders a page from its template whenever dist/ has no file for that route (see Development and production). That is convenient on your machine and wrong in production, so set NODE_ENV=production wherever the server runs. With it set, pages come only from the build, and a missing page gets your 404 page."
        },
        {
          "id": "mock-routes",
          "title": "Keep mock routes out of production",
          "text": "The config's routes are mock endpoints for htmx-ui dev, but the adapters serve them too, ahead of dist/. Where your server has the real endpoints, a mock on the same path would never be reached. On a path your server doesn't have, a mock would answer in production. Leave them out when it is not development: htmx-ui.config.ts import { defineConfig } from \"htmx-ui-engine\"; import { mocks } from \"./mocks\"; export default defineConfig({ routes: process.env.NODE_ENV === \"production\" ? {} : mocks, });"
        },
        {
          "id": "caching",
          "title": "Cache headers and compression",
          "text": "The adapters set content types and nothing else. The usual split: hashed files in dist/assets/ can be cached forever, because a new build gives them new names. HTML should be revalidated on every visit, so a deploy shows up at once. Add the header around the adapter: const cacheControl = (path: string) => path.startsWith(\"/assets/\") ? \"public, max-age=31536000, immutable\" : \"no-cache\"; new Elysia() .onAfterHandle(({ request, response }) => { if (response instanceof Response) response.headers.set(\"Cache-Control\", cacheControl(new URL(request.url).pathname)); }) .use(htmxUi()) .listen(3000); const cacheControl = (path: string) => path.startsWith(\"/assets/\") ? \"public, max-age=31536000, immutable\" : \"no-cache\"; app.addHook(\"onSend\", async (request, reply) => { reply.header(\"Cache-Control\", cacheControl(request.url.split(\"?\")[0]!)); }); app.register(htmxUi()); // compression: @fastify/compress const cacheControl = (path: string) => path.startsWith(\"/assets/\") ? \"public, max-age=31536000, immutable\" : \"no-cache\"; app.use(async (ctx, next) => { await next(); ctx.set(\"Cache-Control\", cacheControl(ctx.path)); }); app.use(htmxUi()); // compression: koa-compress const cacheControl = (path: string) => path.startsWith(\"/assets/\") ? \"public, max-age=31536000, immutable\" : \"no-cache\"; app.use(compression()); // the compression package app.use((req, res, next) => { res.setHeader(\"Cache-Control\", cacheControl(req.path)); next(); }); // ...your routes app.use(htmxUi()); import { compress } from \"hono/compress\"; const cacheControl = (path: string) => path.startsWith(\"/assets/\") ? \"public, max-age=31536000, immutable\" : \"no-cache\"; app.use(compress()); app.use(async (c, next) => { await next(); c.res.headers.set(\"Cache-Control\", cacheControl(c.req.path)); }); // ...your routes app.use(htmxUi()); These set the header on every response, including your API's. Narrow the check if your API sets its own. A CDN or a reverse proxy in front of the server can do both jobs instead (see below)."
        },
        {
          "id": "docker",
          "title": "Docker",
          "text": "Build in one stage and run in another, with production dependencies only. The engine is a regular dependency, so it is installed in the run stage: Dockerfile FROM oven/bun:1 AS build WORKDIR /app COPY package.json bun.lock ./ RUN bun install --frozen-lockfile COPY . . RUN bun run build FROM oven/bun:1 WORKDIR /app ENV NODE_ENV=production COPY package.json bun.lock ./ RUN bun install --frozen-lockfile --production COPY --from=build /app/dist ./dist COPY htmx-ui.config.ts server.ts ./ # Shapes 2 and 3: the templates the server renders from. COPY layouts ./layouts COPY partials ./partials COPY pages ./pages COPY data ./data EXPOSE 3000 CMD [\"bun\", \"server.ts\"] On Node, start from node:22 (22.18 or later runs server.ts directly), use npm ci and npm ci --omit=dev, and CMD [\"node\", \"server.ts\"]. The build stage needs the dev dependencies (Tailwind, and Vite on Node); the run stage doesn't."
        },
        {
          "id": "reverse-proxy",
          "title": "Behind a reverse proxy",
          "text": "nginx or Caddy in front of the server can serve dist/assets/ straight from disk, add cache headers and compression, and pass everything else to your process: nginx.conf server { listen 80; root /srv/app/dist; location /assets/ { add_header Cache-Control \"public, max-age=31536000, immutable\"; try_files $uri =404; } location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } gzip on; gzip_types text/css text/javascript application/javascript application/json image/svg+xml; } To skip the Node or Bun process entirely, with nginx serving the pages and your backend answering htmx, see Django, Rails, Go and others."
        },
        {
          "id": "checklist",
          "title": "Checklist",
          "text": "htmx-ui-engine and your framework are in dependencies, not devDependencies. htmx-ui build runs during the release, before the server starts. NODE_ENV=production is set where the server runs. The templates are deployed when the server renders fragments or pages, and render: true is set. Mock routes are off in production. A pages/404.html if you want your own 404 page. Cache headers: forever for /assets/, revalidate for HTML."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/servers/api",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/servers/api",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/servers/api.md",
      "title": "createSite() API",
      "description": "Reference for createSite(), the Site it returns and the options every server adapter takes.",
      "section": "Servers",
      "headings": [
        {
          "level": 2,
          "id": "site",
          "text": "Site"
        },
        {
          "level": 2,
          "id": "options",
          "text": "Options"
        },
        {
          "level": 2,
          "id": "config",
          "text": "Config options for servers"
        },
        {
          "level": 2,
          "id": "adapters",
          "text": "Adapters"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "createSite() is what every adapter runs underneath, and what you use directly on a server without an adapter. It reads the config, finds the pages, compiles their templates, and returns a Site that renders templates and answers requests. server.ts import { createSite, type Site, type SiteOptions } from \"htmx-ui-engine\"; const site: Site = await createSite({ context: { company: \"Acme\" } }); site.fragment(\"partials/invoice-row.html\", { invoice }); // an htmx fragment, as a string site.render(\"/dashboard\", { user }); // a whole page, as a string await site.handle(request); // a Response for any request Every adapter entry (htmx-ui-engine/elysia, /fastify, /koa, /express, /hono) also re-exports createSite and its types, so one import is enough."
        },
        {
          "id": "site",
          "title": "Site",
          "text": "Member Description site.config The resolved config: absolute root, roots, pagesDir, outDir and publicDir, plus origin, render, and the config as you wrote it (user). site.pages Every page as { file, path, url }, sorted by route. Found at startup; in development, found again when a route has no page or a page's file is gone, so pages you add or remove need no restart. site.render(url, context?) Render the page at a route (\"/\", \"/docs/setup\", trailing slash or not) with its layout and the config's transform(). Throws when no page has that route. site.fragment(path, context?) Render any template by its path from a root (\"partials/row.html\") as an HTML fragment: no document shell, no transform(). Throws when the template doesn't exist. site.handle(request, context?) Answer a web Request: the config's routes, then dist/, then (outside production) the page's template, then the config's fetch, then a 404 page. Always resolves to a Response. context reaches the templates it renders. Both renders see the config's globals, then options.context, then the context you pass, with later values winning. See How a request is answered for handle()'s order, and The 404 page for its last step."
        },
        {
          "id": "options",
          "title": "Options",
          "text": "createSite(options) and every adapter's htmxUi(options) take the same options: Option Description root Project root, where htmx-ui.config.ts and pages/ live. Default: process.cwd(). config A config object to use instead of reading htmx-ui.config.ts: what defineConfig() takes, or a resolved config. context Values for every render, on top of the config's globals. asset(file, page) URL for asset() in render() and fragment(). file is the absolute file, page the template rendering it. Default: a path from the project root (/images/logo.png). cache Keep compiled templates between renders, and compile everything the pages reach at startup. Default: true when NODE_ENV is production, false otherwise, so template edits show on the next request. false re-reads every template on every render. site A Site, or a promise of one, to use instead of creating one: htmxUi({ site }) serves the instance your routes render with. asset() and request-time renders Built pages already carry hashed asset URLs, so handle() never calls asset(). It matters only for render() and fragment(). By default it returns a path from the project root, which your server only serves if the file is in dist/; scripts and stylesheets in a rendered page are swapped for bundles instead. See Asset URLs in fragments."
        },
        {
          "id": "config",
          "title": "Config options for servers",
          "text": "These settings in htmx-ui.config.ts matter when a server is involved (the rest are in Configuration): Option Description routes Request handlers keyed by path (\"/api/users/:id\"). Served by htmx-ui dev and by the adapters, never by a static host. fetch(request) Fallback for requests nothing else matched. Return null to let the 404 page answer. render true when this deployment renders templates at runtime: createSite() checks at startup that the roots and pages are there. See Deploying. debug true, or a list of topics (requests, render, templates, build): log what answered each request and how long renders took."
        },
        {
          "id": "adapters",
          "title": "Adapters",
          "text": "Each adapter is one function, htmxUi(options?), and imports nothing from its framework. Entry Description htmx-ui-engine/elysia An Elysia plugin: a catch-all route your own routes always beat. Returns the instance, so .use(htmxUi()).listen() chains. htmx-ui-engine/fastify A Fastify plugin that sets the not-found handler. Register it once, on the root instance. htmx-ui-engine/koa Koa middleware that answers after the rest of the stack, only when nothing set a body or a status. htmx-ui-engine/express Connect-style middleware for Express 4 and 5, Connect and node:http. Mount it last. htmx-ui-engine/hono Hono middleware. Register it last."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/servers/elysia",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/servers/elysia",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/servers/elysia.md",
      "title": "Elysia",
      "description": "Mount a built htmx-ui site as an Elysia plugin (the recommended adapter), and render pages and fragments from your handlers.",
      "section": "Servers",
      "headings": [
        {
          "level": 2,
          "id": "mount",
          "text": "Mount it"
        },
        {
          "level": 2,
          "id": "fragments",
          "text": "Fragments and pages"
        },
        {
          "level": 2,
          "id": "options",
          "text": "Options"
        },
        {
          "level": 2,
          "id": "testing",
          "text": "Testing"
        },
        {
          "level": 2,
          "id": "run",
          "text": "Build and run"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "htmx-ui-engine/elysia exports htmxUi(), a plugin that answers the paths your own routes don't. Start with this adapter: Elysia always prefers a specific route over the plugin's catch-all, so the order you register things in never matters. It runs on Bun and on Node. bun add htmx-ui-engine elysia npm install htmx-ui-engine elysia pnpm add htmx-ui-engine elysia yarn add htmx-ui-engine elysia"
        },
        {
          "id": "mount",
          "title": "Mount it",
          "text": "server.ts import { Elysia } from \"elysia\"; import { htmxUi } from \"htmx-ui-engine/elysia\"; new Elysia() .get(\"/api/invoices\", () => db.invoices.list()) .post(\"/api/invoices\", ({ body }) => db.invoices.create(body)) .use(htmxUi()) // pages, assets, public files, the config's routes, a 404 page .listen(3000); The plugin returns the instance it was given, so .use(htmxUi()).listen(3000) chains as usual. It handles every method, so the config's routes answer hx-post as well as hx-get. Paths nothing claims get the 404 page: your pages/404.html, or htmx-ui's default. Because the catch-all answers them, Elysia's own NOT_FOUND error never fires."
        },
        {
          "id": "fragments",
          "title": "Fragments and pages",
          "text": "Elysia sends a returned string as text/plain, so wrap rendered HTML in a Response. A small helper keeps routes short: server.ts import { Elysia } from \"elysia\"; import { createSite } from \"htmx-ui-engine\"; import { htmxUi } from \"htmx-ui-engine/elysia\"; const site = await createSite(); const html = (body: string, status = 200) => new Response(body, { status, headers: { \"Content-Type\": \"text/html; charset=utf-8\" } }); new Elysia() .post(\"/api/invoices/:id/refund\", ({ params }) => html(site.fragment(\"partials/invoice-row.html\", { invoice: refund(params.id) })), ) .get(\"/dashboard\", ({ request }) => html(site.render(\"/dashboard\", { user: currentUser(request) }))) .use(htmxUi({ site })) // the same site: templates compiled once .listen(3000); Pages & fragments covers when to use which, forms, htmx headers and error fragments."
        },
        {
          "id": "options",
          "title": "Options",
          "text": "htmxUi(options) takes the options of createSite(): root, config, context, asset, cache and site. For example, to serve a site from another directory: server.ts new Elysia().use(htmxUi({ root: \"../marketing\" })).listen(3000);"
        },
        {
          "id": "testing",
          "title": "Testing",
          "text": "app.handle() answers a Request in-process, with no port: server.test.ts import { expect, test } from \"bun:test\"; import { app } from \"./server\"; test(\"serves the home page\", async () => { const res = await app.handle(new Request(\"http://localhost/\")); expect(res.status).toBe(200); expect(res.headers.get(\"Content-Type\")).toContain(\"text/html\"); });"
        },
        {
          "id": "run",
          "title": "Build and run",
          "text": "In production the plugin serves dist/, so build first and set NODE_ENV: Terminal htmx-ui build && NODE_ENV=production bun server.ts See Deploying for what to ship, cache headers and Docker."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/servers/fastify",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/servers/fastify",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/servers/fastify.md",
      "title": "Fastify",
      "description": "Serve a built htmx-ui site as Fastify's not-found handler, and render pages and fragments from your routes.",
      "section": "Servers",
      "headings": [
        {
          "level": 2,
          "id": "mount",
          "text": "Mount it"
        },
        {
          "level": 2,
          "id": "fragments",
          "text": "Fragments and pages"
        },
        {
          "level": 2,
          "id": "options",
          "text": "Options"
        },
        {
          "level": 2,
          "id": "testing",
          "text": "Testing"
        },
        {
          "level": 2,
          "id": "run",
          "text": "Build and run"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "htmx-ui-engine/fastify exports htmxUi(), a plugin that makes htmx-ui Fastify's not-found handler. It only sees requests no route of yours matched, so it can be registered anywhere. It is tested with Fastify 5, on Node and Bun. bun add htmx-ui-engine fastify npm install htmx-ui-engine fastify pnpm add htmx-ui-engine fastify yarn add htmx-ui-engine fastify"
        },
        {
          "id": "mount",
          "title": "Mount it",
          "text": "server.ts import Fastify from \"fastify\"; import { htmxUi } from \"htmx-ui-engine/fastify\"; const app = Fastify(); app.register(htmxUi()); // pages, assets, public files, the config's routes, a 404 page app.get(\"/api/invoices\", async () => db.invoices.list()); app.post(\"/api/invoices\", async (request) => db.invoices.create(request.body)); await app.listen({ port: 3000 }); Paths nothing claims get the 404 page: your pages/404.html, or htmx-ui's default. Every method reaches the not-found handler, so the config's routes answer hx-post as well as hx-get. One not-found handler Fastify allows one not-found handler per instance, so the plugin conflicts with your own setNotFoundHandler() on the same instance, and registering it twice fails. Register it on the root instance, not inside a prefixed plugin."
        },
        {
          "id": "fragments",
          "title": "Fragments and pages",
          "text": "reply.type(\"text/html\") sends rendered HTML with the right content type: server.ts import Fastify from \"fastify\"; import { createSite } from \"htmx-ui-engine\"; import { htmxUi } from \"htmx-ui-engine/fastify\"; const site = await createSite(); const app = Fastify(); app.post<{ Params: { id: string } }>(\"/api/invoices/:id/refund\", async (request, reply) => reply.type(\"text/html\").send(site.fragment(\"partials/invoice-row.html\", { invoice: refund(request.params.id) })), ); app.get(\"/dashboard\", async (request, reply) => reply.type(\"text/html\").send(site.render(\"/dashboard\", { user: currentUser(request) })), ); app.register(htmxUi({ site })); // the same site: templates compiled once await app.listen({ port: 3000 }); Fastify parses JSON bodies itself, but not HTML forms. Add @fastify/formbody to read hx-post forms in your own routes. The config's routes get the request body either way. See Pages & fragments for forms, htmx headers and error fragments."
        },
        {
          "id": "options",
          "title": "Options",
          "text": "htmxUi(options) takes the options of createSite(): root, config, context, asset, cache and site. For example, to serve a site from another directory: server.ts app.register(htmxUi({ root: \"../marketing\" }));"
        },
        {
          "id": "testing",
          "title": "Testing",
          "text": "app.inject() answers in-process, with no port: server.test.ts import { expect, test } from \"bun:test\"; import { app } from \"./server\"; test(\"serves the home page\", async () => { const res = await app.inject({ method: \"GET\", url: \"/\" }); expect(res.statusCode).toBe(200); });"
        },
        {
          "id": "run",
          "title": "Build and run",
          "text": "In production the plugin serves dist/, so build first and set NODE_ENV: Terminal htmx-ui build && NODE_ENV=production node server.ts Node runs server.ts directly from 22.18. See Deploying for what to ship, cache headers and Docker."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/servers/koa",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/servers/koa",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/servers/koa.md",
      "title": "Koa",
      "description": "Serve a built htmx-ui site as Koa middleware that answers only what nothing else did, and render pages and fragments from your routes.",
      "section": "Servers",
      "headings": [
        {
          "level": 2,
          "id": "mount",
          "text": "Mount it"
        },
        {
          "level": 2,
          "id": "fragments",
          "text": "Fragments and pages"
        },
        {
          "level": 2,
          "id": "options",
          "text": "Options"
        },
        {
          "level": 2,
          "id": "testing",
          "text": "Testing"
        },
        {
          "level": 2,
          "id": "run",
          "text": "Build and run"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "htmx-ui-engine/koa exports htmxUi(), middleware that lets the rest of your stack run first and answers only when nothing else did. Its place in the stack doesn't matter. It is tested with Koa 3, on Node and Bun. bun add htmx-ui-engine koa npm install htmx-ui-engine koa pnpm add htmx-ui-engine koa yarn add htmx-ui-engine koa"
        },
        {
          "id": "mount",
          "title": "Mount it",
          "text": "server.ts import Koa from \"koa\"; import Router from \"@koa/router\"; import { htmxUi } from \"htmx-ui-engine/koa\"; const app = new Koa(); const router = new Router(); router.get(\"/api/invoices\", async (ctx) => { ctx.body = await db.invoices.list(); }); app.use(htmxUi()); // pages, assets, public files, the config's routes, a 404 page app.use(router.routes()); app.listen(3000); The middleware calls await next() first. Afterwards, it answers only if the response still has Koa's default 404 status and no body. A route that sets a body, or a status of its own, keeps its response. Paths nothing claims get the 404 page: your pages/404.html, or htmx-ui's default."
        },
        {
          "id": "fragments",
          "title": "Fragments and pages",
          "text": "Set ctx.type = \"html\" and put the rendered HTML in ctx.body: server.ts import Koa from \"koa\"; import Router from \"@koa/router\"; import { createSite } from \"htmx-ui-engine\"; import { htmxUi } from \"htmx-ui-engine/koa\"; const site = await createSite(); const app = new Koa(); const router = new Router(); router.post(\"/api/invoices/:id/refund\", (ctx) => { ctx.type = \"html\"; ctx.body = site.fragment(\"partials/invoice-row.html\", { invoice: refund(ctx.params.id) }); }); router.get(\"/dashboard\", (ctx) => { ctx.type = \"html\"; ctx.body = site.render(\"/dashboard\", { user: currentUser(ctx) }); }); app.use(htmxUi({ site })); // the same site: templates compiled once app.use(router.routes()); app.listen(3000); A body parser such as @koa/bodyparser works alongside it: the adapter rebuilds the body it read for the config's routes. Errors thrown in the adapter propagate like any other, so a try / catch middleware at the top of the stack sees them. See Pages & fragments for forms, htmx headers and error fragments."
        },
        {
          "id": "options",
          "title": "Options",
          "text": "htmxUi(options) takes the options of createSite(): root, config, context, asset, cache and site. For example, to serve a site from another directory: server.ts app.use(htmxUi({ root: \"../marketing\" }));"
        },
        {
          "id": "testing",
          "title": "Testing",
          "text": "Listen on port 0 to get a free port, and close the server when the test ends: server.test.ts import { expect, test } from \"bun:test\"; import { app } from \"./server\"; test(\"serves the home page\", async () => { const server = app.listen(0); await new Promise((resolve) => server.once(\"listening\", resolve)); try { const { port } = server.address() as { port: number }; const res = await fetch(`http://localhost:${port}/`); expect(res.status).toBe(200); } finally { server.close(); } });"
        },
        {
          "id": "run",
          "title": "Build and run",
          "text": "In production the middleware serves dist/, so build first and set NODE_ENV: Terminal htmx-ui build && NODE_ENV=production node server.ts Node runs server.ts directly from 22.18. See Deploying for what to ship, cache headers and Docker."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/servers/express",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/servers/express",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/servers/express.md",
      "title": "Express",
      "description": "Mount a built htmx-ui site as Express middleware, and render pages and fragments from your routes.",
      "section": "Servers",
      "headings": [
        {
          "level": 2,
          "id": "mount",
          "text": "Mount it last"
        },
        {
          "level": 2,
          "id": "fragments",
          "text": "Fragments and pages"
        },
        {
          "level": 2,
          "id": "options",
          "text": "Options"
        },
        {
          "level": 2,
          "id": "testing",
          "text": "Testing"
        },
        {
          "level": 2,
          "id": "run",
          "text": "Build and run"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "htmx-ui-engine/express exports htmxUi(), Connect-style middleware that serves the built site. It works with Express 4 and 5, and with anything else built on node:http: Connect, a bare http.createServer(), or NestJS. Use it for a Node app that already exists. For a new one, Elysia or Fastify don't care about registration order. bun add htmx-ui-engine express npm install htmx-ui-engine express pnpm add htmx-ui-engine express yarn add htmx-ui-engine express"
        },
        {
          "id": "mount",
          "title": "Mount it last",
          "text": "Middleware runs in order, and htmx-ui answers every request it sees, with a 404 page when nothing matches. Register it after your own routes and middleware, or they will never be reached: server.ts import express from \"express\"; import { htmxUi } from \"htmx-ui-engine/express\"; const app = express(); app.get(\"/api/invoices\", async (req, res) => { res.json(await db.invoices.list()); }); app.use(htmxUi()); // last: pages, assets, public files, the config's routes, a 404 page app.listen(3000); Paths nothing claims get the 404 page: your pages/404.html, or htmx-ui's default. An error thrown while answering goes to next(error), so Express's error middleware handles it as usual. Mount it at the root The adapter matches a request's full path, prefix included. Mounted with app.use(\"/blog\", htmxUi()), it looks for dist/blog/… and finds nothing. Mount it without a path, and build the site with its pages under pages/blog/ if that is where they belong."
        },
        {
          "id": "fragments",
          "title": "Fragments and pages",
          "text": "res.type(\"html\").send() sends rendered HTML with the right content type: server.ts import express from \"express\"; import { createSite } from \"htmx-ui-engine\"; import { htmxUi } from \"htmx-ui-engine/express\"; const site = await createSite(); const app = express(); app.use(express.urlencoded({ extended: false })); app.post(\"/api/invoices/:id/refund\", (req, res) => { res.type(\"html\").send(site.fragment(\"partials/invoice-row.html\", { invoice: refund(req.params.id) })); }); app.get(\"/dashboard\", (req, res) => { res.type(\"html\").send(site.render(\"/dashboard\", { user: currentUser(req) })); }); app.use(htmxUi({ site })); // the same site: templates compiled once app.listen(3000); Body parsers are fine before the adapter: when express.urlencoded() or express.json() has read a request body, the adapter rebuilds it for the config's routes. See Pages & fragments for forms, htmx headers and error fragments."
        },
        {
          "id": "options",
          "title": "Options",
          "text": "htmxUi(options) takes the options of createSite(): root, config, context, asset, cache and site. For example, to serve a site from another directory: server.ts app.use(htmxUi({ root: \"../marketing\" }));"
        },
        {
          "id": "testing",
          "title": "Testing",
          "text": "Listen on port 0 to get a free port, and close the server when the test ends: server.test.ts import { expect, test } from \"bun:test\"; import { app } from \"./server\"; test(\"serves the home page\", async () => { const server = app.listen(0); await new Promise((resolve) => server.once(\"listening\", resolve)); try { const { port } = server.address() as { port: number }; const res = await fetch(`http://localhost:${port}/`); expect(res.status).toBe(200); } finally { server.close(); } });"
        },
        {
          "id": "run",
          "title": "Build and run",
          "text": "In production the middleware serves dist/, so build first and set NODE_ENV: Terminal htmx-ui build && NODE_ENV=production node server.ts Node runs server.ts directly from 22.18. See Deploying for what to ship, cache headers and Docker."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/servers/hono",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/servers/hono",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/servers/hono.md",
      "title": "Hono",
      "description": "Mount a built htmx-ui site as Hono middleware, and render pages and fragments from your handlers.",
      "section": "Servers",
      "headings": [
        {
          "level": 2,
          "id": "mount",
          "text": "Mount it last"
        },
        {
          "level": 2,
          "id": "fragments",
          "text": "Fragments and pages"
        },
        {
          "level": 2,
          "id": "options",
          "text": "Options"
        },
        {
          "level": 2,
          "id": "testing",
          "text": "Testing"
        },
        {
          "level": 2,
          "id": "run",
          "text": "Build and run"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "htmx-ui-engine/hono exports htmxUi(), middleware that serves the built site. It runs wherever Hono has a filesystem: Bun, Node (with @hono/node-server) and Deno. Edge runtimes with no filesystem, such as Cloudflare Workers, can't read dist/, so the adapter doesn't work there. bun add htmx-ui-engine hono npm install htmx-ui-engine hono pnpm add htmx-ui-engine hono yarn add htmx-ui-engine hono"
        },
        {
          "id": "mount",
          "title": "Mount it last",
          "text": "Hono runs middleware in registration order, and htmx-ui answers every request it sees, with a 404 page when nothing matches. Register it after your own handlers: server.ts import { Hono } from \"hono\"; import { htmxUi } from \"htmx-ui-engine/hono\"; const app = new Hono(); app.get(\"/api/invoices\", (c) => c.json(invoices.list())); app.post(\"/api/invoices\", async (c) => c.json(await invoices.create(await c.req.json()), 201)); app.use(htmxUi()); // last: pages, assets, public files, the config's routes, a 404 page export default app; // Bun and Deno serve the default export's fetch Paths nothing claims get the 404 page: your pages/404.html, or htmx-ui's default. Because the middleware answers them, Hono's app.notFound() never runs."
        },
        {
          "id": "fragments",
          "title": "Fragments and pages",
          "text": "c.html() sends rendered HTML with the right content type: server.ts import { Hono } from \"hono\"; import { createSite } from \"htmx-ui-engine\"; import { htmxUi } from \"htmx-ui-engine/hono\"; const site = await createSite(); const app = new Hono(); app.post(\"/api/invoices/:id/refund\", (c) => c.html(site.fragment(\"partials/invoice-row.html\", { invoice: refund(c.req.param(\"id\")) })), ); app.get(\"/dashboard\", (c) => c.html(site.render(\"/dashboard\", { user: currentUser(c) }))); app.use(htmxUi({ site })); // the same site: templates compiled once export default app; See Pages & fragments for forms (c.req.parseBody()), htmx headers and error fragments."
        },
        {
          "id": "options",
          "title": "Options",
          "text": "htmxUi(options) takes the options of createSite(): root, config, context, asset, cache and site. For example, to serve a site from another directory: server.ts app.use(htmxUi({ root: \"../marketing\" }));"
        },
        {
          "id": "testing",
          "title": "Testing",
          "text": "app.request() answers in-process, with no port: server.test.ts import { expect, test } from \"bun:test\"; import app from \"./server\"; test(\"serves the home page\", async () => { const res = await app.request(\"/\"); expect(res.status).toBe(200); });"
        },
        {
          "id": "run",
          "title": "Build and run",
          "text": "In production the middleware serves dist/, so build first and set NODE_ENV: Terminal htmx-ui build && NODE_ENV=production bun server.ts On Node, serve the app with @hono/node-server's serve(app). See Deploying for what to ship, cache headers and Docker."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/servers/bun",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/servers/bun",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/servers/bun.md",
      "title": "Bun.serve",
      "description": "Serve a built htmx-ui site from Bun.serve with no framework: your routes in routes, the site in fetch.",
      "section": "Servers",
      "headings": [
        {
          "level": 2,
          "id": "mount",
          "text": "Serve it"
        },
        {
          "level": 2,
          "id": "fragments",
          "text": "Fragments and pages"
        },
        {
          "level": 2,
          "id": "testing",
          "text": "Testing"
        },
        {
          "level": 2,
          "id": "run",
          "text": "Build and run"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "Bun's own server needs no adapter. Bun.serve checks its routes first and calls fetch for everything else, and site.handle() takes a Request and returns a Response. So your routes go in routes and the site goes in fetch. There is nothing to install beyond htmx-ui-engine."
        },
        {
          "id": "mount",
          "title": "Serve it",
          "text": "server.ts import { createSite } from \"htmx-ui-engine\"; const site = await createSite(); const server = Bun.serve({ port: 3000, routes: { \"/api/invoices\": { GET: async () => Response.json(await db.invoices.list()), POST: async (req) => Response.json(await db.invoices.create(await req.json()), { status: 201 }), }, }, // Pages, assets, public files, the config's routes, a 404 page. fetch: (req) => site.handle(req), }); console.log(`Listening on ${server.url}`); routes always beat fetch, so the order of the two keys doesn't matter. Paths nothing claims get the 404 page: your pages/404.html, or htmx-ui's default."
        },
        {
          "id": "fragments",
          "title": "Fragments and pages",
          "text": "server.ts import { createSite } from \"htmx-ui-engine\"; const site = await createSite(); const html = (body: string, status = 200) => new Response(body, { status, headers: { \"Content-Type\": \"text/html; charset=utf-8\" } }); Bun.serve({ port: 3000, routes: { \"/api/invoices/:id/refund\": { POST: (req) => html(site.fragment(\"partials/invoice-row.html\", { invoice: refund(req.params.id) })), }, \"/dashboard\": (req) => html(site.render(\"/dashboard\", { user: currentUser(req) })), }, fetch: (req) => site.handle(req), }); See Pages & fragments for forms (await req.formData()), htmx headers and error fragments. Bun.serve's error(error) option is the place for an error fragment."
        },
        {
          "id": "testing",
          "title": "Testing",
          "text": "site.handle() is a function from Request to Response, so a test can call it directly. Start the server on port 0 to test your routes too: server.test.ts import { expect, test } from \"bun:test\"; import { createSite } from \"htmx-ui-engine\"; test(\"serves the home page\", async () => { const site = await createSite(); const res = await site.handle(new Request(\"http://localhost/\")); expect(res.status).toBe(200); });"
        },
        {
          "id": "run",
          "title": "Build and run",
          "text": "Terminal htmx-ui build && NODE_ENV=production bun server.ts See Deploying for what to ship, cache headers and Docker. For a framework on top of Bun, Elysia is the closest fit."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/servers/other",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/servers/other",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/servers/other.md",
      "title": "Other JavaScript servers",
      "description": "Serve a built htmx-ui site from Deno.serve, node:http, h3 and Nitro, or NestJS, with createSite() or an existing adapter.",
      "section": "Servers",
      "headings": [
        {
          "level": 2,
          "id": "deno",
          "text": "Deno"
        },
        {
          "level": 2,
          "id": "node-http",
          "text": "node:http"
        },
        {
          "level": 2,
          "id": "h3",
          "text": "h3 and Nitro"
        },
        {
          "level": 2,
          "id": "nestjs",
          "text": "NestJS"
        },
        {
          "level": 2,
          "id": "anything-else",
          "text": "Anything else"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "Anything that can hand over a web Request can call site.handle(request) and send back the Response. Anything built on node:http can use the Express adapter, which is plain (req, res, next) middleware. Between the two, most JavaScript servers are covered. Below are the common ones."
        },
        {
          "id": "deno",
          "title": "Deno",
          "text": "Deno.serve takes a function from Request to Response, which is exactly what site.handle is. Deno reads htmx-ui.config.ts and node_modules as they are: server.ts import { createSite } from \"htmx-ui-engine\"; const site = await createSite(); Deno.serve({ port: 3000 }, (req) => { const { pathname } = new URL(req.url); if (pathname === \"/api/ping\") return Response.json({ ok: true }); return site.handle(req); }); Terminal htmx-ui build && NODE_ENV=production deno run -A server.ts Hono runs on Deno too, if you want a router."
        },
        {
          "id": "node-http",
          "title": "node:http",
          "text": "The Express adapter's middleware works on a bare Node server. Call it for the requests your code doesn't answer: server.ts import { createServer } from \"node:http\"; import { htmxUi } from \"htmx-ui-engine/express\"; const site = htmxUi(); createServer((req, res) => { if (req.url === \"/api/ping\") { res.setHeader(\"Content-Type\", \"application/json\"); return res.end(JSON.stringify({ ok: true })); } site(req, res, (error) => { res.statusCode = 500; res.end(String(error)); }); }).listen(3000); The same goes for Connect and other (req, res, next) stacks: mount it last."
        },
        {
          "id": "h3",
          "title": "h3 and Nitro",
          "text": "h3 2 handlers get a web Request as event.req and can return a Response. A catch-all route hands the rest to the site. h3's router prefers specific routes, so the order doesn't matter: server.ts import { H3, serve } from \"h3\"; import { createSite } from \"htmx-ui-engine\"; const site = await createSite(); const app = new H3(); app.get(\"/api/ping\", () => ({ ok: true })); app.all(\"/**\", (event) => site.handle(event.req)); // everything else is the built site serve(app, { port: 3000 }); In Nitro, the same handler goes in a catch-all route, routes/[...].ts. Nitro 2 is built on h3 1, where event.req is Node's request, so convert it first: site.handle(toWebRequest(event))."
        },
        {
          "id": "nestjs",
          "title": "NestJS",
          "text": "On Nest's default Express platform, the Express adapter does the serving. Don't use app.use() for it: Nest runs global middleware before its controllers, so the site would answer every request. When no route matches, Nest throws a NotFoundException with the message Cannot <METHOD> <path>. An exception filter can catch exactly those and hand them to the site, and leave the NotFoundExceptions your controllers throw alone: src/htmx-ui.filter.ts import { Catch, NotFoundException, type ArgumentsHost, type ExceptionFilter } from \"@nestjs/common\"; import type { Request, Response } from \"express\"; import { htmxUi } from \"htmx-ui-engine/express\"; const site = htmxUi(); @Catch(NotFoundException) export class HtmxUiFilter implements ExceptionFilter { catch(error: NotFoundException, host: ArgumentsHost) { const req = host.switchToHttp().getRequest<Request>(); const res = host.switchToHttp().getResponse<Response>(); // Not \"no route matched\": a controller's own 404, so answer it as Nest would. if (error.message !== `Cannot ${req.method} ${req.originalUrl}`) { return res.status(404).json(error.getResponse()); } site(req, res, (e) => res.status(500).end(String(e))); } } src/main.ts const app = await NestFactory.create(AppModule); app.useGlobalFilters(new HtmxUiFilter()); await app.listen(3000); To render fragments from controllers, create a site with createSite(), pass it to the filter's adapter as htmxUi({ site }), and return site.fragment(...) from handlers decorated with @Header(\"Content-Type\", \"text/html; charset=utf-8\")."
        },
        {
          "id": "anything-else",
          "title": "Anything else",
          "text": "What any integration needs: Create the site once, at startup: const site = await createSite(). Pass the requests your own code doesn't answer to site.handle() as a web Request, and send back the Response it resolves to: status, headers (including several Set-Cookie), body. Use site.fragment() and site.render() for HTML, with an HTML content type. The adapters' source in htmx-ui-engine/src/ is the reference: each is a few dozen lines. See createSite() API for every option."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/servers/any-backend",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/servers/any-backend",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/servers/any-backend.md",
      "title": "Django, Rails, Go and others",
      "description": "Use htmx-ui with a backend in any language: serve the built site as static files and render fragments with your own templates, or run htmx-ui next to it.",
      "section": "Servers",
      "headings": [
        {
          "level": 2,
          "id": "static",
          "text": "Your backend renders the fragments"
        },
        {
          "level": 2,
          "id": "sidecar",
          "text": "htmx-ui renders, next to your backend"
        },
        {
          "level": 2,
          "id": "which",
          "text": "Which one?"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "htmx works with any server that returns HTML, and many htmx apps are written in Python, Ruby, PHP, Go or Elixir. The adapters are JavaScript, but your backend doesn't have to be. There are two ways to combine them. Pick one by asking who renders the per-request HTML."
        },
        {
          "id": "static",
          "title": "Your backend renders the fragments",
          "text": "Build the site with htmx-ui build as part of your release, and serve dist/ as static files. htmx in those pages calls your backend, and your backend answers with fragments written in its own templates, using htmx-ui's classes. The components are CSS classes plus small behaviours that initialise on every swap, so markup from Django, ERB, Blade or Go's html/template works the same as markup from Nunjucks. templates/invoices/_row.html (Django) <tr id=\"invoice-{{ invoice.id }}\"> <td>{{ invoice.number }}</td> <td><span class=\"badge badge-success\">{{ invoice.status }}</span></td> <td> <button class=\"btn btn-sm btn-outline\" hx-post=\"/api/invoices/{{ invoice.id }}/refund\" hx-target=\"#invoice-{{ invoice.id }}\" hx-swap=\"outerHTML\">Refund</button> </td> </tr> Copy the markup for each component from its page in these docs. The classes are the API, and they are the same in any template language. The front server (nginx here) serves the pages with clean URLs and passes the API to your backend: nginx.conf server { listen 80; root /srv/site/dist; # Your backend: Django, Rails, Laravel, Go... location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; } location /assets/ { add_header Cache-Control \"public, max-age=31536000, immutable\"; try_files $uri =404; } # /about -> about.html, /docs -> docs/index.html location / { try_files $uri $uri.html $uri/index.html =404; } error_page 404 /404.html; } Caddyfile example.com { root * /srv/site/dist encode gzip handle /api/* { reverse_proxy 127.0.0.1:8000 } handle { try_files {path} {path}.html {path}/index.html file_server } handle_errors 404 { rewrite * /404.html file_server } } Your framework's static file serving works as well as nginx, as long as it maps /about to about.html. dist/404.html is your pages/404.html, or htmx-ui's default. Nunjucks is close to Jinja, not the same Nunjucks borrows Jinja's syntax, so Django and Flask templates read alike, and moving a partial between them is mostly mechanical. But filters, t"
        },
        {
          "id": "sidecar",
          "title": "htmx-ui renders, next to your backend",
          "text": "To keep one set of templates, run a small htmx-ui server (any adapter) next to your backend. It serves the pages and renders the fragments. Your backend stays the source of the data, which the htmx-ui server reads over HTTP, as JSON: server.ts import { Elysia } from \"elysia\"; import { createSite } from \"htmx-ui-engine\"; import { htmxUi } from \"htmx-ui-engine/elysia\"; const site = await createSite(); const backend = process.env.BACKEND_URL ?? \"http://127.0.0.1:8000\"; const html = (body: string) => new Response(body, { headers: { \"Content-Type\": \"text/html; charset=utf-8\" } }); new Elysia() .get(\"/invoices/table\", async ({ request }) => { // Pass the session cookie along, so the backend knows who is asking. const res = await fetch(`${backend}/api/invoices`, { headers: { cookie: request.headers.get(\"cookie\") ?? \"\" } }); return html(site.fragment(\"partials/invoice-table.html\", { invoices: await res.json() })); }) .use(htmxUi({ site })) .listen(3000); Put both behind one host, with the proxy sending /api/ to your backend and everything else to the htmx-ui server. Pages and API then share an origin, cookies and all. This adds a process to deploy. In exchange, every piece of HTML comes from one place."
        },
        {
          "id": "which",
          "title": "Which one?",
          "text": "Your backend renders the fragments when most of the HTML is per-request and your team already writes it in your backend's templates. htmx-ui provides the pages, the design system and the components. htmx-ui renders next to your backend when the front end is its own project, or the backend is meant to stay a JSON API. Either way, mock endpoints in htmx-ui.config.ts let you build the pages before the backend has the routes."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components.md",
      "title": "Components",
      "description": "Every component, grouped by what it is for: forms, actions, navigation, overlays, feedback, layout, data, typography and chat. Each is a set of CSS classes, plus a small behaviour where one is needed.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "forms",
          "text": "Forms & inputs"
        },
        {
          "level": 2,
          "id": "actions",
          "text": "Actions & menus"
        },
        {
          "level": 2,
          "id": "navigation",
          "text": "Navigation"
        },
        {
          "level": 2,
          "id": "overlays",
          "text": "Overlays"
        },
        {
          "level": 2,
          "id": "feedback",
          "text": "Feedback & status"
        },
        {
          "level": 2,
          "id": "layout",
          "text": "Layout & disclosure"
        },
        {
          "level": 2,
          "id": "data",
          "text": "Data display"
        },
        {
          "level": 2,
          "id": "content",
          "text": "Typography & media"
        },
        {
          "level": 2,
          "id": "chat",
          "text": "Chat & AI"
        },
        {
          "level": 2,
          "id": "anatomy",
          "text": "Anatomy of a component"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "61 components in 9 groups. Jump to one: Forms & inputs 13 Actions & menus 9 Navigation 5 Overlays 7 Feedback & status 8 Layout & disclosure 5 Data display 4 Typography & media 4 Chat & AI 6"
        },
        {
          "id": "forms",
          "title": "Forms & inputs",
          "text": "Controls that take input and submit with a form, and the fields that label them. Checkbox A box that toggles between checked and not checked, with sizes, states and every field feature. Combobox An input with a filtered list of suggestions: groups, multiple picks as chips, a clear button, server search. Field Labels, help text and messages for controls; inline, responsive and grid forms. Input Text inputs, textareas, selects and file pickers with every state. Input group Icons, text, buttons, menus and spinners inside an input's border. Input OTP A one-time code or PIN in a row of slots: 4-digit authenticator codes, digits or letters, groups, paste and SMS autofill, sizes and states. Label An accessible label for any control, wrapping checkboxes and radios, or as a choice card. Password input A password field with a show / hide toggle: eye icon or text, a checkbox, Caps Lock warning, every input state. Radio group Radio buttons as cards or inline rows: one choice of a few, submitting with the form. Select A list of options in a popup opened from a button: searchable, keyboard driven, styled like an input. Slider Pick a value, a range or several values on a track: native range inputs, vertical too, with every form state. Switch An on/off setting that applies at once: a sliding track on a real checkbox, no JavaScript. Textarea Multi-line text that grows as you type: field, character count, every state, and Ctrl/⌘+Enter to submit."
        },
        {
          "id": "actions",
          "title": "Actions & menus",
          "text": "Buttons, toggles and menus that do something. Button Actions in every variant, size and state, including loading. Button group Join buttons, inputs and menus: split buttons, toolbars, vertical groups. Clipboard Copy buttons and copy tags that work on every browser: icon, label or both, a green tick when copied, every button variant and size. Command A command menu for search and quick actions: filtering, keyboard, groups, shortcuts, inline or in a ⌘K dialog. Dropdown A menu of links, actions and options that opens from a button, with checkbox and radio items, shortcuts and submenus. Menubar A persistent row of menus, as in desktop apps: submenus, checkbox and radio items, icons and shortcuts. Scroll button A floating button that jumps back to the bottom or top of a container or the page. Toggle A two-state button, on or off, in any button style. Toggle group A set of toggle buttons: pick one, at most one, or many."
        },
        {
          "id": "navigation",
          "title": "Navigation",
          "text": "Moving between pages and between views of a page. Breadcrumb The path to the current page: custom separators, dropdowns, and collapsing to an ellipsis when space runs out. Navigation menu A site's main navigation: links, and buttons that open panels of links, from simple lists to mega menus. Pagination Links to the pages of a long list, with previous and next, page numbers or icons only. Sidebar Composable app navigation: groups, menus, badges and actions, collapsing to icons or off-canvas, with a mobile panel. Tabs Layered panels shown one at a time: line, pill and outline styles, icons, disabled tabs, vertical, synced and remembered."
        },
        {
          "id": "overlays",
          "title": "Overlays",
          "text": "Content that opens above the page: modals, panels, popups and hints. Alert dialog A modal that interrupts with important content and waits for an answer: small size, media and destructive actions. Dialog A modal window over the page or another dialog: close button or outside click, scrolling content, sticky footer, htmx fragments. Drawer A panel on an edge of the screen that can be swiped away: four directions, a handle, nested drawers, a responsive dialog. Hover card A card that previews what is behind a link when a mouse rests on it or the link gets keyboard focus. Popover A floating panel of rich content that opens from a button: a header, alignment, and forms. Sheet A dialog that slides in from an edge of the screen: four sides, sizes, scrolling content, htmx fragments. Tooltip A short hint on hover or keyboard focus, on any side, with no JavaScript needed to show it."
        },
        {
          "id": "feedback",
          "title": "Feedback & status",
          "text": "Messages, progress, loading and empty states, and labels for status. Alert Callouts for info, success, warnings and errors. Badge Small status labels and counters. Empty An empty state with an icon, a message and a way forward. Loader Pure-CSS loading indicators: dots, waves, pulses and animated text. Progress A bar showing how far a task has got, with an optional label, value and help text. Skeleton Placeholders shaped like loading content: any shape, text, avatars, cards, lists, tables and forms. Spinner Loading indicator from any icon or pure CSS; works in buttons, badges and inputs. Toast Stacking, swipeable notifications: success, info, warning, error and loading, actions, promises and server toasts."
        },
        {
          "id": "layout",
          "title": "Layout & disclosure",
          "text": "Containers that group, separate, reveal or scroll through content. Accordion Stacked headings that reveal sections; one or many open, no JavaScript. Card Header, content and footer, with media, an action slot and scrolling content. Carousel Slides that drag, swipe and snap with momentum (Embla): item sizes, spacing, vertical, options and plugins. Collapsible A button that shows and hides a panel: extra settings, nested file trees, or <details> with no JavaScript. Separator A horizontal or vertical line between content, semantic or purely visual."
        },
        {
          "id": "data",
          "title": "Data display",
          "text": "Rows, tables and people: structured content to scan. Avatar An image with a fallback for a user: sizes, status badges, groups with a count, and menu triggers. Item A row of content with media, a title, a description and actions, alone or in a list. Table Readable data tables with optional hover rows. Timeline Events in chronological order: dots, icons or avatars, colours, sizes, vertical or horizontal, alternating sides."
        },
        {
          "id": "content",
          "title": "Typography & media",
          "text": "Long-form text, code, keys and icons. Code block Highlighted source with a copy button and package-manager tabs. Icon SVG icons inlined at build time, or linked as images. Kbd Keyboard keys and shortcuts, on their own or in buttons, tooltips and input groups. Text Long-form content with .prose (Tailwind Typography), plus helpers for single elements."
        },
        {
          "id": "chat",
          "title": "Chat & AI",
          "text": "Conversations, attachments and assistant replies. Attachment A file, image, video or audio attachment with upload states, sizes, orientation and scrolling groups. Chain of thought A sequence of steps on a connecting line, each folding its details away: reasoning traces, tool calls and process logs. Message A chat message with avatar, header, footer and alignment; groups, attachments, and reasoning and tool calls for AI chat. Message scroller Scrolling for a streaming chat transcript: new questions rise to the top, replies grow beneath them, and the view moves only when the reader does. Prompt input The text box of an AI chat: a growing textarea, tools, send and stop, @ mentions and / commands. Reasoning A collapsible for AI reasoning that opens while it streams and closes, with its duration, when it ends."
        },
        {
          "id": "anatomy",
          "title": "Anatomy of a component",
          "text": "Each component has its own directory, htmx-ui/components/<name>/ (packages/ui/src/components/<name>/ in the repository), holding: <name>.css: classes in @layer components, built from token utilities with @apply. <name>.ts (optional): an init<Name>(root) function that wires up [data-<name>] elements once each, with <name>.test.ts beside it. <name>.html (optional): a Nunjucks macro for markup that is tedious to write by hand."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/checkbox",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/checkbox",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/checkbox.md",
      "title": "Checkbox",
      "description": "A box that toggles between checked and not checked, with sizes, states and every field feature.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "usage",
          "text": "Usage"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Sizes"
        },
        {
          "level": 2,
          "id": "states",
          "text": "States"
        },
        {
          "level": 3,
          "id": "disabled",
          "text": "Disabled"
        },
        {
          "level": 3,
          "id": "invalid",
          "text": "Error, warning, success"
        },
        {
          "level": 2,
          "id": "indeterminate",
          "text": "Partly selected"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx"
        },
        {
          "level": 2,
          "id": "macro",
          "text": "Nunjucks macro"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"field field-horizontal max-w-sm\"> <input class=\"checkbox\" type=\"checkbox\" id=\"checkbox-reminders\" checked /> <label class=\"field-label\" for=\"checkbox-reminders\">Email me about new releases</label> </div> A toggle is a button that switches something on or off on the spot. A checkbox is part of a form: it takes a name and a value, submits with the rest, and is reset by the form."
        },
        {
          "id": "usage",
          "title": "Usage",
          "text": "Add .checkbox to a native <input type=\"checkbox\">. Everything else is plain HTML: checked, required, name, value, disabled. Wrap it in a field with .field-horizontal: the box first, then .field-content holding the .field-label and an optional .field-description. The label is clickable, so give it the for that matches the box's id. Put related boxes in a <fieldset class=\"fieldset\"> with a <legend class=\"field-legend\">, so a screen reader announces the group's question. A required checkbox must be checked to submit, and its label gets the field's asterisk. html <fieldset class=\"fieldset max-w-md\"> <legend class=\"field-legend\">Notifications</legend> <div class=\"field field-horizontal\"> <input class=\"checkbox\" type=\"checkbox\" id=\"checkbox-mentions\" checked /> <div class=\"field-content\"> <label class=\"field-label\" for=\"checkbox-mentions\">Mentions</label> <p class=\"field-description\">When someone @mentions you.</p> </div> </div> <div class=\"field field-horizontal\"> <input class=\"checkbox\" type=\"checkbox\" id=\"checkbox-digest\" /> <label class=\"field-label\" for=\"checkbox-digest\">Weekly digest</label> </div> </fieldset>"
        },
        {
          "id": "sizes",
          "title": "Sizes",
          "text": ".checkbox-sm and .checkbox-lg match the heights of .input-sm and .input-lg. html <div class=\"field field-horizontal\"> <input class=\"checkbox checkbox-sm\" type=\"checkbox\" id=\"checkbox-sm\" aria-label=\"Small, checked\" checked /> <label class=\"field-label text-xs\" for=\"checkbox-sm\">Small</label> </div> <div class=\"field field-horizontal\"> <input class=\"checkbox\" type=\"checkbox\" id=\"checkbox-md\" aria-label=\"Default, checked\" checked /> <label class=\"field-label\" for=\"checkbox-md\">Default</label> </div> <div class=\"field field-horizontal\"> <input class=\"checkbox checkbox-lg\" type=\"checkbox\" id=\"checkbox-lg\" aria-label=\"Large, checked\" checked /> <label class=\"field-label text-base\" for=\"checkbox-lg\">Large</label> </div>"
        },
        {
          "id": "states",
          "title": "States",
          "text": ""
        },
        {
          "id": "disabled",
          "title": "Disabled",
          "text": "disabled fades the box; inside a field the label and description fade with it. html <div class=\"field-group max-w-md\"> <div class=\"field field-horizontal\"> <input class=\"checkbox\" type=\"checkbox\" id=\"checkbox-archived\" disabled checked /> <label class=\"field-label\" for=\"checkbox-archived\">Archived</label> </div> <div class=\"field field-horizontal\"> <input class=\"checkbox\" type=\"checkbox\" id=\"checkbox-beta\" disabled /> <div class=\"field-content\"> <label class=\"field-label\" for=\"checkbox-beta\">Beta features</label> <p class=\"field-description\">Available on the Pro plan.</p> </div> </div> </div>"
        },
        {
          "id": "invalid",
          "title": "Error, warning, success",
          "text": "Set aria-invalid=\"true\" for an error, which turns the border red and the field's label with it, or add .checkbox-warning or .checkbox-success for the softer states. Pair each with a field message linked with aria-describedby. html <div class=\"field-group max-w-md\"> <div class=\"field field-horizontal\"> <input class=\"checkbox\" type=\"checkbox\" id=\"checkbox-terms\" aria-invalid=\"true\" aria-describedby=\"checkbox-terms-msg\" /> <div class=\"field-content\"> <label class=\"field-label\" for=\"checkbox-terms\">I accept the terms</label> <p class=\"field-error\" id=\"checkbox-terms-msg\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m15 9-6 6M9 9l6 6\"/></svg> You must accept the terms to continue.</p> </div> </div> <div class=\"field field-horizontal\"> <input class=\"checkbox checkbox-warning\" type=\"checkbox\" id=\"checkbox-2fa\" checked aria-describedby=\"checkbox-2fa-msg\" /> <div class=\"field-content\"> <label class=\"field-label\" for=\"checkbox-2fa\">Two-factor authentication</label> <p class=\"field-warning\" id=\"checkbox-2fa-msg\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4M12 17h.01\"/></svg> Highly recommended for admin accounts.</p> </div> </div> <div class=\"field field-horizontal\"> <input class=\"checkbox checkbox-success\" type=\"checkbox\" id=\"checkbox-strong\" checked aria-describedby=\"checkbox-strong-msg\" /> <label class=\"field-label\" for=\"checkbox-strong\">Use a strong password</label> <p class=\"field-success\" id=\"checkbox-strong-msg\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fil"
        },
        {
          "id": "indeterminate",
          "title": "Partly selected",
          "text": "A box that controls a group is indeterminate when some, but not all, of its children are checked. The state is a DOM property rather than an attribute, so markup it with data-indeterminate: a small behaviour sets it on load, paints a dash instead of the tick, and clears it when you click the box. html <fieldset class=\"fieldset max-w-md\"> <legend class=\"field-legend\">Services to monitor</legend> <div class=\"field field-horizontal\"> <input class=\"checkbox\" type=\"checkbox\" id=\"checkbox-all\" data-indeterminate /> <label class=\"field-label\" for=\"checkbox-all\">All services</label> </div> <div class=\"field field-horizontal ps-6\"> <input class=\"checkbox checkbox-sm\" type=\"checkbox\" id=\"checkbox-api\" checked /> <label class=\"field-label\" for=\"checkbox-api\">API</label> </div> <div class=\"field field-horizontal ps-6\"> <input class=\"checkbox checkbox-sm\" type=\"checkbox\" id=\"checkbox-worker\" checked /> <label class=\"field-label\" for=\"checkbox-worker\">Worker</label> </div> <div class=\"field field-horizontal ps-6\"> <input class=\"checkbox checkbox-sm\" type=\"checkbox\" id=\"checkbox-db\" /> <label class=\"field-label\" for=\"checkbox-db\">Database</label> </div> </fieldset> Nothing checks the children for you: that is server state, so keep the box in step in your own code, or read the children on change and set the property: app.js const all = document.querySelector(\"#all\"); const children = [...document.querySelectorAll(\"[data-child]\")]; const sync = () => { const checked = children.filter((box) => box.checked).length; all.indeterminate = checked > 0 && checked < children.length; all.checked = checked === children.length; }; all.addEventListener(\"change\", () => children.forEach((box) => (box.checked = all.checked))); children.forEach((box) => box.addEventListener(\"change\", sync));"
        },
        {
          "id": "htmx",
          "title": "With htmx",
          "text": "Checkboxes submit like any other input, so trigger on change and send the state in hx-vals: html <div class=\"field field-horizontal\"> <input class=\"checkbox\" type=\"checkbox\" id=\"checkbox-star\" name=\"starred\" hx-post=\"/api/echo\" hx-trigger=\"change\" hx-vals='js:{starred: this.checked}' hx-target=\"#checkbox-result\" /> <label class=\"field-label\" for=\"checkbox-star\">Star this repository</label> </div> <p id=\"checkbox-result\" class=\"muted\" aria-live=\"polite\"></p>"
        },
        {
          "id": "macro",
          "title": "Nunjucks macro",
          "text": "If your templates use Nunjucks, the checkbox macro writes the field markup, with the description optional and every attribute passed through attrs: page.html {% from \"components/checkbox/checkbox.html\" import checkbox %} {{ checkbox(\"terms\", \"I accept the terms\", required=true) }} {{ checkbox(\"digest\", \"Weekly digest\", description=\"A summary every Monday.\", checked=true) }} {{ checkbox(\"alerts\", \"Deploy alerts\", size=\"sm\", state=\"warning\", attrs={\"hx-post\": \"/api/settings\"}) }}"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class / attribute Description .checkbox Native checkbox. Take checked, name, value, required and disabled as usual. .checkbox-sm / -lg Matches the height of .input-sm / .input-lg. data-indeterminate Paints the mixed state (behaviour: checkbox.ts); cleared when the box is clicked. aria-invalid=\"true\" Error border, and a red label in a field. So does :user-invalid. .checkbox-warning / -success Warning or success border. checkbox(id, label, …) Macro: the whole field. description, checked, indeterminate, disabled, required, size, state, attrs."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/combobox",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/combobox",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/combobox.md",
      "title": "Combobox",
      "description": "An input with a list of suggestions that filters as you type: groups, multiple picks as chips, a clear button and server-side search.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "keyboard",
          "text": "Keyboard"
        },
        {
          "level": 2,
          "id": "filtering",
          "text": "Filtering"
        },
        {
          "level": 2,
          "id": "auto-highlight",
          "text": "Auto highlight"
        },
        {
          "level": 2,
          "id": "groups",
          "text": "Groups"
        },
        {
          "level": 2,
          "id": "multiple",
          "text": "Multiple"
        },
        {
          "level": 2,
          "id": "clear",
          "text": "Clear button"
        },
        {
          "level": 2,
          "id": "states",
          "text": "Form states"
        },
        {
          "level": 2,
          "id": "input-group",
          "text": "Input group"
        },
        {
          "level": 2,
          "id": "server",
          "text": "Searching on the server"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "Submitting with htmx"
        },
        {
          "level": 2,
          "id": "alignment",
          "text": "Alignment"
        },
        {
          "level": 2,
          "id": "macro",
          "text": "Nunjucks macro"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"combobox w-64\" data-combobox> <div class=\"input-group\"> <input class=\"input\" data-combobox-input placeholder=\"Pick a framework\" aria-label=\"Framework\" /> <span class=\"input-group-addon input-group-addon-inline-end\"> <button type=\"button\" class=\"btn btn-ghost btn-icon\" data-combobox-clear aria-label=\"Clear\" hidden><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> <button type=\"button\" class=\"btn btn-ghost btn-icon\" data-combobox-toggle tabindex=\"-1\" aria-label=\"Show suggestions\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m7 15 5 5 5-5\"/><path d=\"m7 9 5-5 5 5\"/></svg></button> </span> </div> <div class=\"combobox-content\" role=\"listbox\" aria-label=\"Frameworks\" hidden> <div class=\"combobox-item\" role=\"option\" value=\"astro\"> Astro <span class=\"combobox-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span> </div> <div class=\"combobox-item\" role=\"option\" value=\"next\" data-keywords=\"react vercel\"> Next.js <span class=\"combobox-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span> </div> <div class=\"combobox-item\" role=\"option\" value=\"nuxt\" data-keywords=\"vue\"> Nuxt <span class=\"combobox-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewB"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "Wrap everything in .combobox with data-combobox. The input has data-combobox-input and a name for screen readers (aria-label, or a <label for>). The behaviour makes it a role=\"combobox\" linked to the list, and turns browser autocomplete off. Put it in an input group to give it buttons, or use a bare .input. The list is a .combobox-content with role=\"listbox\", starting hidden. Each suggestion is a .combobox-item with role=\"option\" and a value; aria-selected=\"true\" marks the starting choice. A .combobox-check inside shows while the option is picked. Optional parts: a [data-combobox-clear] button, shown only when there is something to clear; a [data-combobox-toggle] button that opens and closes the list (tabindex=\"-1\", as the input already takes focus); a .combobox-empty with data-combobox-empty for \"no matches\". To submit the value, add <input type=\"hidden\" name=\"framework\" data-combobox-value>. The text input shows the label; the hidden field carries the value."
        },
        {
          "id": "keyboard",
          "title": "Keyboard",
          "text": "Focus stays in the input the whole time, so typing never stops working. The highlighted option is announced through aria-activedescendant. Key Action Typing Open the list and filter it ↓ ↑ Open the list, or move the highlight (wrapping round) Enter Pick the highlighted option Esc Close the list Tab, click outside Close the list and move on Backspace in an empty input Multiple: remove the last pick Picking writes the option's label into the input and closes the list. Leaving the input with other text in it puts the label back; leaving it empty clears the choice."
        },
        {
          "id": "filtering",
          "title": "Filtering",
          "text": "Typing matches like a command palette rather than a plain prefix search: case and accents don't matter (zurich finds Zürich), and every word typed must appear somewhere in the option, in any order. Add data-keywords to make an option findable by words that aren't in its label: above, typing react finds Next.js and Remix. Groups with nothing left in them hide, along with their separators, and the empty message shows when nothing matches."
        },
        {
          "id": "auto-highlight",
          "title": "Auto highlight",
          "text": "With data-autohighlight on the wrapper, the first match is highlighted as you type, so Enter takes it straight away. Without it, nothing is highlighted until an arrow key or the pointer picks one, and Enter submits the form as usual. html <div class=\"combobox w-64\" data-combobox data-autohighlight> <div class=\"input-group\"> <input class=\"input\" id=\"cb-autohighlight\" data-combobox-input placeholder=\"Type, then Enter\" aria-label=\"Framework\" /> <span class=\"input-group-addon input-group-addon-inline-end\"> <button type=\"button\" class=\"btn btn-ghost btn-icon\" data-combobox-clear aria-label=\"Clear\" hidden><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> <button type=\"button\" class=\"btn btn-ghost btn-icon\" data-combobox-toggle tabindex=\"-1\" aria-label=\"Show suggestions\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m7 15 5 5 5-5\"/><path d=\"m7 9 5-5 5 5\"/></svg></button> </span> </div> <div class=\"combobox-content\" role=\"listbox\" aria-label=\"Framework\" hidden> <div class=\"combobox-item\" role=\"option\" value=\"astro\" aria-selected=\"false\">Astro<span class=\"combobox-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span></div> <div class=\"combobox-item\" role=\"option\" value=\"next\" aria-selected=\"false\" data-keywords=\"react vercel\">Next.js<span class=\"combobox-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-wid"
        },
        {
          "id": "groups",
          "title": "Groups",
          "text": "Wrap a run of options in a .combobox-group with role=\"group\", headed by a .combobox-label that names it through aria-labelledby. A .combobox-separator with role=\"separator\" goes between groups. html <div class=\"combobox w-72\" data-combobox> <div class=\"input-group\"> <input class=\"input\" id=\"cb-timezone\" data-combobox-input placeholder=\"Search timezones\" aria-label=\"Timezone\" /> <span class=\"input-group-addon input-group-addon-inline-end\"> <button type=\"button\" class=\"btn btn-ghost btn-icon\" data-combobox-clear aria-label=\"Clear\" hidden><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> <button type=\"button\" class=\"btn btn-ghost btn-icon\" data-combobox-toggle tabindex=\"-1\" aria-label=\"Show suggestions\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m7 15 5 5 5-5\"/><path d=\"m7 9 5-5 5 5\"/></svg></button> </span> </div> <div class=\"combobox-content\" role=\"listbox\" aria-label=\"Timezone\" hidden> <div class=\"combobox-group\" role=\"group\" aria-labelledby=\"cb-timezone-group-1\"> <div class=\"combobox-label\" id=\"cb-timezone-group-1\">Americas</div> <div class=\"combobox-item\" role=\"option\" value=\"america/new_york\" aria-selected=\"false\">New York (GMT-5)<span class=\"combobox-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span></div> <div class=\"combobox-item\" role=\"option\" value=\"america/chicago\" aria-selected=\"false\">Chicago (GMT-6)<span class=\"combobox-check\"><svg xmlns=\"http://www.w3.org/200"
        },
        {
          "id": "multiple",
          "title": "Multiple",
          "text": "Add data-multiple to the wrapper and replace the input group with a .combobox-chips box holding a .combobox-chips-input, both with their data attributes. Picking toggles an option and keeps the list open; each pick shows as a chip before the input, with a button to remove it, and Backspace in the empty input removes the last one. The hidden field is repeated, one per value, so the form submits frameworks=next&frameworks=astro. html <div class=\"combobox w-80\" data-combobox data-multiple> <div class=\"combobox-chips\" data-combobox-chips> <input class=\"combobox-chips-input\" data-combobox-input placeholder=\"Add frameworks\" aria-label=\"Frameworks\" /> <button type=\"button\" class=\"btn btn-ghost btn-icon\" data-combobox-clear aria-label=\"Clear all\" hidden><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> </div> <div class=\"combobox-content\" role=\"listbox\" aria-label=\"Frameworks\" hidden> <div class=\"combobox-item\" role=\"option\" value=\"astro\" aria-selected=\"false\"> Astro <span class=\"combobox-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span> </div> <div class=\"combobox-item\" role=\"option\" value=\"next\" aria-selected=\"true\"> Next.js <span class=\"combobox-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span> </div> <div class=\"combobox-item\" role=\"option\" value=\"nuxt\" aria-selected=\"false\"> Nuxt <span class=\"combobox-check\"><svg xmlns=\"http://www.w3.org/2000"
        },
        {
          "id": "clear",
          "title": "Clear button",
          "text": "A [data-combobox-clear] button empties the input and the choice (every chip, with data-multiple) and returns focus to the input. It hides itself while there is nothing to clear, so start it hidden unless the combobox starts with a value. Leave it out when a value is required."
        },
        {
          "id": "states",
          "title": "Form states",
          "text": "The combobox takes the input states, shown through its input group or chip box: aria-invalid=\"true\" for an error, disabled (its buttons are disabled with it), and required, which native validation checks against the text input — it only holds text once something is picked. Put it in a field for the label and messages. html <div class=\"field w-64\"> <label class=\"label\" for=\"cb-invalid\">Framework</label> <div class=\"combobox\" data-combobox> <div class=\"input-group\"> <input class=\"input\" id=\"cb-invalid\" data-combobox-input placeholder=\"Search…\" aria-invalid=\"true\" required aria-describedby=\"cb-invalid-error\" /> <span class=\"input-group-addon input-group-addon-inline-end\"> <button type=\"button\" class=\"btn btn-ghost btn-icon\" data-combobox-clear aria-label=\"Clear\" hidden><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> <button type=\"button\" class=\"btn btn-ghost btn-icon\" data-combobox-toggle tabindex=\"-1\" aria-label=\"Show suggestions\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m7 15 5 5 5-5\"/><path d=\"m7 9 5-5 5 5\"/></svg></button> </span> </div> <div class=\"combobox-content\" role=\"listbox\" hidden> <div class=\"combobox-item\" role=\"option\" value=\"astro\" aria-selected=\"false\">Astro<span class=\"combobox-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span></div> <div class=\"combobox-item\" role=\"option\" value=\"next\" aria-selected=\"false\" data-keywords=\"react vercel\">Next.js<span cla"
        },
        {
          "id": "input-group",
          "title": "Input group",
          "text": "The input sits in an ordinary input group, so any addon works beside it: an icon, text, a button. Here an icon leads, and each option carries its region as a .combobox-item-description. html <div class=\"combobox w-72\" data-combobox data-autohighlight> <div class=\"input-group\"> <input class=\"input\" data-combobox-input placeholder=\"Search timezones\" aria-label=\"Timezone\" /> <span class=\"input-group-addon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"20\" height=\"14\" x=\"2\" y=\"3\" rx=\"2\"/><path d=\"M8 21h8M12 17v4\"/></svg></span> <span class=\"input-group-addon input-group-addon-inline-end\"> <button type=\"button\" class=\"btn btn-ghost btn-icon\" data-combobox-clear aria-label=\"Clear\" hidden><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> </span> </div> <div class=\"combobox-content\" role=\"listbox\" aria-label=\"Timezones\" hidden> <div class=\"combobox-item\" role=\"option\" value=\"america/new_york\">New York (GMT-5)<span class=\"combobox-item-description\">Americas</span></div> <div class=\"combobox-item\" role=\"option\" value=\"america/chicago\">Chicago (GMT-6)<span class=\"combobox-item-description\">Americas</span></div> <div class=\"combobox-item\" role=\"option\" value=\"america/los_angeles\">Los Angeles (GMT-8)<span class=\"combobox-item-description\">Americas</span></div> <div class=\"combobox-item\" role=\"option\" value=\"europe/london\">London (GMT+0)<span class=\"combobox-item-description\">Europe</span></div> <div class=\"combobox-item\" role=\"option\" value=\"europe/paris\">Paris (GMT+1)<span class=\"combobox-item-description\">Europe</span></div> <div class=\"combobox-item\" role=\"option\" value=\"europe/athens\">At"
        },
        {
          "id": "server",
          "title": "Searching on the server",
          "text": "For a list too long to send with the page, let htmx fetch the options as the user types and swap them into the list. data-filter=\"none\" turns the browser-side filter off, as the server already did it. The behaviour watches the list, so the new options are highlighted, keyboard-ready and marked if already picked — picks are remembered by value, so they survive the list changing under them, chips included. html <div class=\"combobox w-64\" data-combobox data-filter=\"none\" data-autohighlight> <div class=\"input-group\"> <input class=\"input\" data-combobox-input name=\"q\" placeholder=\"Search countries\" aria-label=\"Country\" hx-get=\"/api/countries\" hx-trigger=\"input changed delay:200ms\" hx-target=\"next [role=listbox]\" hx-indicator=\"#cb-country-spinner\" /> <span class=\"input-group-addon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg></span> <span class=\"input-group-addon input-group-addon-inline-end\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" id=\"cb-country-spinner\" class=\"spinner htmx-indicator\" role=\"status\" aria-label=\"Searching\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg></span> </div> <div class=\"combobox-content\" role=\"listbox\" aria-label=\"Countries\" hidden> <div class=\"combobox-item\" role=\"option\" value=\"argentina\">Argentina</div> <div class=\"combobox-item\" role=\"option\" value=\"australia\">Australia</div> <div class=\"combobox-item\" role=\"option\" value=\"austria\">Austria</div> <div class=\"combobox-item\" role=\"option\" value=\"belgium\">Belgium</div> </div> <input type=\"hidden\" name=\"country\" data-combobox-value /> </div> The endpoint returns plain option markup (<div class=\"combobox-item\" role=\"option\" value=\"…\">)"
        },
        {
          "id": "htmx",
          "title": "Submitting with htmx",
          "text": "Every change fires a bubbling combobox:change with detail.value and detail.label (the first pick) and detail.values / detail.labels (all of them). Inside a form, trigger on it and htmx posts the hidden [data-combobox-value] fields with the rest: html <form class=\"w-80\" onsubmit=\"return false\" hx-post=\"/api/echo\" hx-trigger=\"combobox:change\" hx-target=\"#cb-result\"> <div class=\"combobox w-80\" data-combobox data-multiple> <div class=\"combobox-chips\" data-combobox-chips> <input class=\"combobox-chips-input\" id=\"cb-htmx\" data-combobox-input placeholder=\"Add frameworks\" aria-label=\"Frameworks\" /> <button type=\"button\" class=\"btn btn-ghost btn-icon\" data-combobox-clear aria-label=\"Clear\" hidden><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> </div> <div class=\"combobox-content\" role=\"listbox\" aria-label=\"Frameworks\" hidden> <div class=\"combobox-item\" role=\"option\" value=\"astro\" aria-selected=\"false\">Astro<span class=\"combobox-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span></div> <div class=\"combobox-item\" role=\"option\" value=\"next\" aria-selected=\"false\" data-keywords=\"react vercel\">Next.js<span class=\"combobox-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span></div> <div class=\"combobox-item\" role=\"option\" value=\"nuxt\" aria-selected=\"false\" data-keywords=\"vue\">Nuxt<span class=\"combobox-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" h"
        },
        {
          "id": "alignment",
          "title": "Alignment",
          "text": "The list opens below the input, as wide as the combobox. .combobox-content-up opens it above, and .combobox-content-end aligns a list wider than the input (a w-80 utility) to the right edge."
        },
        {
          "id": "macro",
          "title": "Nunjucks macro",
          "text": "The combobox macro writes the input group or chip box, the options, groups and separators, and the hidden field. Options are strings, {value, label, keywords, disabled} objects, or groups {label, options}. page.html {% from \"components/combobox/combobox.html\" import combobox %} <div class=\"field\"> <label class=\"label\" for=\"framework\">Framework</label> {{ combobox(\"framework\", json(\"data/frameworks.json\"), name=\"framework\", autohighlight=true) }} </div> {{ combobox(\"tags\", [\"Bug\", \"Feature\", \"Docs\"], multiple=true, value=[\"Bug\"], name=\"tags\", label=\"Tags\") }} {{ combobox(\"tz\", [{label: \"Europe\", options: [...]}, {label: \"Asia\", options: [...]}], label=\"Timezone\") }}"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class / attribute Description .combobox Positioning wrapper. Add data-combobox. .combobox-content The listbox. Opens below, as wide as the combobox. .combobox-content-up / -end Open above / align to the right edge. .combobox-item One option (role=\"option\", value). data-keywords: extra words to match. .combobox-check Icon in an option, shown while it is picked. .combobox-item-description Muted text at the end of an option. .combobox-group / .combobox-label A group of options (role=\"group\") and its heading. .combobox-separator A line between groups (role=\"separator\"); hidden when a filter empties one side. .combobox-empty Shown when nothing matches. Add data-combobox-empty. .combobox-chips / .combobox-chips-input Multiple: the box that looks like an input, and the input inside it. .combobox-chip / .combobox-chip-remove A pick and its remove button, built by the behaviour. [data-combobox-input] The text input (becomes role=\"combobox\"). [data-combobox-value] Hidden field with the value; one per value with data-multiple. [data-combobox-clear] / [data-combobox-toggle] Clear button (hides itself when empty) / open-close button. data-multiple / data-autohighlight Several picks as chips / highlight the first match as you type. data-filter=\"none\" No browser-side filtering, for lists the server filters. combobox:change Event with detail { value, label, values, labels }."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/field",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/field",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/field.md",
      "title": "Field",
      "description": "Labels, descriptions, help and error messages for form controls, and layouts for whole forms: stacked, inline, responsive and grid.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "anatomy",
          "text": "Anatomy"
        },
        {
          "level": 2,
          "id": "description",
          "text": "Description and help"
        },
        {
          "level": 2,
          "id": "required",
          "text": "Required"
        },
        {
          "level": 2,
          "id": "messages",
          "text": "Messages"
        },
        {
          "level": 2,
          "id": "disabled",
          "text": "Disabled"
        },
        {
          "level": 2,
          "id": "horizontal",
          "text": "Inline"
        },
        {
          "level": 3,
          "id": "choice",
          "text": "Checkboxes, radios and switches"
        },
        {
          "level": 2,
          "id": "responsive",
          "text": "Responsive"
        },
        {
          "level": 2,
          "id": "grid",
          "text": "Grid"
        },
        {
          "level": 2,
          "id": "groups",
          "text": "With input and button groups"
        },
        {
          "level": 2,
          "id": "separator",
          "text": "Separator"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <form class=\"field-group max-w-md\" onsubmit=\"return false\"> <div class=\"field\"> <label class=\"field-label\" for=\"field-email\">Email</label> <input class=\"input\" id=\"field-email\" type=\"email\" placeholder=\"you@example.com\" required aria-describedby=\"field-email-help\" /> <p class=\"field-description\" id=\"field-email-help\">We'll only use this to send receipts.</p> </div> <div class=\"field\"> <label class=\"field-label\" for=\"field-bio\">Bio <span class=\"badge badge-outline\">Optional</span></label> <textarea class=\"textarea\" id=\"field-bio\" placeholder=\"A sentence or two about you\"></textarea> </div> <button class=\"btn btn-primary w-fit\">Save</button> </form>"
        },
        {
          "id": "anatomy",
          "title": "Anatomy",
          "text": ".field wraps one control with its .field-label and any .field-description or message, stacked. Link the description and messages to the control with aria-describedby, so screen readers read them with it. Stack fields in a .field-group, and group related ones in a <fieldset class=\"fieldset\"> with a <legend class=\"field-legend\">."
        },
        {
          "id": "description",
          "title": "Description and help",
          "text": "A .field-description can sit above or below the control. Links inside it are underlined. html <div class=\"field max-w-sm\"> <label class=\"field-label\" for=\"field-key\">API key</label> <p class=\"field-description\" id=\"field-key-help-top\">Create one in your account settings.</p> <input class=\"input\" id=\"field-key\" placeholder=\"sk_live_…\" aria-describedby=\"field-key-help-top field-key-help\" /> <p class=\"field-description\" id=\"field-key-help\">Keep it secret. <a href=\"#rotate\">How to rotate keys</a>.</p> </div>"
        },
        {
          "id": "required",
          "title": "Required",
          "text": "The label gets a red asterisk when the field holds a required control. For a control that can't carry required, add .field-label-required to the label instead. html <div class=\"field-group max-w-sm\"> <div class=\"field\"> <label class=\"field-label\" for=\"field-required\">Full name</label> <input class=\"input\" id=\"field-required\" required /> </div> <div class=\"field\"> <span class=\"field-label field-label-required\" id=\"field-plan-label\">Plan</span> <div class=\"toggle-group btn-group\" role=\"group\" aria-labelledby=\"field-plan-label\" data-toggle-group=\"single\" data-toggle-group-required> <button type=\"button\" class=\"btn btn-outline\" aria-pressed=\"true\" value=\"free\">Free</button> <button type=\"button\" class=\"btn btn-outline\" aria-pressed=\"false\" value=\"pro\">Pro</button> </div> </div> </div>"
        },
        {
          "id": "messages",
          "title": "Messages",
          "text": ".field-error, .field-warning and .field-success colour a message and size a leading icon. A field with an aria-invalid=\"true\" control turns its label red. Several errors can go in a list. html <div class=\"field max-w-sm\"> <label class=\"field-label\" for=\"field-password\">Password</label> <input class=\"input\" id=\"field-password\" type=\"password\" value=\"abc\" aria-invalid=\"true\" aria-describedby=\"field-password-error\" /> <div class=\"field-error\" id=\"field-password-error\"> <ul> <li>At least 8 characters</li> <li>At least one number</li> </ul> </div> </div>"
        },
        {
          "id": "disabled",
          "title": "Disabled",
          "text": "When the control is disabled, the label and description fade with it. html <div class=\"field max-w-sm\"> <label class=\"field-label\" for=\"field-disabled\">Workspace URL</label> <input class=\"input\" id=\"field-disabled\" value=\"acme.example.com\" disabled aria-describedby=\"field-disabled-help\" /> <p class=\"field-description\" id=\"field-disabled-help\">Contact an admin to change it.</p> </div>"
        },
        {
          "id": "horizontal",
          "title": "Inline",
          "text": ".field-horizontal puts the label beside the control. Inputs, selects and groups take the remaining width. Give labels a fixed width (w-24) to line them up. html <div class=\"field-group max-w-md\"> <div class=\"field field-horizontal\"> <label class=\"field-label w-24 shrink-0\" for=\"field-first\">First name</label> <input class=\"input\" id=\"field-first\" /> </div> <div class=\"field field-horizontal\"> <label class=\"field-label w-24 shrink-0\" id=\"field-role-label\" for=\"field-role\">Role</label> <div class=\"select\" data-select> <button type=\"button\" id=\"field-role\" class=\"select-trigger\" data-select-trigger aria-haspopup=\"listbox\" aria-expanded=\"false\" aria-labelledby=\"field-role-label field-role\"> <span class=\"select-value\" data-placeholder=\"Select…\">Developer</span> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg> </button> <div class=\"select-content\" id=\"field-role-listbox\" role=\"listbox\" aria-labelledby=\"field-role-label\" hidden> <span class=\"select-item\" role=\"option\" value=\"Developer\" aria-selected=\"true\">Developer<span class=\"select-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span></span> <span class=\"select-item\" role=\"option\" value=\"Designer\" aria-selected=\"false\">Designer<span class=\"select-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span></span> </div> <input type=\"hidden\" name=\"role\" value=\"Developer\" data-select-input /></div> </div> </div>"
        },
        {
          "id": "choice",
          "title": "Checkboxes, radios and switches",
          "text": "Put the control first. Wrap the label and a description in .field-content to stack them beside it. The checkbox and radio group components style native inputs, and the switch is a checkbox that reads as a preference. A bare .radio in a horizontal field is the smallest useful option, like the ones here. html <fieldset class=\"fieldset max-w-md\"> <legend class=\"field-legend\">Notifications</legend> <div class=\"field field-horizontal\"> <input class=\"checkbox\" type=\"checkbox\" id=\"field-mentions\" checked /> <div class=\"field-content\"> <label class=\"field-label\" for=\"field-mentions\">Mentions</label> <p class=\"field-description\">When someone @mentions you.</p> </div> </div> <div class=\"field field-horizontal\"> <input class=\"checkbox\" type=\"checkbox\" id=\"field-digest\" /> <label class=\"field-label\" for=\"field-digest\">Weekly digest</label> </div> <div class=\"field field-horizontal\"> <input class=\"radio\" type=\"radio\" name=\"field-cadence\" id=\"field-daily\" checked /> <label class=\"field-label\" for=\"field-daily\">Daily email</label> </div> <div class=\"field field-horizontal\"> <input class=\"switch\" type=\"checkbox\" role=\"switch\" id=\"field-push\" checked /> <div class=\"field-content\"> <label class=\"field-label\" for=\"field-push\">Push notifications</label> <p class=\"field-description\">Applies as soon as you flip it.</p> </div> </div> </fieldset>"
        },
        {
          "id": "responsive",
          "title": "Responsive",
          "text": ".field-responsive stacks the label above the control in a narrow container and puts them side by side once the surrounding .field-group is at least 28rem wide. It reacts to the group's width, not the viewport's, so it also works in sidebars and dialogs. Resize the window to see it. html <div class=\"field-group max-w-2xl\"> <div class=\"field field-responsive\"> <div class=\"field-content\"> <label class=\"field-label\" for=\"field-r-name\">Display name</label> <p class=\"field-description\">Shown on your profile.</p> </div> <input class=\"input\" id=\"field-r-name\" placeholder=\"Ada\" /> </div> <div class=\"field field-responsive\"> <div class=\"field-content\"> <label class=\"field-label\" for=\"field-r-email\">Email</label> <p class=\"field-description\">For sign-in and receipts.</p> </div> <input class=\"input\" id=\"field-r-email\" type=\"email\" required /> </div> </div>"
        },
        {
          "id": "grid",
          "title": "Grid",
          "text": ".field-grid lays fields out in two columns from the sm breakpoint. Span a field across both with sm:col-span-2. html <form class=\"fieldset max-w-lg\" onsubmit=\"return false\"> <legend class=\"field-legend\">Shipping address</legend> <p class=\"field-description\">Where should we send your order?</p> <div class=\"field-grid\"> <div class=\"field\"> <label class=\"field-label\" for=\"field-g-first\">First name</label> <input class=\"input\" id=\"field-g-first\" required /> </div> <div class=\"field\"> <label class=\"field-label\" for=\"field-g-last\">Last name</label> <input class=\"input\" id=\"field-g-last\" required /> </div> <div class=\"field sm:col-span-2\"> <label class=\"field-label\" for=\"field-g-street\">Street</label> <input class=\"input\" id=\"field-g-street\" /> </div> <div class=\"field\"> <label class=\"field-label\" for=\"field-g-city\">City</label> <input class=\"input\" id=\"field-g-city\" /> </div> <div class=\"field\"> <label class=\"field-label\" id=\"field-g-country-label\" for=\"field-g-country\">Country</label> <div class=\"select w-full\" data-select> <button type=\"button\" id=\"field-g-country\" class=\"select-trigger\" data-select-trigger aria-haspopup=\"listbox\" aria-expanded=\"false\" aria-labelledby=\"field-g-country-label field-g-country\"> <span class=\"select-value\" data-placeholder=\"Select…\">Australia</span> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg> </button> <div class=\"select-content\" id=\"field-g-country-listbox\" role=\"listbox\" aria-labelledby=\"field-g-country-label\" hidden> <span class=\"select-item\" role=\"option\" value=\"Australia\" aria-selected=\"true\">Australia<span class=\"select-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\""
        },
        {
          "id": "groups",
          "title": "With input and button groups",
          "text": "html <div class=\"field-group max-w-sm\"> <div class=\"field\"> <label class=\"field-label\" for=\"field-site\">Website</label> <div class=\"input-group\"> <input class=\"input\" id=\"field-site\" placeholder=\"example.com\" aria-describedby=\"field-site-help\" /> <span class=\"input-group-addon\">https://</span> </div> <p class=\"field-description\" id=\"field-site-help\">Your public homepage.</p> </div> <div class=\"field\"> <label class=\"field-label\" for=\"field-invite\">Invite by email</label> <div class=\"btn-group w-full\" role=\"group\" aria-label=\"Invite\"> <input class=\"input\" id=\"field-invite\" type=\"email\" placeholder=\"teammate@example.com\" /> <button class=\"btn btn-outline\">Send invite</button> </div> </div> </div>"
        },
        {
          "id": "separator",
          "title": "Separator",
          "text": ".field-separator divides sections of a form, with optional text. html <div class=\"field-group max-w-sm\"> <button class=\"btn btn-outline w-full\">Continue with GitHub</button> <div class=\"field-separator\">Or continue with</div> <div class=\"field\"> <label class=\"field-label\" for=\"field-sep-email\">Email</label> <input class=\"input\" id=\"field-sep-email\" type=\"email\" /> </div> </div>"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .field One control with its label and messages, stacked. .field-horizontal Label beside the control. .field-responsive Stacked, then side by side once the .field-group is 28rem wide. .field-label Label text. Gets an asterisk when the field holds a required control. .field-label-required Forces the asterisk. .field-content Stacks a label and description beside a control in horizontal fields. .field-description Muted help text, above or below the control. .field-error / -warning / -success Coloured message with an optional leading icon or list. .field-group Vertical stack of fields; the container .field-responsive measures. .field-grid Two columns of fields from the sm breakpoint. .fieldset / .field-legend Group of related fields with a heading. .field-separator Horizontal rule with optional centred text."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/input",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/input",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/input.md",
      "title": "Input",
      "description": "Text inputs, textareas, selects and file pickers, with focus, disabled, error, warning and success states.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "usage",
          "text": "Usage"
        },
        {
          "level": 2,
          "id": "types",
          "text": "Types"
        },
        {
          "level": 2,
          "id": "file",
          "text": "File"
        },
        {
          "level": 2,
          "id": "textarea",
          "text": "Textarea"
        },
        {
          "level": 2,
          "id": "select",
          "text": "Select"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Sizes"
        },
        {
          "level": 2,
          "id": "states",
          "text": "States"
        },
        {
          "level": 3,
          "id": "disabled",
          "text": "Disabled"
        },
        {
          "level": 3,
          "id": "required",
          "text": "Required"
        },
        {
          "level": 3,
          "id": "invalid",
          "text": "Error, warning, success"
        },
        {
          "level": 3,
          "id": "validate-htmx",
          "text": "Validating with htmx"
        },
        {
          "level": 2,
          "id": "combining",
          "text": "Combining"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <input class=\"input max-w-sm\" type=\"email\" placeholder=\"you@example.com\" aria-label=\"Email\" />"
        },
        {
          "id": "usage",
          "title": "Usage",
          "text": "Add .input to any text-like <input>. It fills its container's width; constrain it with a max-w-* utility or a wrapper. Every input needs a label: wrap it in a field with a .field-label, or give it an aria-label. html <div class=\"field max-w-sm\"> <label class=\"field-label\" for=\"input-name\">Name</label> <input class=\"input\" id=\"input-name\" placeholder=\"Ada Lovelace\" /> </div>"
        },
        {
          "id": "types",
          "title": "Types",
          "text": "html <div class=\"grid w-full max-w-sm gap-3\"> <input class=\"input\" type=\"password\" value=\"hunter22\" aria-label=\"Password\" /> <input class=\"input\" type=\"number\" value=\"42\" aria-label=\"Quantity\" /> <input class=\"input\" type=\"date\" aria-label=\"Date\" /> <input class=\"input\" type=\"search\" placeholder=\"Search…\" aria-label=\"Search\" /> </div>"
        },
        {
          "id": "file",
          "title": "File",
          "text": "File inputs keep the native picker, with its button restyled. multiple and accept work as usual. html <div class=\"field max-w-sm\"> <label class=\"field-label\" for=\"input-avatar\">Picture</label> <input class=\"input\" id=\"input-avatar\" type=\"file\" accept=\"image/*\" /> <p class=\"field-description\">PNG or JPG, up to 2 MB.</p> </div> <div class=\"field max-w-sm\"> <label class=\"field-label\" for=\"input-docs\">Documents</label> <input class=\"input\" id=\"input-docs\" type=\"file\" multiple /> </div>"
        },
        {
          "id": "textarea",
          "title": "Textarea",
          "text": ".textarea grows with its content in browsers that support field-sizing. html <textarea class=\"textarea max-w-sm\" placeholder=\"Type your message here.\" aria-label=\"Message\"></textarea>"
        },
        {
          "id": "select",
          "title": "Select",
          "text": ".select styles a native <select>. html <select class=\"select max-w-48\" aria-label=\"Fruit\"> <option>Apple</option> <option>Banana</option> <option>Blueberry</option> </select>"
        },
        {
          "id": "sizes",
          "title": "Sizes",
          "text": ".input-sm and .input-lg match the heights of .btn-sm and .btn-lg. html <div class=\"flex w-full max-w-sm items-center gap-2\"> <input class=\"input input-sm\" placeholder=\"Small\" aria-label=\"Small\" /> <button class=\"btn btn-outline btn-sm\">Go</button> </div> <div class=\"flex w-full max-w-sm items-center gap-2\"> <input class=\"input\" placeholder=\"Default\" aria-label=\"Default\" /> <button class=\"btn btn-outline\">Go</button> </div> <div class=\"flex w-full max-w-sm items-center gap-2\"> <input class=\"input input-lg\" placeholder=\"Large\" aria-label=\"Large\" /> <button class=\"btn btn-outline btn-lg\">Go</button> </div>"
        },
        {
          "id": "states",
          "title": "States",
          "text": ""
        },
        {
          "id": "disabled",
          "title": "Disabled",
          "text": "The disabled attribute fades the input; inside a field, the label fades too. html <div class=\"field max-w-sm\"> <label class=\"field-label\" for=\"input-disabled\">Email</label> <input class=\"input\" id=\"input-disabled\" type=\"email\" placeholder=\"you@example.com\" disabled /> </div>"
        },
        {
          "id": "required",
          "title": "Required",
          "text": "A field holding a required input marks its label with an asterisk automatically. html <div class=\"field max-w-sm\"> <label class=\"field-label\" for=\"input-required\">Username</label> <input class=\"input\" id=\"input-required\" required /> </div>"
        },
        {
          "id": "invalid",
          "title": "Error, warning, success",
          "text": "Set aria-invalid=\"true\" for an error (the border turns red; so does the field's label). Native validation styles the same way once the user has interacted (:user-invalid). For softer states, add .input-warning or .input-success. Pair each with a message from the field classes and link it with aria-describedby. html <div class=\"field-group max-w-sm\"> <div class=\"field\"> <label class=\"field-label\" for=\"input-error\">Email</label> <input class=\"input\" id=\"input-error\" type=\"email\" value=\"ada@\" aria-invalid=\"true\" aria-describedby=\"input-error-msg\" /> <p class=\"field-error\" id=\"input-error-msg\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m15 9-6 6M9 9l6 6\"/></svg> Enter a complete email address.</p> </div> <div class=\"field\"> <label class=\"field-label\" for=\"input-warning\">Password</label> <input class=\"input input-warning\" id=\"input-warning\" type=\"password\" value=\"password\" aria-describedby=\"input-warning-msg\" /> <p class=\"field-warning\" id=\"input-warning-msg\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4M12 17h.01\"/></svg> This password is common. Consider a longer one.</p> </div> <div class=\"field\"> <label class=\"field-label\" for=\"input-success\">Username</label> <input class=\"input input-success\" id=\"input-success\" value=\"ada\" aria-describedby=\"input-success-msg\" /> <p class=\"field-success\" id=\"input-success-msg\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class"
        },
        {
          "id": "validate-htmx",
          "title": "Validating with htmx",
          "text": "The server can return the whole field with its state, swapped in on change. Here the mock server just echoes the value back: html <div class=\"field max-w-sm\"> <label class=\"field-label\" for=\"input-htmx\">Username</label> <input class=\"input\" id=\"input-htmx\" name=\"username\" placeholder=\"Pick a username\" hx-post=\"/api/echo\" hx-trigger=\"change\" hx-target=\"#input-htmx-msg\" /> <p class=\"field-description\" id=\"input-htmx-msg\" aria-live=\"polite\">Checked when you leave the field.</p> </div>"
        },
        {
          "id": "combining",
          "title": "Combining",
          "text": "Join an input to buttons with a button group, put icons, text, badges or buttons inside its border with an input group, and lay out labels, help text and grids of inputs with field. html <div class=\"btn-group w-full max-w-sm\" role=\"group\" aria-label=\"Subscribe\"> <input class=\"input\" type=\"email\" placeholder=\"you@example.com\" aria-label=\"Email\" /> <button class=\"btn btn-outline\">Subscribe</button> </div> <div class=\"input-group max-w-sm\"> <input class=\"input\" placeholder=\"Add tags…\" aria-label=\"Tags\" /> <span class=\"input-group-addon\"><span class=\"badge badge-secondary\">htmx</span><span class=\"badge badge-secondary\">css</span></span> </div>"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class / attribute Description .input Text-like inputs, including type=\"file\". .textarea Multi-line input; grows with its content where supported. .select Native select with a chevron. .input-sm / .input-lg 32px or 44px tall instead of 36px. Also on .select. .input-warning / .input-success Amber or green border and focus ring. aria-invalid=\"true\" Error state: red border and focus ring. :user-invalid looks the same."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/input-group",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/input-group",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/input-group.md",
      "title": "Input group",
      "description": "Icons, text, buttons, menus and spinners inside an input's border, on either side, above or below.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "usage",
          "text": "Usage"
        },
        {
          "level": 2,
          "id": "align",
          "text": "Align"
        },
        {
          "level": 2,
          "id": "icons-text",
          "text": "Icons and text"
        },
        {
          "level": 2,
          "id": "buttons",
          "text": "Buttons"
        },
        {
          "level": 2,
          "id": "badges",
          "text": "Badges"
        },
        {
          "level": 2,
          "id": "dropdown",
          "text": "Dropdown"
        },
        {
          "level": 2,
          "id": "spinner",
          "text": "Spinner"
        },
        {
          "level": 2,
          "id": "textarea",
          "text": "Textarea"
        },
        {
          "level": 2,
          "id": "states",
          "text": "States"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"input-group max-w-sm\"> <input class=\"input\" type=\"search\" placeholder=\"Search…\" aria-label=\"Search\" /> <span class=\"input-group-addon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg></span> <span class=\"input-group-addon input-group-addon-inline-end\">12 results</span> </div>"
        },
        {
          "id": "usage",
          "title": "Usage",
          "text": "Wrap a .input, .textarea or .select in .input-group. The group draws the border and the focus ring; the control drops its own. Add each addon as a .input-group-addon. Put addons after the control in the source. CSS places them, so Tab reaches the control first. States come from the control: aria-invalid=\"true\" turns the whole group red and disabled fades it."
        },
        {
          "id": "align",
          "title": "Align",
          "text": "An addon goes at the inline start by default. .input-group-addon-inline-end puts it at the end, .input-group-addon-block-start above the control and .input-group-addon-block-end below it. html <div class=\"input-group max-w-sm\"> <input class=\"input\" placeholder=\"Inline start\" aria-label=\"Inline start\" /> <span class=\"input-group-addon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg></span> </div> <div class=\"input-group max-w-sm\"> <input class=\"input\" type=\"password\" placeholder=\"Inline end\" aria-label=\"Password\" /> <span class=\"input-group-addon input-group-addon-inline-end\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg></span> </div> <div class=\"input-group max-w-sm\"> <input class=\"input\" id=\"ig-block-start\" placeholder=\"Block start\" /> <label class=\"input-group-addon input-group-addon-block-start\" for=\"ig-block-start\">Full name</label> </div> <div class=\"input-group max-w-sm\"> <input class=\"input\" placeholder=\"Block end\" aria-label=\"Amount\" /> <span class=\"input-group-addon input-group-addon-block-end\"><span class=\"input-group-text\">Billed in USD</span></span> </div>"
        },
        {
          "id": "icons-text",
          "title": "Icons and text",
          "text": "Addons hold icons, text or both. .input-group-text styles plain text inside a busier addon. html <div class=\"input-group max-w-sm\"> <input class=\"input\" type=\"email\" placeholder=\"you@example.com\" aria-label=\"Email\" /> <span class=\"input-group-addon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"20\" height=\"16\" x=\"2\" y=\"4\" rx=\"2\"/><path d=\"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7\"/></svg></span> </div> <div class=\"input-group max-w-sm\"> <input class=\"input\" placeholder=\"example.com\" aria-label=\"Website\" /> <span class=\"input-group-addon\">https://</span> <span class=\"input-group-addon input-group-addon-inline-end\">.com</span> </div> <div class=\"input-group max-w-sm\"> <input class=\"input\" type=\"number\" placeholder=\"0.00\" aria-label=\"Price\" /> <span class=\"input-group-addon\">$</span> <span class=\"input-group-addon input-group-addon-inline-end\">USD</span> </div>"
        },
        {
          "id": "buttons",
          "title": "Buttons",
          "text": "Buttons inside an addon shrink to fit (24px tall). Use any variant; ghost and secondary sit most naturally. html <div class=\"input-group max-w-sm\"> <input class=\"input\" value=\"https://htmx-ui.dev/r/3f9a\" readonly aria-label=\"Share link\" /> <span class=\"input-group-addon input-group-addon-inline-end\"> <button type=\"button\" class=\"btn btn-ghost btn-icon clipboard\" data-clipboard data-clipboard-target=\"input\" aria-label=\"Copy link\"><span class=\"clipboard-idle\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg></span><span class=\"clipboard-done\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span></button> </span> </div> <div class=\"input-group max-w-sm\"> <input class=\"input\" type=\"search\" placeholder=\"Search the docs…\" aria-label=\"Search\" /> <span class=\"input-group-addon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg></span> <span class=\"input-group-addon input-group-addon-inline-end\"> <button class=\"btn btn-secondary\">Search</button> </span> </div>"
        },
        {
          "id": "badges",
          "title": "Badges",
          "text": "html <div class=\"input-group max-w-sm\"> <input class=\"input\" placeholder=\"Add a label…\" aria-label=\"Labels\" /> <span class=\"input-group-addon\"> <span class=\"badge badge-info\">bug</span> <span class=\"badge badge-secondary\">ui</span> </span> </div>"
        },
        {
          "id": "dropdown",
          "title": "Dropdown",
          "text": "A dropdown in an addon, for picking a unit or scope. html <div class=\"input-group max-w-sm\"> <input class=\"input\" placeholder=\"Search…\" aria-label=\"Search\" /> <span class=\"input-group-addon input-group-addon-inline-end\"> <span class=\"dropdown\" data-dropdown> <button class=\"btn btn-ghost\" data-dropdown-trigger aria-haspopup=\"menu\" aria-expanded=\"false\">All files <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg></button> <span class=\"dropdown-menu dropdown-menu-end\" role=\"menu\" hidden> <button class=\"dropdown-item\" role=\"menuitem\">All files</button> <button class=\"dropdown-item\" role=\"menuitem\">Pages</button> <button class=\"dropdown-item\" role=\"menuitem\">Components</button> </span> </span> </span> </div>"
        },
        {
          "id": "spinner",
          "title": "Spinner",
          "text": "A spinner shows work in progress. With .htmx-indicator it appears only while a request runs: type below to search the mock server. html <div class=\"input-group max-w-sm\"> <input class=\"input\" type=\"search\" placeholder=\"Saving…\" disabled aria-label=\"Saving\" /> <span class=\"input-group-addon input-group-addon-inline-end\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner\" role=\"status\" aria-label=\"Loading\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg></span> </div> <div class=\"input-group max-w-sm\"> <input class=\"input\" type=\"search\" name=\"q\" placeholder=\"Search…\" aria-label=\"Search\" hx-get=\"/api/echo\" hx-trigger=\"input changed delay:300ms\" hx-target=\"#ig-search-result\" hx-indicator=\"#ig-search-spinner\" /> <span class=\"input-group-addon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg></span> <span class=\"input-group-addon input-group-addon-inline-end\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" id=\"ig-search-spinner\" class=\"spinner htmx-indicator\" role=\"status\" aria-label=\"Searching\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg></span> </div> <p id=\"ig-search-result\" class=\"muted text-sm\" aria-live=\"polite\"></p>"
        },
        {
          "id": "textarea",
          "title": "Textarea",
          "text": "Block addons above or below a textarea make a compose box. A border-t or border-b utility on the addon draws a divider. html <div class=\"input-group max-w-md\"> <textarea class=\"textarea\" placeholder=\"Ask, search or chat…\" aria-label=\"Prompt\"></textarea> <span class=\"input-group-addon input-group-addon-block-start border-b\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4M10 9H8M16 13H8M16 17H8\"/></svg> <span class=\"input-group-text\">button.css</span></span> <span class=\"input-group-addon input-group-addon-block-end\"> <button class=\"btn btn-outline btn-icon btn-rounded\" aria-label=\"Attach\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5v14\"/></svg></button> <span class=\"input-group-text ms-auto\">52% used</span> <button class=\"btn btn-primary btn-icon btn-rounded\" aria-label=\"Send\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z\"/><path d=\"m21.854 2.147-10.94 10.939\"/></svg></button> </span> </div>"
        },
        {
          "id": "states",
          "title": "States",
          "text": "html <div class=\"field max-w-sm\"> <label class=\"field-label\" for=\"ig-invalid\">Username</label> <div class=\"input-group\"> <input class=\"input\" id=\"ig-invalid\" value=\"ada lovelace\" aria-invalid=\"true\" aria-describedby=\"ig-invalid-msg\" /> <span class=\"input-group-addon\">@</span> </div> <p class=\"field-error\" id=\"ig-invalid-msg\">Usernames can't contain spaces.</p> </div> <div class=\"input-group max-w-sm\"> <input class=\"input\" placeholder=\"Disabled\" disabled aria-label=\"Disabled\" /> <span class=\"input-group-addon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"20\" height=\"16\" x=\"2\" y=\"4\" rx=\"2\"/><path d=\"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7\"/></svg></span> </div>"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .input-group Border and focus ring around a control and its addons. .input-group-addon Addon at the inline start. Holds icons, text, badges, buttons, menus or spinners. .input-group-addon-inline-end Addon at the end of the row. .input-group-addon-block-start Full-width addon above the control. .input-group-addon-block-end Full-width addon below the control. .input-group-text Plain muted text inside an addon."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/input-otp",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/input-otp",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/input-otp.md",
      "title": "Input OTP",
      "description": "An accessible one-time password input: a row of slots over one real input, so typing, pasting and SMS autofill just work.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "pin",
          "text": "PIN and authenticator codes"
        },
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "pattern",
          "text": "Pattern"
        },
        {
          "level": 2,
          "id": "separator",
          "text": "Groups and separators"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Sizes and states"
        },
        {
          "level": 2,
          "id": "keyboard",
          "text": "Typing, pasting and the keyboard"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx"
        },
        {
          "level": 2,
          "id": "macro",
          "text": "Nunjucks macro"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"input-otp\" data-input-otp> <input class=\"input-otp-input\" data-input-otp-input name=\"code\" aria-label=\"Verification code\" /> <div class=\"input-otp-group\" aria-hidden=\"true\"> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> </div> <div class=\"input-otp-separator\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14\"/></svg></div> <div class=\"input-otp-group\" aria-hidden=\"true\"> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> </div> </div>"
        },
        {
          "id": "pin",
          "title": "PIN and authenticator codes",
          "text": "A short numeric code, such as the 4-digit code from an authenticator app or a PIN the user reads off a screen, is the same component with fewer slots. Give each slot its own box with .input-otp-spaced and leave the digits visible, so the user can check what they typed against the code in front of them. Next to it, the usual 6-digit code from a text message or email: html <div class=\"field w-fit\"> <label class=\"label\" for=\"otp-authenticator\">Authenticator code</label> <div class=\"input-otp input-otp-spaced input-otp-lg\" data-input-otp> <input class=\"input-otp-input\" data-input-otp-input id=\"otp-authenticator\" name=\"pin\" aria-describedby=\"otp-authenticator-help\" /> <div class=\"input-otp-group\" aria-hidden=\"true\"> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> </div> </div> <p class=\"field-description\" id=\"otp-authenticator-help\">The 4-digit code shown in your authenticator app.</p> </div> <div class=\"field w-fit\"> <label class=\"label\" for=\"otp-six\">Verification code</label> <div class=\"input-otp input-otp-spaced input-otp-lg\" data-input-otp> <input class=\"input-otp-input\" data-input-otp-input id=\"otp-six\" name=\"code\" aria-describedby=\"otp-six-help\" /> <div class=\"input-otp-group\" aria-hidden=\"true\"> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> </div> <div class=\"input-otp-separator\" aria-hidden=\"true\"></div> <div class=\"input-otp-group\" aria-hidden=\"true\"> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> </div> </div> <p class=\"field-description\" id=\"otp-six-help\">The 6-digit code we sent to your phone.</p> </div> With the macro: page.html {{ input_otp(\"pin\", length=4, spaced=true, size=\"lg\", label=\"Authenticator code\") }} {{ input_otp(\"code\", groups=[3, 3], spaced=true, size=\"lg\", label=\"Verification code\") }} Both open the number pad on phones. Add data-mask on"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "Wrap everything in .input-otp with data-input-otp. Inside goes one real <input class=\"input-otp-input\" data-input-otp-input> with the name the form submits and an aria-label (or a <label for>). It lies transparent over the slots and takes every click and key. Then one empty .input-otp-slot per character, in .input-otp-groups, with an optional .input-otp-separator between groups. Mark the groups and separators aria-hidden=\"true\": screen readers read the input, not the boxes. The number of slots is the length of the code: the behaviour sets the input's maxlength to match. Because the value lives in one ordinary input, everything a browser does for inputs keeps working: pasting a whole code, the code from a text message offered above the keyboard (autocomplete=\"one-time-code\", set for you), password managers, undo, form submission and validation. The behaviour only filters what goes in and paints the slots."
        },
        {
          "id": "pattern",
          "title": "Pattern",
          "text": "data-pattern on the wrapper decides which characters are allowed; anything else is dropped as it is typed or pasted, so \"123-456\" or \" 123 456 \" pasted from an email becomes 123456. data-pattern Allows Mobile keyboard digits (default) 0–9 Number pad (inputmode=\"numeric\") alphanumeric A–Z, a–z, 0–9 Text alpha A–Z, a–z Text [0-9a-f] Any regular-expression character class Text data-case=\"upper\" (or \"lower\") converts letters as they are typed and asks phone keyboards for capitals. The input also gets a pattern attribute for the full code ([0-9]{6}), so a required input with a partial code fails native form validation. Set inputmode, autocomplete or pattern yourself to override them. html <div class=\"field w-fit\"> <label class=\"label\" for=\"otp-pin\">Masked PIN (digits only)</label> <div class=\"input-otp input-otp-spaced\" data-input-otp data-mask> <input class=\"input-otp-input\" data-input-otp-input id=\"otp-pin\" name=\"pin\" /> <div class=\"input-otp-group\" aria-hidden=\"true\"> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> </div> </div> </div> <div class=\"field w-fit\"> <label class=\"label\" for=\"otp-invite\">Invite code (letters and digits)</label> <div class=\"input-otp\" data-input-otp data-pattern=\"alphanumeric\" data-case=\"upper\"> <input class=\"input-otp-input\" data-input-otp-input id=\"otp-invite\" name=\"invite\" /> <div class=\"input-otp-group\" aria-hidden=\"true\"> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> </div> <div class=\"input-otp-separator\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14\"/></svg></div> <div class=\"input-otp-group\" aria-hidden=\"true\"> <div class=\"input-otp-slot\"></div> <di"
        },
        {
          "id": "separator",
          "title": "Groups and separators",
          "text": "Split the slots into as many groups as the code reads in. A separator holds an icon or text; left empty it draws a short dash. html <div class=\"input-otp\" data-input-otp> <input class=\"input-otp-input\" data-input-otp-input name=\"otp-sep-a\" aria-label=\"Code in pairs\" /> <div class=\"input-otp-group\" aria-hidden=\"true\"> <div class=\"input-otp-slot\"></div><div class=\"input-otp-slot\"></div> </div> <div class=\"input-otp-separator\" aria-hidden=\"true\"></div> <div class=\"input-otp-group\" aria-hidden=\"true\"> <div class=\"input-otp-slot\"></div><div class=\"input-otp-slot\"></div> </div> <div class=\"input-otp-separator\" aria-hidden=\"true\"></div> <div class=\"input-otp-group\" aria-hidden=\"true\"> <div class=\"input-otp-slot\"></div><div class=\"input-otp-slot\"></div> </div> </div> <div class=\"input-otp\" data-input-otp> <input class=\"input-otp-input\" data-input-otp-input name=\"otp-sep-b\" aria-label=\"Code with a dot\" /> <div class=\"input-otp-group\" aria-hidden=\"true\"> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> </div> <div class=\"input-otp-separator\" aria-hidden=\"true\">·</div> <div class=\"input-otp-group\" aria-hidden=\"true\"> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> </div> </div>"
        },
        {
          "id": "sizes",
          "title": "Sizes and states",
          "text": ".input-otp-sm and .input-otp-lg match .input-sm and .input-lg. aria-invalid=\"true\" on the input (or a required code left unfinished, :user-invalid) turns the slots red, disabled fades them, and .input-otp-warning / .input-otp-success on the wrapper colour them. Pair them with field messages: html <div class=\"input-otp input-otp-sm\" data-input-otp> <input class=\"input-otp-input\" data-input-otp-input name=\"otp-sm\" aria-label=\"Small\" /> <div class=\"input-otp-group\" aria-hidden=\"true\"> <div class=\"input-otp-slot\"></div><div class=\"input-otp-slot\"></div><div class=\"input-otp-slot\"></div> </div> <div class=\"input-otp-separator\" aria-hidden=\"true\"></div> <div class=\"input-otp-group\" aria-hidden=\"true\"> <div class=\"input-otp-slot\"></div><div class=\"input-otp-slot\"></div><div class=\"input-otp-slot\"></div> </div> </div> <div class=\"input-otp input-otp-lg\" data-input-otp> <input class=\"input-otp-input\" data-input-otp-input name=\"otp-lg\" aria-label=\"Large\" /> <div class=\"input-otp-group\" aria-hidden=\"true\"> <div class=\"input-otp-slot\"></div><div class=\"input-otp-slot\"></div><div class=\"input-otp-slot\"></div> </div> <div class=\"input-otp-separator\" aria-hidden=\"true\"></div> <div class=\"input-otp-group\" aria-hidden=\"true\"> <div class=\"input-otp-slot\"></div><div class=\"input-otp-slot\"></div><div class=\"input-otp-slot\"></div> </div> </div> <div class=\"field w-fit\"> <label class=\"label\" for=\"otp-invalid\">Verification code</label> <div class=\"input-otp\" data-input-otp> <input class=\"input-otp-input\" data-input-otp-input id=\"otp-invalid\" name=\"code\" value=\"123\" aria-invalid=\"true\" aria-describedby=\"otp-invalid-error\" /> <div class=\"input-otp-group\" aria-hidden=\"true\"> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> <div class=\"input-otp-slot\"></div> </div> </div> <p class=\"field-error\" id=\"otp-invalid-error\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" h"
        },
        {
          "id": "keyboard",
          "title": "Typing, pasting and the keyboard",
          "text": "Focus puts the caret after the last character; the active slot shows a blinking caret. Backspace deletes backwards. ← → Home End move between slots, and clicking a filled slot selects it: typing then replaces that character. Pasting a full code replaces the whole value, even when the input is already full; a shorter paste goes in at the caret. On phones, the digits pattern opens the number pad, and the code from a text message is offered above the keyboard."
        },
        {
          "id": "htmx",
          "title": "With htmx",
          "text": "Filling the last slot fires a bubbling input-otp:complete with detail.value. Trigger a request on it to check the code as soon as it's entered, or add data-submit to submit the input's form: html <form class=\"flex flex-col items-center gap-3\" hx-post=\"/api/echo\" hx-trigger=\"input-otp:complete\" hx-target=\"#otp-result\"> <div class=\"input-otp\" data-input-otp> <input class=\"input-otp-input\" data-input-otp-input name=\"code\" aria-label=\"Verification code\" /> <div class=\"input-otp-group\" aria-hidden=\"true\"> <div class=\"input-otp-slot\"></div><div class=\"input-otp-slot\"></div><div class=\"input-otp-slot\"></div> </div> <div class=\"input-otp-separator\" aria-hidden=\"true\"></div> <div class=\"input-otp-group\" aria-hidden=\"true\"> <div class=\"input-otp-slot\"></div><div class=\"input-otp-slot\"></div><div class=\"input-otp-slot\"></div> </div> </div> <p id=\"otp-result\" class=\"muted\" aria-live=\"polite\">Enter any six digits.</p> </form>"
        },
        {
          "id": "macro",
          "title": "Nunjucks macro",
          "text": "The input_otp macro writes the input, groups, slots and separators from a few options. page.html {% from \"components/input-otp/input-otp.html\" import input_otp %} {{ input_otp(\"code\", groups=[3, 3], label=\"Verification code\", required=true) }} {{ input_otp(\"invite\", length=8, groups=[4, 4], pattern=\"alphanumeric\", case=\"upper\") }} {{ input_otp(\"pin\", length=4, spaced=true, size=\"lg\", label=\"Authenticator code\") }} {{ input_otp(\"pin\", length=4, mask=true, spaced=true, size=\"lg\") }} {{ input_otp(\"code\", invalid=true, attrs={\"aria-describedby\": \"code-error\"}) }}"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class / attribute Description .input-otp Wrapper. Add data-input-otp. .input-otp-input The real input, transparent over the slots. Add data-input-otp-input. .input-otp-group / .input-otp-slot A run of slots joined into one border / one character. .input-otp-separator Between groups: an icon, text, or a dash when empty. .input-otp-spaced Every slot a separate box. .input-otp-sm / .input-otp-lg Sizes, matching .input-sm / .input-lg. .input-otp-warning / .input-otp-success State colours. Errors: aria-invalid on the input. data-pattern digits (default), alphanumeric, alpha, or a regex character class. data-case upper or lower: convert letters as they are typed. data-mask Show • in the slots. data-submit Submit the form when the last slot is filled. input-otp:complete Event on the wrapper with detail { value }."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/label",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/label",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/label.md",
      "title": "Label",
      "description": "An accessible label for a control: beside it, around a checkbox or radio, or as a whole choice card.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "field",
          "text": "In a field"
        },
        {
          "level": 2,
          "id": "form",
          "text": "In a form"
        },
        {
          "level": 2,
          "id": "controls",
          "text": "Checkbox, radio and switch"
        },
        {
          "level": 2,
          "id": "card",
          "text": "Choice card"
        },
        {
          "level": 2,
          "id": "disabled",
          "text": "Disabled"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <label class=\"label\"> <input class=\"checkbox\" type=\"checkbox\" name=\"terms\" /> Accept terms and conditions </label>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": ".label styles a <label>. Point it at its control with for and the control's id, or put the control inside it: either way clicking the text focuses or toggles the control, and screen readers read the text as its name. Around a checkbox, radio or switch, the label lines the box up with the first line of text and shows a pointer. A disabled control dims its label and shows the not-allowed cursor: wrapped in it, or as the label's sibling right before or after it. Use a real <label> for a form control. For something that isn't one — a custom select trigger, a group — give the text an id and point the control's aria-labelledby at it."
        },
        {
          "id": "field",
          "title": "In a field",
          "text": "Inside a field, .label works like .field-label: it gets a red asterisk when the control is required, turns red with aria-invalid=\"true\" and fades when the control is disabled. html <div class=\"field max-w-sm\"> <label class=\"label\" for=\"label-email\">Email</label> <input class=\"input\" id=\"label-email\" type=\"email\" placeholder=\"you@example.com\" required aria-describedby=\"label-email-help\" /> <p class=\"field-description\" id=\"label-email-help\">We send the receipt here.</p> </div>"
        },
        {
          "id": "form",
          "title": "In a form",
          "text": "Labels in a .field-group, one per control. A label can hold more than text: an icon, or, beside it, a link that belongs to the field. html <form class=\"field-group max-w-sm\" onsubmit=\"return false\"> <div class=\"field\"> <label class=\"label\" for=\"label-name\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/></svg> Name</label> <input class=\"input\" id=\"label-name\" autocomplete=\"name\" /> </div> <div class=\"field\"> <div class=\"flex items-center justify-between\"> <label class=\"label\" for=\"label-password\">Password</label> <a class=\"link text-sm\" href=\"#\">Forgot it?</a> </div> <input class=\"input\" id=\"label-password\" type=\"password\" autocomplete=\"current-password\" aria-invalid=\"true\" aria-describedby=\"label-password-error\" /> <p class=\"field-error\" id=\"label-password-error\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m15 9-6 6M9 9l6 6\"/></svg> That password is not right.</p> </div> <div class=\"field field-horizontal\"> <input class=\"switch\" type=\"checkbox\" role=\"switch\" id=\"label-remember\" checked /> <label class=\"label\" for=\"label-remember\">Remember me</label> </div> <button class=\"btn btn-primary w-full\">Sign in</button> </form>"
        },
        {
          "id": "controls",
          "title": "Checkbox, radio and switch",
          "text": "Wrapping the control in the label makes the text part of its hit area with no for or id. Long text wraps beside the box rather than under it. html <label class=\"label\"> <input class=\"checkbox\" type=\"checkbox\" name=\"news\" checked /> Send me product news </label> <label class=\"label max-w-xs\"> <input class=\"checkbox\" type=\"checkbox\" name=\"digest\" /> Email me a weekly digest of everything that changed in the projects I follow </label> <fieldset class=\"flex flex-col gap-3\"> <legend class=\"label mb-3\">Notify me about</legend> <label class=\"label\"><input class=\"radio\" type=\"radio\" name=\"notify\" value=\"all\" checked /> All new messages</label> <label class=\"label\"><input class=\"radio\" type=\"radio\" name=\"notify\" value=\"mentions\" /> Direct messages and mentions</label> <label class=\"label\"><input class=\"radio\" type=\"radio\" name=\"notify\" value=\"none\" /> Nothing</label> </fieldset> <label class=\"label\"> <input class=\"switch\" type=\"checkbox\" role=\"switch\" name=\"airplane\" /> Airplane mode </label>"
        },
        {
          "id": "card",
          "title": "Choice card",
          "text": ".label-card is a bordered label around a control and its text, highlighted while checked. Put the title and a .label-description in a .label-content: it fills the row, so a switch placed after it sits at the right edge. html <div class=\"flex w-full max-w-sm flex-col gap-3\"> <label class=\"label-card\"> <input class=\"checkbox\" type=\"checkbox\" name=\"alerts\" checked /> <span class=\"label-content\"> Enable notifications <span class=\"label-description\">You can turn them off again at any time.</span> </span> </label> <label class=\"label-card\"> <span class=\"label-content\"> Share usage data <span class=\"label-description\">Anonymous statistics that help us improve.</span> </span> <input class=\"switch\" type=\"checkbox\" role=\"switch\" name=\"telemetry\" /> </label> <label class=\"label-card\"> <input class=\"checkbox\" type=\"checkbox\" name=\"beta\" disabled /> <span class=\"label-content\"> Beta features <span class=\"label-description\">Not available on the free plan.</span> </span> </label> </div> For a set of radio cards, .radio-card adds the group layout."
        },
        {
          "id": "disabled",
          "title": "Disabled",
          "text": "The label follows its control: no extra class. html <label class=\"label\"><input class=\"checkbox\" type=\"checkbox\" disabled /> Wrapped</label> <div class=\"flex items-center gap-2\"> <input class=\"checkbox\" type=\"checkbox\" id=\"label-disabled\" disabled /> <label class=\"label\" for=\"label-disabled\">After the control</label> </div> <div class=\"flex flex-col gap-2\"> <label class=\"label\" for=\"label-disabled-input\">Before the control</label> <input class=\"input w-64\" id=\"label-disabled-input\" disabled value=\"Read only for now\" /> </div>"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .label A label for a control. Around a checkbox, radio or switch it lines them up with the text. .label-card A bordered label around a control, tinted while checked, ringed while focused. .label-content Title and description beside the control in a card; fills the row. .label-description Muted supporting text in a card."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/password-input",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/password-input",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/password-input.md",
      "title": "Password input",
      "description": "A password field with a button that shows and hides what was typed, built on input group.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "text",
          "text": "Text toggle"
        },
        {
          "level": 2,
          "id": "checkbox",
          "text": "Checkbox"
        },
        {
          "level": 2,
          "id": "caps-lock",
          "text": "Caps Lock warning"
        },
        {
          "level": 2,
          "id": "states",
          "text": "States"
        },
        {
          "level": 2,
          "id": "browsers",
          "text": "Browsers and password managers"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx"
        },
        {
          "level": 2,
          "id": "macro",
          "text": "Nunjucks macro"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"field w-full max-w-sm\"> <label class=\"label\" for=\"pw-demo\">Password</label> <div class=\"input-group password-input\" data-password-input> <input class=\"input\" id=\"pw-demo\" type=\"password\" name=\"password\" autocomplete=\"current-password\" value=\"correct horse battery\" /> <span class=\"input-group-addon input-group-addon-inline-end\"> <button type=\"button\" class=\"btn btn-ghost btn-icon password-toggle\" data-password-toggle aria-label=\"Show password\" aria-pressed=\"false\"> <span class=\"password-toggle-show\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg></span> <span class=\"password-toggle-hide\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49\"/><path d=\"M14.084 14.158a3 3 0 0 1-4.242-4.242\"/><path d=\"M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143\"/><path d=\"m2 2 20 20\"/></svg></span> </button> </span> </div> </div>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "Wrap a type=\"password\" .input in an input group with .password-input and data-password-input. Put the toggle in an .input-group-addon-inline-end: a .btn.btn-ghost.btn-icon.password-toggle with data-password-toggle, holding .password-toggle-show (the eye) and .password-toggle-hide (the eye crossed out). Give it type=\"button\" so it never submits the form, and aria-label=\"Show password\". The behaviour switches the input between type=\"password\" and type=\"text\" and sets aria-pressed on the button, which swaps the icons. The label stays \"Show password\": screen readers say \"Show password, toggle button, pressed\" once it's on, the pattern assistive technology expects. The caret and any selection stay where they were, and tapping the toggle while typing keeps focus (and the phone keyboard) in the input."
        },
        {
          "id": "text",
          "title": "Text toggle",
          "text": "Words instead of an icon: drop .btn-icon and write Show and Hide in the two spans. They share one space, so the button doesn't change width. html <div class=\"field w-full max-w-sm\"> <label class=\"label\" for=\"pw-text\">Password</label> <div class=\"input-group password-input\" data-password-input> <input class=\"input\" id=\"pw-text\" type=\"password\" name=\"password\" autocomplete=\"current-password\" /> <span class=\"input-group-addon input-group-addon-inline-end\"> <button type=\"button\" class=\"btn btn-ghost password-toggle\" data-password-toggle aria-label=\"Show password\" aria-pressed=\"false\"> <span class=\"password-toggle-show\">Show</span> <span class=\"password-toggle-hide\">Hide</span> </button> </span> </div> </div>"
        },
        {
          "id": "checkbox",
          "title": "Checkbox",
          "text": "A checkbox with data-password-toggle anywhere inside the wrapper works as the toggle too. Here one checkbox shows both fields of a new-password form: html <div class=\"password-input flex w-full max-w-sm flex-col gap-4\" data-password-input> <div class=\"field\"> <label class=\"label\" for=\"pw-new\">New password</label> <input class=\"input\" id=\"pw-new\" type=\"password\" name=\"new-password\" autocomplete=\"new-password\" data-password /> </div> <div class=\"field\"> <label class=\"label\" for=\"pw-confirm\">Confirm password</label> <input class=\"input\" id=\"pw-confirm\" type=\"password\" name=\"confirm-password\" autocomplete=\"new-password\" data-password /> </div> <div class=\"field field-horizontal\"> <input class=\"checkbox\" type=\"checkbox\" id=\"pw-show-both\" data-password-toggle /> <label class=\"field-label\" for=\"pw-show-both\">Show passwords</label> </div> </div> Every input with data-password (or type=\"password\") inside the wrapper is shown and hidden together."
        },
        {
          "id": "caps-lock",
          "title": "Caps Lock warning",
          "text": "Elements with data-password-caps in the field are shown only while Caps Lock is on as the user types in the password, and hidden again when it goes off or the input loses focus. Try it: html <div class=\"field w-full max-w-sm\"> <label class=\"label\" for=\"pw-caps\">Password</label> <div class=\"input-group password-input\" data-password-input> <input class=\"input\" type=\"password\" name=\"password\" id=\"pw-caps\" autocomplete=\"current-password\" autocapitalize=\"off\" spellcheck=\"false\" /> <span class=\"input-group-addon input-group-addon-inline-end\"> <button type=\"button\" class=\"btn btn-ghost btn-icon password-toggle\" data-password-toggle aria-label=\"Show password\" aria-pressed=\"false\"> <span class=\"password-toggle-show\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg></span> <span class=\"password-toggle-hide\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49\"/><path d=\"M14.084 14.158a3 3 0 0 1-4.242-4.242\"/><path d=\"M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143\"/><path d=\"m2 2 20 20\"/></svg></span> </button> </span> </div> <p class=\"field-warning\" data-password-caps hidden><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 "
        },
        {
          "id": "states",
          "title": "States",
          "text": "As with any input group, aria-invalid=\"true\" on the input turns the border red and disabled fades the whole field; a disabled input disables its toggle. Pair them with field messages: html <div class=\"field w-full max-w-sm self-center\"> <label class=\"label\" for=\"pw-invalid\">Password</label> <div class=\"input-group password-input\" data-password-input> <input class=\"input\" type=\"password\" name=\"password\" id=\"pw-invalid\" autocomplete=\"current-password\" autocapitalize=\"off\" spellcheck=\"false\" value=\"hunter2\" aria-invalid=\"true\" aria-describedby=\"pw-invalid-error\" /> <span class=\"input-group-addon input-group-addon-inline-end\"> <button type=\"button\" class=\"btn btn-ghost btn-icon password-toggle\" data-password-toggle aria-label=\"Show password\" aria-pressed=\"false\"> <span class=\"password-toggle-show\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg></span> <span class=\"password-toggle-hide\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49\"/><path d=\"M14.084 14.158a3 3 0 0 1-4.242-4.242\"/><path d=\"M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143\"/><path d=\"m2 2 20 20\"/></svg></span> </button> </span> </div> <p class=\"field-error\" id=\"pw-invalid-error\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden="
        },
        {
          "id": "browsers",
          "title": "Browsers and password managers",
          "text": "Saving the password. Submitting the form hides the password again first, so browsers and password managers see a password field and offer to save it, and it isn't left on screen after an htmx swap. Autofill. Use autocomplete=\"current-password\" on sign-in forms and \"new-password\" on sign-up and change-password forms, so password managers fill the right one and suggest strong passwords. The macro sets autocapitalize=\"off\" and spellcheck=\"false\" too, so a shown password isn't capitalised or sent to a spell checker. Edge's own eye. Microsoft Edge draws a reveal button inside password inputs; .password-input hides it so there is only one toggle. Safari's key icon for saved passwords stays."
        },
        {
          "id": "htmx",
          "title": "With htmx",
          "text": "The toggle is a type=\"button\", so it never sends the form. The behaviour runs again on inputs htmx swaps in, so a server re-render with an error keeps working. It also fires a bubbling password-input:toggle with detail.visible on the wrapper."
        },
        {
          "id": "macro",
          "title": "Nunjucks macro",
          "text": "The password_input macro writes the group, input and toggle. page.html {% from \"components/password-input/password-input.html\" import password_input %} <div class=\"field\"> <label class=\"label\" for=\"password\">Password</label> {{ password_input(\"password\", id=\"password\", required=true, caps=true) }} </div> {{ password_input(\"new-password\", autocomplete=\"new-password\", toggle=\"text\", minlength=12) }} {{ password_input(\"password\", invalid=true, attrs={\"aria-describedby\": \"password-error\"}) }}"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class / attribute Description .password-input On the .input-group. Add data-password-input. .password-toggle The show / hide button. Add data-password-toggle. .password-toggle-show / .password-toggle-hide Its look while hidden / while shown. data-password-toggle On the button, or on a checkbox. data-password Marks more inputs to show and hide together (default: type=\"password\"). data-password-caps Shown only while Caps Lock is on. data-visible Set on the wrapper while the password is shown. password-input:toggle Event on the wrapper with detail { visible }."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/radio-group",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/radio-group",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/radio-group.md",
      "title": "Radio group",
      "description": "A set of radio buttons: pick one option of a few, on a card or inline, submitting with the form.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "inline",
          "text": "Inline"
        },
        {
          "level": 2,
          "id": "layouts",
          "text": "Layouts"
        },
        {
          "level": 2,
          "id": "states",
          "text": "States"
        },
        {
          "level": 3,
          "id": "disabled",
          "text": "Disabled"
        },
        {
          "level": 3,
          "id": "invalid",
          "text": "Required and error"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx"
        },
        {
          "level": 2,
          "id": "buttons",
          "text": "Buttons instead of radios?"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <fieldset class=\"fieldset max-w-lg\"> <legend class=\"field-legend\">Plan</legend> <div class=\"radio-group\"> <label class=\"radio-card\"> <input class=\"radio\" type=\"radio\" name=\"radio-plan\" value=\"free\" checked /> <span class=\"radio-content\"> <span class=\"radio-title\">Free</span> <span class=\"radio-description\">One project, community support.</span> </span> </label> <label class=\"radio-card\"> <input class=\"radio\" type=\"radio\" name=\"radio-plan\" value=\"pro\" /> <span class=\"radio-content\"> <span class=\"radio-title\">Pro</span> <span class=\"radio-description\">Unlimited projects, email support.</span> </span> </label> <label class=\"radio-card\"> <input class=\"radio\" type=\"radio\" name=\"radio-plan\" value=\"team\" /> <span class=\"radio-content\"> <span class=\"radio-title\">Team</span> <span class=\"radio-description\">Shared workspaces and invoicing.</span> </span> </label> </div> </fieldset> A radio group is for a choice between a few options that are all worth showing at once. Every option is a native <input type=\"radio\">, so the browser enforces the whole contract for free: one is checked at a time, the arrow keys move between them, Space picks, and the value submits with the form. There is no behaviour to wire up."
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "Put the options in a <fieldset class=\"fieldset\"> with a <legend class=\"field-legend\">, so a screen reader announces the question before each option. Give every radio the same name — that is what makes them one group — and a value for the form. A .radio is the dot; a .radio-card wraps it, the text and a description in one clickable block. The card is a <label>, so clicking the text picks the option too. Mark the starting option with checked, and put required on one of them to make a choice necessary."
        },
        {
          "id": "inline",
          "title": "Inline",
          "text": "When an option is just a short word, skip the card: a bare .radio in a field with .field-horizontal, like a checkbox. html <fieldset class=\"fieldset w-36\"> <legend class=\"field-legend\">Invoice</legend> <div class=\"field-group\"> <div class=\"field field-horizontal\"> <input class=\"radio\" type=\"radio\" name=\"radio-invoice\" id=\"radio-invoice-card\" value=\"card\" checked /> <label class=\"field-label\" for=\"radio-invoice-card\">Charge a card</label> </div> <div class=\"field field-horizontal\"> <input class=\"radio\" type=\"radio\" name=\"radio-invoice\" id=\"radio-invoice-invoice\" value=\"invoice\" /> <label class=\"field-label\" for=\"radio-invoice-invoice\">Send an invoice (net 30)</label> </div> </div> </fieldset>"
        },
        {
          "id": "layouts",
          "title": "Layouts",
          "text": ".radio-group stacks the options; .radio-group-row puts them side by side from the sm breakpoint, each taking an equal share, so the row below becomes a column on a phone. They take any layout utility too, so grid grid-cols-3 works just as well. html <fieldset class=\"fieldset max-w-2xl\"> <legend class=\"field-legend\">Region</legend> <div class=\"radio-group radio-group-row\"> <label class=\"radio-card\"> <input class=\"radio\" type=\"radio\" name=\"radio-region\" value=\"eu\" checked /> <span class=\"radio-content\"> <span class=\"radio-title\">Europe</span> <span class=\"radio-description\">Dublin</span> </span> </label> <label class=\"radio-card\"> <input class=\"radio\" type=\"radio\" name=\"radio-region\" value=\"us\" /> <span class=\"radio-content\"> <span class=\"radio-title\">US East</span> <span class=\"radio-description\">Virginia</span> </span> </label> <label class=\"radio-card\"> <input class=\"radio\" type=\"radio\" name=\"radio-region\" value=\"ap\" /> <span class=\"radio-content\"> <span class=\"radio-title\">Asia Pacific</span> <span class=\"radio-description\">Mumbai</span> </span> </label> </div> </fieldset>"
        },
        {
          "id": "states",
          "title": "States",
          "text": ""
        },
        {
          "id": "disabled",
          "title": "Disabled",
          "text": "disabled on the radio takes the whole card out of play: it fades, ignores clicks, and its value is not submitted. Leave it off every other radio of the group, since the browser keeps the group's value on the one that is left. html <fieldset class=\"fieldset max-w-lg\"> <legend class=\"field-legend\">Region</legend> <div class=\"radio-group\"> <label class=\"radio-card\"> <input class=\"radio\" type=\"radio\" name=\"radio-region-2\" value=\"eu\" checked /> <span class=\"radio-content\"> <span class=\"radio-title\">Europe (Dublin)</span> <span class=\"radio-description\">4 ms latency.</span> </span> </label> <label class=\"radio-card\"> <input class=\"radio\" type=\"radio\" name=\"radio-region-2\" value=\"us\" /> <span class=\"radio-content\"> <span class=\"radio-title\">US East (Virginia)</span> <span class=\"radio-description\">80 ms latency.</span> </span> </label> <label class=\"radio-card\"> <input class=\"radio\" type=\"radio\" name=\"radio-region-2\" value=\"ap\" disabled /> <span class=\"radio-content\"> <span class=\"radio-title\">Asia Pacific (Mumbai)</span> <span class=\"radio-description\">Sold out this quarter.</span> </span> </label> </div> </fieldset>"
        },
        {
          "id": "invalid",
          "title": "Required and error",
          "text": "Put required on one radio: the group then has to have a choice before the form submits, and once the user has tried, :user-invalid turns every dot in the group red. aria-invalid=\"true\" does the same on demand, which is what a server response should set. html <fieldset class=\"fieldset max-w-lg\" aria-describedby=\"radio-region-error\"> <legend class=\"field-legend\">Billing region</legend> <div class=\"radio-group\"> <label class=\"radio-card\"> <input class=\"radio\" type=\"radio\" name=\"radio-billing\" value=\"eu\" aria-invalid=\"true\" /> <span class=\"radio-content\"> <span class=\"radio-title\">Europe</span> <span class=\"radio-description\">VAT charged in your local currency.</span> </span> </label> <label class=\"radio-card\"> <input class=\"radio\" type=\"radio\" name=\"radio-billing\" value=\"us\" aria-invalid=\"true\" /> <span class=\"radio-content\"> <span class=\"radio-title\">United States</span> <span class=\"radio-description\">Tax calculated from your state.</span> </span> </label> </div> <p class=\"field-error\" id=\"radio-region-error\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m15 9-6 6M9 9l6 6\"/></svg> Choose a billing region.</p> </fieldset>"
        },
        {
          "id": "htmx",
          "title": "With htmx",
          "text": "The chosen radio's name and value are posted like any other form value, so a request on change carries the choice. html <form onsubmit=\"return false\" hx-post=\"/api/echo\" hx-trigger=\"change\" hx-target=\"#radio-result\"> <fieldset class=\"fieldset max-w-lg\"> <legend class=\"field-legend\">Region</legend> <div class=\"radio-group radio-group-row\"> <label class=\"radio-card\"> <input class=\"radio\" type=\"radio\" name=\"region\" value=\"eu-west\" checked /> <span class=\"radio-content\"> <span class=\"radio-title\">Dublin</span> <span class=\"radio-description\">eu-west-1</span> </span> </label> <label class=\"radio-card\"> <input class=\"radio\" type=\"radio\" name=\"region\" value=\"us-east\" /> <span class=\"radio-content\"> <span class=\"radio-title\">Virginia</span> <span class=\"radio-description\">us-east-1</span> </span> </label> <label class=\"radio-card\"> <input class=\"radio\" type=\"radio\" name=\"region\" value=\"ap-south\" /> <span class=\"radio-content\"> <span class=\"radio-title\">Mumbai</span> <span class=\"radio-description\">ap-south-1</span> </span> </label> </div> </fieldset> </form> <p id=\"radio-result\" class=\"muted\" aria-live=\"polite\"></p>"
        },
        {
          "id": "buttons",
          "title": "Buttons instead of radios?",
          "text": "A radio group is a form control. For a row of buttons that switch a view or a style — text alignment, a filter bar — use a toggle group with data-toggle-group=\"single\", which presses one button at a time."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class / attribute Description .radio-group Stacks the options in a gap-2 column. .radio-group-row Side by side from the sm breakpoint, each option taking an equal share. .radio-card The clickable card around a radio, a title and a description. A <label>. .radio-content / .radio-title / .radio-description The card's text, its title and its muted description. .radio The dot. Same shape language as .checkbox. aria-invalid=\"true\" Error border on the option, or on every option in the group when a required group is empty (:user-invalid)."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/select",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/select",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/select.md",
      "title": "Select",
      "description": "A list of options in a popup, opened from a button: searchable, keyboard driven, and styled like an input.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "keyboard",
          "text": "Keyboard"
        },
        {
          "level": 2,
          "id": "search",
          "text": "Searching"
        },
        {
          "level": 2,
          "id": "groups",
          "text": "Groups"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Sizes and states"
        },
        {
          "level": 2,
          "id": "alignment",
          "text": "Alignment"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx"
        },
        {
          "level": 2,
          "id": "htmx-load",
          "text": "Loading options with htmx"
        },
        {
          "level": 2,
          "id": "macro",
          "text": "Nunjucks macro"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"field w-64\"> <span class=\"field-label\" id=\"select-country-label\">Country</span> <div class=\"select\" data-select> <button type=\"button\" class=\"select-trigger\" data-select-trigger aria-haspopup=\"listbox\" aria-expanded=\"false\" aria-labelledby=\"select-country-label\" aria-describedby=\"select-country-help\"> <span class=\"select-value\" data-placeholder=\"Choose a country\"></span> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg> </button> <div class=\"select-content\" role=\"listbox\" aria-labelledby=\"select-country-label\" hidden> <div class=\"select-search\"> <input class=\"input input-sm\" data-select-search placeholder=\"Search countries…\" aria-label=\"Search countries\" /> </div> <span class=\"select-item\" role=\"option\" aria-selected=\"false\" value=\"au\"> Australia <span class=\"select-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span> </span> <span class=\"select-item\" role=\"option\" aria-selected=\"false\" value=\"br\"> Brazil <span class=\"select-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span> </span> <span class=\"select-item\" role=\"option\" aria-selected=\"false\" value=\"ca\"> Canada <span class=\"select-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span> </s"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "Wrap the trigger and the list in .select with data-select, like a dropdown. The trigger is a button with data-select-trigger, aria-haspopup=\"listbox\" and aria-expanded=\"false\". Its text goes in an empty .select-value span, with data-placeholder for the text shown until something is picked. Name the button after the field's label with aria-labelledby. The list is a .select-content with role=\"listbox\" that starts hidden. Each entry is a .select-item with role=\"option\", a value, and aria-selected=\"true\" on the picked one. Add aria-disabled=\"true\" to an option the user can't pick: it is skipped by the keyboard and can't be clicked. To submit the value, put a hidden field inside the select: <input type=\"hidden\" name=\"country\" data-select-input>. A preselected option fills it, and every choice rewrites it. An option holding more than text (a flag, a badge) can carry data-label, the plain text the trigger shows."
        },
        {
          "id": "keyboard",
          "title": "Keyboard",
          "text": "Key Action ↓ ↑ on the trigger Open on the picked option, or the first / last ↓ ↑ Home End Move between options A letter Move to the next option starting with it (without a search box) Enter Space Pick the focused option and close Esc Close and return focus to the trigger Tab, click outside Close"
        },
        {
          "id": "search",
          "title": "Searching",
          "text": "Put an .input in a .select-search wrapper inside the list with data-select-search. Typing filters the options (ignoring case and accents, matching every word in an option's text or its data-keywords; groups left empty hide), ↓ moves into them, and a matching option can carry aria-selected=\"true\" so it comes first. Add a .select-empty with data-select-empty for the \"no matches\" message: it stays hidden until a search finds nothing, and the filter resets when the list closes. html <div class=\"select w-56\" data-select> <button type=\"button\" class=\"select-trigger\" data-select-trigger aria-haspopup=\"listbox\" aria-expanded=\"false\" aria-label=\"Timezone\"> <span class=\"select-value\" data-placeholder=\"Pick a timezone\"></span> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg> </button> <div class=\"select-content\" role=\"listbox\" aria-label=\"Timezone\" hidden> <div class=\"select-search\"> <input class=\"input input-sm\" data-select-search placeholder=\"Search…\" aria-label=\"Search timezones\" /> </div> <span class=\"select-item\" role=\"option\" aria-selected=\"true\" value=\"utc\">UTC</span> <span class=\"select-item\" role=\"option\" aria-selected=\"false\" value=\"cet\">Central European Time</span> <span class=\"select-item\" role=\"option\" aria-selected=\"false\" value=\"jst\">Japan Standard Time</span> <span class=\"select-item\" role=\"option\" aria-selected=\"false\" value=\"pst\">Pacific Time</span> <p class=\"select-empty\" data-select-empty hidden>No timezone matches that.</p> </div> </div>"
        },
        {
          "id": "groups",
          "title": "Groups",
          "text": "A .select-label heads a run of options and a .select-separator divides them. They are text, not options, so the keyboard skips them. Wrap each run in <div role=\"group\" aria-labelledby=\"…\"> (as the macro does) and a search hides the groups it empties, with their separators. html <div class=\"select w-56\" data-select> <button type=\"button\" class=\"select-trigger\" data-select-trigger aria-haspopup=\"listbox\" aria-expanded=\"false\" aria-label=\"Region\"> <span class=\"select-value\" data-placeholder=\"All regions\"></span> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg> </button> <div class=\"select-content\" role=\"listbox\" aria-label=\"Region\" hidden> <p class=\"select-label\">Americas</p> <span class=\"select-item\" role=\"option\" aria-selected=\"false\" value=\"na\">North America</span> <span class=\"select-item\" role=\"option\" aria-selected=\"false\" value=\"sa\">South America</span> <div class=\"select-separator\" role=\"separator\"></div> <p class=\"select-label\">Europe and Africa</p> <span class=\"select-item\" role=\"option\" aria-selected=\"false\" value=\"eu\">Europe</span> <span class=\"select-item\" role=\"option\" aria-selected=\"false\" value=\"af\">Africa</span> <span class=\"select-item\" role=\"option\" aria-selected=\"false\" value=\"me\" aria-disabled=\"true\">Middle East (coming soon)</span> </div> </div>"
        },
        {
          "id": "sizes",
          "title": "Sizes and states",
          "text": ".select-sm and .select-lg match .input-sm and .input-lg. disabled on the trigger, and aria-invalid=\"true\" for an error, work as they do on an input. html <div class=\"select w-56\"> <button type=\"button\" class=\"select-trigger select-sm\" data-select-trigger aria-haspopup=\"listbox\" aria-expanded=\"false\" aria-label=\"Small\"> <span class=\"select-value\" data-placeholder=\"Small\"></span> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg> </button> <div class=\"select-content\" role=\"listbox\" aria-label=\"Small\" hidden> <span class=\"select-item\" role=\"option\" aria-selected=\"false\" value=\"a\">A</span> <span class=\"select-item\" role=\"option\" aria-selected=\"false\" value=\"b\">B</span> </div> </div> <div class=\"select w-56\"> <button type=\"button\" class=\"select-trigger\" data-select-trigger aria-haspopup=\"listbox\" aria-expanded=\"false\" aria-invalid=\"true\" aria-label=\"Invalid\"> <span class=\"select-value\" data-placeholder=\"Invalid\"></span> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg> </button> <div class=\"select-content\" role=\"listbox\" aria-label=\"Invalid\" hidden> <span class=\"select-item\" role=\"option\" aria-selected=\"false\" value=\"a\">A</span> <span class=\"select-item\" role=\"option\" aria-selected=\"false\" value=\"b\">B</span> </div> </div> <div class=\"select w-56\"> <button type=\"button\" class=\"select-trigger\" data-select-trigger aria-haspopup=\"listbox\" aria-expanded=\"false\" disabled aria-label=\"Disabled\"> <span class=\"select-value\" data-placeholder=\"Disabled\"></span> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke"
        },
        {
          "id": "alignment",
          "title": "Alignment",
          "text": "The list opens below the trigger, as wide as the trigger and aligned to its left edge. Add .select-content-end to align it to the right edge, or .select-content-up to open it upwards. html <div class=\"select w-48\"> <button type=\"button\" class=\"select-trigger\" data-select-trigger aria-haspopup=\"listbox\" aria-expanded=\"false\" aria-label=\"Right-aligned\"> <span class=\"select-value\" data-placeholder=\"Right-aligned\"></span> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg> </button> <div class=\"select-content select-content-end\" role=\"listbox\" aria-label=\"Right-aligned\" hidden> <span class=\"select-item\" role=\"option\" aria-selected=\"false\" value=\"au\">Australia</span> <span class=\"select-item\" role=\"option\" aria-selected=\"false\" value=\"nz\">New Zealand</span> </div> </div>"
        },
        {
          "id": "htmx",
          "title": "With htmx",
          "text": "Every choice fires a bubbling select:change with detail.value (the option's value), detail.label and detail.option. Trigger requests on it rather than on click, so they run once the value has been set. Inside a form, htmx posts the form's values — the hidden [data-select-input] among them — so the request carries the choice with no extra wiring: html <form class=\"select w-56\" data-select onsubmit=\"return false\" hx-post=\"/api/echo\" hx-trigger=\"select:change\" hx-target=\"#select-result\"> <button type=\"button\" class=\"select-trigger\" data-select-trigger aria-haspopup=\"listbox\" aria-expanded=\"false\" aria-label=\"Deploy region\"> <span class=\"select-value\" data-placeholder=\"Deploy region\"></span> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg> </button> <div class=\"select-content\" role=\"listbox\" aria-label=\"Deploy region\" hidden> <span class=\"select-item\" role=\"option\" aria-selected=\"false\" value=\"eu-west\">Europe (Dublin)</span> <span class=\"select-item\" role=\"option\" aria-selected=\"false\" value=\"us-east\">US East (Virginia)</span> <span class=\"select-item\" role=\"option\" aria-selected=\"false\" value=\"ap-south\">Mumbai</span> </div> <input type=\"hidden\" name=\"region\" data-select-input /> </form> <p id=\"select-result\" class=\"muted\" aria-live=\"polite\"></p> Outside a form, pass the value yourself with the event: hx-vals='js:{region: event.detail.value}'."
        },
        {
          "id": "htmx-load",
          "title": "Loading options with htmx",
          "text": "Swap the option list in on load, or after a request elsewhere: the behaviour looks options up on every event, so a list that arrives from the server keeps filtering and navigating. Mark the picked one with aria-selected=\"true\" and put the value in [data-select-input] in the swapped markup and the new list starts out in step."
        },
        {
          "id": "macro",
          "title": "Nunjucks macro",
          "text": "The select macro writes the trigger, the options and the hidden field from a list. Options are [{value, label}] pairs, or plain strings when the value is the label; an option can also carry disabled, icon and keywords (more words the filter matches), and {label, options} makes a group. The preselected value is written into the trigger and the hidden field, so the page shows and submits it before any JavaScript runs. html <div class=\"field w-64\"> <label class=\"field-label\" id=\"macro-food-label\" for=\"macro-food\">Ingredient</label> <div class=\"select w-full\" data-select> <button type=\"button\" id=\"macro-food\" class=\"select-trigger\" data-select-trigger aria-haspopup=\"listbox\" aria-expanded=\"false\" aria-labelledby=\"macro-food-label macro-food\" aria-describedby=\"macro-food-help\"> <span class=\"select-value\" data-placeholder=\"Select…\">Banana</span> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg> </button> <div class=\"select-content\" id=\"macro-food-listbox\" role=\"listbox\" aria-labelledby=\"macro-food-label\" hidden> <div class=\"select-search\"><input class=\"input input-sm\" data-select-search placeholder=\"Search…\" aria-label=\"Filter options\" /></div> <div role=\"group\" aria-labelledby=\"macro-food-group-1\"> <div class=\"select-label\" id=\"macro-food-group-1\">Fruit</div> <span class=\"select-item\" role=\"option\" value=\"Apple\" aria-selected=\"false\">Apple<span class=\"select-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span></span> <span class=\"select-item\" role=\"option\" value=\"Banana\" aria-selected=\"true\">Banana<span class=\"select-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class / attribute Description .select Positioning wrapper (relative, inline-block). .select-trigger The button that opens the list. Looks like an .input. .select-sm / .select-lg Trigger sizes, matching .input-sm / .input-lg. .select-value The picked text. Empty, with data-placeholder, until something is picked. .select-content The listbox. Opens below, as wide as the trigger. .select-content-end / .select-content-up Align to the right edge / open above. .select-search Sticky wrapper for a [data-select-search] filter input. .select-item One option. aria-selected marks the picked one, aria-disabled an unavailable one. .select-check Icon inside an option, shown only while it is picked. .select-label / .select-separator Heading and divider inside the list. .select-empty Shown by the behaviour when a search finds nothing. [data-select] / [data-select-trigger] Behaviour: open/close, keyboard, filtering, and select:change { value, label, option }. [data-select-input] Hidden field mirroring the picked value, so a form can submit it."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/slider",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/slider",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/slider.md",
      "title": "Slider",
      "description": "Pick a value, a range or several values on a track: native range inputs, horizontal or vertical, with every form state.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "range",
          "text": "Range"
        },
        {
          "level": 2,
          "id": "multiple",
          "text": "Multiple thumbs"
        },
        {
          "level": 2,
          "id": "orientation",
          "text": "Orientation"
        },
        {
          "level": 2,
          "id": "field",
          "text": "In a field, with its value"
        },
        {
          "level": 2,
          "id": "states",
          "text": "States"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Sizes"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx"
        },
        {
          "level": 2,
          "id": "macro",
          "text": "Macro"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"slider w-3/5\" data-slider style=\"--slider-start: 0%; --slider-end: 50%\"> <div class=\"slider-track\"><div class=\"slider-range\"></div></div> <input type=\"range\" name=\"volume\" min=\"0\" max=\"100\" value=\"50\" aria-label=\"Volume\" /> </div>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "Each thumb is a native <input type=\"range\"> inside .slider, after a .slider-track holding the .slider-range fill. The inputs submit with the form and take the keyboard (← →, Page Up Page Down, Home End) with no JavaScript. Give every input an accessible name: aria-label, or a <label for>. data-slider adds the behaviour: it paints the range as the values change, keeps thumbs from crossing, moves the nearest thumb to a press on the track, and fills <output for>. The inline --slider-start / --slider-end paint the range before it runs; the slider() macro writes them for you."
        },
        {
          "id": "range",
          "title": "Range",
          "text": "Two inputs make a range: the fill spans from one thumb to the other. Give them the same name and the form sends both values (price=20&price=80), or a name each. html <div class=\"slider w-3/5\" data-slider style=\"--slider-start: 20%; --slider-end: 80%\"> <div class=\"slider-track\"><div class=\"slider-range\"></div></div> <input type=\"range\" name=\"price\" min=\"0\" max=\"100\" value=\"20\" aria-label=\"Minimum price\" /> <input type=\"range\" name=\"price\" min=\"0\" max=\"100\" value=\"80\" aria-label=\"Maximum price\" /> </div>"
        },
        {
          "id": "multiple",
          "title": "Multiple thumbs",
          "text": "Any number of inputs, in ascending order. A thumb stops at its neighbours; data-slider-gap keeps them at least that far apart, in value units. html <div class=\"slider w-3/5\" data-slider data-slider-gap=\"10\" style=\"--slider-start: 10%; --slider-end: 80%\"> <div class=\"slider-track\"><div class=\"slider-range\"></div></div> <input type=\"range\" min=\"0\" max=\"100\" step=\"5\" value=\"10\" aria-label=\"Low\" /> <input type=\"range\" min=\"0\" max=\"100\" step=\"5\" value=\"45\" aria-label=\"Middle\" /> <input type=\"range\" min=\"0\" max=\"100\" step=\"5\" value=\"80\" aria-label=\"High\" /> </div>"
        },
        {
          "id": "orientation",
          "title": "Orientation",
          "text": "data-orientation=\"vertical\" stands the slider up, the minimum at the bottom. Give it (or its parent) a height; ↑ ↓ move it. html <div class=\"slider h-44\" data-slider data-orientation=\"vertical\" style=\"--slider-start: 0%; --slider-end: 60%\"> <div class=\"slider-track\"><div class=\"slider-range\"></div></div> <input type=\"range\" min=\"0\" max=\"100\" value=\"60\" aria-label=\"Bass\" aria-orientation=\"vertical\" /> </div> <div class=\"slider h-44\" data-slider data-orientation=\"vertical\" style=\"--slider-start: 25%; --slider-end: 75%\"> <div class=\"slider-track\"><div class=\"slider-range\"></div></div> <input type=\"range\" min=\"0\" max=\"100\" value=\"25\" aria-label=\"Lowest\" aria-orientation=\"vertical\" /> <input type=\"range\" min=\"0\" max=\"100\" value=\"75\" aria-label=\"Highest\" aria-orientation=\"vertical\" /> </div>"
        },
        {
          "id": "field",
          "title": "In a field, with its value",
          "text": ".slider-field puts a label and a .slider-value above the slider and a description below it. An <output for=\"…\"> naming the inputs' ids shows their values, joined with an en dash, and screen readers announce it as the result of those inputs. html <div class=\"slider-field max-w-sm\"> <label class=\"field-label\" for=\"budget-min\">Budget</label> <span class=\"slider-value\">$<output for=\"budget-min budget-max\">200 – 800</output></span> <div class=\"slider\" data-slider style=\"--slider-start: 20%; --slider-end: 80%\"> <div class=\"slider-track\"><div class=\"slider-range\"></div></div> <input type=\"range\" id=\"budget-min\" name=\"budget_min\" min=\"0\" max=\"1000\" step=\"10\" value=\"200\" aria-label=\"Minimum budget\" aria-describedby=\"budget-help\" /> <input type=\"range\" id=\"budget-max\" name=\"budget_max\" min=\"0\" max=\"1000\" step=\"10\" value=\"800\" aria-label=\"Maximum budget\" aria-describedby=\"budget-help\" /> </div> <p class=\"field-description\" id=\"budget-help\">Per month, before tax.</p> </div>"
        },
        {
          "id": "states",
          "title": "States",
          "text": "Disable the inputs (or a <fieldset disabled> around them) to fade the slider. aria-invalid=\"true\" on an input turns it red, as on inputs; .slider-warning and .slider-success tint it too. html <div class=\"slider\" data-slider style=\"--slider-start: 0%; --slider-end: 40%\"> <div class=\"slider-track\"><div class=\"slider-range\"></div></div> <input type=\"range\" min=\"0\" max=\"100\" value=\"40\" aria-label=\"Disabled\" disabled /> </div> <div class=\"slider\" data-slider style=\"--slider-start: 0%; --slider-end: 90%\"> <div class=\"slider-track\"><div class=\"slider-range\"></div></div> <input type=\"range\" min=\"0\" max=\"100\" value=\"90\" aria-label=\"Invalid\" aria-invalid=\"true\" /> </div> <div class=\"slider slider-warning\" data-slider style=\"--slider-start: 0%; --slider-end: 70%\"> <div class=\"slider-track\"><div class=\"slider-range\"></div></div> <input type=\"range\" min=\"0\" max=\"100\" value=\"70\" aria-label=\"Warning\" /> </div> <div class=\"slider slider-success\" data-slider style=\"--slider-start: 0%; --slider-end: 30%\"> <div class=\"slider-track\"><div class=\"slider-range\"></div></div> <input type=\"range\" min=\"0\" max=\"100\" value=\"30\" aria-label=\"Success\" /> </div>"
        },
        {
          "id": "sizes",
          "title": "Sizes",
          "text": "html <div class=\"slider slider-sm\" data-slider style=\"--slider-start: 0%; --slider-end: 40%\"> <div class=\"slider-track\"><div class=\"slider-range\"></div></div> <input type=\"range\" min=\"0\" max=\"100\" value=\"40\" aria-label=\"Small\" /> </div> <div class=\"slider\" data-slider style=\"--slider-start: 0%; --slider-end: 55%\"> <div class=\"slider-track\"><div class=\"slider-range\"></div></div> <input type=\"range\" min=\"0\" max=\"100\" value=\"55\" aria-label=\"Default\" /> </div> <div class=\"slider slider-lg\" data-slider style=\"--slider-start: 0%; --slider-end: 70%\"> <div class=\"slider-track\"><div class=\"slider-range\"></div></div> <input type=\"range\" min=\"0\" max=\"100\" value=\"70\" aria-label=\"Large\" /> </div>"
        },
        {
          "id": "htmx",
          "title": "With htmx",
          "text": "The inputs are ordinary form controls, so hx-trigger=\"change\" sends the value once a drag ends (the behaviour fires change after a press on the track too), or input changed delay:300ms while it moves. html <form hx-post=\"/api/echo\" hx-trigger=\"change\" hx-target=\"#slider-result\"> <div class=\"slider\" data-slider style=\"--slider-start: 30%; --slider-end: 70%\"> <div class=\"slider-track\"><div class=\"slider-range\"></div></div> <input type=\"range\" name=\"from\" min=\"0\" max=\"100\" value=\"30\" aria-label=\"From\" /> <input type=\"range\" name=\"to\" min=\"0\" max=\"100\" value=\"70\" aria-label=\"To\" /> </div> </form> <p class=\"text-sm text-muted-foreground\" id=\"slider-result\">Move a thumb.</p>"
        },
        {
          "id": "macro",
          "title": "Macro",
          "text": "slider() writes the inputs, their names and labels, and the starting fill. Pass a number for one thumb or a list for several. jinja {% from \"components/slider/slider.html\" import slider %} {{ slider(50, aria_label=\"Volume\", name=\"volume\") }} {{ slider([20, 80], name=\"price\", min=0, max=500, step=10) }} {{ slider([10, 40, 70], labels=[\"Low\", \"Mid\", \"High\"], gap=10) }} {{ slider(60, orientation=\"vertical\", aria_label=\"Bass\", class=\"h-44\") }} {{ slider(30, id=\"volume\", disabled=true, variant=\"warning\", size=\"sm\") }}"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .slider The slider: a track and its thumbs (range inputs) laid over each other. .slider-track / .slider-range The track, and the filled part between --slider-start and --slider-end. .slider-field / .slider-value Label and value above the slider, description below. .slider-warning / .slider-success Tints the range and thumbs. .slider-sm / .slider-lg Thumb and track sizes. --slider-color, --slider-thumb, --slider-track The colour, thumb size and track thickness, to override. [data-slider] Behaviour: paints the range, orders thumbs, track presses, outputs. [data-orientation=\"vertical\"] Vertical slider, minimum at the bottom. [data-slider-gap] The least distance between neighbouring thumbs, in value units."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/switch",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/switch",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/switch.md",
      "title": "Switch",
      "description": "An on/off setting that takes effect at once: a sliding track on a real checkbox, so it submits with the form and needs no JavaScript.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "usage",
          "text": "Usage"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Sizes"
        },
        {
          "level": 2,
          "id": "states",
          "text": "States"
        },
        {
          "level": 3,
          "id": "disabled",
          "text": "Disabled"
        },
        {
          "level": 3,
          "id": "invalid",
          "text": "Error, warning, success"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx"
        },
        {
          "level": 2,
          "id": "which",
          "text": "Switch, checkbox or toggle?"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"field field-horizontal max-w-sm\"> <input class=\"switch\" type=\"checkbox\" role=\"switch\" id=\"switch-digest\" checked /> <label class=\"field-label\" for=\"switch-digest\">Email me a digest every Monday</label> </div> A switch is for a preference you flip and forget: dark mode, notifications, autosave. It is a real <input type=\"checkbox\"> with role=\"switch\", so it takes a name and a value, submits with the form, is skipped by Tab when disabled, and screen readers announce it as \"on\" and \"off\" rather than \"checked\"."
        },
        {
          "id": "usage",
          "title": "Usage",
          "text": "Add .switch and role=\"switch\" to a checkbox input, and put the id its .field-label points at. Everything is the browser's: checked, disabled, required, and the change event. There is no behaviour to wire up. Wrap it in a field with .field-horizontal so the label sits beside it, or use .field-responsive to stack them in a narrow container. html <div class=\"field-group max-w-md\"> <div class=\"field field-horizontal\"> <input class=\"switch\" type=\"checkbox\" role=\"switch\" id=\"switch-public\" /> <label class=\"field-label\" for=\"switch-public\">Public profile</label> </div> <div class=\"field field-horizontal\"> <input class=\"switch\" type=\"checkbox\" role=\"switch\" id=\"switch-2fa\" checked /> <div class=\"field-content\"> <label class=\"field-label\" for=\"switch-2fa\">Two-factor authentication</label> <p class=\"field-description\">Required for admin accounts.</p> </div> </div> </div>"
        },
        {
          "id": "sizes",
          "title": "Sizes",
          "text": ".switch-sm and .switch-lg match the height of .input-sm and .input-lg. html <div class=\"field field-horizontal\"> <input class=\"switch switch-sm\" type=\"checkbox\" role=\"switch\" id=\"switch-sm\" checked /> <label class=\"field-label text-xs\" for=\"switch-sm\">Small</label> </div> <div class=\"field field-horizontal\"> <input class=\"switch\" type=\"checkbox\" role=\"switch\" id=\"switch-md\" checked /> <label class=\"field-label\" for=\"switch-md\">Default</label> </div> <div class=\"field field-horizontal\"> <input class=\"switch switch-lg\" type=\"checkbox\" role=\"switch\" id=\"switch-lg\" checked /> <label class=\"field-label text-base\" for=\"switch-lg\">Large</label> </div>"
        },
        {
          "id": "states",
          "title": "States",
          "text": ""
        },
        {
          "id": "disabled",
          "title": "Disabled",
          "text": "disabled fades the switch; inside a field the label fades with it. html <div class=\"field-group max-w-md\"> <div class=\"field field-horizontal\"> <input class=\"switch\" type=\"checkbox\" role=\"switch\" id=\"switch-beta\" disabled /> <label class=\"field-label\" for=\"switch-beta\">Beta features</label> </div> <div class=\"field field-horizontal\"> <input class=\"switch\" type=\"checkbox\" role=\"switch\" id=\"switch-sync\" disabled checked /> <div class=\"field-content\"> <label class=\"field-label\" for=\"switch-sync\">Sync with mobile</label> <p class=\"field-description\">Coming back soon.</p> </div> </div> </div>"
        },
        {
          "id": "invalid",
          "title": "Error, warning, success",
          "text": "Set aria-invalid=\"true\" for an error, which turns the border red and the field's label with it, or add .switch-warning or .switch-success for a track in those colours. Pair an error with a field message linked using aria-describedby. html <div class=\"field-group max-w-md\"> <div class=\"field field-horizontal\"> <input class=\"switch\" type=\"checkbox\" role=\"switch\" id=\"switch-terms\" aria-invalid=\"true\" aria-describedby=\"switch-terms-msg\" /> <div class=\"field-content\"> <label class=\"field-label\" for=\"switch-terms\">I accept the terms</label> <p class=\"field-error\" id=\"switch-terms-msg\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m15 9-6 6M9 9l6 6\"/></svg> Turn this on to continue.</p> </div> </div> <div class=\"field field-horizontal\"> <input class=\"switch switch-warning\" type=\"checkbox\" role=\"switch\" id=\"switch-quota\" checked aria-describedby=\"switch-quota-msg\" /> <div class=\"field-content\"> <label class=\"field-label\" for=\"switch-quota\">Metered requests</label> <p class=\"field-warning\" id=\"switch-quota-msg\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4M12 17h.01\"/></svg> This may raise your bill.</p> </div> </div> <div class=\"field field-horizontal\"> <input class=\"switch switch-success\" type=\"checkbox\" role=\"switch\" id=\"switch-backup\" checked aria-describedby=\"switch-backup-msg\" /> <label class=\"field-label\" for=\"switch-backup\">Nightly backups</label> <p class=\"field-success\" id=\"switch-backup-msg\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\""
        },
        {
          "id": "htmx",
          "title": "With htmx",
          "text": "A switch posts like any other checkbox: give it a name and trigger on change. html <form class=\"field field-horizontal\" onsubmit=\"return false\" hx-post=\"/api/echo\" hx-trigger=\"change\" hx-target=\"#switch-result\"> <input class=\"switch\" type=\"checkbox\" role=\"switch\" name=\"backups\" value=\"on\" id=\"switch-htmx\" checked /> <label class=\"field-label\" for=\"switch-htmx\">Nightly backups</label> </form> <p id=\"switch-result\" class=\"muted\" aria-live=\"polite\"></p>"
        },
        {
          "id": "which",
          "title": "Switch, checkbox or toggle?",
          "text": "Use When Switch A preference that applies the moment it is flipped. Checkbox Part of a form the user submits: terms, a list of choices, a confirmation. Toggle A button that switches formatting or a view on the spot, like bold in a toolbar."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class / attribute Description .switch The track and its sliding thumb. Add role=\"switch\" to the input. .switch-sm / .switch-lg Matches the height of .input-sm / .input-lg. aria-invalid=\"true\" Error border, and a red label in a field. So does :user-invalid. .switch-warning / .switch-success Warning or success track, for the on state."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/textarea",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/textarea",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/textarea.md",
      "title": "Textarea",
      "description": "Multi-line text that grows with what's typed: field labels and messages, a character count, every form state, and submitting with Ctrl/⌘+Enter.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "field",
          "text": "Field"
        },
        {
          "level": 2,
          "id": "count",
          "text": "Character count"
        },
        {
          "level": 2,
          "id": "states",
          "text": "States"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Sizes and height"
        },
        {
          "level": 2,
          "id": "submit",
          "text": "Submit with the keyboard"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <textarea class=\"textarea max-w-sm\" placeholder=\"Type your message here.\" aria-label=\"Message\"></textarea>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "A native <textarea class=\"textarea\">. It shares its border, focus ring and states with .input. It grows with its content (CSS field-sizing) from min-h-16; add a max-h-* and it scrolls beyond that. .textarea-fixed keeps the height of its rows instead. Optional behaviour, data-textarea: growing in browsers without field-sizing, a character count and submitting with the keyboard."
        },
        {
          "id": "field",
          "title": "Field",
          "text": "In a .field, with a label, a description linked by aria-describedby, and the required asterisk when the textarea is required. html <div class=\"field mx-auto max-w-sm\"> <label class=\"field-label\" for=\"textarea-bio\">Bio</label> <textarea class=\"textarea\" id=\"textarea-bio\" name=\"bio\" placeholder=\"Tell us a little about yourself\" aria-describedby=\"textarea-bio-help\" required></textarea> <p class=\"field-description\" id=\"textarea-bio-help\">A sentence or two, shown on your profile.</p> </div>"
        },
        {
          "id": "count",
          "title": "Character count",
          "text": "With data-textarea, a [data-textarea-count] in the same field shows the length, against maxlength when there is one. It turns amber from 90% and red at the limit. Put it in a .field-footer to sit opposite the description. html <div class=\"field mx-auto max-w-sm\"> <label class=\"field-label\" for=\"textarea-status\">Status</label> <textarea class=\"textarea\" id=\"textarea-status\" maxlength=\"80\" data-textarea aria-describedby=\"textarea-status-help\">Shipping the 0.2 release</textarea> <div class=\"field-footer\"> <p class=\"field-description\" id=\"textarea-status-help\">Shown next to your name.</p> <span class=\"textarea-count\" data-textarea-count aria-live=\"polite\"></span> </div> </div>"
        },
        {
          "id": "states",
          "title": "States",
          "text": "As on inputs: disabled, readonly, aria-invalid=\"true\" (with a .field-error message), .textarea-warning and .textarea-success. html <div class=\"field\"> <label class=\"field-label\" for=\"textarea-disabled\">Disabled</label> <textarea class=\"textarea\" id=\"textarea-disabled\" disabled>Can't be changed.</textarea> </div> <div class=\"field\"> <label class=\"field-label\" for=\"textarea-readonly\">Read only</label> <textarea class=\"textarea\" id=\"textarea-readonly\" readonly>Can be selected and copied.</textarea> </div> <div class=\"field\"> <label class=\"field-label\" for=\"textarea-error\">Description</label> <textarea class=\"textarea\" id=\"textarea-error\" aria-invalid=\"true\" aria-describedby=\"textarea-error-msg\">Too short</textarea> <p class=\"field-error\" id=\"textarea-error-msg\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m15 9-6 6M9 9l6 6\"/></svg> Write at least 20 characters.</p> </div> <div class=\"field\"> <label class=\"field-label\" for=\"textarea-warning\">Notes</label> <textarea class=\"textarea textarea-warning\" id=\"textarea-warning\" aria-describedby=\"textarea-warning-msg\">Call the supplier on Monday</textarea> <p class=\"field-warning\" id=\"textarea-warning-msg\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4M12 17h.01\"/></svg> Visible to the whole team.</p> </div> <div class=\"field sm:col-span-2\"> <label class=\"field-label\" for=\"textarea-success\">Address</label> <textarea class=\"textarea textarea-success\" id=\"textarea-success\" aria-describedby=\"textarea-success-msg\">1 Martin Place, Sydney NSW 2000</textarea> <p cl"
        },
        {
          "id": "sizes",
          "title": "Sizes and height",
          "text": ".textarea-sm and .textarea-lg change the text size and padding. .textarea-fixed with rows holds a height and drops the resize handle; max-h-* caps a growing one. html <textarea class=\"textarea textarea-sm\" placeholder=\"Small\" aria-label=\"Small\"></textarea> <textarea class=\"textarea textarea-lg\" placeholder=\"Large\" aria-label=\"Large\"></textarea> <textarea class=\"textarea textarea-fixed\" rows=\"3\" placeholder=\"Fixed at three rows\" aria-label=\"Fixed\"></textarea> <textarea class=\"textarea max-h-32\" placeholder=\"Grows up to 8rem, then scrolls\" aria-label=\"Capped\"></textarea>"
        },
        {
          "id": "submit",
          "title": "Submit with the keyboard",
          "text": "data-textarea-submit submits the form on Ctrl/⌘+Enter; data-textarea-submit=\"enter\" on Enter, with Shift+Enter for a new line, as in chat. It calls requestSubmit(), so validation and htmx run as for a click. html <form class=\"field mx-auto max-w-sm\" hx-post=\"/api/echo\" hx-target=\"#textarea-result\" hx-on::after:request=\"this.reset()\"> <label class=\"field-label\" for=\"textarea-reply\">Reply</label> <div class=\"input-group\"> <textarea class=\"textarea\" id=\"textarea-reply\" name=\"reply\" placeholder=\"Write a reply…\" data-textarea data-textarea-submit required></textarea> <div class=\"input-group-addon input-group-addon-block-end\"> <span class=\"text-xs text-muted-foreground\"><kbd class=\"kbd\">Ctrl</kbd> <kbd class=\"kbd\">Enter</kbd> to send</span> <button class=\"btn btn-primary btn-xs ms-auto\" type=\"submit\">Send</button> </div> </div> <p class=\"field-description\" id=\"textarea-result\">Nothing sent yet.</p> </form>"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .textarea Multi-line input; grows with its content where supported. .textarea-fixed Keeps its rows height, no resize handle. .textarea-sm / .textarea-lg Smaller or larger text and padding. .textarea-warning / .textarea-success Warning and success borders (aria-invalid=\"true\" for errors). .field-footer A row under a field's control: description and count. .textarea-count The character count; data-state=\"near\" / \"limit\" colour it. [data-textarea] Behaviour: grows without field-sizing, fills the count. [data-textarea-count] Where the count goes (in the same .field, or by id). [data-textarea-submit] Ctrl/⌘+Enter submits; =\"enter\": Enter submits, Shift+Enter is a new line."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/button",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/button",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/button.md",
      "title": "Button",
      "description": "Triggers an action or an htmx request. Works on <button>, <a> and <input type=\"submit\">.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "variants",
          "text": "Variants"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Sizes"
        },
        {
          "level": 2,
          "id": "icon",
          "text": "Icon"
        },
        {
          "level": 2,
          "id": "with-icon",
          "text": "With icon"
        },
        {
          "level": 2,
          "id": "rounded",
          "text": "Rounded"
        },
        {
          "level": 2,
          "id": "press",
          "text": "Press effect"
        },
        {
          "level": 2,
          "id": "links",
          "text": "As a link"
        },
        {
          "level": 2,
          "id": "disabled",
          "text": "Disabled"
        },
        {
          "level": 2,
          "id": "loading",
          "text": "Loading"
        },
        {
          "level": 3,
          "id": "loading-htmx",
          "text": "While an htmx request runs"
        },
        {
          "level": 2,
          "id": "more",
          "text": "Groups and toggles"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <button class=\"btn btn-primary\">Save changes</button>"
        },
        {
          "id": "variants",
          "title": "Variants",
          "text": "Combine .btn with one variant class. html <button class=\"btn btn-primary\">Primary</button> <button class=\"btn btn-secondary\">Secondary</button> <button class=\"btn btn-outline\">Outline</button> <button class=\"btn btn-ghost\">Ghost</button> <button class=\"btn btn-danger\">Danger</button> <button class=\"btn btn-link\">Link</button>"
        },
        {
          "id": "sizes",
          "title": "Sizes",
          "text": "html <button class=\"btn btn-outline btn-xs\">Extra small</button> <button class=\"btn btn-outline btn-sm\">Small</button> <button class=\"btn btn-outline\">Default</button> <button class=\"btn btn-outline btn-lg\">Large</button>"
        },
        {
          "id": "icon",
          "title": "Icon",
          "text": ".btn-icon makes a square button for a single icon. It combines with every size. Give it an aria-label, since there's no visible text. html <button class=\"btn btn-outline btn-icon btn-xs\" aria-label=\"Add\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5v14\"/></svg></button> <button class=\"btn btn-outline btn-icon btn-sm\" aria-label=\"Add\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5v14\"/></svg></button> <button class=\"btn btn-outline btn-icon\" aria-label=\"Add\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5v14\"/></svg></button> <button class=\"btn btn-outline btn-icon btn-lg\" aria-label=\"Add\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5v14\"/></svg></button> <button class=\"btn btn-ghost btn-icon\" aria-label=\"Delete\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"/></svg></button>"
        },
        {
          "id": "with-icon",
          "title": "With icon",
          "text": "Inline SVGs next to the text are sized to match the button (16px, 12px for .btn-xs). html <button class=\"btn btn-primary\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3\"/></svg> Download</button> <button class=\"btn btn-outline\">Next <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5l7 7-7 7\"/></svg></button> <button class=\"btn btn-secondary btn-sm\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"20\" height=\"16\" x=\"2\" y=\"4\" rx=\"2\"/><path d=\"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7\"/></svg> Email</button> <button class=\"btn btn-ghost btn-xs\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5v14\"/></svg> Add row</button>"
        },
        {
          "id": "rounded",
          "title": "Rounded",
          "text": ".btn-rounded gives fully rounded ends; an icon button becomes a circle. html <button class=\"btn btn-primary btn-rounded\">Get started</button> <button class=\"btn btn-outline btn-rounded\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z\"/></svg> Star</button> <button class=\"btn btn-secondary btn-icon btn-rounded\" aria-label=\"Add\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5v14\"/></svg></button>"
        },
        {
          "id": "press",
          "title": "Press effect",
          "text": "A pressed button nudges down by a pixel. .btn-no-press turns that off, for buttons that should stay put: controls laid over other content, such as a carousel's previous and next buttons, or buttons with an animation of their own. html <button class=\"btn btn-outline\">Nudges when pressed</button> <button class=\"btn btn-outline btn-no-press\">Stays put</button>"
        },
        {
          "id": "links",
          "title": "As a link",
          "text": "html <a href=\"/docs/installation\" class=\"btn btn-secondary\">Read the guide</a>"
        },
        {
          "id": "disabled",
          "title": "Disabled",
          "text": "Use the disabled attribute on buttons, or aria-disabled=\"true\" on links. html <button class=\"btn btn-primary\" disabled>Disabled</button> <a class=\"btn btn-outline\" aria-disabled=\"true\">Disabled link</a>"
        },
        {
          "id": "loading",
          "title": "Loading",
          "text": "Put a spinner in the button. aria-busy=\"true\" blocks clicks while keeping full colour; disabled blocks them and fades the button. html <button class=\"btn btn-primary\" aria-busy=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner\" aria-hidden=\"true\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg> Saving</button> <button class=\"btn btn-outline\" disabled><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner\" aria-hidden=\"true\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg> Please wait</button> <button class=\"btn btn-secondary btn-icon\" aria-busy=\"true\" aria-label=\"Loading\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner\" aria-hidden=\"true\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg></button>"
        },
        {
          "id": "loading-htmx",
          "title": "While an htmx request runs",
          "text": "Give the spinner .htmx-indicator. htmx adds .htmx-request to the button during the request, which reveals it; until then it takes no space. hx-disable=\"this\" prevents double submits. html <button class=\"btn btn-primary\" hx-post=\"/api/slow\" hx-target=\"next .result\" hx-disable=\"this\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner htmx-indicator\" aria-hidden=\"true\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg> Save </button> <span class=\"result muted\" aria-live=\"polite\"></span> The /api/slow endpoint is a mock that only exists under bun run dev."
        },
        {
          "id": "more",
          "title": "Groups and toggles",
          "text": "Join buttons with a button group, make one switch on and off with toggle, or pick between options with a toggle group."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .btn Base class. Required on every button. .btn-primary Solid brand colour, for the main action. .btn-secondary Muted fill, for secondary actions. .btn-outline Bordered, transparent background. .btn-ghost No border or fill until hovered. .btn-danger Destructive actions. .btn-link Looks like a text link, keeps button semantics. .btn-xs / .btn-sm / .btn-lg 24px, 32px or 44px tall instead of the default 36px. .btn-icon Square button for a single icon. Combine with a size. .btn-rounded Fully rounded ends; a circle with .btn-icon. .btn-no-press No 1px nudge when pressed. aria-busy=\"true\" Loading: ignores clicks without fading."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/button-group",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/button-group",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/button-group.md",
      "title": "Button group",
      "description": "A container that joins related buttons, inputs and menus into one unit with shared borders.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "accessibility",
          "text": "Accessibility"
        },
        {
          "level": 2,
          "id": "orientation",
          "text": "Orientation"
        },
        {
          "level": 2,
          "id": "size",
          "text": "Size"
        },
        {
          "level": 2,
          "id": "icons",
          "text": "Icons"
        },
        {
          "level": 2,
          "id": "separator",
          "text": "Separator"
        },
        {
          "level": 2,
          "id": "nested",
          "text": "Nested"
        },
        {
          "level": 2,
          "id": "input",
          "text": "With an input"
        },
        {
          "level": 3,
          "id": "input-group",
          "text": "With an input group"
        },
        {
          "level": 2,
          "id": "split",
          "text": "Split button"
        },
        {
          "level": 2,
          "id": "popover",
          "text": "Popover"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"btn-group\" role=\"group\" aria-label=\"Message actions\"> <button class=\"btn btn-outline\">Archive</button> <button class=\"btn btn-outline\">Report</button> <button class=\"btn btn-outline\">Snooze</button> </div> Use a button group for buttons that perform actions. For buttons that switch a state on and off (bold, italic, text alignment), use a toggle group."
        },
        {
          "id": "accessibility",
          "title": "Accessibility",
          "text": "Give the container role=\"group\" and an aria-label so assistive technology announces the buttons as a set. For a group of groups, use role=\"toolbar\" on the outer one."
        },
        {
          "id": "orientation",
          "title": "Orientation",
          "text": "Add .btn-group-vertical to stack the buttons. html <div class=\"btn-group btn-group-vertical\" role=\"group\" aria-label=\"Zoom\"> <button class=\"btn btn-outline btn-icon\" aria-label=\"Zoom in\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5v14\"/></svg></button> <button class=\"btn btn-outline btn-icon\" aria-label=\"Zoom out\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14\"/></svg></button> </div> <div class=\"btn-group btn-group-vertical\" role=\"group\" aria-label=\"Views\"> <button class=\"btn btn-outline\">Day</button> <button class=\"btn btn-outline\">Week</button> <button class=\"btn btn-outline\">Month</button> </div>"
        },
        {
          "id": "size",
          "title": "Size",
          "text": ".btn-group-sm and .btn-group-lg resize every button, input and text in the group, so you don't size them one by one. html <div class=\"btn-group btn-group-sm\" role=\"group\" aria-label=\"Small\"> <button class=\"btn btn-outline\">Small</button> <button class=\"btn btn-outline\">Group</button> <button class=\"btn btn-outline btn-icon\" aria-label=\"Add\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5v14\"/></svg></button> </div> <div class=\"btn-group\" role=\"group\" aria-label=\"Default\"> <button class=\"btn btn-outline\">Default</button> <button class=\"btn btn-outline\">Group</button> <button class=\"btn btn-outline btn-icon\" aria-label=\"Add\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5v14\"/></svg></button> </div> <div class=\"btn-group btn-group-lg\" role=\"group\" aria-label=\"Large\"> <button class=\"btn btn-outline\">Large</button> <button class=\"btn btn-outline\">Group</button> <button class=\"btn btn-outline btn-icon\" aria-label=\"Add\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5v14\"/></svg></button> </div>"
        },
        {
          "id": "icons",
          "title": "Icons",
          "text": "html <div class=\"btn-group\" role=\"group\" aria-label=\"Post\"> <button class=\"btn btn-outline\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z\"/></svg> Star</button> <button class=\"btn btn-outline\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z\"/></svg> Save</button> <button class=\"btn btn-outline btn-icon\" aria-label=\"Copy link\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></svg></button> </div>"
        },
        {
          "id": "separator",
          "title": "Separator",
          "text": "Buttons without a border of their own (secondary, primary) run into each other. Put a .btn-group-separator between them. html <div class=\"btn-group\" role=\"group\" aria-label=\"Clipboard\"> <button class=\"btn btn-secondary\">Copy</button> <div class=\"btn-group-separator\" role=\"separator\"></div> <button class=\"btn btn-secondary\">Paste</button> </div> <div class=\"btn-group btn-group-vertical\" role=\"group\" aria-label=\"Order\"> <button class=\"btn btn-secondary btn-icon\" aria-label=\"Move up\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m18 15-6-6-6 6\"/></svg></button> <div class=\"btn-group-separator\" role=\"separator\"></div> <button class=\"btn btn-secondary btn-icon\" aria-label=\"Move down\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg></button> </div>"
        },
        {
          "id": "nested",
          "title": "Nested",
          "text": "Groups inside a group are spaced apart instead of joined: a toolbar. html <div class=\"btn-group\" role=\"toolbar\" aria-label=\"Pagination\"> <div class=\"btn-group\" role=\"group\" aria-label=\"Pages\"> <button class=\"btn btn-outline btn-sm\">1</button> <button class=\"btn btn-outline btn-sm\">2</button> <button class=\"btn btn-outline btn-sm\">3</button> </div> <div class=\"btn-group\" role=\"group\" aria-label=\"Step\"> <button class=\"btn btn-outline btn-sm btn-icon\" aria-label=\"Previous\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M19 12H5M12 19l-7-7 7-7\"/></svg></button> <button class=\"btn btn-outline btn-sm btn-icon\" aria-label=\"Next\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5l7 7-7 7\"/></svg></button> </div> </div>"
        },
        {
          "id": "input",
          "title": "With an input",
          "text": "Inputs and selects join the group and fill the space left by the buttons. html <div class=\"btn-group w-full max-w-sm\" role=\"group\" aria-label=\"Search\"> <input class=\"input\" type=\"search\" placeholder=\"Search…\" aria-label=\"Search\" /> <button class=\"btn btn-outline btn-icon\" aria-label=\"Search\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg></button> </div> <div class=\"btn-group w-full max-w-sm\" role=\"group\" aria-label=\"Website\"> <span class=\"btn-group-text\">https://</span> <input class=\"input\" placeholder=\"example.com\" aria-label=\"Website\" /> <div class=\"select w-24 flex-none\" data-select> <button type=\"button\" id=\"bg-domain\" class=\"select-trigger\" data-select-trigger aria-haspopup=\"listbox\" aria-expanded=\"false\" aria-label=\"Domain\"> <span class=\"select-value\" data-placeholder=\"Select…\">.com</span> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg> </button> <div class=\"select-content select-content-end\" id=\"bg-domain-listbox\" role=\"listbox\" aria-label=\"Domain\" hidden> <span class=\"select-item\" role=\"option\" value=\".com\" aria-selected=\"true\">.com<span class=\"select-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span></span> <span class=\"select-item\" role=\"option\" value=\".org\" aria-selected=\"false\">.org<span class=\"select-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" s"
        },
        {
          "id": "input-group",
          "title": "With an input group",
          "text": "An input group can sit in a button group too, with addons of its own. html <div class=\"btn-group w-full max-w-md\" role=\"group\" aria-label=\"Message\"> <button class=\"btn btn-outline btn-icon\" aria-label=\"Attach\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5v14\"/></svg></button> <div class=\"input-group\"> <input class=\"input\" placeholder=\"Send a message…\" aria-label=\"Message\" /> <span class=\"input-group-addon input-group-addon-inline-end\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner\" role=\"status\" aria-label=\"Loading\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg></span> </div> <button class=\"btn btn-primary btn-icon\" aria-label=\"Send\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z\"/><path d=\"m21.854 2.147-10.94 10.939\"/></svg></button> </div>"
        },
        {
          "id": "split",
          "title": "Split button",
          "text": "A main action plus a dropdown of related ones. The .dropdown wrapper joins the group like a button. html <div class=\"btn-group\" role=\"group\" aria-label=\"Merge\"> <button class=\"btn btn-primary\">Merge pull request</button> <div class=\"btn-group-separator\" role=\"separator\"></div> <div class=\"dropdown\" data-dropdown> <button class=\"btn btn-primary btn-icon\" data-dropdown-trigger aria-haspopup=\"menu\" aria-expanded=\"false\" aria-label=\"More merge options\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg></button> <div class=\"dropdown-menu dropdown-menu-end\" role=\"menu\" hidden> <button class=\"dropdown-item\" role=\"menuitem\">Squash and merge</button> <button class=\"dropdown-item\" role=\"menuitem\">Rebase and merge</button> </div> </div> </div> <div class=\"btn-group\" role=\"group\" aria-label=\"Follow\"> <button class=\"btn btn-outline\">Follow</button> <div class=\"dropdown\" data-dropdown> <button class=\"btn btn-outline btn-icon\" data-dropdown-trigger aria-haspopup=\"menu\" aria-expanded=\"false\" aria-label=\"More\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"19\" cy=\"12\" r=\"1\"/><circle cx=\"5\" cy=\"12\" r=\"1\"/></svg></button> <div class=\"dropdown-menu dropdown-menu-end\" role=\"menu\" hidden> <button class=\"dropdown-item\" role=\"menuitem\">Mute conversation</button> <button class=\"dropdown-item\" role=\"menuitem\">Copy link</button> <div class=\"dropdown-separator\" role=\"separator\"></div> <button class=\"dropdown-item dropdown-item-destructive\" role=\"menuitem\">Report</button> </div> </div> </div>"
        },
        {
          "id": "popover",
          "title": "Popover",
          "text": "A popover trigger works the same way, for content that isn't a menu. html <div class=\"btn-group\" role=\"group\" aria-label=\"Copilot\"> <button class=\"btn btn-outline\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12 8V4H8\"/><rect width=\"16\" height=\"12\" x=\"4\" y=\"8\" rx=\"2\"/><path d=\"M2 14h2M20 14h2M15 13v2M9 13v2\"/></svg> Copilot</button> <div class=\"popover\" data-popover> <button class=\"btn btn-outline btn-icon\" data-popover-trigger aria-expanded=\"false\" aria-label=\"Open popover\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg></button> <div class=\"popover-content popover-content-end\" data-popover-content hidden> <p class=\"popover-title\">Start a new task</p> <p class=\"popover-description\">Describe what you want done.</p> <textarea class=\"textarea mt-3\" placeholder=\"Fix the flaky test in…\" aria-label=\"Task\" autofocus></textarea> </div> </div> </div>"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .btn-group Joins its children: inner corners squared, shared borders overlapped. Groups inside it are spaced instead. .btn-group-vertical Stacks the children. .btn-group-sm / -lg Resizes every button, input and text in the group. .btn-group-text Static label styled to sit in a group, like https://. .btn-group-separator 1px divider between buttons that have no border."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/clipboard",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/clipboard",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/clipboard.md",
      "title": "Clipboard",
      "description": "Copy buttons and copy tags: copy text, an input's value or an element's text, and show a green tick for a moment, on every browser.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "labels",
          "text": "Icon, label or both"
        },
        {
          "level": 2,
          "id": "variants",
          "text": "Variants"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Sizes"
        },
        {
          "level": 2,
          "id": "tag",
          "text": "Copy tag"
        },
        {
          "level": 2,
          "id": "source",
          "text": "What it copies"
        },
        {
          "level": 2,
          "id": "browsers",
          "text": "Browsers and platforms"
        },
        {
          "level": 2,
          "id": "events",
          "text": "Events and htmx"
        },
        {
          "level": 2,
          "id": "macro",
          "text": "Nunjucks macros"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <button type=\"button\" class=\"btn btn-outline clipboard\" data-clipboard data-clipboard-text=\"npm i htmx-ui\"> <span class=\"clipboard-idle\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg> Copy</span> <span class=\"clipboard-done\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg> Copied</span> </button> <button type=\"button\" class=\"btn btn-outline btn-icon clipboard\" data-clipboard data-clipboard-text=\"npm i htmx-ui\" aria-label=\"Copy\"> <span class=\"clipboard-idle\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg></span> <span class=\"clipboard-done\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span> </button> <button type=\"button\" class=\"clipboard-tag\" data-clipboard aria-label=\"Copy npm i htmx-ui\"> <code data-clipboard-content>npm i htmx-ui</code> <span class=\"clipboard\" aria-hidden=\"true\"><span class=\"clipboard-idle\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\""
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "Any button with data-clipboard copies when clicked. Say what with data-clipboard-text (see What it copies). Add .clipboard and put both of its looks inside: .clipboard-idle (a copy icon, a label or both) and .clipboard-done (a tick, \"Copied\"). After a copy the button gets data-copied for two seconds, which shows .clipboard-done with a green tick. The two looks share one space, so the button keeps its width when \"Copy\" becomes \"Copied\". An icon-only button needs aria-label=\"Copy\" (or \"Copy link\", \"Copy API key\"…). Screen readers hear \"Copied\" (or \"Copy failed\") through a polite live region, because a button's changed text isn't read out by itself. Set data-clipboard-announce to say something else, and data-clipboard-timeout (ms) to keep the tick longer."
        },
        {
          "id": "labels",
          "title": "Icon, label or both",
          "text": "Leave out what you don't need: the icons, the labels, or one of each. html <button type=\"button\" class=\"btn btn-outline clipboard\" data-clipboard data-clipboard-text=\"Icon and label\"> <span class=\"clipboard-idle\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg>Copy</span> <span class=\"clipboard-done\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg>Copied</span> </button> <button type=\"button\" class=\"btn btn-outline btn-icon clipboard\" data-clipboard data-clipboard-text=\"Icon only\" aria-label=\"Copy\"> <span class=\"clipboard-idle\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg></span> <span class=\"clipboard-done\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span> </button> <button type=\"button\" class=\"btn btn-outline clipboard\" data-clipboard data-clipboard-text=\"Label only\"> <span class=\"clipboard-idle\">Copy</span> <span class=\"clipboard-done\">Copied</span> </button> <button type=\"button\" class=\"btn btn-outline clipboard\" data-clipboard data-clipboard-text=\"Custom labels\"> <span class=\"clipboard-idle\"><"
        },
        {
          "id": "variants",
          "title": "Variants",
          "text": "Every button variant works. The tick is green on outline, ghost, secondary and link buttons; on a filled primary or danger button it keeps the button's text colour, so it stays readable. html <button type=\"button\" class=\"btn btn-outline clipboard\" data-clipboard data-clipboard-text=\"Outline\"> <span class=\"clipboard-idle\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg>Copy</span> <span class=\"clipboard-done\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg>Copied</span> </button> <button type=\"button\" class=\"btn btn-secondary clipboard\" data-clipboard data-clipboard-text=\"Secondary\"> <span class=\"clipboard-idle\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg>Copy</span> <span class=\"clipboard-done\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg>Copied</span> </button> <button type=\"button\" class=\"btn btn-ghost clipboard\" data-clipboard data-clipboard-text=\"Ghost\"> <span class=\"clipboard-idle\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor"
        },
        {
          "id": "sizes",
          "title": "Sizes",
          "text": "The button sizes: .btn-xs, .btn-sm, default and .btn-lg, with or without .btn-icon. html <div class=\"flex items-center gap-3\"> <button type=\"button\" class=\"btn btn-outline btn-xs clipboard\" data-clipboard data-clipboard-text=\"xs\"> <span class=\"clipboard-idle\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg>Copy</span> <span class=\"clipboard-done\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg>Copied</span> </button> <button type=\"button\" class=\"btn btn-outline btn-sm clipboard\" data-clipboard data-clipboard-text=\"sm\"> <span class=\"clipboard-idle\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg>Copy</span> <span class=\"clipboard-done\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg>Copied</span> </button> <button type=\"button\" class=\"btn btn-outline clipboard\" data-clipboard data-clipboard-text=\"default\"> <span class=\"clipboard-idle\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"ro"
        },
        {
          "id": "tag",
          "title": "Copy tag",
          "text": "A .clipboard-tag is a monospace chip that copies the text it shows: an install command, an id, a coupon code. The whole chip is the button. Put the text in a data-clipboard-content element and the two icons in a .clipboard span; give the button an aria-label. Long text is cut short with an ellipsis, but the whole of it is copied. html <div class=\"flex flex-wrap items-center justify-center gap-3\"> <button type=\"button\" class=\"clipboard-tag clipboard-tag-sm\" data-clipboard aria-label=\"Copy bun add htmx-ui\"> <code data-clipboard-content>bun add htmx-ui</code> <span class=\"clipboard\" aria-hidden=\"true\"><span class=\"clipboard-idle\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg></span><span class=\"clipboard-done\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span></span> </button> <button type=\"button\" class=\"clipboard-tag\" data-clipboard aria-label=\"Copy bun add htmx-ui\"> <code data-clipboard-content>bun add htmx-ui</code> <span class=\"clipboard\" aria-hidden=\"true\"><span class=\"clipboard-idle\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg></span><span class=\"clipboard-done\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-widt"
        },
        {
          "id": "source",
          "title": "What it copies",
          "text": "The first of these that is set: Attribute Copies data-clipboard-text=\"…\" That text. data-clipboard-url=\"/page.md\" The text of that URL, fetched when clicked. The docs' \"Copy Markdown\" button works this way. data-clipboard-target=\"selector\" The nearest element matching the selector, looking outward from the button: its own block first, then further out, so #id works and so does a class shared by many blocks (.message-content, or pre:not([hidden]) code in code blocks). For an input, textarea or select, its value, read when clicked (so it copies what the user typed); for any other element, its text. data-clipboard-content The text of that element inside the button (a copy tag). An input with a copy button: a read-only API key in an input group, and a share link the user can edit before copying. html <div class=\"field w-full max-w-sm\"> <label class=\"label\" for=\"clip-key\">API key</label> <div class=\"input-group\"> <input class=\"input font-mono\" id=\"clip-key\" value=\"hx_live_7d2f9a41c0b8e6\" readonly /> <span class=\"input-group-addon input-group-addon-inline-end\"> <button type=\"button\" class=\"btn btn-ghost btn-icon clipboard\" data-clipboard data-clipboard-target=\"#clip-key\" aria-label=\"Copy API key\"> <span class=\"clipboard-idle\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg></span> <span class=\"clipboard-done\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span> </button> </span> </div> </div> <div class=\"field w-full max-w-sm\"> <label class=\"label\" for=\"clip-link\">Share link</label> <div c"
        },
        {
          "id": "browsers",
          "title": "Browsers and platforms",
          "text": "Secure pages (https, and localhost while developing) copy with the asynchronous Clipboard API, supported by every current browser on desktop, Android and iOS. Plain http, older browsers and iframes without the clipboard-write permission fall back to document.execCommand(\"copy\") on a hidden, read-only textarea. It is selected the way iOS Safari needs, doesn't open the phone keyboard or zoom the page, and focus goes back where it was. If both fail (a browser that blocks clipboard access), the button gets data-copy-failed for the same two seconds, its icon turns red, \"Copy failed\" is announced and clipboard:error fires. Browsers only allow copying in response to the user, so call it from a click, not on page load. With data-clipboard-url, the fetch is handed to the clipboard as a promise where the browser accepts one (Safari, Chrome), so the click still counts once the text arrives."
        },
        {
          "id": "events",
          "title": "Events and htmx",
          "text": "A copy fires a bubbling clipboard:copy with detail.text (or clipboard:error). Use it as an htmx trigger to record the copy, or add a toast with data-toast on the same button: html <button type=\"button\" class=\"btn btn-outline clipboard\" data-clipboard data-clipboard-text=\"https://example.com/r/8f2a\" data-toast=\"Link copied\" data-toast-type=\"success\"> <span class=\"clipboard-idle\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg>Copy referral link</span> <span class=\"clipboard-done\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg>Copied</span> </button> Count copies on the server <button type=\"button\" class=\"btn btn-outline clipboard\" data-clipboard data-clipboard-text=\"WELCOME20\" hx-post=\"/coupons/WELCOME20/copied\" hx-trigger=\"clipboard:copy\" hx-swap=\"none\">…</button> Copy buttons htmx swaps in work at once: the behaviour runs again on new content."
        },
        {
          "id": "macro",
          "title": "Nunjucks macros",
          "text": "clipboard() writes a copy button, clipboard_tag() a copy tag. page.html {% from \"components/clipboard/clipboard.html\" import clipboard, clipboard_tag %} {{ clipboard(text=\"npm i htmx-ui\") }} {{ clipboard(target=\"#api-key\", icon_only=true, variant=\"ghost\", size=\"sm\", label=\"Copy API key\") }} {{ clipboard(text=url, label=\"Copy link\", copied=\"Link copied\", variant=\"secondary\", timeout=3000) }} {{ clipboard(text=code, icon=false, copied_icon=false) }} {{ clipboard_tag(\"npm i htmx-ui\") }} {{ clipboard_tag(\"hx_secret_••••3f9a\", copy=key, variant=\"outline\", size=\"sm\", label=\"Copy secret key\") }}"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class / attribute Description data-clipboard Copy when clicked. On a button or a .clipboard-tag. .clipboard Stacks .clipboard-idle and .clipboard-done in one space. On the button, or a span inside a tag. .clipboard-idle / .clipboard-done The look before / after copying (shown while data-copied is set). .clipboard-tag A monospace chip that copies its text. Sizes: .clipboard-tag-sm / -lg. .clipboard-tag-outline. data-clipboard-text The text to copy. data-clipboard-target A selector: copy the nearest match's value or text. data-clipboard-url Copy the text of this URL, fetched when clicked. data-clipboard-content Inside the button: copy this element's text. data-clipboard-timeout How long data-copied stays, in ms (2000). data-clipboard-announce What screen readers hear (\"Copied\"). data-copied / data-copy-failed Set for the timeout after a copy / a failed copy. clipboard:copy / clipboard:error Events with detail { text }."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/command",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/command",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/command.md",
      "title": "Command",
      "description": "A command menu for search and quick actions: filtered as you type, driven by the keyboard, with groups, icons, shortcuts and separators, inline or in a dialog opened with ⌘K.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "dialog",
          "text": "In a dialog"
        },
        {
          "level": 2,
          "id": "shortcuts",
          "text": "Keyboard shortcuts"
        },
        {
          "level": 2,
          "id": "scrollable",
          "text": "Scrollable"
        },
        {
          "level": 2,
          "id": "server",
          "text": "Searching on the server"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"command mx-auto max-w-md shadow-md\" data-command> <div class=\"command-input-wrapper\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg> <input class=\"command-input\" data-command-input placeholder=\"Type a command or search…\" aria-label=\"Search commands\" /> </div> <div class=\"command-list\" aria-label=\"Commands\"> <div class=\"command-group\" role=\"group\" aria-labelledby=\"cmd-demo-suggestions\"> <div class=\"command-label\" id=\"cmd-demo-suggestions\">Suggestions</div> <div class=\"command-item\" data-value=\"calendar\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"4\" rx=\"2\"/><path d=\"M16 2v4\"/><path d=\"M8 2v4\"/><path d=\"M3 10h18\"/></svg> Calendar</div> <div class=\"command-item\" data-value=\"emoji\" data-keywords=\"smiley face\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12 3l1.9 5.8L20 11l-6.1 2.2L12 19l-1.9-5.8L4 11l6.1-2.2z\"/><path d=\"M19 3v4M17 5h4\"/></svg> Search emoji</div> <div class=\"command-item\" data-value=\"calculator\" aria-disabled=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12 20V10\"/><path d=\"M18 20V4\"/><path d=\"M6 20v-4\"/></svg> Calculator</div> </div> <div class=\"command-separator\" role=\"separator\"></div> <div class=\"command-group\" role=\"group\" aria-labelledby=\"c"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": ".command holds a .command-input (in a .command-input-wrapper with an icon) and a .command-list of .command-items, optionally in .command-groups with a .command-label, split by .command-separators. data-command wires it up. Typing filters the items by their text, data-value and data-keywords, matching words or letters in order (\"stng\" finds Settings); empty groups and stray separators hide, and [data-command-empty] shows when nothing matches. ↑ ↓ (wrapping), Home End and the pointer move the highlight; Enter activates it. The input is a combobox with aria-activedescendant, so screen readers follow the highlight. aria-disabled=\"true\" items are skipped. Activating an item clicks it, so a link navigates and an item with hx-get sends its request. It also fires command:select on the item, with detail.value."
        },
        {
          "id": "dialog",
          "title": "In a dialog",
          "text": "Put the command in a <dialog class=\"dialog command-dialog\" data-dialog> and open it with a commandfor button. data-command-hotkey=\"mod+k\" on the command opens it from anywhere on the page (⌘+K on a Mac, Ctrl+K elsewhere; the key closes it again). Each time it opens the query clears; choosing an item closes it, unless the item or the command has data-command-keep-open. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"command-dialog-demo\" command=\"show-modal\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg> Open menu <span class=\"kbd-group\"><kbd class=\"kbd\">Ctrl</kbd><kbd class=\"kbd\">J</kbd></span> </button> <dialog id=\"command-dialog-demo\" class=\"dialog command-dialog\" data-dialog aria-label=\"Command menu\"> <div class=\"command\" data-command data-command-hotkey=\"mod+j\"> <div class=\"command-input-wrapper\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg> <input class=\"command-input\" data-command-input placeholder=\"Type a command or search…\" aria-label=\"Search commands\" autofocus /> </div> <div class=\"command-list\" aria-label=\"Commands\"> <div class=\"command-group\" role=\"group\" aria-labelledby=\"cmd-dialog-pages\"> <div class=\"command-label\" id=\"cmd-dialog-pages\">Pages</div> <a class=\"command-item\" href=\"/docs\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2zM22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3"
        },
        {
          "id": "shortcuts",
          "title": "Keyboard shortcuts",
          "text": "data-command-hotkey on the .command: opens its dialog, or focuses its input when it isn't in one. A key with no modifier (such as /) is ignored while typing in another field. data-command-hotkey on an item: activates it while focus is in the command (mod+shift+c above). Combos are keys joined with +: mod (⌘ on Apple devices, Ctrl elsewhere), ctrl, meta, alt, shift and the key. Show a shortcut with a .command-shortcut at the end of the item; it is only a label."
        },
        {
          "id": "scrollable",
          "title": "Scrollable",
          "text": "The list scrolls past max-h-72 (in a dialog, 24rem or 60% of the screen); the highlight keeps itself in view without scrolling the page. Set another max-h-* on .command-list. html <div class=\"command mx-auto max-w-sm shadow-md\" data-command> <div class=\"command-input-wrapper\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg> <input class=\"command-input\" data-command-input placeholder=\"Search people…\" aria-label=\"Search people\" /> </div> <div class=\"command-list max-h-56\" aria-label=\"People\"> <div class=\"command-group\" role=\"group\" aria-label=\"People\"> <div class=\"command-item\" data-value=\"Ada Lovelace\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/></svg> Ada Lovelace</div> <div class=\"command-item\" data-value=\"Alan Turing\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/></svg> Alan Turing</div> <div class=\"command-item\" data-value=\"Barbara Liskov\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/></svg> Barbara Liskov</div> <div class=\"command-item\" data-value=\"Claude Shannon\"><svg xmlns=\"http://www.w3.or"
        },
        {
          "id": "server",
          "title": "Searching on the server",
          "text": "data-command-filter=\"false\" leaves filtering to the server: an hx-get on the input swaps in the matching items, and the highlight follows whatever the list holds. html <div class=\"command mx-auto max-w-sm shadow-md\" data-command data-command-filter=\"false\"> <div class=\"command-input-wrapper\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg> <input class=\"command-input\" name=\"q\" data-command-input placeholder=\"Search countries…\" aria-label=\"Search countries\" hx-get=\"/api/commands\" hx-trigger=\"input changed delay:200ms, load\" hx-target=\"next .command-list\" /> </div> <div class=\"command-list\" aria-label=\"Countries\"></div> <p class=\"command-empty\" data-command-empty hidden>No country found.</p> </div>"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .command The menu: a bordered panel of an input and a list. .command-input-wrapper / .command-input The search row and its borderless input. .command-list The scrolling list of items and groups (max-h-72). .command-group / .command-label A group of items and its heading. .command-item An item (div, a or button); highlighted with data-highlighted. .command-shortcut A shortcut label at the end of an item. .command-separator A line between groups. .command-empty Shown when nothing matches ([data-command-empty]). .command-footer A row of hints under the list. .command-dialog On a .dialog: no padding, the command fills it. [data-command] Behaviour: filtering, keyboard, selection. [data-keywords] / [data-value] More words to match, and the value command:select reports. [data-command-hotkey] On the command: opens its dialog. On an item: activates it. [data-command-filter=\"false\"] Leave filtering to the server. [data-command-keep-open] Don't close the dialog when an item is chosen. command:select Event on the chosen item; detail: { value, item }."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/dropdown",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/dropdown",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/dropdown.md",
      "title": "Dropdown",
      "description": "A menu of links, actions and options that opens from a button, with checkbox and radio items, shortcuts and submenus. Keyboard and screen-reader friendly. The docs version switcher in the sidebar is one.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "icons",
          "text": "Icons and shortcuts"
        },
        {
          "level": 2,
          "id": "checkbox-items",
          "text": "Checkbox items"
        },
        {
          "level": 2,
          "id": "radio-items",
          "text": "Radio items"
        },
        {
          "level": 2,
          "id": "submenus",
          "text": "Submenus"
        },
        {
          "level": 2,
          "id": "destructive",
          "text": "Destructive and disabled items"
        },
        {
          "level": 2,
          "id": "alignment",
          "text": "Alignment"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "Actions with htmx"
        },
        {
          "level": 2,
          "id": "keyboard",
          "text": "Keyboard"
        },
        {
          "level": 2,
          "id": "accessibility",
          "text": "Accessibility"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"dropdown\" data-dropdown> <button type=\"button\" class=\"btn btn-outline\" data-dropdown-trigger aria-haspopup=\"menu\" aria-expanded=\"false\"> Options <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg> </button> <div class=\"dropdown-menu w-56\" role=\"menu\" hidden> <p class=\"dropdown-label\">My account</p> <a class=\"dropdown-item\" role=\"menuitem\" href=\"#profile\">Profile <span class=\"dropdown-shortcut\">⇧⌘P</span></a> <a class=\"dropdown-item\" role=\"menuitem\" href=\"#billing\">Billing <span class=\"badge badge-secondary\">Pro</span></a> <a class=\"dropdown-item\" role=\"menuitem\" href=\"#settings\">Settings <span class=\"dropdown-shortcut\">⌘,</span></a> <div class=\"dropdown-separator\" role=\"separator\"></div> <a class=\"dropdown-item\" role=\"menuitem\" href=\"#team\">Team</a> <div class=\"dropdown-sub\"> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\" aria-haspopup=\"menu\" aria-expanded=\"false\">Invite users <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg></button> <div class=\"dropdown-menu\" role=\"menu\" hidden> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">Email</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">Message</button> <div class=\"dropdown-separator\" role=\"separator\"></div> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">More…</button> </div> </div> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\" aria-disabled=\"true\">API</button> <div class=\"dropdown-separator\" role=\"separator\"></div> <button type=\"button\" class=\"dropdown-item dropdown-item-destructive\" role=\"menuitem\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\""
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "Wrap the trigger and menu in .dropdown with data-dropdown. The trigger gets data-dropdown-trigger, aria-haspopup=\"menu\" and aria-expanded=\"false\". The menu gets role=\"menu\" and starts hidden. Each entry is a link or button with role=\"menuitem\". Mark the selected entry with aria-current=\"true\", and unavailable ones with aria-disabled=\"true\". Add a .dropdown-label heading and .dropdown-separator lines (role=\"separator\") between sections as needed. The behaviour sets the rest: menu items get tabindex=\"-1\" (focus moves with the arrow keys) and the trigger is linked to the menu with aria-controls. The same menu classes and behaviour make up each menu of a menubar."
        },
        {
          "id": "icons",
          "title": "Icons and shortcuts",
          "text": "Put an icon() before an item's text; icons in items are muted and sized to match. .dropdown-shortcut right-aligns a keyboard hint (a .kbd or a .badge sits there too). The hint is only text: the dropdown doesn't bind the key. If your page handles it, say so with aria-keyshortcuts on the item. html <div class=\"dropdown\" data-dropdown> <button type=\"button\" class=\"btn btn-outline\" data-dropdown-trigger aria-haspopup=\"menu\" aria-expanded=\"false\">Edit <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg></button> <div class=\"dropdown-menu w-56\" role=\"menu\" hidden> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg> Copy <span class=\"dropdown-shortcut\">⌘C</span></button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></svg> Copy link <span class=\"dropdown-shortcut\">⇧⌘C</span></button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M21 15v4a2 2 0 0 "
        },
        {
          "id": "checkbox-items",
          "title": "Checkbox items",
          "text": "An item with role=\"menuitemcheckbox\" and aria-checked=\"true\" or \"false\" toggles when chosen, showing a check in an inset slot on the left. Give plain items and labels next to them .dropdown-item-inset so their text lines up. html <div class=\"dropdown\" data-dropdown> <button type=\"button\" class=\"btn btn-outline\" data-dropdown-trigger aria-haspopup=\"menu\" aria-expanded=\"false\">Appearance <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg></button> <div class=\"dropdown-menu w-56\" role=\"menu\" hidden> <p class=\"dropdown-label dropdown-item-inset\">Show</p> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemcheckbox\" aria-checked=\"true\">Status bar</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemcheckbox\" aria-checked=\"false\" aria-disabled=\"true\">Activity bar</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemcheckbox\" aria-checked=\"false\">Panel</button> <div class=\"dropdown-separator\" role=\"separator\"></div> <button type=\"button\" class=\"dropdown-item dropdown-item-inset\" role=\"menuitem\">Reset</button> </div> </div>"
        },
        {
          "id": "radio-items",
          "title": "Radio items",
          "text": "Items with role=\"menuitemradio\" inside a role=\"group\" are a set: choosing one checks it and unchecks the rest of the group (without a group, the rest of the menu). Label the group with aria-label, or with aria-labelledby pointing at its .dropdown-label. html <div class=\"dropdown\" data-dropdown> <button type=\"button\" class=\"btn btn-outline\" data-dropdown-trigger aria-haspopup=\"menu\" aria-expanded=\"false\">Panel position <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg></button> <div class=\"dropdown-menu w-56\" role=\"menu\" hidden> <p class=\"dropdown-label dropdown-item-inset\" id=\"dropdown-position-label\">Panel position</p> <div role=\"group\" aria-labelledby=\"dropdown-position-label\"> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemradio\" aria-checked=\"true\">Top</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemradio\" aria-checked=\"false\">Bottom</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemradio\" aria-checked=\"false\">Right</button> </div> </div> </div> Choosing a checkbox or radio item closes the menu like any other item. Add data-keep-open to an item, a group or the whole menu to keep it open instead, so several options can be set in a row: html <div class=\"dropdown\" data-dropdown> <button type=\"button\" class=\"btn btn-outline\" data-dropdown-trigger aria-haspopup=\"menu\" aria-expanded=\"false\">Columns <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg></button> <div class=\"dropdown-menu\" role=\"menu\" data-keep-open hidden> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemcheckbox\" aria-checked=\"true\">Name</button> <button type=\"button\""
        },
        {
          "id": "submenus",
          "title": "Submenus",
          "text": "Wrap an item and a nested menu in .dropdown-sub. The item gets aria-haspopup=\"menu\", aria-expanded=\"false\" and a chevron-right icon, which sits at the right edge. The submenu opens beside it on hover (after a short delay), on click, or with →, Enter or Space; ← and Esc close it. Near the edge of the window it opens to the left or upwards instead. html <div class=\"dropdown\" data-dropdown> <button type=\"button\" class=\"btn btn-outline\" data-dropdown-trigger aria-haspopup=\"menu\" aria-expanded=\"false\">Share <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg></button> <div class=\"dropdown-menu\" role=\"menu\" hidden> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></svg> Copy link</button> <div class=\"dropdown-sub\"> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\" aria-haspopup=\"menu\" aria-expanded=\"false\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z\"/><path d=\"m21.854 2.147-10.94 10.939\"/></svg> Send to <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\""
        },
        {
          "id": "destructive",
          "title": "Destructive and disabled items",
          "text": ".dropdown-item-destructive colours an item that deletes or discards something. aria-disabled=\"true\" (or disabled on a button) fades an item out; it can't be chosen and the arrow keys skip it. html <div class=\"dropdown\" data-dropdown> <button type=\"button\" class=\"btn btn-outline btn-icon\" data-dropdown-trigger aria-haspopup=\"menu\" aria-expanded=\"false\" aria-label=\"Project actions\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"19\" cy=\"12\" r=\"1\"/><circle cx=\"5\" cy=\"12\" r=\"1\"/></svg></button> <div class=\"dropdown-menu\" role=\"menu\" hidden> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">Rename</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\" aria-disabled=\"true\">Transfer</button> <div class=\"dropdown-separator\" role=\"separator\"></div> <button type=\"button\" class=\"dropdown-item dropdown-item-destructive\" role=\"menuitem\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"/></svg> Delete project</button> </div> </div>"
        },
        {
          "id": "alignment",
          "title": "Alignment",
          "text": "The menu opens below the trigger, aligned to its left edge. Add .dropdown-menu-end to align it to the right edge, or .dropdown-menu-up to open it above the trigger. html <div class=\"dropdown\" data-dropdown> <button type=\"button\" class=\"btn btn-ghost btn-icon\" data-dropdown-trigger aria-haspopup=\"menu\" aria-expanded=\"false\" aria-label=\"More\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M4 6h16M4 12h16M4 18h16\"/></svg></button> <div class=\"dropdown-menu dropdown-menu-end\" role=\"menu\" hidden> <a class=\"dropdown-item\" role=\"menuitem\" href=\"#edit\">Edit</a> <a class=\"dropdown-item\" role=\"menuitem\" href=\"#duplicate\">Duplicate</a> <a class=\"dropdown-item\" role=\"menuitem\" href=\"#archive\" aria-disabled=\"true\">Archive</a> </div> </div>"
        },
        {
          "id": "htmx",
          "title": "Actions with htmx",
          "text": "Menu items can make htmx requests like any other button. The menu closes when an item is chosen. html <div class=\"dropdown\" data-dropdown> <button type=\"button\" class=\"btn btn-primary\" data-dropdown-trigger aria-haspopup=\"menu\" aria-expanded=\"false\">Export <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg></button> <div class=\"dropdown-menu\" role=\"menu\" hidden> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\" hx-post=\"/api/slow\" hx-target=\"#export-result\">CSV</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\" hx-post=\"/api/slow\" hx-target=\"#export-result\">JSON</button> </div> </div> <span id=\"export-result\" class=\"muted\"></span> Checkbox and radio items fire a bubbling menu:change event when they change, with detail.checked, detail.name and detail.value. Give them a name (and a value; a checkbox item defaults to on) and each keeps a hidden input inside it in step, so a surrounding form submits the checked ones. This form posts on every change: html <form hx-post=\"/api/echo\" hx-trigger=\"menu:change\" hx-target=\"#dropdown-filter-result\"> <div class=\"dropdown\" data-dropdown> <button type=\"button\" class=\"btn btn-outline\" data-dropdown-trigger aria-haspopup=\"menu\" aria-expanded=\"false\">Filter <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg></button> <div class=\"dropdown-menu\" role=\"menu\" hidden> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemcheckbox\" aria-checked=\"true\" name=\"open\">Open issues</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemcheckbox\" aria-checked=\"false\" name=\"closed\">Closed issues</button> <div class=\"dropdown-"
        },
        {
          "id": "keyboard",
          "title": "Keyboard",
          "text": "Key Action Enter Space on the trigger Open on the first item ↓ ↑ on the trigger Open on the first / last item ↓ ↑ Home End Next / previous / first / last item (wraps), skipping disabled ones Letters Next item starting with the typed letters Enter Space Choose the item (toggle a checkbox, check a radio) and close; open a submenu → ← Open / close a submenu Esc Close the innermost menu and return focus to what opened it Tab, click outside Close With the mouse, hovering an item highlights (focuses) it, and hovering a submenu's item opens the submenu."
        },
        {
          "id": "accessibility",
          "title": "Accessibility",
          "text": "The markup follows the WAI-ARIA menu button pattern: menu, menuitem, menuitemcheckbox, menuitemradio, group and separator roles, with aria-expanded, aria-controls and aria-checked kept in step. Name an icon-only trigger with aria-label, and label each radio group. Use a dropdown for actions and navigation. For picking one value of a form field, use a select."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class / attribute Description .dropdown Positioning wrapper (relative, inline-block). .dropdown-menu The floating panel. Opens below, left-aligned. .dropdown-menu-end / .dropdown-menu-up Align to the right edge / open above. .dropdown-item Link or button inside the menu: role=\"menuitem\", \"menuitemcheckbox\" or \"menuitemradio\". aria-current, aria-checked and aria-disabled are styled. .dropdown-item-inset Indents an item or label to line up with checkbox and radio items. .dropdown-item-destructive Danger-coloured item, for deleting or discarding. .dropdown-shortcut Right-aligned, muted keyboard hint inside an item. .dropdown-label Small muted heading inside the menu. .dropdown-separator Divider line (role=\"separator\"). .dropdown-sub Wraps a submenu trigger (aria-haspopup=\"menu\", aria-expanded) and its nested .dropdown-menu. role=\"group\" Groups radio items: checking one unchecks the others in the group. data-keep-open On an item, group or menu: choosing an item doesn't close the menu. name / value On checkbox and radio items: kept in a hidden input so a surrounding form submits them. menu:change Bubbling event from a checkbox or radio item that changed; detail { checked, name, value }. data-flip-x / data-flip-y Set on a submenu that would leave the window: it opens to the left / upwards. [data-dropdown] / [data-dropdown-trigger] Behaviour: open/close, keyboard navigation, typeahead, submenus, checkbox and radio items, outside click."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/menubar",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/menubar",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/menubar.md",
      "title": "Menubar",
      "description": "A visually persistent menu common in desktop applications that provides quick access to a consistent set of commands.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "shortcuts",
          "text": "Shortcuts and separators"
        },
        {
          "level": 2,
          "id": "submenus",
          "text": "Submenus"
        },
        {
          "level": 2,
          "id": "checkbox-items",
          "text": "Checkbox items"
        },
        {
          "level": 2,
          "id": "radio-items",
          "text": "Radio items"
        },
        {
          "level": 2,
          "id": "icons",
          "text": "Icons"
        },
        {
          "level": 2,
          "id": "vertical",
          "text": "Vertical"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx"
        },
        {
          "level": 2,
          "id": "keyboard",
          "text": "Keyboard"
        },
        {
          "level": 2,
          "id": "accessibility",
          "text": "Accessibility"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"menubar\" role=\"menubar\" aria-label=\"Browser\" data-menubar> <div class=\"menubar-menu\"> <button type=\"button\" class=\"menubar-trigger\" role=\"menuitem\" aria-haspopup=\"menu\" aria-expanded=\"false\">File</button> <div class=\"dropdown-menu\" role=\"menu\" hidden> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">New tab <span class=\"dropdown-shortcut\">⌘T</span></button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">New window <span class=\"dropdown-shortcut\">⌘N</span></button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\" aria-disabled=\"true\">New incognito window</button> <div class=\"dropdown-separator\" role=\"separator\"></div> <div class=\"dropdown-sub\"> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\" aria-haspopup=\"menu\" aria-expanded=\"false\">Share <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg></button> <div class=\"dropdown-menu\" role=\"menu\" hidden> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">Email link</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">Messages</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">Notes</button> </div> </div> <div class=\"dropdown-separator\" role=\"separator\"></div> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">Print… <span class=\"dropdown-shortcut\">⌘P</span></button> </div> </div> <div class=\"menubar-menu\"> <button type=\"button\" class=\"menubar-trigger\" role=\"menuitem\" aria-haspopup=\"menu\" aria-expanded=\"false\">Edit</button> <div class=\"dropdown-menu\" role=\"menu\" hidden> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">Undo <span class=\"dropdown-shortcut\">⌘Z</span></button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">Redo <span class=\"dropdown-shortcut\">⇧⌘Z</span></button> <div class=\"dropdown-se"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "The bar is .menubar with role=\"menubar\", data-menubar and an aria-label. Each top-level menu is a .menubar-menu holding its trigger and its panel. The trigger is a <button class=\"menubar-trigger\"> with role=\"menuitem\", aria-haspopup=\"menu\" and aria-expanded=\"false\". The panel is a dropdown menu: .dropdown-menu with role=\"menu\", starting hidden. Everything a dropdown menu can hold works here: items, labels, separators, shortcuts, icons, checkbox and radio items, and submenus. A top-level role=\"menuitem\" without a panel (a link, say) is fine too: it takes part in the keyboard navigation and has no menu to open. The behaviour sets the rest: only one trigger is in the tab order at a time, menu items get tabindex=\"-1\", and each trigger is linked to its menu with aria-controls."
        },
        {
          "id": "shortcuts",
          "title": "Shortcuts and separators",
          "text": ".dropdown-shortcut right-aligns a keyboard hint inside an item. It is only a hint: the menubar does not bind the key. If your page handles the shortcut, say so with aria-keyshortcuts on the item. .dropdown-separator with role=\"separator\" divides an item list into sections. html <div class=\"menubar\" role=\"menubar\" aria-label=\"Document\" data-menubar> <div class=\"menubar-menu\"> <button type=\"button\" class=\"menubar-trigger\" role=\"menuitem\" aria-haspopup=\"menu\" aria-expanded=\"false\">Document</button> <div class=\"dropdown-menu\" role=\"menu\" hidden> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\" aria-keyshortcuts=\"Control+S\">Save <span class=\"dropdown-shortcut\">Ctrl S</span></button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">Save as… <span class=\"dropdown-shortcut\">⇧ Ctrl S</span></button> <div class=\"dropdown-separator\" role=\"separator\"></div> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">Close <span class=\"kbd\">Esc</span></button> </div> </div> </div>"
        },
        {
          "id": "submenus",
          "title": "Submenus",
          "text": "Wrap an item and a nested menu in .dropdown-sub. The item gets aria-haspopup=\"menu\" and aria-expanded=\"false\", and a chevron-right icon, which sits at the right edge. The submenu opens beside the item when it is hovered, clicked, or focused and →, Enter or Space is pressed; ← and Esc close it. Near the edge of the window it opens to the left or upwards instead. Submenus can nest. html <div class=\"menubar\" role=\"menubar\" aria-label=\"Insert\" data-menubar> <div class=\"menubar-menu\"> <button type=\"button\" class=\"menubar-trigger\" role=\"menuitem\" aria-haspopup=\"menu\" aria-expanded=\"false\">Insert</button> <div class=\"dropdown-menu\" role=\"menu\" hidden> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">Image</button> <div class=\"dropdown-sub\"> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\" aria-haspopup=\"menu\" aria-expanded=\"false\">Table <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg></button> <div class=\"dropdown-menu\" role=\"menu\" hidden> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">2 × 2</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">3 × 3</button> <div class=\"dropdown-sub\"> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\" aria-haspopup=\"menu\" aria-expanded=\"false\">From template <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg></button> <div class=\"dropdown-menu\" role=\"menu\" hidden> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">Invoice</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">Timetable</button> </div> </div> </div> </div> <button type=\"button\" class=\"dropdown-item\" role=\"me"
        },
        {
          "id": "checkbox-items",
          "title": "Checkbox items",
          "text": "An item with role=\"menuitemcheckbox\" and aria-checked toggles on and off. A check shows in the inset slot on the left; add .dropdown-item-inset to the plain items around it so their text lines up. html <div class=\"menubar\" role=\"menubar\" aria-label=\"Layout\" data-menubar> <div class=\"menubar-menu\"> <button type=\"button\" class=\"menubar-trigger\" role=\"menuitem\" aria-haspopup=\"menu\" aria-expanded=\"false\">Layout</button> <div class=\"dropdown-menu\" role=\"menu\" hidden> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemcheckbox\" aria-checked=\"true\">Status bar</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemcheckbox\" aria-checked=\"false\">Activity bar</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemcheckbox\" aria-checked=\"false\" aria-disabled=\"true\">Panel</button> <div class=\"dropdown-separator\" role=\"separator\"></div> <button type=\"button\" class=\"dropdown-item dropdown-item-inset\" role=\"menuitem\">Reset layout</button> </div> </div> </div>"
        },
        {
          "id": "radio-items",
          "title": "Radio items",
          "text": "Items with role=\"menuitemradio\" inside a role=\"group\" form a set: checking one unchecks the others in the same group (or, without a group, the others directly in the same menu). Label each group: aria-label, or aria-labelledby pointing at a .dropdown-label. html <div class=\"menubar\" role=\"menubar\" aria-label=\"Text\" data-menubar> <div class=\"menubar-menu\"> <button type=\"button\" class=\"menubar-trigger\" role=\"menuitem\" aria-haspopup=\"menu\" aria-expanded=\"false\">Text</button> <div class=\"dropdown-menu\" role=\"menu\" hidden> <p class=\"dropdown-label dropdown-item-inset\" id=\"menubar-size-label\">Size</p> <div role=\"group\" aria-labelledby=\"menubar-size-label\"> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemradio\" aria-checked=\"false\">Small</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemradio\" aria-checked=\"true\">Medium</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemradio\" aria-checked=\"false\">Large</button> </div> <div class=\"dropdown-separator\" role=\"separator\"></div> <p class=\"dropdown-label dropdown-item-inset\" id=\"menubar-align-label\">Alignment</p> <div role=\"group\" aria-labelledby=\"menubar-align-label\"> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemradio\" aria-checked=\"true\">Left</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemradio\" aria-checked=\"false\">Centre</button> </div> </div> </div> </div> Choosing a checkbox or radio item closes the menu, like any other item. To keep it open so several can be set in a row, add data-keep-open to the item, to its group or to the whole menu."
        },
        {
          "id": "icons",
          "title": "Icons",
          "text": "Put an icon() before the text. Icons in items are muted and sized to match; a submenu's chevron and a trailing icon sit at the right edge. .dropdown-item-destructive marks an item that deletes or discards something. html <div class=\"menubar\" role=\"menubar\" aria-label=\"Files\" data-menubar> <div class=\"menubar-menu\"> <button type=\"button\" class=\"menubar-trigger\" role=\"menuitem\" aria-haspopup=\"menu\" aria-expanded=\"false\">File</button> <div class=\"dropdown-menu\" role=\"menu\" hidden> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"/></svg> New file <span class=\"dropdown-shortcut\">⌘N</span></button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2\"/></svg> Open… <span class=\"dropdown-shortcut\">⌘O</span></button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3\"/></svg> Download</button> <div class=\"dropdown-sub\"> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\" aria-haspopup=\"menu\" aria-expanded=\"false\"><svg xmlns=\"http://www.w3.org/2000/svg"
        },
        {
          "id": "vertical",
          "title": "Vertical",
          "text": "aria-orientation=\"vertical\" stacks the triggers and opens each menu to their right (to the left near the edge of the window). ↑ ↓ then move between the triggers, → opens a menu and ← closes it. html <div class=\"menubar\" role=\"menubar\" aria-label=\"Drawing tools\" aria-orientation=\"vertical\" data-menubar> <div class=\"menubar-menu\"> <button type=\"button\" class=\"menubar-trigger\" role=\"menuitem\" aria-haspopup=\"menu\" aria-expanded=\"false\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z\"/><path d=\"m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65M22 12.65l-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65\"/></svg> Arrange <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg></button> <div class=\"dropdown-menu\" role=\"menu\" hidden> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">Bring to front</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">Send to back</button> </div> </div> <div class=\"menubar-menu\"> <button type=\"button\" class=\"menubar-trigger\" role=\"menuitem\" aria-haspopup=\"menu\" aria-expanded=\"false\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 12H3M17 18H3M21 6H3\"/></svg> Align <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6"
        },
        {
          "id": "htmx",
          "title": "With htmx",
          "text": "Items are buttons and links, so they make htmx requests like any other: give an item hx-post and the menu closes as the request starts. Checkbox and radio items fire a bubbling menu:change event when they change, with detail.checked, detail.name and detail.value. Give them a name (and a value; a checkbox item defaults to on) and each keeps a hidden input inside it in step, so a surrounding form submits the checked ones like real checkboxes and radios. Here the form posts on every change with hx-trigger=\"menu:change\": html <form hx-post=\"/api/echo\" hx-trigger=\"menu:change\" hx-target=\"#menubar-result\"> <div class=\"menubar\" role=\"menubar\" aria-label=\"Editor\" data-menubar> <div class=\"menubar-menu\"> <button type=\"button\" class=\"menubar-trigger\" role=\"menuitem\" aria-haspopup=\"menu\" aria-expanded=\"false\">File</button> <div class=\"dropdown-menu\" role=\"menu\" hidden> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\" hx-post=\"/api/slow\" hx-target=\"#menubar-result\">Save</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\" hx-post=\"/api/slow\" hx-target=\"#menubar-result\">Save all</button> </div> </div> <div class=\"menubar-menu\"> <button type=\"button\" class=\"menubar-trigger\" role=\"menuitem\" aria-haspopup=\"menu\" aria-expanded=\"false\">View</button> <div class=\"dropdown-menu\" role=\"menu\" hidden> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemcheckbox\" aria-checked=\"true\" name=\"minimap\">Minimap</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemcheckbox\" aria-checked=\"false\" name=\"wrap\">Word wrap</button> <div class=\"dropdown-separator\" role=\"separator\"></div> <div role=\"group\" aria-label=\"Zoom\"> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemradio\" aria-checked=\"true\" name=\"zoom\" value=\"100\">100%</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemradio\" aria-checked=\"false\" name=\"zoom\" value=\"125\">125%</button> </div> </div> </div> </div> </form> <span id=\"menubar-result\" class=\"muted\" aria-live=\"polite\""
        },
        {
          "id": "keyboard",
          "title": "Keyboard",
          "text": "Key Action Tab Into the menubar (the last trigger used), or on out of it, closing any menu ← → on a trigger Previous / next trigger (wraps); if a menu is open, opens that trigger's menu instead Home End on a trigger First / last trigger ↓ Enter Space on a trigger Open its menu on the first item ↑ on a trigger Open its menu on the last item ↓ ↑ Home End in a menu Next / previous / first / last item, skipping disabled ones Letters Next trigger or item starting with the typed letters Enter Space in a menu Choose the item (toggle a checkbox, check a radio) and close; open a submenu → in a menu Open the submenu; on any other item, open the next top-level menu ← in a menu Close the submenu; in a top-level menu, open the previous one Esc Close the innermost menu and return focus to what opened it In a vertical menubar, ↑ ↓ move between triggers, → opens a menu and ← in a top-level menu closes it. With the mouse: click a trigger to open or close its menu; while one is open, hovering another trigger switches to it. Clicking outside closes it."
        },
        {
          "id": "accessibility",
          "title": "Accessibility",
          "text": "The markup follows the WAI-ARIA menubar pattern: menubar, menuitem, menu, menuitemcheckbox, menuitemradio, group and separator roles, with aria-haspopup, aria-expanded, aria-controls and aria-checked kept in step. Give the menubar an aria-label, and each radio group a label. Name icon-only items with aria-label. Only one trigger is in the tab order (roving tabindex), so Tab moves past the whole bar in one step. Disabled items use aria-disabled=\"true\": screen readers still announce them, but they can't be chosen and the arrow keys skip them. A shortcut's text is read as part of the item's name. Add aria-keyshortcuts only for shortcuts your page really handles."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class / attribute Description .menubar The bar: a row of triggers in a bordered surface. Needs role=\"menubar\", data-menubar and aria-label. .menubar[aria-orientation=\"vertical\"] Stacks the triggers; menus open to the side. .menubar-menu Wraps one trigger and its menu (positioning). .menubar-trigger Top-level item: role=\"menuitem\", aria-haspopup=\"menu\", aria-expanded. Highlighted while its menu is open. .dropdown-menu Each menu panel (role=\"menu\", hidden). All the dropdown item classes work inside it; see Dropdown. .dropdown-sub Wraps a submenu trigger (aria-haspopup=\"menu\") and its nested menu. data-keep-open On an item, group or menu: choosing an item doesn't close the menu. name / value On checkbox and radio items: kept in a hidden input so a surrounding form submits them. menu:change Bubbling event from a checkbox or radio item that changed; detail { checked, name, value }. data-flip-x / data-flip-y Set on a menu that would leave the window: it opens the other way. [data-menubar] Behaviour: roving focus, keyboard navigation, opening and closing, hover switching."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/scroll-button",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/scroll-button",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/scroll-button.md",
      "title": "Scroll button",
      "description": "A floating button that appears once the reader scrolls away from an edge and jumps back to the bottom or the top, of a scroll container or the whole page.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "container",
          "text": "In a scroll container"
        },
        {
          "level": 2,
          "id": "page",
          "text": "Back to top of the page"
        },
        {
          "level": 2,
          "id": "options",
          "text": "Options"
        },
        {
          "level": 2,
          "id": "javascript",
          "text": "JavaScript"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"relative w-full\"> <div class=\"h-72 overflow-y-auto rounded-(--radius) border border-border bg-surface p-4 text-sm\" data-scroll-container tabindex=\"0\" aria-label=\"Activity log\"> <ol class=\"space-y-2\"> <li class=\"flex justify-between gap-4\"><span>Event 1: build step finished</span><span class=\"text-muted-foreground tabular-nums\">12:01</span></li> <li class=\"flex justify-between gap-4\"><span>Event 2: build step finished</span><span class=\"text-muted-foreground tabular-nums\">12:02</span></li> <li class=\"flex justify-between gap-4\"><span>Event 3: build step finished</span><span class=\"text-muted-foreground tabular-nums\">12:03</span></li> <li class=\"flex justify-between gap-4\"><span>Event 4: build step finished</span><span class=\"text-muted-foreground tabular-nums\">12:04</span></li> <li class=\"flex justify-between gap-4\"><span>Event 5: build step finished</span><span class=\"text-muted-foreground tabular-nums\">12:05</span></li> <li class=\"flex justify-between gap-4\"><span>Event 6: build step finished</span><span class=\"text-muted-foreground tabular-nums\">12:06</span></li> <li class=\"flex justify-between gap-4\"><span>Event 7: build step finished</span><span class=\"text-muted-foreground tabular-nums\">12:07</span></li> <li class=\"flex justify-between gap-4\"><span>Event 8: build step finished</span><span class=\"text-muted-foreground tabular-nums\">12:08</span></li> <li class=\"flex justify-between gap-4\"><span>Event 9: build step finished</span><span class=\"text-muted-foreground tabular-nums\">12:09</span></li> <li class=\"flex justify-between gap-4\"><span>Event 10: build step finished</span><span class=\"text-muted-foreground tabular-nums\">12:10</span></li> <li class=\"flex justify-between gap-4\"><span>Event 11: build step finished</span><span class=\"text-muted-foreground tabular-nums\">12:11</span></li> <li class=\"flex justify-between gap-4\"><span>Event 12: build step finished</span><span class=\"text-muted-foreground tabular-nums\">12:12</span></li> <li class=\"flex "
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "The button is an ordinary icon .btn plus .scroll-button. Give it type=\"button\" and an aria-label such as \"Scroll to bottom\". data-scroll-button adds the behaviour. Its value is the edge it jumps to: bottom (the default) or top. While its scroll target is near that edge the button is hidden: data-state=\"hidden\" fades and shrinks it, and inert takes it out of the tab order and the accessibility tree. Once the reader scrolls away it turns data-state=\"visible\". It stays in the DOM throughout. It follows the content too: when messages are appended or grow, the button appears without a scroll event, so a chat's \"jump to bottom\" shows up as replies stream in."
        },
        {
          "id": "container",
          "title": "In a scroll container",
          "text": "Lay the button over the box it scrolls: wrap both in a relative element, mark the box data-scroll-container and add .scroll-button-float, which centres the button at the bottom of the wrapper (at the top for a top button). Give the box tabindex=\"0\" so keyboard users can scroll it; when the button hides under the focus, focus moves back to the box. html <div class=\"relative mx-auto w-full max-w-md\"> <div class=\"flex h-80 flex-col gap-3 overflow-y-auto rounded-(--radius) border border-border bg-surface p-4 text-sm\" data-scroll-container tabindex=\"0\" aria-label=\"Conversation\"> <p class=\"max-w-[80%] rounded-(--radius) px-3 py-2 self-end bg-primary text-primary-foreground\"> Can you summarise the release notes? </p> <p class=\"max-w-[80%] rounded-(--radius) px-3 py-2 self-start bg-muted\"> Sure: the release adds a loader, a scroll button and a faster build, and fixes three bugs in the docs search. </p> <p class=\"max-w-[80%] rounded-(--radius) px-3 py-2 self-end bg-primary text-primary-foreground\"> Can you summarise the release notes? </p> <p class=\"max-w-[80%] rounded-(--radius) px-3 py-2 self-start bg-muted\"> Sure: the release adds a loader, a scroll button and a faster build, and fixes three bugs in the docs search. </p> <p class=\"max-w-[80%] rounded-(--radius) px-3 py-2 self-end bg-primary text-primary-foreground\"> Can you summarise the release notes? </p> <p class=\"max-w-[80%] rounded-(--radius) px-3 py-2 self-start bg-muted\"> Sure: the release adds a loader, a scroll button and a faster build, and fixes three bugs in the docs search. </p> <p class=\"max-w-[80%] rounded-(--radius) px-3 py-2 self-end bg-primary text-primary-foreground\"> Can you summarise the release notes? </p> <p class=\"max-w-[80%] rounded-(--radius) px-3 py-2 self-start bg-muted\"> Sure: the release adds a loader, a scroll button and a faster build, and fixes three bugs in the docs search. </p> <p class=\"max-w-[80%] rounded-(--radius) px-3 py-2 self-end bg-primary text-primary-foreground\"> Can you summar"
        },
        {
          "id": "page",
          "title": "Back to top of the page",
          "text": "Without a container around it, the button scrolls the page. .scroll-button-fixed pins it to the bottom-right corner of the viewport (bottom-left in right-to-left text). This page has one: scroll down and it appears in the corner. html <button type=\"button\" class=\"btn btn-outline btn-icon scroll-button scroll-button-fixed\" data-scroll-button=\"top\" data-scroll-threshold=\"600\" aria-label=\"Back to top\"> <svg>…arrow-up…</svg> </button>"
        },
        {
          "id": "options",
          "title": "Options",
          "text": "data-scroll-button=\"bottom\" | \"top\": the edge to jump to (default bottom). data-scroll-target=\"#selector\": the element to scroll, when it isn't found from the markup. data-scroll-threshold=\"200\": how far from the edge, in pixels, the reader must scroll before the button shows (default 200). data-scroll-behavior=\"smooth\" | \"instant\": how it scrolls (default smooth; always instant when the reader prefers reduced motion). A text button works as well as an icon one: anything with .scroll-button fades in and out. html <div class=\"relative mx-auto w-full max-w-md\"> <div class=\"h-56 overflow-y-auto rounded-(--radius) border border-border bg-surface p-4 text-sm\" data-scroll-container tabindex=\"0\" aria-label=\"Build output\"> <p class=\"py-1\">Line 1 of the build output</p><p class=\"py-1\">Line 2 of the build output</p><p class=\"py-1\">Line 3 of the build output</p><p class=\"py-1\">Line 4 of the build output</p><p class=\"py-1\">Line 5 of the build output</p><p class=\"py-1\">Line 6 of the build output</p><p class=\"py-1\">Line 7 of the build output</p><p class=\"py-1\">Line 8 of the build output</p><p class=\"py-1\">Line 9 of the build output</p><p class=\"py-1\">Line 10 of the build output</p><p class=\"py-1\">Line 11 of the build output</p><p class=\"py-1\">Line 12 of the build output</p><p class=\"py-1\">Line 13 of the build output</p><p class=\"py-1\">Line 14 of the build output</p><p class=\"py-1\">Line 15 of the build output</p><p class=\"py-1\">Line 16 of the build output</p><p class=\"py-1\">Line 17 of the build output</p><p class=\"py-1\">Line 18 of the build output</p><p class=\"py-1\">Line 19 of the build output</p><p class=\"py-1\">Line 20 of the build output</p><p class=\"py-1\">Line 21 of the build output</p><p class=\"py-1\">Line 22 of the build output</p><p class=\"py-1\">Line 23 of the build output</p><p class=\"py-1\">Line 24 of the build output</p> </div> <button type=\"button\" class=\"btn btn-secondary btn-sm scroll-button scroll-button-float\" data-scroll-button data-scroll-threshold=\"40\" data-scroll-b"
        },
        {
          "id": "javascript",
          "title": "JavaScript",
          "text": "initScrollButton(root) runs with the other components, on load and on content htmx inserts. The helpers it uses are exported from its module too, for a \"scroll to bottom\" after your own updates: ts import { distanceFromEdge, scrollToEdge } from \"htmx-ui\"; const log = document.querySelector<HTMLElement>(\"#log\")!; // Keep following new lines, unless the reader has scrolled up to read. const following = distanceFromEdge(log, \"bottom\") < 40; log.insertAdjacentHTML(\"beforeend\", \"<p>New line</p>\"); if (following) scrollToEdge(log, \"bottom\", \"instant\"); scrollToEdge(null, \"top\"); // null is the page: smooth, or instant under reduced motion"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .scroll-button Fades and shrinks out while data-state=\"hidden\", back in when visible. .scroll-button-float Absolutely centred at the bottom of the nearest positioned ancestor (top for a top button). .scroll-button-fixed Fixed to the viewport's bottom-right corner. [data-scroll-button=\"bottom\" | \"top\"] Behaviour: shows away from that edge, scrolls to it on click. [data-scroll-container] Marks the element a nearby scroll button scrolls. [data-scroll-target] A selector for the element to scroll. [data-scroll-threshold] Distance from the edge, in px, before the button shows (200). [data-scroll-behavior] smooth (default) or instant. [data-state=\"visible\" | \"hidden\"] Set by the behaviour; hidden is also inert. Function Description initScrollButton(root = document) Initialises every [data-scroll-button] under root (idempotent). scrollToEdge(target, edge = \"bottom\", behavior?) Scrolls an element, or the page for null, to its top or bottom. distanceFromEdge(target, edge) How far, in px, the element (or the page) is from that edge. scrollTargetOf(button) The element a scroll button scrolls, or null for the page."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/toggle",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/toggle",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/toggle.md",
      "title": "Toggle",
      "description": "A two-state button that is either on or off. Any button variant can be a toggle.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "usage",
          "text": "Usage"
        },
        {
          "level": 2,
          "id": "variants",
          "text": "Any button"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Sizes and shapes"
        },
        {
          "level": 2,
          "id": "disabled",
          "text": "Disabled"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "Saving with htmx"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <button class=\"btn btn-toggle btn-icon\" data-toggle aria-pressed=\"false\" aria-label=\"Bold\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8\"/></svg></button>"
        },
        {
          "id": "usage",
          "title": "Usage",
          "text": "Add data-toggle and aria-pressed=\"false\" (or \"true\" to start on) to any .btn. Each click flips aria-pressed, which screen readers announce as \"pressed\" or \"not pressed\". Styles key off the same attribute. The button fires a bubbling toggle:change event with detail.pressed. Keep the label the same in both states. \"Bold\" pressed reads correctly; switching the text to \"Not bold\" doesn't."
        },
        {
          "id": "variants",
          "title": "Any button",
          "text": ".btn-toggle is the dedicated look: muted when off, filled when on. Every other variant works too: ghost and outline fill when on, secondary turns the brand colour, and primary and danger are outlined when off and solid when on. Click them: html <button class=\"btn btn-toggle\" data-toggle aria-pressed=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M19 4h-9M14 20H5M15 4 9 20\"/></svg> Toggle</button> <button class=\"btn btn-ghost\" data-toggle aria-pressed=\"false\">Ghost</button> <button class=\"btn btn-outline\" data-toggle aria-pressed=\"false\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z\"/></svg> Outline</button> <button class=\"btn btn-secondary\" data-toggle aria-pressed=\"false\">Secondary</button> <button class=\"btn btn-primary\" data-toggle aria-pressed=\"false\">Primary</button> <button class=\"btn btn-danger\" data-toggle aria-pressed=\"false\">Danger</button> <button class=\"btn btn-link\" data-toggle aria-pressed=\"false\">Link</button>"
        },
        {
          "id": "sizes",
          "title": "Sizes and shapes",
          "text": "html <button class=\"btn btn-toggle btn-outline btn-icon btn-sm\" data-toggle aria-pressed=\"false\" aria-label=\"Underline\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M6 4v6a6 6 0 0 0 12 0V4M4 20h16\"/></svg></button> <button class=\"btn btn-toggle btn-outline btn-icon\" data-toggle aria-pressed=\"true\" aria-label=\"Underline\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M6 4v6a6 6 0 0 0 12 0V4M4 20h16\"/></svg></button> <button class=\"btn btn-toggle btn-outline btn-icon btn-lg\" data-toggle aria-pressed=\"false\" aria-label=\"Underline\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M6 4v6a6 6 0 0 0 12 0V4M4 20h16\"/></svg></button> <button class=\"btn btn-outline btn-rounded btn-sm\" data-toggle aria-pressed=\"false\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z\"/></svg> Save</button>"
        },
        {
          "id": "disabled",
          "title": "Disabled",
          "text": "html <button class=\"btn btn-toggle btn-icon\" data-toggle aria-pressed=\"false\" aria-label=\"Bold\" disabled><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8\"/></svg></button> <button class=\"btn btn-toggle btn-icon\" data-toggle aria-pressed=\"true\" aria-label=\"Italic\" disabled><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M19 4h-9M14 20H5M15 4 9 20\"/></svg></button>"
        },
        {
          "id": "htmx",
          "title": "Saving with htmx",
          "text": "Trigger the request on toggle:change rather than click, so it runs after aria-pressed has flipped, and send the new state with hx-vals. html <button class=\"btn btn-outline\" data-toggle aria-pressed=\"false\" hx-post=\"/api/echo\" hx-trigger=\"toggle:change\" hx-vals='js:{starred: this.getAttribute(\"aria-pressed\")}' hx-target=\"#toggle-result\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z\"/></svg> Star</button> <p id=\"toggle-result\" class=\"muted\" aria-live=\"polite\"></p>"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class / attribute Description .btn-toggle Toggle look: muted when off, filled when on. Combine with .btn-outline for a bordered one. data-toggle Flips aria-pressed on click and fires toggle:change { pressed }. aria-pressed \"true\" or \"false\". Drives the pressed style on every button variant."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/toggle-group",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/toggle-group",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/toggle-group.md",
      "title": "Toggle group",
      "description": "A set of two-state buttons. Pick exactly one, at most one, or any number.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "usage",
          "text": "Usage"
        },
        {
          "level": 2,
          "id": "single",
          "text": "Single"
        },
        {
          "level": 2,
          "id": "multiple",
          "text": "Multiple"
        },
        {
          "level": 2,
          "id": "joined",
          "text": "Joined or spaced"
        },
        {
          "level": 2,
          "id": "vertical",
          "text": "Vertical"
        },
        {
          "level": 2,
          "id": "disabled",
          "text": "Disabled"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"toggle-group btn-group\" role=\"group\" aria-label=\"Text alignment\" data-toggle-group=\"single\" data-toggle-group-required> <button class=\"btn btn-outline btn-icon\" aria-pressed=\"true\" value=\"left\" aria-label=\"Align left\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 12H3M17 18H3M21 6H3\"/></svg></button> <button class=\"btn btn-outline btn-icon\" aria-pressed=\"false\" value=\"center\" aria-label=\"Align center\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M17 12H7M19 18H5M21 6H3\"/></svg></button> <button class=\"btn btn-outline btn-icon\" aria-pressed=\"false\" value=\"right\" aria-label=\"Align right\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M21 12H9M21 18H7M21 6H3\"/></svg></button> </div> Use a toggle group for buttons that switch a state. For buttons that perform actions, use a button group."
        },
        {
          "id": "usage",
          "title": "Usage",
          "text": "Wrap the buttons in .toggle-group with data-toggle-group, role=\"group\" and an aria-label. Each button gets aria-pressed, and a value to identify it. They don't need data-toggle; the group handles them. The group is one tab stop. ← → ↑ ↓ Home End move between buttons; Space or Enter presses. Every change fires a bubbling toggle-group:change event with detail.value: the pressed buttons' values."
        },
        {
          "id": "single",
          "title": "Single",
          "text": "data-toggle-group=\"single\": pressing a button releases the others. Pressing the pressed one releases it, leaving none. Add data-toggle-group-required to keep exactly one pressed, as in the alignment example at the top. html <div class=\"toggle-group\" role=\"group\" aria-label=\"Plan\" data-toggle-group=\"single\"> <button class=\"btn btn-toggle btn-sm\" aria-pressed=\"false\" value=\"monthly\">Monthly</button> <button class=\"btn btn-toggle btn-sm\" aria-pressed=\"true\" value=\"yearly\">Yearly</button> <button class=\"btn btn-toggle btn-sm\" aria-pressed=\"false\" value=\"lifetime\">Lifetime</button> </div>"
        },
        {
          "id": "multiple",
          "title": "Multiple",
          "text": "data-toggle-group=\"multiple\": each button toggles on its own. html <div class=\"toggle-group\" role=\"group\" aria-label=\"Text formatting\" data-toggle-group=\"multiple\"> <button class=\"btn btn-toggle btn-icon\" aria-pressed=\"true\" value=\"bold\" aria-label=\"Bold\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8\"/></svg></button> <button class=\"btn btn-toggle btn-icon\" aria-pressed=\"false\" value=\"italic\" aria-label=\"Italic\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M19 4h-9M14 20H5M15 4 9 20\"/></svg></button> <button class=\"btn btn-toggle btn-icon\" aria-pressed=\"false\" value=\"underline\" aria-label=\"Underline\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M6 4v6a6 6 0 0 0 12 0V4M4 20h16\"/></svg></button> </div>"
        },
        {
          "id": "joined",
          "title": "Joined or spaced",
          "text": "A .toggle-group spaces its buttons. Add .btn-group to join them like a button group, with all of its options: sizes, vertical, separators. html <div class=\"toggle-group btn-group btn-group-sm\" role=\"group\" aria-label=\"Size\" data-toggle-group=\"single\" data-toggle-group-required> <button class=\"btn btn-outline\" aria-pressed=\"false\" value=\"s\">S</button> <button class=\"btn btn-outline\" aria-pressed=\"true\" value=\"m\">M</button> <button class=\"btn btn-outline\" aria-pressed=\"false\" value=\"l\">L</button> <button class=\"btn btn-outline\" aria-pressed=\"false\" value=\"xl\">XL</button> </div> <div class=\"toggle-group btn-group\" role=\"group\" aria-label=\"View\" data-toggle-group=\"single\" data-toggle-group-required> <button class=\"btn btn-secondary\" aria-pressed=\"true\" value=\"list\">List</button> <div class=\"btn-group-separator\" role=\"separator\"></div> <button class=\"btn btn-secondary\" aria-pressed=\"false\" value=\"board\">Board</button> </div>"
        },
        {
          "id": "vertical",
          "title": "Vertical",
          "text": "html <div class=\"toggle-group toggle-group-vertical\" role=\"group\" aria-label=\"Filters\" data-toggle-group=\"multiple\"> <button class=\"btn btn-toggle justify-start\" aria-pressed=\"true\" value=\"open\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m9 12 2 2 4-4\"/></svg> Open</button> <button class=\"btn btn-toggle justify-start\" aria-pressed=\"false\" value=\"closed\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m15 9-6 6M9 9l6 6\"/></svg> Closed</button> <button class=\"btn btn-toggle justify-start\" aria-pressed=\"false\" value=\"draft\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4M10 9H8M16 13H8M16 17H8\"/></svg> Draft</button> </div>"
        },
        {
          "id": "disabled",
          "title": "Disabled",
          "text": "Disabled buttons are skipped by clicks and arrow keys. html <div class=\"toggle-group\" role=\"group\" aria-label=\"Text formatting\" data-toggle-group=\"multiple\"> <button class=\"btn btn-toggle btn-icon\" aria-pressed=\"false\" value=\"bold\" aria-label=\"Bold\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8\"/></svg></button> <button class=\"btn btn-toggle btn-icon\" aria-pressed=\"false\" value=\"italic\" aria-label=\"Italic\" disabled><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M19 4h-9M14 20H5M15 4 9 20\"/></svg></button> <button class=\"btn btn-toggle btn-icon\" aria-pressed=\"false\" value=\"underline\" aria-label=\"Underline\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M6 4v6a6 6 0 0 0 12 0V4M4 20h16\"/></svg></button> </div>"
        },
        {
          "id": "htmx",
          "title": "With htmx",
          "text": "Buttons with a name send their value with the request they trigger, so each button can post the choice by itself: html <div class=\"toggle-group btn-group\" role=\"group\" aria-label=\"Theme\" data-toggle-group=\"single\" data-toggle-group-required> <button class=\"btn btn-outline\" aria-pressed=\"true\" name=\"theme\" value=\"light\" hx-post=\"/api/echo\" hx-target=\"#toggle-group-result\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"4\"/><path d=\"M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41\"/></svg> Light</button> <button class=\"btn btn-outline\" aria-pressed=\"false\" name=\"theme\" value=\"dark\" hx-post=\"/api/echo\" hx-target=\"#toggle-group-result\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\"/></svg> Dark</button> <button class=\"btn btn-outline\" aria-pressed=\"false\" name=\"theme\" value=\"system\" hx-post=\"/api/echo\" hx-target=\"#toggle-group-result\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"20\" height=\"14\" x=\"2\" y=\"3\" rx=\"2\"/><path d=\"M8 21h8M12 17v4\"/></svg> System</button> </div> <p id=\"toggle-group-result\" class=\"muted\" aria-live=\"polite\"></p>"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class / attribute Description .toggle-group Spaced row of toggle buttons. Add .btn-group to join them. .toggle-group-vertical Stacks the buttons. data-toggle-group=\"single\" Pressing a button releases the others. data-toggle-group=\"multiple\" Buttons toggle independently. data-toggle-group-required With single: one button always stays pressed. toggle-group:change Event fired on the group; detail.value lists the pressed buttons' values."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/breadcrumb",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/breadcrumb",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/breadcrumb.md",
      "title": "Breadcrumb",
      "description": "Displays the path to the current resource using a hierarchy of links.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "custom-separator",
          "text": "Custom separator"
        },
        {
          "level": 2,
          "id": "dropdown",
          "text": "Dropdown"
        },
        {
          "level": 2,
          "id": "collapsed",
          "text": "Collapsed"
        },
        {
          "level": 2,
          "id": "collapsing",
          "text": "Collapsing when it doesn't fit"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <nav aria-label=\"Breadcrumb\"> <ol class=\"breadcrumb\"> <li class=\"breadcrumb-item\"><a class=\"breadcrumb-link\" href=\"#home\">Home</a></li> <li class=\"breadcrumb-separator\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg></li> <li class=\"breadcrumb-item\"><a class=\"breadcrumb-link\" href=\"#docs\">Docs</a></li> <li class=\"breadcrumb-separator\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg></li> <li class=\"breadcrumb-item\"><a class=\"breadcrumb-link\" href=\"#components\">Components</a></li> <li class=\"breadcrumb-separator\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg></li> <li class=\"breadcrumb-item\"><span class=\"breadcrumb-page\" aria-current=\"page\">Breadcrumb</span></li> </ol> </nav>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "A <nav aria-label=\"Breadcrumb\"> around an <ol class=\"breadcrumb\">. Each step is an <li class=\"breadcrumb-item\"> holding an <a class=\"breadcrumb-link\">. The last one is the current page: <span class=\"breadcrumb-page\" aria-current=\"page\">, not a link. Between steps, an <li class=\"breadcrumb-separator\" aria-hidden=\"true\">. Screen readers skip it; the list already says how many steps there are. A long breadcrumb wraps onto more lines. To keep it on one, let it collapse."
        },
        {
          "id": "custom-separator",
          "title": "Custom separator",
          "text": "The separator is whatever you put in it: any icon, or text. html <nav aria-label=\"Breadcrumb\"> <ol class=\"breadcrumb\"> <li class=\"breadcrumb-item\"><a class=\"breadcrumb-link\" href=\"#home\">Home</a></li> <li class=\"breadcrumb-separator\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M16 3 8 21\"/></svg></li> <li class=\"breadcrumb-item\"><a class=\"breadcrumb-link\" href=\"#components\">Components</a></li> <li class=\"breadcrumb-separator\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M16 3 8 21\"/></svg></li> <li class=\"breadcrumb-item\"><span class=\"breadcrumb-page\" aria-current=\"page\">Breadcrumb</span></li> </ol> </nav> <nav aria-label=\"Breadcrumb\"> <ol class=\"breadcrumb\"> <li class=\"breadcrumb-item\"><a class=\"breadcrumb-link\" href=\"#home\">Home</a></li> <li class=\"breadcrumb-separator\" aria-hidden=\"true\">·</li> <li class=\"breadcrumb-item\"><a class=\"breadcrumb-link\" href=\"#components\">Components</a></li> <li class=\"breadcrumb-separator\" aria-hidden=\"true\">·</li> <li class=\"breadcrumb-item\"><span class=\"breadcrumb-page\" aria-current=\"page\">Breadcrumb</span></li> </ol> </nav> <nav aria-label=\"Breadcrumb\"> <ol class=\"breadcrumb\"> <li class=\"breadcrumb-item\"><a class=\"breadcrumb-link\" href=\"#home\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8\"/><path d=\"M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\"/></svg> Home</a>"
        },
        {
          "id": "dropdown",
          "title": "Dropdown",
          "text": "A step can be a dropdown instead of a link, for jumping to a sibling page. Make the trigger a <button class=\"breadcrumb-link\">; it stays highlighted while the menu is open. html <nav aria-label=\"Breadcrumb\"> <ol class=\"breadcrumb\"> <li class=\"breadcrumb-item\"><a class=\"breadcrumb-link\" href=\"#home\">Home</a></li> <li class=\"breadcrumb-separator\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg></li> <li class=\"breadcrumb-item\"> <div class=\"dropdown\" data-dropdown> <button type=\"button\" class=\"breadcrumb-link\" data-dropdown-trigger aria-haspopup=\"menu\" aria-expanded=\"false\"> Components <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg> </button> <div class=\"dropdown-menu\" role=\"menu\" hidden> <a class=\"dropdown-item\" role=\"menuitem\" href=\"#docs\">Documentation</a> <a class=\"dropdown-item\" role=\"menuitem\" href=\"#themes\">Themes</a> <a class=\"dropdown-item\" role=\"menuitem\" href=\"#github\">GitHub</a> </div> </div> </li> <li class=\"breadcrumb-separator\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg></li> <li class=\"breadcrumb-item\"><span class=\"breadcrumb-page\" aria-current=\"page\">Breadcrumb</span></li> </ol> </nav>"
        },
        {
          "id": "collapsed",
          "title": "Collapsed",
          "text": ".breadcrumb-ellipsis stands in for steps left out of a long path. Make it the trigger of a dropdown that lists them, and give it an aria-label. html <nav aria-label=\"Breadcrumb\"> <ol class=\"breadcrumb\"> <li class=\"breadcrumb-item\"><a class=\"breadcrumb-link\" href=\"#home\">Home</a></li> <li class=\"breadcrumb-separator\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg></li> <li class=\"breadcrumb-item\"> <div class=\"dropdown\" data-dropdown> <button type=\"button\" class=\"breadcrumb-ellipsis\" data-dropdown-trigger aria-haspopup=\"menu\" aria-expanded=\"false\" aria-label=\"Show hidden pages\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"19\" cy=\"12\" r=\"1\"/><circle cx=\"5\" cy=\"12\" r=\"1\"/></svg></button> <div class=\"dropdown-menu\" role=\"menu\" hidden> <a class=\"dropdown-item\" role=\"menuitem\" href=\"#docs\">Docs</a> <a class=\"dropdown-item\" role=\"menuitem\" href=\"#components\">Components</a> <a class=\"dropdown-item\" role=\"menuitem\" href=\"#forms\">Forms</a> </div> </div> </li> <li class=\"breadcrumb-separator\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg></li> <li class=\"breadcrumb-item\"><a class=\"breadcrumb-link\" href=\"#inputs\">Inputs</a></li> <li class=\"breadcrumb-separator\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-"
        },
        {
          "id": "collapsing",
          "title": "Collapsing when it doesn't fit",
          "text": "With data-breadcrumb on the <nav>, the breadcrumb stays on one line and collapses by itself. Add a hidden ellipsis item, and a hidden separator, after the first step. Whenever the breadcrumb changes width, steps in the middle are hidden from the left until it fits, the ellipsis appears, and its menu lists the hidden steps as links. The first step and the current page always stay; the page name truncates as a last resort. Drag the corner of the box to resize it: html <div class=\"h-56 w-full max-w-full min-w-40 resize-x overflow-hidden rounded-(--radius) border border-dashed border-border p-3\"> <nav aria-label=\"Breadcrumb\" data-breadcrumb> <ol class=\"breadcrumb\"> <li class=\"breadcrumb-item\"><a class=\"breadcrumb-link\" href=\"#home\">Home</a></li> <li class=\"breadcrumb-separator\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg></li> <li class=\"breadcrumb-item\" data-breadcrumb-ellipsis hidden> <div class=\"dropdown\" data-dropdown> <button type=\"button\" class=\"breadcrumb-ellipsis\" data-dropdown-trigger aria-haspopup=\"menu\" aria-expanded=\"false\" aria-label=\"Show hidden pages\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"19\" cy=\"12\" r=\"1\"/><circle cx=\"5\" cy=\"12\" r=\"1\"/></svg></button> <div class=\"dropdown-menu\" role=\"menu\" hidden></div> </div> </li> <li class=\"breadcrumb-separator\" aria-hidden=\"true\" hidden><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg></li> "
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .breadcrumb The <ol>: a wrapping row of steps. .breadcrumb-item One step (<li>). .breadcrumb-link A link, or a button opening a dropdown; highlighted while expanded. .breadcrumb-page The current page, with aria-current=\"page\". .breadcrumb-separator Between steps, aria-hidden. Any icon or text. .breadcrumb-ellipsis \"…\" button standing in for hidden steps. [data-breadcrumb] On the <nav>: one line, collapsing middle steps into [data-breadcrumb-ellipsis] when it doesn't fit. [data-breadcrumb-ellipsis] The hidden ellipsis item; its [role=menu] is filled with the hidden steps."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/navigation-menu",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/navigation-menu",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/navigation-menu.md",
      "title": "Navigation menu",
      "description": "A site's main navigation: links, and buttons that open panels of links, from simple lists to mega menus.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "opening",
          "text": "Opening and closing"
        },
        {
          "level": 2,
          "id": "active",
          "text": "Active link"
        },
        {
          "level": 2,
          "id": "alignment",
          "text": "Panel alignment"
        },
        {
          "level": 2,
          "id": "indicator",
          "text": "Indicator"
        },
        {
          "level": 2,
          "id": "vertical",
          "text": "Vertical"
        },
        {
          "level": 2,
          "id": "responsive",
          "text": "Small screens"
        },
        {
          "level": 2,
          "id": "keyboard",
          "text": "Keyboard"
        },
        {
          "level": 2,
          "id": "accessibility",
          "text": "Accessibility"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <nav class=\"navigation-menu\" aria-label=\"Main\" data-navigation-menu> <ul class=\"navigation-menu-list flex-wrap justify-start\"> <li class=\"navigation-menu-item\"> <button class=\"navigation-menu-trigger\" aria-expanded=\"false\">Getting started <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg></button> <div class=\"navigation-menu-content\"> <ul class=\"navigation-menu-grid w-[400px] md:w-[460px] md:grid-cols-[.75fr_1fr]\"> <li class=\"md:row-span-3\"> <a class=\"navigation-menu-featured\" href=\"/docs\"> <span class=\"navigation-menu-link-title\">htmx-ui</span> <span class=\"navigation-menu-link-description\">Components for htmx apps, styled with Tailwind CSS.</span> </a> </li> <li> <a class=\"navigation-menu-link\" href=\"/docs\"> <span class=\"navigation-menu-link-title\">Introduction</span> <span class=\"navigation-menu-link-description\">Plain HTML and CSS, with a little TypeScript where it helps.</span> </a> </li> <li> <a class=\"navigation-menu-link\" href=\"/docs/installation\"> <span class=\"navigation-menu-link-title\">Installation</span> <span class=\"navigation-menu-link-description\">How to install the packages and structure your app.</span> </a> </li> <li> <a class=\"navigation-menu-link\" href=\"/docs/components/text\"> <span class=\"navigation-menu-link-title\">Typography</span> <span class=\"navigation-menu-link-description\">Styles for headings, paragraphs, lists and more.</span> </a> </li> </ul> </div> </li> <li class=\"navigation-menu-item\"> <button class=\"navigation-menu-trigger\" aria-expanded=\"false\">Components <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg></button> <div class=\"navigation-menu-co"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "A <nav> with .navigation-menu, data-navigation-menu and an aria-label naming it (Main), holding a <ul class=\"navigation-menu-list\"> of <li class=\"navigation-menu-item\">. An item is either a link, <a class=\"navigation-menu-link\">, or a <button class=\"navigation-menu-trigger\" aria-expanded=\"false\"> followed by its panel, <div class=\"navigation-menu-content\">. End the trigger's label with icon(\"chevron-down\"); it turns over while the panel is open. The behaviour gives each panel an id, points its trigger's aria-controls at it and keeps aria-expanded in sync. CSS shows the panel of an expanded trigger. In a panel, a .navigation-menu-grid lists .navigation-menu-links. A link holds a .navigation-menu-link-title and a muted .navigation-menu-link-description, or an icon and a label, or just a label. A .navigation-menu-featured link is a large tile. Size and lay out the grid with utilities (w-[400px], md:grid-cols-2, md:row-span-3 on the tile's item). It is the disclosure navigation pattern: plain buttons and links, not role=\"menu\", which is for application menus like a dropdown."
        },
        {
          "id": "opening",
          "title": "Opening and closing",
          "text": "Clicking a trigger opens its panel, and clicking it again closes it. Only one panel is open at a time. With a mouse, resting on a trigger opens its panel after 200ms. While a panel is open, moving onto another trigger switches to its panel at once. A panel opened by hovering closes 300ms after the pointer leaves its item, so the pointer can cross the gap into it. Clicking the trigger of a hover-opened panel keeps it open instead: it then stays until it is closed on purpose. Esc, a click outside the menu, focus leaving the menu or moving to another item, and following a link in the panel all close it. --navigation-menu-open-delay and --navigation-menu-close-delay on the nav (or any ancestor) change the delays. Without JavaScript, CSS opens a panel while its item is hovered or holds focus."
        },
        {
          "id": "active",
          "title": "Active link",
          "text": "Mark the link to the current page with aria-current=\"page\": screen readers announce it and it gets the accent background. It works on top-level links and on links in a panel. html <nav class=\"navigation-menu\" aria-label=\"Site\" data-navigation-menu> <ul class=\"navigation-menu-list\"> <li class=\"navigation-menu-item\"><a class=\"navigation-menu-link\" href=\"#\">Home</a></li> <li class=\"navigation-menu-item\"><a class=\"navigation-menu-link\" href=\"#\" aria-current=\"page\">Docs</a></li> <li class=\"navigation-menu-item\"><a class=\"navigation-menu-link\" href=\"#\">Components</a></li> <li class=\"navigation-menu-item\"><a class=\"navigation-menu-link\" href=\"#\">Blog</a></li> </ul> </nav> A menu of links only, like this one, needs no panels; the behaviour still adds arrow-key movement between them."
        },
        {
          "id": "alignment",
          "title": "Panel alignment",
          "text": "A panel opens below its trigger, lined up with the trigger's left edge. .navigation-menu-content-center centres it and .navigation-menu-content-end lines it up with the right edge, for the last items of a menu at the right of the page. html <nav class=\"navigation-menu\" aria-label=\"Alignment\" data-navigation-menu> <ul class=\"navigation-menu-list\"> <li class=\"navigation-menu-item\"> <button class=\"navigation-menu-trigger\" aria-expanded=\"false\">Start <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg></button> <div class=\"navigation-menu-content\"> <ul class=\"navigation-menu-grid w-[220px]\"> <li><a class=\"navigation-menu-link\" href=\"#\">Overview</a></li> <li><a class=\"navigation-menu-link\" href=\"#\">Pricing</a></li> <li><a class=\"navigation-menu-link\" href=\"#\">Customers</a></li> </ul> </div> </li> <li class=\"navigation-menu-item\"> <button class=\"navigation-menu-trigger\" aria-expanded=\"false\">Center <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg></button> <div class=\"navigation-menu-content navigation-menu-content-center\"> <ul class=\"navigation-menu-grid w-[220px]\"> <li><a class=\"navigation-menu-link\" href=\"#\">Overview</a></li> <li><a class=\"navigation-menu-link\" href=\"#\">Pricing</a></li> <li><a class=\"navigation-menu-link\" href=\"#\">Customers</a></li> </ul> </div> </li> <li class=\"navigation-menu-item\"> <button class=\"navigation-menu-trigger\" aria-expanded=\"false\">End <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m"
        },
        {
          "id": "indicator",
          "title": "Indicator",
          "text": ".navigation-menu-indicator on the nav adds an arrow pointing from the open trigger to its panel. html <nav class=\"navigation-menu navigation-menu-indicator\" aria-label=\"Products\" data-navigation-menu> <ul class=\"navigation-menu-list\"> <li class=\"navigation-menu-item\"> <button class=\"navigation-menu-trigger\" aria-expanded=\"false\">Products <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg></button> <div class=\"navigation-menu-content\"> <ul class=\"navigation-menu-grid w-[240px]\"> <li><a class=\"navigation-menu-link\" href=\"#\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z\"/><path d=\"m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65M22 12.65l-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65\"/></svg> Components</a></li> <li><a class=\"navigation-menu-link\" href=\"#\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"20\" height=\"8\" x=\"2\" y=\"2\" rx=\"2\"/><rect width=\"20\" height=\"8\" x=\"2\" y=\"14\" rx=\"2\"/><path d=\"M6 6h.01M6 18h.01\"/></svg> Engine</a></li> <li><a class=\"navigation-menu-link\" href=\"#\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg> Search plugin</a></li> </ul> </div> </li> <li class=\"navigation-menu-item\"><a clas"
        },
        {
          "id": "vertical",
          "title": "Vertical",
          "text": ".navigation-menu-vertical stacks the items, for a sidebar or a menu on a small screen. Panels open in place under their trigger and push the items below down; they open by click or keyboard only, never on hover, and grids become a single column. html <nav class=\"navigation-menu navigation-menu-vertical w-64\" aria-label=\"Sections\" data-navigation-menu> <ul class=\"navigation-menu-list\"> <li class=\"navigation-menu-item\"> <button class=\"navigation-menu-trigger\" aria-expanded=\"false\">Getting started <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg></button> <div class=\"navigation-menu-content\"> <ul class=\"navigation-menu-grid\"> <li><a class=\"navigation-menu-link\" href=\"#\">Introduction</a></li> <li><a class=\"navigation-menu-link\" href=\"#\">Installation</a></li> </ul> </div> </li> <li class=\"navigation-menu-item\"> <button class=\"navigation-menu-trigger\" aria-expanded=\"false\">Components <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg></button> <div class=\"navigation-menu-content\"> <ul class=\"navigation-menu-grid\"> <li><a class=\"navigation-menu-link\" href=\"#\">Dialog</a></li> <li><a class=\"navigation-menu-link\" href=\"#\">Hover card</a></li> <li><a class=\"navigation-menu-link\" href=\"#\">Tooltip</a></li> </ul> </div> </li> <li class=\"navigation-menu-item\"><a class=\"navigation-menu-link\" href=\"#\">Docs</a></li> </ul> </nav>"
        },
        {
          "id": "responsive",
          "title": "Small screens",
          "text": "A horizontal menu with wide panels doesn't fit a phone, and there is no hover there. Show it from the md breakpoint up, and below that a button that opens the same links in a vertical menu: in a collapsible under the header, as here, or in a sheet from the side. Narrow the window to see the switch. html <div class=\"w-full\"> <nav class=\"navigation-menu hidden md:flex\" aria-label=\"Main (wide screens)\" data-navigation-menu> <ul class=\"navigation-menu-list\"> <li class=\"navigation-menu-item\"> <button class=\"navigation-menu-trigger\" aria-expanded=\"false\">Products <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg></button> <div class=\"navigation-menu-content\"> <ul class=\"navigation-menu-grid w-[220px]\"> <li><a class=\"navigation-menu-link\" href=\"#\">Components</a></li> <li><a class=\"navigation-menu-link\" href=\"#\">Engine</a></li> </ul> </div> </li> <li class=\"navigation-menu-item\"><a class=\"navigation-menu-link\" href=\"#\">Pricing</a></li> <li class=\"navigation-menu-item\"><a class=\"navigation-menu-link\" href=\"#\">Docs</a></li> </ul> </nav> <div class=\"collapsible md:hidden\" data-collapsible> <button type=\"button\" class=\"btn btn-outline btn-sm w-fit\" data-collapsible-trigger aria-expanded=\"false\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M4 6h16M4 12h16M4 18h16\"/></svg> Menu</button> <div class=\"collapsible-content mt-2\" data-collapsible-content hidden> <nav class=\"navigation-menu navigation-menu-vertical\" aria-label=\"Main (small screens)\" data-navigation-menu> <ul class=\"navigation-menu-list\"> <li class=\"navigation-menu-item\"> <button class=\"navigation-menu-trigger\" aria-expanded=\"false\">Products <svg xmlns=\""
        },
        {
          "id": "keyboard",
          "title": "Keyboard",
          "text": "Key Action Tab Moves through the triggers and links in page order, and into an open panel after its trigger. Leaving an item closes its panel. Enter Space On a trigger, opens or closes its panel. On a link, follows it (Enter). → ← On the top level, moves to the next or previous trigger or link, wrapping round. Home End Moves to the first or last top-level item; in a panel, to its first or last link. ↓ On a trigger, opens its panel and moves to its first link. In a panel, moves to the next link. ↑ In a panel, moves to the previous link, or from the first link back to the trigger. Esc Closes the open panel and returns focus to its trigger. In a vertical menu, ↓ and ↑ move through the top-level items and the links of an open panel as one list."
        },
        {
          "id": "accessibility",
          "title": "Accessibility",
          "text": "The nav is a navigation landmark: give it an aria-label, and a different one to each navigation on the page. Triggers are buttons with aria-expanded and aria-controls, so screen readers announce collapsed or expanded. Links stay links, read in order, and there are no menu roles to change how the keyboard works. A closed panel is hidden with visibility, so it is out of the tab order and the accessibility tree, and can fade out as well as in. Arrow keys are a shortcut on top of Tab, which reaches everything. Hover-opened panels can be dismissed with Esc without moving the pointer, and wait for the pointer to cross into them (WCAG 1.4.13). With reduced motion, panels fade without zooming and the chevron turns without animating."
        },
        {
          "id": "htmx",
          "title": "With htmx",
          "text": "Boost the menu's links with hx-boost:inherited=\"true\" on the nav, or on <body> as this site does, so following one swaps the page without a full reload. The panel closes when one of its links is followed. If the response replaces the whole body, the server renders the new aria-current; if it swaps only the main content (hx-target and hx-select), also return the menu with hx-swap-oob so the current link moves. html <nav class=\"navigation-menu\" aria-label=\"Main\" data-navigation-menu hx-boost:inherited=\"true\"> <ul class=\"navigation-menu-list\"> <li class=\"navigation-menu-item\"><a class=\"navigation-menu-link\" href=\"/\" aria-current=\"page\">Home</a></li> <li class=\"navigation-menu-item\"><a class=\"navigation-menu-link\" href=\"/docs\">Docs</a></li> </ul> </nav> A panel fires navigation-menu:open and navigation-menu:close (they bubble) each time it opens and closes. Fill a panel from the server the first time it opens with hx-trigger=\"navigation-menu:open once\": html <nav class=\"navigation-menu\" aria-label=\"Account\" data-navigation-menu> <ul class=\"navigation-menu-list\"> <li class=\"navigation-menu-item\"> <button class=\"navigation-menu-trigger\" aria-expanded=\"false\">Recent <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg></button> <div class=\"navigation-menu-content w-64\" hx-get=\"/api/recent\" hx-trigger=\"navigation-menu:open once\"> <span class=\"flex items-center gap-2 p-2 text-muted-foreground\"> <span class=\"spinner\" role=\"status\" aria-label=\"Loading\"></span> Loading… </span> </div> </li> </ul> </nav>"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class / attribute Description .navigation-menu The <nav>. Add data-navigation-menu and an aria-label. .navigation-menu-list The <ul> of top-level items, in a row. .navigation-menu-item An <li>: a link, or a trigger and its panel. .navigation-menu-trigger Button that opens the panel after it. Add aria-expanded=\"false\"; a chevron icon inside turns over while open. .navigation-menu-link A link: top-level, styled like a trigger, or in a panel, with a title and description or an icon. aria-current=\"page\" marks the current page. .navigation-menu-content A panel, below its trigger at its left edge. .navigation-menu-content-center / -end Centre the panel on its trigger, or line it up with the right edge. .navigation-menu-grid A <ul> of links in a panel. Lay it out with grid and width utilities. .navigation-menu-link-title A link's title. .navigation-menu-link-description A link's description: muted, at most two lines. .navigation-menu-featured A large tile link, with a gradient background. .navigation-menu-indicator On the nav: an arrow from the open trigger to its panel. .navigation-menu-vertical On the nav: stacked items, panels opening in place, no hover. [data-navigation-menu] Behaviour: aria-expanded/aria-controls, hover delays, closing, arrow keys, events. --navigation-menu-open-delay How long the pointer rests on a trigger before its panel opens (200ms). --navigation-menu-close-delay How long a hover-opened panel stays after the pointer leaves (300ms). navigation-menu:open / navigation-menu:close Events fired on the panel (bubbling) when it opens and closes."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/pagination",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/pagination",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/pagination.md",
      "title": "Pagination",
      "description": "Pagination with page navigation, next and previous links.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "page-numbers",
          "text": "Page numbers only"
        },
        {
          "level": 2,
          "id": "disabled",
          "text": "Disabled"
        },
        {
          "level": 2,
          "id": "first-and-last",
          "text": "First and last"
        },
        {
          "level": 2,
          "id": "icons-only",
          "text": "Icons only, for tables"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Sizes"
        },
        {
          "level": 2,
          "id": "responsive",
          "text": "Responsive"
        },
        {
          "level": 2,
          "id": "macro",
          "text": "Macro"
        },
        {
          "level": 3,
          "id": "page-range",
          "text": "Which pages are shown"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx"
        },
        {
          "level": 2,
          "id": "keyboard",
          "text": "Keyboard"
        },
        {
          "level": 2,
          "id": "accessibility",
          "text": "Accessibility"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <nav class=\"pagination\" aria-label=\"Pagination\"> <ul class=\"pagination-content\"> <li><a class=\"pagination-link pagination-previous\" href=\"#\" rel=\"prev\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m15 18-6-6 6-6\"/></svg><span class=\"pagination-label\">Previous</span></a></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 1\">1</a></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 2\" aria-current=\"page\">2</a></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 3\">3</a></li> <li><span class=\"pagination-ellipsis\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"19\" cy=\"12\" r=\"1\"/><circle cx=\"5\" cy=\"12\" r=\"1\"/></svg></span></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 10\">10</a></li> <li><a class=\"pagination-link pagination-next\" href=\"#\" rel=\"next\"><span class=\"pagination-label\">Next</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg></a></li> </ul> </nav>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "A <nav class=\"pagination\" aria-label=\"Pagination\"> around a <ul class=\"pagination-content\">. Give each pagination on a page its own label. Each page is an <a class=\"pagination-link\"> in an <li>, with aria-label=\"Page 2\" so it isn't read as a bare number. The current page has aria-current=\"page\", which outlines it. It stays a link, so it can be reloaded or copied. Previous and next add .pagination-previous / .pagination-next (and rel=\"prev\" / rel=\"next\"), with their text in a .pagination-label. .pagination-ellipsis stands in for pages left out. It is aria-hidden: the page numbers either side already show the gap. Plain links work without JavaScript; add htmx attributes to swap only the list (see With htmx). The macro works out which pages to show and writes all of this for you."
        },
        {
          "id": "page-numbers",
          "title": "Page numbers only",
          "text": "Leave out previous and next when there are few pages. html <nav class=\"pagination\" aria-label=\"Pagination\"> <ul class=\"pagination-content\"> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 1\">1</a></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 2\" aria-current=\"page\">2</a></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 3\">3</a></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 4\">4</a></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 5\">5</a></li> </ul> </nav>"
        },
        {
          "id": "disabled",
          "title": "Disabled",
          "text": "On the first page there is no previous one. Keep the link in place, so nothing shifts, but drop its href and add aria-disabled=\"true\": it fades, can't be clicked and leaves the tab order. A <button disabled> works the same. html <nav class=\"pagination\" aria-label=\"Pagination\"> <ul class=\"pagination-content\"> <li><a class=\"pagination-link pagination-previous\" aria-disabled=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m15 18-6-6 6-6\"/></svg><span class=\"pagination-label\">Previous</span></a></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 1\" aria-current=\"page\">1</a></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 2\">2</a></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 3\">3</a></li> <li><a class=\"pagination-link pagination-next\" href=\"#\" rel=\"next\"><span class=\"pagination-label\">Next</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg></a></li> </ul> </nav>"
        },
        {
          "id": "first-and-last",
          "title": "First and last",
          "text": ".pagination-first and .pagination-last jump to the ends. They are usually icon-only, so give them an aria-label. html <nav class=\"pagination\" aria-label=\"Pagination\"> <ul class=\"pagination-content\"> <li><a class=\"pagination-link pagination-first\" href=\"#\" aria-label=\"First page\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m11 17-5-5 5-5\"/><path d=\"m18 17-5-5 5-5\"/></svg></a></li> <li><a class=\"pagination-link pagination-previous\" href=\"#\" rel=\"prev\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m15 18-6-6 6-6\"/></svg><span class=\"pagination-label\">Previous</span></a></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 4\">4</a></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 5\" aria-current=\"page\">5</a></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 6\">6</a></li> <li><a class=\"pagination-link pagination-next\" href=\"#\" rel=\"next\"><span class=\"pagination-label\">Next</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg></a></li> <li><a class=\"pagination-link pagination-last\" href=\"#\" aria-label=\"Last page\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 17 5-5-5-5\"/><path d=\"m13 17 5-5-5-5\"/></svg></a></li> </ul> </nav>"
        },
        {
          "id": "icons-only",
          "title": "Icons only, for tables",
          "text": "Without page numbers and labels, previous and next become square icon buttons with an aria-label. A data table's footer pairs them with a page size and the position. .pagination-outline outlines every link; mx-0 w-auto stops the <nav> filling and centring in the row. html <div class=\"flex flex-wrap items-center justify-between gap-x-6 gap-y-3 text-sm\"> <p class=\"text-muted-foreground\">2 of 97 rows selected.</p> <div class=\"flex flex-wrap items-center gap-x-6 gap-y-3\"> <div class=\"flex items-center gap-2\"> <label class=\"font-medium\" id=\"rows-per-page-label\" for=\"rows-per-page\">Rows per page</label> <div class=\"select w-20\" data-select> <button type=\"button\" id=\"rows-per-page\" class=\"select-trigger select-sm\" data-select-trigger aria-haspopup=\"listbox\" aria-expanded=\"false\" aria-labelledby=\"rows-per-page-label rows-per-page\"> <span class=\"select-value\" data-placeholder=\"Select…\">20</span> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg> </button> <div class=\"select-content\" id=\"rows-per-page-listbox\" role=\"listbox\" aria-labelledby=\"rows-per-page-label\" hidden> <span class=\"select-item\" role=\"option\" value=\"10\" aria-selected=\"false\">10<span class=\"select-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span></span> <span class=\"select-item\" role=\"option\" value=\"20\" aria-selected=\"true\">20<span class=\"select-check\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span></span>"
        },
        {
          "id": "sizes",
          "title": "Sizes",
          "text": ".pagination-sm and .pagination-lg on the <nav> match .btn-sm and .btn-lg heights. html <nav class=\"pagination pagination-sm\" aria-label=\"Small pagination\"> <ul class=\"pagination-content\"> <li><a class=\"pagination-link pagination-previous\" href=\"#\" rel=\"prev\" aria-label=\"Previous page\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m15 18-6-6 6-6\"/></svg></a></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 1\">1</a></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 2\" aria-current=\"page\">2</a></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 3\">3</a></li> <li><a class=\"pagination-link pagination-next\" href=\"#\" rel=\"next\" aria-label=\"Next page\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg></a></li> </ul> </nav> <nav class=\"pagination pagination-lg\" aria-label=\"Large pagination\"> <ul class=\"pagination-content\"> <li><a class=\"pagination-link pagination-previous\" href=\"#\" rel=\"prev\" aria-label=\"Previous page\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m15 18-6-6 6-6\"/></svg></a></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 1\">1</a></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 2\" aria-current=\"page\">2</a></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 3\">3</a></li> <li><a class=\"pagination-link pagination-next\" href=\"#\" rel=\"next\" aria-label=\"Next page\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fil"
        },
        {
          "id": "responsive",
          "title": "Responsive",
          "text": ".pagination-responsive fits a phone: below the sm breakpoint (640px) only previous, the current page and next show, and the previous / next labels are hidden visually but still read out. Narrow the window to see it. html <nav class=\"pagination pagination-responsive\" aria-label=\"Pagination\"> <ul class=\"pagination-content\"> <li><a class=\"pagination-link pagination-previous\" href=\"#\" rel=\"prev\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m15 18-6-6 6-6\"/></svg><span class=\"pagination-label\">Previous</span></a></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 1\">1</a></li> <li><span class=\"pagination-ellipsis\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"19\" cy=\"12\" r=\"1\"/><circle cx=\"5\" cy=\"12\" r=\"1\"/></svg></span></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 4\">4</a></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 5\" aria-current=\"page\">5</a></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 6\">6</a></li> <li><span class=\"pagination-ellipsis\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"19\" cy=\"12\" r=\"1\"/><circle cx=\"5\" cy=\"12\" r=\"1\"/></svg></span></li> <li><a class=\"pagination-link\" href=\"#\" aria-label=\"Page 10\">10</a></li> <li><a class=\"pagination-link pagination-next\" href=\"#\" rel=\"next\"><span class=\"pagination-label\">Next</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\""
        },
        {
          "id": "macro",
          "title": "Macro",
          "text": "pagination(page, total) works out which pages to show and writes the whole <nav>. {page} in href becomes each link's page number. jinja {% from \"components/pagination/pagination.html\" import pagination %} {{ pagination(5, 10, href=\"/invoices?page={page}\") }} {{ pagination(2, 5, numbers=false, icons=true, first=true, last=true, size=\"sm\", outline=true) }}"
        },
        {
          "id": "page-range",
          "title": "Which pages are shown",
          "text": "The first and last boundaries pages always show, and siblings pages either side of the current one. An ellipsis stands in for two or more pages left out; a gap of one page shows that page instead. Once there are more pages than fit, the list always has the same number of items (2 × siblings + 2 × boundaries + 3, so 7 by default), so the links don't move about as the current page changes. Call Pages shown pagination(1, 10) [1] 2 3 4 5 … 10 pagination(4, 10) 1 2 3 [4] 5 … 10 pagination(5, 10) 1 … 4 [5] 6 … 10 pagination(7, 10) 1 … 6 [7] 8 9 10 pagination(10, 10) 1 … 6 7 8 9 [10] pagination(3, 7) 1 2 [3] 4 5 6 7 (all fit) pagination(10, 20, siblings=2, boundaries=2) 1 2 … 8 9 [10] 11 12 … 19 20 pagination(10, 20, boundaries=0) … 9 [10] 11 … The first, a middle and the last page of ten, rendered: html <nav class=\"pagination\" aria-label=\"Pagination, first page\"> <ul class=\"pagination-content\"> <li><a class=\"pagination-link pagination-previous\" aria-disabled=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m15 18-6-6 6-6\"/></svg><span class=\"pagination-label\">Previous</span></a></li> <li><a class=\"pagination-link\" href=\"#page-1\" aria-label=\"Page 1\" aria-current=\"page\">1</a></li> <li><a class=\"pagination-link\" href=\"#page-2\" aria-label=\"Page 2\">2</a></li> <li><a class=\"pagination-link\" href=\"#page-3\" aria-label=\"Page 3\">3</a></li> <li><a class=\"pagination-link\" href=\"#page-4\" aria-label=\"Page 4\">4</a></li> <li><a class=\"pagination-link\" href=\"#page-5\" aria-label=\"Page 5\">5</a></li> <li><span class=\"pagination-ellipsis\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"19\""
        },
        {
          "id": "htmx",
          "title": "With htmx",
          "text": "Keep the links' hrefs, so the pages work without JavaScript, and let htmx fetch the same URLs and swap only the region holding the table and its pagination. With the macro, attrs puts the attributes on every link: jinja <div id=\"invoices\"> <table class=\"table\">…rows for this page…</table> {{ pagination(page, pages, href=\"/invoices?page={page}\", attrs={ \"hx-get\": \"/invoices?page={page}\", \"hx-target\": \"#invoices\", \"hx-select\": \"#invoices\", \"hx-swap\": \"outerHTML\", \"hx-push-url\": \"true\" }) }} </div> which writes links like: html <a class=\"pagination-link\" href=\"/invoices?page=3\" aria-label=\"Page 3\" hx-get=\"/invoices?page=3\" hx-target=\"#invoices\" hx-select=\"#invoices\" hx-swap=\"outerHTML\" hx-push-url=\"true\">3</a> hx-select=\"#invoices\" picks the region out of the full page the server returns, so one route serves both normal visits and htmx requests. A server that checks the HX-Request header can return just the region instead and drop hx-select. The region swapped includes the pagination, so the current page and the disabled ends update with the rows. hx-push-url=\"true\" puts ?page=3 in the address bar, so reloading and the back button land on the same page. Disabled links have no href and the macro leaves their htmx attributes off, so they send nothing. After the swap, focus is lost with the old link. Move it to the top of the region (for example a heading with tabindex=\"-1\") if keyboard users would otherwise start again from the top of the page. To write the attributes once rather than on every link, put them on the <nav> as inherited attributes (hx-target:inherited=\"#invoices\" and so on) and use hx-boost:inherited=\"true\", which makes htmx fetch each link's href."
        },
        {
          "id": "keyboard",
          "title": "Keyboard",
          "text": "Key Action Tab Shift+Tab Move between the links; disabled ones are skipped Enter Go to that page"
        },
        {
          "id": "accessibility",
          "title": "Accessibility",
          "text": "The <nav> is a navigation landmark; its aria-label tells it apart from the site's other navigation. Use different labels when a page has two. aria-current=\"page\" tells screen readers which page is showing; don't rely on the outline alone. Icon-only links need an aria-label. Links with visible text don't: an aria-label that differs from the text breaks voice control (\"click Next\"). Page links are labelled \"Page 3\" rather than \"3\"; pass page_label to the macro to translate it."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .pagination The <nav aria-label>: centres the list in its container. .pagination-content The <ul> of items. .pagination-link A page, previous, next, first or last link (or button): a ghost button. [aria-current=\"page\"] On a .pagination-link: the current page, outlined. [aria-disabled=\"true\"], :disabled On a .pagination-link (with no href): faded and not clickable. .pagination-previous / .pagination-next Previous / next links, with an icon and a .pagination-label (or an aria-label alone). .pagination-first / .pagination-last First / last page links, usually icon-only with an aria-label. .pagination-label The text of previous / next; hidden visually on small screens with .pagination-responsive. .pagination-ellipsis \"…\" standing in for pages left out, aria-hidden. .pagination-sm / .pagination-lg On the <nav>: 32px / 40px links (36px by default). .pagination-outline On the <nav>: every link outlined; the current page filled. .pagination-responsive On the <nav>: below sm, only previous, the current page and next, with labels visually hidden."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/sidebar",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/sidebar",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/sidebar.md",
      "title": "Sidebar",
      "description": "A composable, themeable app navigation column: groups, menus, badges, actions and sub-menus, collapsing to an icon rail or off the screen, with an off-canvas panel on mobile.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "preview",
          "text": "Preview"
        },
        {
          "level": 2,
          "id": "structure",
          "text": "Structure"
        },
        {
          "level": 2,
          "id": "layout",
          "text": "Layout and inset"
        },
        {
          "level": 2,
          "id": "header-footer",
          "text": "Header and footer"
        },
        {
          "level": 2,
          "id": "content",
          "text": "Content"
        },
        {
          "level": 2,
          "id": "group",
          "text": "Group"
        },
        {
          "level": 2,
          "id": "menu",
          "text": "Menu and menu button"
        },
        {
          "level": 2,
          "id": "menu-action",
          "text": "Menu action"
        },
        {
          "level": 2,
          "id": "menu-badge",
          "text": "Menu badge"
        },
        {
          "level": 2,
          "id": "menu-sub",
          "text": "Menu sub"
        },
        {
          "level": 2,
          "id": "menu-skeleton",
          "text": "Menu skeleton"
        },
        {
          "level": 2,
          "id": "separator-input",
          "text": "Separator and input"
        },
        {
          "level": 2,
          "id": "trigger",
          "text": "Trigger"
        },
        {
          "level": 2,
          "id": "rail",
          "text": "Rail"
        },
        {
          "level": 2,
          "id": "side",
          "text": "Side"
        },
        {
          "level": 2,
          "id": "variant",
          "text": "Variant"
        },
        {
          "level": 2,
          "id": "collapsible",
          "text": "Collapsible"
        },
        {
          "level": 2,
          "id": "icon-mode",
          "text": "Icon mode and tooltips"
        },
        {
          "level": 2,
          "id": "width",
          "text": "Width"
        },
        {
          "level": 2,
          "id": "mobile",
          "text": "Mobile"
        },
        {
          "level": 2,
          "id": "keyboard",
          "text": "Keyboard"
        },
        {
          "level": 2,
          "id": "persistence",
          "text": "Persistence and server rendering"
        },
        {
          "level": 2,
          "id": "theming",
          "text": "Theming"
        },
        {
          "level": 2,
          "id": "accessibility",
          "text": "Accessibility"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "htmx"
        },
        {
          "level": 2,
          "id": "events",
          "text": "Events"
        },
        {
          "level": 2,
          "id": "docs-navigation",
          "text": "The docs navigation"
        },
        {
          "level": 2,
          "id": "upgrading",
          "text": "Upgrading from 0.1"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "preview",
          "title": "Preview",
          "text": "Switchboard, a made-up console for watching an htmx app's requests: an environment switcher and a route search in the header, a Monitor group with counts, collapsible Ship items with sub-menus, collapsible Saved views with actions, a usage meter and an account menu in the footer, a rail and a trigger. Collapse it with the button, the rail on its edge or Ctrl B (⌘ B on a Mac) after clicking inside the frame: it shrinks to icons, and each one shows its label as a tooltip. Open it on its own page, where it also remembers its state and turns into an off-canvas panel on a narrow window."
        },
        {
          "id": "structure",
          "title": "Structure",
          "text": "A sidebar is plain markup with classes for each part. The layout holds the sidebar and the main area; the sidebar holds a header, a scrolling content area and a footer; the content holds groups of menus. app.html <div class=\"sidebar-layout\"> <aside id=\"app-sidebar\" class=\"sidebar\" data-sidebar data-collapsible=\"icon\" aria-label=\"Switchboard\"> <div class=\"sidebar-header\">…</div> <nav class=\"sidebar-content\" aria-label=\"Main\"> <div class=\"sidebar-group\"> <p class=\"sidebar-group-label\">Monitor</p> <ul class=\"sidebar-menu\"> <li class=\"sidebar-menu-item\"> <a href=\"/requests\" class=\"sidebar-menu-button\" aria-current=\"page\">{{ icon(\"zap\") }}<span>Requests</span></a> </li> </ul> </div> </nav> <div class=\"sidebar-footer\">…</div> <button type=\"button\" class=\"sidebar-rail\" data-sidebar-rail tabindex=\"-1\" aria-label=\"Toggle sidebar\"></button> </aside> <main class=\"sidebar-inset\"> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm sidebar-trigger\" data-sidebar-trigger aria-controls=\"app-sidebar\" aria-label=\"Toggle sidebar\">{{ icon(\"panel-left\") }}</button> … </main> </div> .sidebar-layout places the sidebar and .sidebar-inset (the page's main area) side by side. .sidebar with data-sidebar is the column. Options are data attributes on it: side, variant, collapsible and breakpoint. .sidebar-header and .sidebar-footer stay put; .sidebar-content between them scrolls. .sidebar-group is a section of the content, with a .sidebar-group-label, an optional .sidebar-group-action and its .sidebar-menu. .sidebar-menu is a <ul> of .sidebar-menu-items, each holding a .sidebar-menu-button (a link or button) and optionally an action, a badge or a sub-menu. .sidebar-trigger and .sidebar-rail toggle it. The behaviour (sidebar.ts, run by initComponents()) handles toggling, the keyboard shortcut, the cookie and the mobile panel. Everything else is CSS. The previews below show parts on their own, so they add h-auto (fit the content rather than the window's height) and data-breakpoint=\"none\" ("
        },
        {
          "id": "layout",
          "title": "Layout and inset",
          "text": ".sidebar-layout is a flex row at least as tall as the window. The sidebar sticks to the top of the window while the page scrolls, and is as tall as the window. If a fixed or sticky header sits above the layout, set --sidebar-top to its height, so the sidebar sticks below it and is shortened to fit. Put the page's content in <main class=\"sidebar-inset\">, which takes the remaining width. html <header class=\"sticky top-0 h-14\">…</header> <div class=\"sidebar-layout [--sidebar-top:3.5rem]\"> <aside class=\"sidebar\" data-sidebar>…</aside> <main class=\"sidebar-inset\">…</main> </div> The navigation of these docs is one, set up this way (see the docs navigation)."
        },
        {
          "id": "header-footer",
          "title": "Header and footer",
          "text": "Use the header for a logo, a workspace or environment switcher, or a search field, and the footer for the signed-in user or the plan's usage. A menu in either works like one in the content. Here a large menu button opens a dropdown; in the footer, .dropdown-menu-up opens it upwards. html <aside class=\"sidebar h-auto rounded-lg border\" data-breakpoint=\"none\" aria-label=\"Header example\"> <div class=\"sidebar-header\"> <ul class=\"sidebar-menu\"> <li class=\"sidebar-menu-item\"> <div class=\"dropdown w-full\" data-dropdown> <button type=\"button\" class=\"sidebar-menu-button sidebar-menu-button-lg\" data-dropdown-trigger aria-haspopup=\"menu\" aria-expanded=\"false\"> <span class=\"grid size-8 shrink-0 place-items-center rounded-full border-2 border-sidebar-primary text-sidebar-primary\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M13 2 3 14h9l-1 8 10-12h-9l1-8z\"/></svg></span> <span class=\"grid flex-1 leading-tight\"> <span class=\"truncate font-semibold\">Switchboard</span> <span class=\"truncate font-mono text-xs text-muted-foreground\">production</span> </span> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"ml-auto\" aria-hidden=\"true\"><path d=\"m7 15 5 5 5-5\"/><path d=\"m7 9 5-5 5 5\"/></svg> </button> <div class=\"dropdown-menu w-full\" role=\"menu\" aria-label=\"Environments\" hidden> <div role=\"group\" aria-label=\"Environment\"> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemradio\" aria-checked=\"true\">Production</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitemradio\" aria-checked=\"false\">Staging</button> </div> </div> </div> </li> </ul> <input class=\"input sidebar-input\" type=\"search\" placeholder=\"Jump to route…\" aria-label=\"Jump to route\" /> </div> </"
        },
        {
          "id": "content",
          "title": "Content",
          "text": ".sidebar-content fills the height between the header and the footer and scrolls when its groups don't fit. Make it the <nav> (with an aria-label) when it holds the site's navigation. Separate groups with a .sidebar-separator if they need a line."
        },
        {
          "id": "group",
          "title": "Group",
          "text": "A .sidebar-group is a labelled section. .sidebar-group-action is an icon button in its top-right corner (give it an aria-label). Wrap the menu in .sidebar-group-content when the group is collapsible: make the group the collapsible, the label its trigger and the content its content. A chevron with .collapsible-icon turns a quarter while open. html <aside class=\"sidebar h-auto rounded-lg border\" data-breakpoint=\"none\" aria-label=\"Group example\"> <div class=\"sidebar-group collapsible\" data-collapsible> <button type=\"button\" class=\"sidebar-group-label\" data-collapsible-trigger aria-expanded=\"true\"> Saved views <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"collapsible-icon\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg> </button> <button type=\"button\" class=\"sidebar-group-action\" aria-label=\"Save the current view\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5v14\"/></svg></button> <div class=\"sidebar-group-content\" data-collapsible-content> <ul class=\"sidebar-menu\"> <li class=\"sidebar-menu-item\"><a href=\"#group\" class=\"sidebar-menu-button\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z\"/></svg><span>Slower th"
        },
        {
          "id": "menu",
          "title": "Menu and menu button",
          "text": "A .sidebar-menu is a list; each .sidebar-menu-item holds a .sidebar-menu-button: an <a> for navigation, a <button> for an action. Put an icon first and the label in a <span>, which is cut short with an ellipsis when it doesn't fit. Current page: aria-current=\"page\" (or data-active for anything else that should look selected). It is tinted with --sidebar-primary and marked by a bar on its leading edge, which stays visible in the icon rail. Sizes: .sidebar-menu-button-sm (28px), the default (32px), .sidebar-menu-button-lg (48px, for two lines or an avatar). Variant: .sidebar-menu-button-outline draws a border and a background. Disabled: disabled on a button, aria-disabled=\"true\" on a link. html <aside class=\"sidebar h-auto rounded-lg border\" data-breakpoint=\"none\" aria-label=\"Menu example\"> <div class=\"sidebar-group\"> <ul class=\"sidebar-menu\"> <li class=\"sidebar-menu-item\"><a href=\"#menu\" class=\"sidebar-menu-button\" aria-current=\"page\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M13 2 3 14h9l-1 8 10-12h-9l1-8z\"/></svg><span>Requests</span></a></li> <li class=\"sidebar-menu-item\"><a href=\"#menu\" class=\"sidebar-menu-button\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"4\" rx=\"2\"/><path d=\"M16 2v4\"/><path d=\"M8 2v4\"/><path d=\"M3 10h18\"/></svg><span>Fragments swapped in the last 24 hours</span></a></li> <li class=\"sidebar-menu-item\"><a href=\"#menu\" class=\"sidebar-menu-button sidebar-menu-button-sm\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" ar"
        },
        {
          "id": "menu-action",
          "title": "Menu action",
          "text": "A .sidebar-menu-action is a small icon button at the end of an item, next to the menu button rather than inside it (a button can't hold another). Add .sidebar-menu-action-hover to show it only while the item is hovered or focused, or its menu is open; screens that can't hover always show it. To open a dropdown, wrap the action in the dropdown and make it the trigger. html <aside class=\"sidebar h-auto rounded-lg border\" data-breakpoint=\"none\" aria-label=\"Action example\"> <div class=\"sidebar-group\"> <ul class=\"sidebar-menu\"> <li class=\"sidebar-menu-item\"> <a href=\"#menu-action\" class=\"sidebar-menu-button\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z\"/></svg><span>Checkout swaps</span></a> <div class=\"dropdown\" data-dropdown> <button type=\"button\" class=\"sidebar-menu-action sidebar-menu-action-hover\" data-dropdown-trigger aria-haspopup=\"menu\" aria-expanded=\"false\" aria-label=\"Options for Checkout swaps\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"19\" cy=\"12\" r=\"1\"/><circle cx=\"5\" cy=\"12\" r=\"1\"/></svg></button> <div class=\"dropdown-menu dropdown-menu-end\" role=\"menu\" aria-label=\"Checkout swaps\" hidden> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy link</span></button> <button ty"
        },
        {
          "id": "menu-badge",
          "title": "Menu badge",
          "text": "A .sidebar-menu-badge after the menu button shows a count or a short status at the end of the item. It ignores the pointer, so the whole item stays clickable. Screen readers read it after the link, so keep it meaningful on its own or add hidden text (<span class=\"sr-only\"> unread</span>). html <aside class=\"sidebar h-auto rounded-lg border\" data-breakpoint=\"none\" aria-label=\"Badge example\"> <div class=\"sidebar-group\"> <ul class=\"sidebar-menu\"> <li class=\"sidebar-menu-item\"><a href=\"#menu-badge\" class=\"sidebar-menu-button\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M13 2 3 14h9l-1 8 10-12h-9l1-8z\"/></svg><span>Requests</span></a><span class=\"sidebar-menu-badge\">1.2k<span class=\"sr-only\"> in the last hour</span></span></li> <li class=\"sidebar-menu-item\"><a href=\"#menu-badge\" class=\"sidebar-menu-button\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9\"/><path d=\"M10.3 21a1.94 1.94 0 0 0 3.4 0\"/></svg><span>Alerts</span></a><span class=\"sidebar-menu-badge\"><span class=\"badge badge-danger badge-solid px-1.5\">3</span></span></li> </ul> </div> </aside>"
        },
        {
          "id": "menu-sub",
          "title": "Menu sub",
          "text": "A .sidebar-menu-sub nests a list of .sidebar-menu-sub-items with .sidebar-menu-sub-buttons under an item, indented along a line. Usually it's the content of a collapsible item: the item is the collapsible, its menu button the trigger with a trailing chevron-right, which turns down while open. The current sub-item lights up its stretch of the line, like a branch of a tree. .sidebar-menu-sub-button-sm makes the text smaller. html <aside class=\"sidebar h-auto rounded-lg border\" data-breakpoint=\"none\" aria-label=\"Sub-menu example\"> <div class=\"sidebar-group\"> <ul class=\"sidebar-menu\"> <li class=\"sidebar-menu-item collapsible\" data-collapsible> <button type=\"button\" class=\"sidebar-menu-button\" data-collapsible-trigger aria-expanded=\"true\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></svg><span>Routes</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"collapsible-icon ml-auto\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg> </button> <ul class=\"sidebar-menu-sub\" data-collapsible-content> <li class=\"sidebar-menu-sub-item\"><a href=\"#menu-sub\" class=\"sidebar-menu-sub-button font-mono text-xs\"><span>/orders</span></a></li> <li class=\"sidebar-menu-sub-item\"><a href=\"#menu-sub\" class=\"sidebar-menu-sub-button font-mono text-xs\" aria-current=\"page\"><span>/search</span></a></li> <li class=\"sidebar-menu-sub-item\"><a href=\"#menu-sub\" class=\"sidebar-menu-sub-button font-mono text-xs\"><span>/cart/items</span></a></li> </ul> </li> <li class=\"sidebar-menu-item collapsible\" data-collapsible> <button type=\"button\" class=\"sidebar-menu-bu"
        },
        {
          "id": "menu-skeleton",
          "title": "Menu skeleton",
          "text": ".sidebar-menu-skeleton is a placeholder item for a menu that is still loading, with an optional .sidebar-menu-skeleton-icon and a .sidebar-menu-skeleton-text bar whose width is --skeleton-width (70% by default). It pulses, except with reduced motion. html <aside class=\"sidebar h-auto rounded-lg border\" data-breakpoint=\"none\" aria-label=\"Skeleton example\"> <div class=\"sidebar-group\"> <p class=\"sidebar-group-label\">Saved views</p> <ul class=\"sidebar-menu\" aria-busy=\"true\"> <li class=\"sr-only\">Loading saved views…</li> <li class=\"sidebar-menu-skeleton\" aria-hidden=\"true\"><span class=\"sidebar-menu-skeleton-icon\"></span><span class=\"sidebar-menu-skeleton-text\" style=\"--skeleton-width: 80%\"></span></li> <li class=\"sidebar-menu-skeleton\" aria-hidden=\"true\"><span class=\"sidebar-menu-skeleton-icon\"></span><span class=\"sidebar-menu-skeleton-text\" style=\"--skeleton-width: 55%\"></span></li> <li class=\"sidebar-menu-skeleton\" aria-hidden=\"true\"><span class=\"sidebar-menu-skeleton-icon\"></span><span class=\"sidebar-menu-skeleton-text\" style=\"--skeleton-width: 65%\"></span></li> </ul> </div> </aside> With htmx, render the skeleton in the page and let the menu replace itself once it's loaded. The server answers with the finished <ul class=\"sidebar-menu\">; see htmx. html <ul class=\"sidebar-menu\" hx-get=\"/fragments/saved-views\" hx-trigger=\"load\" hx-swap=\"outerHTML\" aria-busy=\"true\"> <li class=\"sr-only\">Loading saved views…</li> <li class=\"sidebar-menu-skeleton\" aria-hidden=\"true\"> <span class=\"sidebar-menu-skeleton-icon\"></span> <span class=\"sidebar-menu-skeleton-text\" style=\"--skeleton-width: 80%\"></span> </li> … </ul>"
        },
        {
          "id": "separator-input",
          "title": "Separator and input",
          "text": ".sidebar-separator is a line between groups or parts (on an <hr>, or a <div role=\"separator\">). .sidebar-input goes with .input (Input) for a compact field, such as a search box in the header. html <aside class=\"sidebar h-auto rounded-lg border\" data-breakpoint=\"none\" aria-label=\"Separator example\"> <div class=\"sidebar-header\"> <input class=\"input sidebar-input\" type=\"search\" placeholder=\"Jump to route…\" aria-label=\"Jump to route\" /> </div> <hr class=\"sidebar-separator\" /> <div class=\"sidebar-group\"> <ul class=\"sidebar-menu\"> <li class=\"sidebar-menu-item\"><a href=\"#separator-input\" class=\"sidebar-menu-button\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12 20V10\"/><path d=\"M18 20V4\"/><path d=\"M6 20v-4\"/></svg><span>Overview</span></a></li> </ul> </div> </aside>"
        },
        {
          "id": "trigger",
          "title": "Trigger",
          "text": "Any button with data-sidebar-trigger toggles the sidebar named by its aria-controls. Without aria-controls it toggles the sidebar of the .sidebar-layout it's in, and gets aria-controls set. Its aria-expanded is kept in sync: whether the sidebar is expanded, or, in mobile mode, whether the panel is open. .sidebar-trigger is a small ghost icon button for it; a .btn works as well. Give it an aria-label. html <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm sidebar-trigger\" data-sidebar-trigger aria-controls=\"app-sidebar\" aria-label=\"Toggle sidebar\">{{ icon(\"panel-left\") }}</button> A button with data-sidebar-close only closes: the mobile panel when it's open, else it collapses the sidebar. Use it for a close button inside the panel."
        },
        {
          "id": "rail",
          "title": "Rail",
          "text": ".sidebar-rail with data-sidebar-rail is a thin strip along the sidebar's inner edge. Hovering it shows a line and a resize cursor; clicking it toggles the sidebar. When the sidebar has slid off the screen the rail stays at the window's edge, so the sidebar can be brought back from there. It's a pointer shortcut, kept out of the tab order (tabindex=\"-1\"): keyboard users have the trigger and the shortcut. Put it last in the sidebar. It's hidden in mobile mode and with data-collapsible=\"none\"."
        },
        {
          "id": "side",
          "title": "Side",
          "text": "data-side=\"right\" puts the sidebar on the right: its border, rail, slide direction and the inset's margin all follow. In a right sidebar, use .tooltip-content-left for icon-mode tooltips and the panel-right icon for the trigger. A layout can hold a sidebar on each side, each with its own trigger. html <aside class=\"sidebar\" data-sidebar data-side=\"right\">…</aside>"
        },
        {
          "id": "variant",
          "title": "Variant",
          "text": "data-variant sets how the sidebar sits in the page: sidebar (the default): a full-height column with a border on its inner edge. See the preview. floating: a rounded panel with a border and a shadow, inset from the window's edges. inset: the sidebar blends into the page background, and the main area becomes a raised, rounded panel beside it. html <aside class=\"sidebar\" data-sidebar data-variant=\"floating\">…</aside> <aside class=\"sidebar\" data-sidebar data-variant=\"inset\">…</aside>"
        },
        {
          "id": "collapsible",
          "title": "Collapsible",
          "text": "data-collapsible sets what collapsing does: offcanvas (the default): the sidebar slides fully out, and the main area takes the space. Once out of view it is hidden, so nothing in it can take focus. icon: the sidebar shrinks to a rail of icons; see Icon mode. none: it never collapses (on mobile it's still an off-canvas panel). The state is data-state=\"expanded\" or \"collapsed\" on the sidebar. Render the one you want; the behaviour sets expanded when there's none. html <aside class=\"sidebar\" data-sidebar data-collapsible=\"offcanvas\" data-state=\"collapsed\">…</aside>"
        },
        {
          "id": "icon-mode",
          "title": "Icon mode and tooltips",
          "text": "Collapsed with data-collapsible=\"icon\", the sidebar is --sidebar-width-icon wide. Menu buttons become squares showing just their icon, large ones just their avatar or logo; group labels, badges, actions, sub-menus and .sidebar-input hide. The labels stay in the page (clipped, not removed), so links keep their accessible names. For a tooltip, make the item a tooltip and add its text after the button. It only shows while the sidebar is collapsed to icons. It repeats the button's name, so hide it from screen readers with aria-hidden=\"true\"; Esc still dismisses it. html <li class=\"sidebar-menu-item tooltip\"> <a href=\"/alerts\" class=\"sidebar-menu-button\">{{ icon(\"bell\") }}<span>Alerts</span></a> <span class=\"tooltip-content tooltip-content-right\" aria-hidden=\"true\">Alerts</span> </li> In icon mode the content doesn't scroll, so the tooltips and menus can reach outside it: keep the icons to what fits the height of the window."
        },
        {
          "id": "width",
          "title": "Width",
          "text": "Three CSS variables set the widths. Set them on the sidebar, the layout or any ancestor, for example with Tailwind's arbitrary properties. Variable Description --sidebar-width Expanded width, 16rem by default. Floating and inset sidebars include their 0.5rem margin in it. --sidebar-width-icon Width of the icon rail, 3rem by default (a 2rem button and its padding). --sidebar-width-mobile Width of the mobile panel, 18rem by default. --sidebar-top Where the sidebar sticks, and how much shorter than the window it is: the height of a header above it. 0 by default. html <aside class=\"sidebar [--sidebar-width:20rem] [--sidebar-width-mobile:85vw]\" data-sidebar>…</aside>"
        },
        {
          "id": "mobile",
          "title": "Mobile",
          "text": "Below a breakpoint the sidebar becomes an off-canvas panel over the page, with a dimmed backdrop. The same trigger and shortcut open it. It closes on Esc, a click on the backdrop, following a link inside it, or a data-sidebar-close button. While it's open, focus moves into it (to the current page's link), stays there, the rest of the layout is inert and the page doesn't scroll; closing returns focus to the trigger. The panel always shows the full sidebar, whatever its desktop state. data-breakpoint picks the breakpoint: sm (40rem), md (48rem, the default), lg (64rem), or none to stay a column at every width. The switch is a media query on the window's width, in sidebar.css; the behaviour reads it from the stylesheet (the --sidebar-mobile custom property), so the two always agree, and it marks the sidebar data-mobile while it applies. The frame below is phone-sized:"
        },
        {
          "id": "keyboard",
          "title": "Keyboard",
          "text": "Key Action Ctrl B / ⌘ B Toggles the sidebar (opens or closes the panel in mobile mode). Ignored while typing in a field. Tab Shift Tab Moves through the links and buttons. Inside the open mobile panel, wraps around within it. Enter Space Follows a link, presses a button, opens or closes a collapsible item or group. Esc Closes the mobile panel and returns focus to the trigger; otherwise hides an icon-mode tooltip. data-shortcut=\"j\" moves the shortcut to another letter; data-shortcut=\"none\" turns it off. Every sidebar listens for its own letter, so with two sidebars on a page give them different letters or turn one off."
        },
        {
          "id": "persistence",
          "title": "Persistence and server rendering",
          "text": "With data-cookie=\"sidebar_state\" (any cookie name) the behaviour stores the desktop state each time it changes: true for expanded, false for collapsed, for seven days, on path /, SameSite=Lax. On load it restores a stored state without animating. Without data-cookie nothing is stored. The behaviour only runs once the page has loaded, so a page that renders the sidebar expanded and is then restored to collapsed would show it open for a moment. Render the stored state instead. On a server, read the cookie and pass it to the template (see rendering a page per request): server.ts app.get(\"/dashboard\", ({ request }) => { const collapsed = /(?:^|;\\s*)sidebar_state=false/.test(request.headers.get(\"cookie\") ?? \"\"); return html(site.render(\"/dashboard\", { sidebarState: collapsed ? \"collapsed\" : \"expanded\" })); }); pages/dashboard.html <aside id=\"app-sidebar\" class=\"sidebar\" data-sidebar data-collapsible=\"icon\" data-cookie=\"sidebar_state\" data-state=\"{{ sidebarState | default('expanded') }}\">…</aside> A static site has no request to read, so set the state from the cookie with a small inline script as the sidebar's first child. It runs before the page is painted: Static pages <aside id=\"app-sidebar\" class=\"sidebar\" data-sidebar data-collapsible=\"icon\" data-cookie=\"sidebar_state\"> <script> if (/(?:^|;\\s*)sidebar_state=false/.test(document.cookie)) document.currentScript.parentElement.dataset.state = \"collapsed\"; </script> … </aside>"
        },
        {
          "id": "theming",
          "title": "Theming",
          "text": "The sidebar has its own colour tokens, so it can be tinted apart from the page. By default they follow the page's (Theming), with a slightly darker background in light mode. Override them on :root, under .dark, or on one sidebar. Each is also a Tailwind colour: bg-sidebar, text-sidebar-primary, border-sidebar-border… Token Description --sidebar Background. zinc-50 in light mode, zinc-900 in dark. --sidebar-foreground Text and icons. Defaults to --foreground. --sidebar-primary Accent fills, such as a logo tile. Defaults to --primary. --sidebar-primary-foreground Text on --sidebar-primary. --sidebar-accent Hovered and current items. Defaults to --accent. --sidebar-accent-foreground Text on --sidebar-accent. --sidebar-border Borders, the sub-menu line, separators, the rail. Defaults to --border. --sidebar-ring Focus rings. Defaults to --ring. css /* A dark sidebar in both themes */ .sidebar { --sidebar: var(--color-zinc-900); --sidebar-foreground: var(--color-zinc-300); --sidebar-accent: var(--color-zinc-800); --sidebar-accent-foreground: var(--color-white); --sidebar-border: var(--color-zinc-800); }"
        },
        {
          "id": "accessibility",
          "title": "Accessibility",
          "text": "The <aside> is a complementary landmark; give it an aria-label. Make the part holding the site's links a <nav> with its own label. Mark the current page with aria-current=\"page\": it's both what screen readers announce and the style hook. Menus are lists of links and buttons, not ARIA menus, so Tab moves through them as through any navigation. Collapsible items and groups are disclosure buttons with aria-expanded. The trigger is a button with aria-controls and an aria-expanded that follows the sidebar. Label it. Collapsed to icons, labels are clipped rather than removed, so links keep their names; the tooltips that repeat them are aria-hidden, appear on keyboard focus too, and Esc hides them. A sidebar slid off the screen and a closed mobile panel are hidden, so nothing in them can be focused. The open mobile panel keeps focus inside it and makes the rest of the layout inert; closing it returns focus to where it was. Action buttons need an aria-label naming their item (\"Options for Checkout swaps\"), since several share an icon. With reduced motion the sidebar switches states without animating."
        },
        {
          "id": "htmx",
          "title": "htmx",
          "text": "The behaviour runs again on every element htmx inserts (initComponents on htmx:after:process), so a sidebar, a trigger or a menu returned by the server works without extra code, and the existing triggers follow a sidebar that was replaced. Load slow parts of the menu after the page, as in Menu skeleton: the fragment the server returns is ordinary sidebar markup. The fragment for hx-get=\"/fragments/saved-views\" <ul class=\"sidebar-menu\"> {% for view in saved_views %} <li class=\"sidebar-menu-item tooltip\"> <a href=\"/views/{{ view.id }}\" class=\"sidebar-menu-button\">{{ icon(\"bookmark\") }}<span>{{ view.name }}</span></a> <span class=\"tooltip-content tooltip-content-right\" aria-hidden=\"true\">{{ view.name }}</span> </li> {% endfor %} </ul> With hx-boost, mark the current link from the page being rendered (aria-current=\"page\") as usual; following a link closes the mobile panel before the new page is swapped in."
        },
        {
          "id": "events",
          "title": "Events",
          "text": "Every change fires a bubbling sidebar:toggle event on the sidebar, with detail { expanded, open, mobile }: the desktop state, whether the mobile panel is open, and whether the sidebar is in mobile mode. To toggle it from a script, click its trigger. js document.addEventListener(\"sidebar:toggle\", (e) => { console.log(e.target.id, e.detail.expanded ? \"expanded\" : \"collapsed\"); }); document.querySelector(\"[data-sidebar-trigger]\").click();"
        },
        {
          "id": "docs-navigation",
          "title": "The docs navigation",
          "text": "The navigation of these docs is a sidebar composed for a docs site: it never collapses on a desktop (data-collapsible=\"none\"), it's an off-canvas panel below lg opened by the header's menu button, it sticks under the 56px header with --sidebar-top, and its colours come from the tokens: transparent from lg up, with muted links. layouts/docs.html (shortened) <div class=\"sidebar-layout mx-auto min-h-0 max-w-7xl gap-12 px-4\"> <aside id=\"docs-nav\" class=\"sidebar text-foreground [--sidebar-foreground:var(--muted-foreground)] [--sidebar-top:3.5rem] [--sidebar-width:15rem] [--sidebar:var(--background)] lg:border-r-0 lg:[--sidebar:transparent]\" data-sidebar data-collapsible=\"none\" data-breakpoint=\"lg\" data-shortcut=\"none\" aria-label=\"Documentation\"> <div class=\"sidebar-header flex-row items-center justify-between lg:hidden\"> <span class=\"font-semibold\">Documentation</span> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm\" data-sidebar-close aria-label=\"Close navigation\">…</button> </div> <div class=\"sidebar-content gap-6 p-6 lg:px-0 lg:py-8\"> <nav class=\"sidebar-group p-0\" aria-labelledby=\"docs-nav-1\"> <p id=\"docs-nav-1\" class=\"sidebar-group-label …\">Getting started</p> <ul class=\"sidebar-menu gap-0\"> <li class=\"sidebar-menu-item\"><a href=\"/docs\" class=\"sidebar-menu-button\" aria-current=\"page\"><span>Introduction</span></a></li> </ul> </nav> </div> </aside> <main class=\"min-w-0 flex-1\">…</main> </div>"
        },
        {
          "id": "upgrading",
          "title": "Upgrading from 0.1",
          "text": "The sidebar of 0.1 was a single navigation column. Its classes map to the new parts: 0.1 Description .sidebar-label .sidebar-group-label (inside a .sidebar-group). .sidebar-link .sidebar-menu-button, in a <li class=\"sidebar-menu-item\"> of a <ul class=\"sidebar-menu\">. .sidebar-group Still the section, now with padding and no margin between groups: wrap the groups in .sidebar-content and set its gap. .sidebar-backdrop Remove it: the backdrop is drawn by .sidebar-layout. [data-sidebar-toggle] [data-sidebar-trigger]. [data-sidebar-close] Same name; now for close buttons, not the backdrop. [data-open] Same meaning (the mobile panel is open), but set only in mobile mode. .sidebar (lg sticky column) Wrap it with its main area in .sidebar-layout. For the old look: data-collapsible=\"none\" data-breakpoint=\"lg\", --sidebar-top for the header, --sidebar-width: 15rem, and a transparent --sidebar from lg up (see the docs navigation above)."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .sidebar-layout The layout: a flex row of the sidebar and .sidebar-inset, at least as tall as the window. Draws the mobile backdrop. .sidebar-inset The main area beside the sidebar. A raised panel with data-variant=\"inset\". .sidebar The sidebar column. Sticky; full height minus --sidebar-top. .sidebar-header Top part: logo, workspace or environment switcher, search. .sidebar-content Middle part; scrolls. .sidebar-footer Bottom part: user menu. .sidebar-separator A line between parts or groups. .sidebar-input A compact .input for the sidebar. .sidebar-group A section of the content. .sidebar-group-label Group heading. On a button or summary, a collapsible trigger. .sidebar-group-action Icon button in the group's top-right corner. .sidebar-group-content Wraps the group's menu (the collapsible content). .sidebar-menu A list of items (ul). .sidebar-menu-item An item (li). Add .tooltip for an icon-mode tooltip. .sidebar-menu-button The item's link or button. Current with aria-current=\"page\" or data-active. .sidebar-menu-button-sm 28px tall, smaller text. .sidebar-menu-button-lg 48px tall, for two lines or an avatar. .sidebar-menu-button-outline With a border and a background. .sidebar-menu-action Icon button at the end of an item (or a dropdown trigger there). .sidebar-menu-action-hover Shows the action only while the item is hovered or focused. .sidebar-menu-badge A count or status at the end of an item. .sidebar-menu-sub A nested list under an item. .sidebar-menu-sub-item An item of a sub-menu. .sidebar-menu-sub-button A sub-menu link. Current with aria-current=\"page\" or data-active. .sidebar-menu-sub-button-sm Smaller sub-menu text. .sidebar-menu-skeleton A loading placeholder item. .sidebar-menu-skeleton-icon Its icon square. .sidebar-menu-skeleton-text Its text bar; width from --skeleton-width (70%). .sidebar-trigger A small icon button for data-sidebar-trigger. .sidebar-rail Thin toggle strip along the inner edge (with data-sidebar-rail). data-sid"
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/tabs",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/tabs",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/tabs.md",
      "title": "Tabs",
      "description": "Layered panels of content shown one at a time: segmented, underlined, pill and outline styles, icons, disabled tabs, vertical lists, and groups that sync and remember the choice.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "line",
          "text": "Underlined"
        },
        {
          "level": 2,
          "id": "variants",
          "text": "Pills and outline"
        },
        {
          "level": 2,
          "id": "full-width",
          "text": "Full width"
        },
        {
          "level": 2,
          "id": "icons",
          "text": "Icons"
        },
        {
          "level": 2,
          "id": "disabled",
          "text": "Disabled"
        },
        {
          "level": 2,
          "id": "vertical",
          "text": "Vertical"
        },
        {
          "level": 2,
          "id": "sync",
          "text": "Synced and remembered"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "Lazy panels with htmx"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"tabs mx-auto max-w-md\" data-tabs> <div class=\"tabs-list\" role=\"tablist\" aria-label=\"Account\"> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"account\" aria-selected=\"true\">Account</button> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"password\" aria-selected=\"false\">Password</button> </div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"account\"> <p class=\"muted\">Change your name and email address.</p> </div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"password\" hidden> <p class=\"muted\">Change your password. You'll be signed out on other devices.</p> </div> </div>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "Each [data-tab] button shows the [data-tab-panel] with the same value and hides the others. Mark the initial tab with aria-selected=\"true\" and add hidden to the other panels, so the page is right before JavaScript runs. The behaviour fills in id, aria-controls and aria-labelledby, and moves focus with ← → Home End. .tabs on the wrapper spaces the list and the panels and lays them out side by side when vertical."
        },
        {
          "id": "line",
          "title": "Underlined",
          "text": "html <div class=\"tabs mx-auto max-w-md\" data-tabs> <div class=\"tabs-list tabs-line\" role=\"tablist\"> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"overview\" aria-selected=\"true\">Overview</button> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"activity\" aria-selected=\"false\">Activity</button> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"settings\" aria-selected=\"false\">Settings</button> </div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"overview\"><p class=\"muted\">Overview panel</p></div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"activity\" hidden><p class=\"muted\">Activity panel</p></div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"settings\" hidden><p class=\"muted\">Settings panel</p></div> </div>"
        },
        {
          "id": "variants",
          "title": "Pills and outline",
          "text": ".tabs-pills fills the selected tab with the primary colour; .tabs-outline sets it in a bordered chip on a plain background. html <div class=\"tabs\" data-tabs> <div class=\"tabs-list tabs-pills\" role=\"tablist\" aria-label=\"Period\"> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"day\" aria-selected=\"true\">Day</button> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"week\" aria-selected=\"false\">Week</button> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"month\" aria-selected=\"false\">Month</button> </div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"day\"><p class=\"muted\">Today's numbers.</p></div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"week\" hidden><p class=\"muted\">This week's numbers.</p></div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"month\" hidden><p class=\"muted\">This month's numbers.</p></div> </div> <div class=\"tabs\" data-tabs> <div class=\"tabs-list tabs-outline\" role=\"tablist\" aria-label=\"Period\"> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"day\" aria-selected=\"true\">Day</button> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"week\" aria-selected=\"false\">Week</button> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"month\" aria-selected=\"false\">Month</button> </div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"day\"><p class=\"muted\">Today's numbers.</p></div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"week\" hidden><p class=\"muted\">This week's numbers.</p></div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"month\" hidden><p class=\"muted\">This month's numbers.</p></div> </div>"
        },
        {
          "id": "full-width",
          "title": "Full width",
          "text": ".tabs-full stretches the list across its container and shares the row between the triggers. html <div class=\"tabs mx-auto max-w-md\" data-tabs> <div class=\"tabs-list tabs-full\" role=\"tablist\" aria-label=\"Sign in\"> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"email\" aria-selected=\"true\">Email</button> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"phone\" aria-selected=\"false\">Phone</button> </div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"email\"><input class=\"input\" type=\"email\" placeholder=\"you@example.com\" aria-label=\"Email\" /></div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"phone\" hidden><input class=\"input\" type=\"tel\" placeholder=\"+61 400 000 000\" aria-label=\"Phone\" /></div> </div>"
        },
        {
          "id": "icons",
          "title": "Icons",
          "text": "Put an icon() before (or instead of) the label. An icon-only tab needs an aria-label. html <div class=\"tabs\" data-tabs> <div class=\"tabs-list\" role=\"tablist\" aria-label=\"View\"> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"preview\" aria-selected=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg> Preview</button> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"code\" aria-selected=\"false\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m16 18 6-6-6-6M8 6l-6 6 6 6\"/></svg> Code</button> </div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"preview\"><p class=\"muted\">The rendered result.</p></div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"code\" hidden><p class=\"muted\">The source.</p></div> </div> <div class=\"tabs\" data-tabs> <div class=\"tabs-list\" role=\"tablist\" aria-label=\"Alignment\"> <button type=\"button\" class=\"tabs-trigger px-2\" role=\"tab\" data-tab=\"left\" aria-selected=\"true\" aria-label=\"Left\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 12H3M17 18H3M21 6H3\"/></svg></button> <button type=\"button\" class=\"tabs-trigger px-2\" role=\"tab\" data-tab=\"center\" aria-selected=\"false\" aria-label=\"Centre\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"roun"
        },
        {
          "id": "disabled",
          "title": "Disabled",
          "text": "A disabled trigger fades, can't be picked, and the arrow keys skip it. html <div class=\"tabs mx-auto max-w-md\" data-tabs> <div class=\"tabs-list\" role=\"tablist\" aria-label=\"Plan\"> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"free\" aria-selected=\"true\">Free</button> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"team\" aria-selected=\"false\" disabled>Team</button> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"enterprise\" aria-selected=\"false\">Enterprise</button> </div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"free\"><p class=\"muted\">Free plan.</p></div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"team\" hidden><p class=\"muted\">Team plan.</p></div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"enterprise\" hidden><p class=\"muted\">Enterprise plan.</p></div> </div>"
        },
        {
          "id": "vertical",
          "title": "Vertical",
          "text": "data-orientation=\"vertical\" on the .tabs wrapper puts the list beside the panels, its triggers stacked. The behaviour sets aria-orientation on the tablist and moves with ↑ ↓. Every variant works; .tabs-line draws its line on the side. html <div class=\"tabs\" data-tabs data-orientation=\"vertical\"> <div class=\"tabs-list\" role=\"tablist\" aria-label=\"Settings\"> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"general\" aria-selected=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg> General</button> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"team\" aria-selected=\"false\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\"/><circle cx=\"9\" cy=\"7\" r=\"4\"/><path d=\"M22 21v-2a4 4 0 0 0-3-3.87\"/><path d=\"M16 3.13a4 4 0 0 1 0 7.75\"/></svg> Team</button> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"billing\" aria-selected=\"false\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stro"
        },
        {
          "id": "sync",
          "title": "Synced and remembered",
          "text": "Give groups the same data-tabs-sync key and they switch together. The choice is saved to localStorage (key tabs:<key>), restored on the next visit, and followed in other open browser tabs. The package-manager tabs on code blocks use data-tabs-sync=\"pm\". Switch one of these two: html <div class=\"tabs\" data-tabs data-tabs-sync=\"demo-lang\"> <div class=\"tabs-list\" role=\"tablist\" aria-label=\"Language 1\"> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"html\" aria-selected=\"true\">HTML</button> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"css\" aria-selected=\"false\">CSS</button> </div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"html\"><p class=\"muted\">Group 1: HTML panel</p></div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"css\" hidden><p class=\"muted\">Group 1: CSS panel</p></div> </div> <div class=\"tabs\" data-tabs data-tabs-sync=\"demo-lang\"> <div class=\"tabs-list\" role=\"tablist\" aria-label=\"Language 2\"> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"html\" aria-selected=\"true\">HTML</button> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"css\" aria-selected=\"false\">CSS</button> </div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"html\"><p class=\"muted\">Group 2: HTML panel</p></div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"css\" hidden><p class=\"muted\">Group 2: CSS panel</p></div> </div>"
        },
        {
          "id": "htmx",
          "title": "Lazy panels with htmx",
          "text": "Load a panel's content the first time it's shown with hx-trigger=\"intersect once\": html <div class=\"tabs mx-auto max-w-md\" data-tabs> <div class=\"tabs-list\" role=\"tablist\"> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"local\" aria-selected=\"true\">Local</button> <button type=\"button\" class=\"tabs-trigger\" role=\"tab\" data-tab=\"server\" aria-selected=\"false\">From server</button> </div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"local\"><p class=\"muted\">Rendered with the page.</p></div> <div class=\"tabs-panel\" role=\"tabpanel\" data-tab-panel=\"server\" hidden> <div hx-get=\"/api/hello\" hx-trigger=\"intersect once\"><p class=\"muted\">Loading…</p></div> </div> </div>"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .tabs Wrapper: spaces the list and panels; side by side when vertical. .tabs-list Segmented control holding the tab buttons. .tabs-trigger Tab button; selected style comes from aria-selected=\"true\", faded when disabled. .tabs-panel Panel focus ring. .tabs-line / .tabs-pills / .tabs-outline Underlined, primary-filled and bordered variants of .tabs-list. .tabs-full List and triggers stretched to the full width. [data-tabs] Behaviour: wires up a tab group. [data-orientation=\"vertical\"] On .tabs: list beside the panels, Up/Down keys. [data-tab] / [data-tab-panel] Matching values pair a tab with its panel. [data-tabs-sync=\"key\"] Sync groups with the same key and remember the choice."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/alert-dialog",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/alert-dialog",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/alert-dialog.md",
      "title": "Alert dialog",
      "description": "A modal dialog that interrupts the user with important content and expects a response.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "small",
          "text": "Small"
        },
        {
          "level": 2,
          "id": "media",
          "text": "Media"
        },
        {
          "level": 2,
          "id": "small-with-media",
          "text": "Small with media"
        },
        {
          "level": 2,
          "id": "destructive",
          "text": "Destructive"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "Confirming an htmx request"
        },
        {
          "level": 2,
          "id": "macro",
          "text": "Macro"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"alert-basic\" command=\"show-modal\">Show dialog</button> <dialog id=\"alert-basic\" class=\"dialog\" data-dialog role=\"alertdialog\" aria-labelledby=\"alert-basic-title\" aria-describedby=\"alert-basic-description\"> <div class=\"dialog-header\"> <h2 class=\"dialog-title\" id=\"alert-basic-title\">Are you absolutely sure?</h2> <p class=\"dialog-description\" id=\"alert-basic-description\">This action cannot be undone. This will permanently delete your account and remove your data from our servers.</p> </div> <div class=\"dialog-footer\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close=\"cancel\" autofocus>Cancel</button> <button type=\"button\" class=\"btn btn-primary\" data-dialog-close=\"continue\">Continue</button> </div> </dialog>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "An alert dialog is a dialog with role=\"alertdialog\": the same classes, the same behaviour, the same ways to open it. The role changes two things. Screen readers announce it as an alert, and a click outside doesn't close it, because the user has to answer. Esc still closes it, as a cancel. Give it a title and a description, linked with aria-labelledby and aria-describedby. Leave out the corner close button; the footer's buttons are the answers. Put autofocus on the least destructive button, usually Cancel, so Enter can't confirm by accident. Give each button a data-dialog-close value. It becomes the dialog's returnValue, which tells the close event which answer was given."
        },
        {
          "id": "small",
          "title": "Small",
          "text": ".dialog-sm makes it narrower, centres the text and sets the two buttons side by side at equal width. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"alert-small\" command=\"show-modal\">Show dialog</button> <dialog id=\"alert-small\" class=\"dialog dialog-sm\" data-dialog role=\"alertdialog\" aria-labelledby=\"alert-small-title\" aria-describedby=\"alert-small-description\"> <div class=\"dialog-header\"> <h2 class=\"dialog-title\" id=\"alert-small-title\">Allow accessory to connect?</h2> <p class=\"dialog-description\" id=\"alert-small-description\">Do you want to allow the USB accessory to connect to this device?</p> </div> <div class=\"dialog-footer\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close=\"deny\" autofocus>Don't allow</button> <button type=\"button\" class=\"btn btn-primary\" data-dialog-close=\"allow\">Allow</button> </div> </dialog>"
        },
        {
          "id": "media",
          "title": "Media",
          "text": ".dialog-media, first in the header, adds an icon or an image. In the default size it sits beside the title and description from the sm breakpoint, and above them on smaller screens. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"alert-media\" command=\"show-modal\">Share project</button> <dialog id=\"alert-media\" class=\"dialog\" data-dialog role=\"alertdialog\" aria-labelledby=\"alert-media-title\" aria-describedby=\"alert-media-description\"> <div class=\"dialog-header\"> <div class=\"dialog-media\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-8\" aria-hidden=\"true\"><path d=\"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z\"/><path d=\"m21.854 2.147-10.94 10.939\"/></svg></div> <h2 class=\"dialog-title\" id=\"alert-media-title\">Share this project?</h2> <p class=\"dialog-description\" id=\"alert-media-description\">Anyone with the link will be able to view and edit this project.</p> </div> <div class=\"dialog-footer\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close=\"cancel\" autofocus>Cancel</button> <button type=\"button\" class=\"btn btn-primary\" data-dialog-close=\"share\">Share</button> </div> </dialog>"
        },
        {
          "id": "small-with-media",
          "title": "Small with media",
          "text": "In a small alert dialog the media is centred above the title. It can hold an image as well as an icon. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"alert-small-icon\" command=\"show-modal\">With an icon</button> <button type=\"button\" class=\"btn btn-outline\" commandfor=\"alert-small-image\" command=\"show-modal\">With an image</button> <dialog id=\"alert-small-icon\" class=\"dialog dialog-sm\" data-dialog role=\"alertdialog\" aria-labelledby=\"alert-small-icon-title\" aria-describedby=\"alert-small-icon-description\"> <div class=\"dialog-header\"> <div class=\"dialog-media\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-8\" aria-hidden=\"true\"><path d=\"M12 8V4H8\"/><rect width=\"16\" height=\"12\" x=\"4\" y=\"8\" rx=\"2\"/><path d=\"M2 14h2M20 14h2M15 13v2M9 13v2\"/></svg></div> <h2 class=\"dialog-title\" id=\"alert-small-icon-title\">Enable the assistant?</h2> <p class=\"dialog-description\" id=\"alert-small-icon-description\">It can read the pages you open in this workspace.</p> </div> <div class=\"dialog-footer\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close=\"cancel\" autofocus>Not now</button> <button type=\"button\" class=\"btn btn-primary\" data-dialog-close=\"enable\">Enable</button> </div> </dialog> <dialog id=\"alert-small-image\" class=\"dialog dialog-sm\" data-dialog role=\"alertdialog\" aria-labelledby=\"alert-small-image-title\" aria-describedby=\"alert-small-image-description\"> <div class=\"dialog-header\"> <div class=\"dialog-media size-20 rounded-full\"><img src=\"../../../images/landscape.svg\" alt=\"\" /></div> <h2 class=\"dialog-title\" id=\"alert-small-image-title\">Use as cover image?</h2> <p class=\"dialog-description\" id=\"alert-small-image-description\">Everyone in the workspace will see it at the top of the project.</p> </div> <div class=\"dialog-footer\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close=\"cancel\" autofocus>C"
        },
        {
          "id": "destructive",
          "title": "Destructive",
          "text": "For an action that destroys something, make the confirming button .btn-danger and tint the media with .dialog-media-danger. Name the action on the button (“Delete chat”), not just “Continue”. html <button type=\"button\" class=\"btn btn-danger\" commandfor=\"alert-destructive\" command=\"show-modal\">Delete chat</button> <dialog id=\"alert-destructive\" class=\"dialog dialog-sm\" data-dialog role=\"alertdialog\" aria-labelledby=\"alert-destructive-title\" aria-describedby=\"alert-destructive-description\"> <div class=\"dialog-header\"> <div class=\"dialog-media dialog-media-danger\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-8\" aria-hidden=\"true\"><path d=\"M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"/></svg></div> <h2 class=\"dialog-title\" id=\"alert-destructive-title\">Delete chat?</h2> <p class=\"dialog-description\" id=\"alert-destructive-description\">This will permanently delete this chat conversation. View <a class=\"link\" href=\"#settings\">Settings</a> to delete any memories saved during this chat.</p> </div> <div class=\"dialog-footer\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close=\"cancel\" autofocus>Cancel</button> <button type=\"button\" class=\"btn btn-danger\" data-dialog-close=\"delete\">Delete</button> </div> </dialog>"
        },
        {
          "id": "htmx",
          "title": "Confirming an htmx request",
          "text": "Put the request on the confirming button. data-dialog-close closes the dialog at once and htmx sends the request; the response goes to the button's hx-target. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"alert-htmx\" command=\"show-modal\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\"/><path d=\"M21 3v5h-5\"/><path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\"/><path d=\"M8 16H3v5\"/></svg> Reset API key</button> <dialog id=\"alert-htmx\" class=\"dialog\" data-dialog role=\"alertdialog\" aria-labelledby=\"alert-htmx-title\" aria-describedby=\"alert-htmx-description\"> <div class=\"dialog-header\"> <h2 class=\"dialog-title\" id=\"alert-htmx-title\">Reset your API key?</h2> <p class=\"dialog-description\" id=\"alert-htmx-description\">Apps using the current key will stop working until you update them.</p> </div> <div class=\"dialog-footer\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close=\"cancel\" autofocus>Cancel</button> <button type=\"button\" class=\"btn btn-danger\" data-dialog-close=\"reset\" hx-post=\"/api/slow\" hx-target=\"#alert-htmx-result\">Reset key</button> </div> </dialog> <span id=\"alert-htmx-result\" class=\"muted\"></span> To act on the answer in your own script instead, listen for the dialog's close event and read returnValue: ts const dialog = document.querySelector<HTMLDialogElement>(\"#alert-basic\")!; dialog.addEventListener(\"close\", () => { if (dialog.returnValue === \"continue\") deleteAccount(); dialog.returnValue = \"\"; // reset for the next time it opens });"
        },
        {
          "id": "macro",
          "title": "Macro",
          "text": "dialog() with alert=true writes an alert dialog: the role, no corner button, and the header with optional media. jinja {% from \"components/dialog/dialog.html\" import dialog %} <button class=\"btn btn-danger\" commandfor=\"delete-project\" command=\"show-modal\">Delete project</button> {% call dialog(\"delete-project\", \"Delete project?\", \"All of its pages and files will be removed.\", alert=true, size=\"sm\", media=\"trash\", media_variant=\"danger\") %} <div class=\"dialog-footer\"> <button class=\"btn btn-outline\" data-dialog-close=\"cancel\" autofocus>Cancel</button> <button class=\"btn btn-danger\" data-dialog-close=\"delete\" hx-delete=\"/projects/42\">Delete</button> </div> {% endcall %}"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Alert dialogs use the dialog classes. These are the parts specific to them: Class Description role=\"alertdialog\" On the .dialog: announced as an alert, and a click outside doesn't close it. .dialog-sm Narrow, centred text, buttons side by side at equal width. .dialog-media Icon or image first in the header: beside the title in the default size from sm, centred above it in .dialog-sm. .dialog-media-danger Danger-tinted media, for destructive actions. [data-dialog-close=\"value\"] An answer button: closes the dialog and sets returnValue."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/dialog",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/dialog",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/dialog.md",
      "title": "Dialog",
      "description": "A window overlaid on the page or on another dialog, rendering the content underneath inert. Built on the native <dialog> element.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "open-and-close",
          "text": "Opening and closing"
        },
        {
          "level": 2,
          "id": "no-close-button",
          "text": "No close button"
        },
        {
          "level": 2,
          "id": "custom-close-button",
          "text": "Custom close button"
        },
        {
          "level": 2,
          "id": "scrollable-content",
          "text": "Scrollable content"
        },
        {
          "level": 2,
          "id": "sticky-footer",
          "text": "Sticky footer"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Sizes"
        },
        {
          "level": 2,
          "id": "nested",
          "text": "Dialog over a dialog"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx"
        },
        {
          "level": 3,
          "id": "from-the-server",
          "text": "A dialog from the server"
        },
        {
          "level": 3,
          "id": "close-from-the-server",
          "text": "Closing it from the server"
        },
        {
          "level": 2,
          "id": "macro",
          "text": "Macro"
        },
        {
          "level": 2,
          "id": "accessibility",
          "text": "Accessibility"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"edit-profile\" command=\"show-modal\">Edit profile</button> <dialog id=\"edit-profile\" class=\"dialog dialog-sm\" data-dialog aria-labelledby=\"edit-profile-title\" aria-describedby=\"edit-profile-description\"> <div class=\"dialog-header\"> <h2 class=\"dialog-title\" id=\"edit-profile-title\">Edit profile</h2> <p class=\"dialog-description\" id=\"edit-profile-description\">Make changes to your profile here. Click save when you're done.</p> </div> <form class=\"contents\" method=\"dialog\"> <div class=\"grid gap-4\"> <div class=\"field\"> <label class=\"field-label\" for=\"edit-profile-name\">Name</label> <input class=\"input\" id=\"edit-profile-name\" name=\"name\" value=\"Pedro Duarte\" /> </div> <div class=\"field\"> <label class=\"field-label\" for=\"edit-profile-username\">Username</label> <input class=\"input\" id=\"edit-profile-username\" name=\"username\" value=\"@peduarte\" /> </div> </div> <div class=\"dialog-footer\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close>Cancel</button> <button class=\"btn btn-primary\" value=\"save\">Save changes</button> </div> </form> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm dialog-close\" data-dialog-close aria-label=\"Close\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> </dialog>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "A dialog is a native <dialog> with .dialog and data-dialog. Opened as a modal, the browser puts it in the top layer above everything else, makes the rest of the page inert (no clicks, no focus, hidden from screen readers), moves focus into it and returns focus to the button that opened it when it closes. The page behind it stops scrolling. .dialog-header holds .dialog-title and .dialog-description. Point aria-labelledby and aria-describedby at them. .dialog-content is an optional body that scrolls on its own, keeping the header and footer in view. .dialog-footer is a row of actions: stacked on small screens with the last button on top, right-aligned from the sm breakpoint. .dialog-close is the corner close button. It is optional. The same behaviour drives the sheet, a dialog that slides in from an edge of the screen, and the drawer, one that can also be swiped away."
        },
        {
          "id": "open-and-close",
          "title": "Opening and closing",
          "text": "Open it with an invoker command: a button with commandfor=\"<dialog id>\" and command=\"show-modal\". Browsers with invoker commands need no script for it; for the rest, the dialog behaviour handles the click. From script, call dialog.showModal(). It closes on any of these: Esc. A click outside it, on the backdrop. (Not for an alert dialog.) A click on any element inside with data-dialog-close. Its value, or the button's value, becomes dialog.returnValue. Submitting a <form method=\"dialog\"> inside it (no JavaScript). The submit button's value becomes returnValue. A button with commandfor and command=\"close\". Each fires the native close event on the <dialog>, so hx-trigger=\"close\" on it, or a listener reading returnValue, can act on the answer."
        },
        {
          "id": "no-close-button",
          "title": "No close button",
          "text": "Leave out .dialog-close for a dialog that closes only with Esc or a click outside. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"no-close\" command=\"show-modal\">No close button</button> <dialog id=\"no-close\" class=\"dialog\" data-dialog aria-labelledby=\"no-close-title\" aria-describedby=\"no-close-description\"> <div class=\"dialog-header\"> <h2 class=\"dialog-title\" id=\"no-close-title\">No close button</h2> <p class=\"dialog-description\" id=\"no-close-description\">This dialog has no close button. Press Escape or click outside to close it.</p> </div> </dialog>"
        },
        {
          "id": "custom-close-button",
          "title": "Custom close button",
          "text": "Any element with data-dialog-close closes the dialog it is in, so the footer's own button can do it. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"share-link\" command=\"show-modal\">Share</button> <dialog id=\"share-link\" class=\"dialog dialog-sm\" data-dialog aria-labelledby=\"share-link-title\" aria-describedby=\"share-link-description\"> <div class=\"dialog-header\"> <h2 class=\"dialog-title\" id=\"share-link-title\">Share link</h2> <p class=\"dialog-description\" id=\"share-link-description\">Anyone who has this link will be able to view this.</p> </div> <div class=\"field\"> <label class=\"field-label sr-only\" for=\"share-link-url\">Link</label> <input class=\"input\" id=\"share-link-url\" value=\"https://htmx-ui.dev/docs/installation\" readonly /> </div> <div class=\"dialog-footer sm:justify-start\"> <button type=\"button\" class=\"btn btn-secondary\" data-dialog-close>Close</button> </div> </dialog>"
        },
        {
          "id": "scrollable-content",
          "title": "Scrollable content",
          "text": "Long content scrolls inside .dialog-content while the header stays in view. A dialog is never taller than the viewport less a margin; give it a max-h-* to cap it lower. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"scrollable\" command=\"show-modal\">Scrollable content</button> <dialog id=\"scrollable\" class=\"dialog max-h-[min(30rem,calc(100dvh-2rem))]\" data-dialog aria-labelledby=\"scrollable-title\"> <div class=\"dialog-header\"> <h2 class=\"dialog-title\" id=\"scrollable-title\">Scrollable content</h2> <p class=\"dialog-description\">This is a dialog with scrollable content.</p> </div> <div class=\"dialog-content space-y-4 leading-relaxed\"> <p>Pages are plain HTML rendered by Nunjucks at build time. Components are CSS classes, with a small behaviour where one is needed.</p> <p>htmx swaps fragments from your server into the page. Components inside a fragment initialise as soon as it arrives, so a dialog returned by a request works like one written in the page.</p> <p>Every colour is a CSS variable. Override the tokens on :root, or on any element, to theme the whole library, in light and dark.</p> <p>The native dialog element does the hard parts: the top layer, an inert page underneath, focus moving in and coming back, and Escape.</p> <p>Dialogs can open over other dialogs. Each new one stacks on top with its own backdrop, and Escape closes the topmost first.</p> <p>A form inside a dialog can post with htmx. The server answers with a fragment for the page and closes the dialog with a response header.</p> <p>Alert dialogs are the same element with role=\"alertdialog\". They don't close on a click outside, because they need an answer.</p> <p>Without JavaScript, a form with method=\"dialog\" still closes it, and browsers with invoker commands still open it.</p> </div> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm dialog-close\" data-dialog-close aria-label=\"Close\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" "
        },
        {
          "id": "sticky-footer",
          "title": "Sticky footer",
          "text": "With a .dialog-content between them, the footer stays visible while the content scrolls. .dialog-footer-sticky sets it apart: tinted, edge to edge, with a border on top. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"terms\" command=\"show-modal\">Sticky footer</button> <dialog id=\"terms\" class=\"dialog max-h-[min(30rem,calc(100dvh-2rem))]\" data-dialog aria-labelledby=\"terms-title\"> <div class=\"dialog-header\"> <h2 class=\"dialog-title\" id=\"terms-title\">Terms of service</h2> <p class=\"dialog-description\">Please read and accept the terms to continue.</p> </div> <div class=\"dialog-content space-y-4 leading-relaxed\"> <p><strong>1. Acceptance.</strong> By using the service you agree to these terms. If you don't agree, don't use it.</p> <p><strong>2. Accounts.</strong> You are responsible for your account and for everything done with it. Keep your password safe.</p> <p><strong>3. Content.</strong> You keep the rights to what you upload. You give us the permission we need to store and show it to the people you share it with.</p> <p><strong>4. Acceptable use.</strong> Don't break the law, don't attack the service, and don't use it to harm other people.</p> <p><strong>5. Payment.</strong> Paid plans are billed in advance and renew until you cancel. Cancelling stops the next renewal.</p> <p><strong>6. Termination.</strong> You can stop using the service at any time. We may suspend accounts that break these terms.</p> <p><strong>7. Changes.</strong> We will tell you before these terms change. Using the service after a change means you accept it.</p> </div> <div class=\"dialog-footer dialog-footer-sticky\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close=\"decline\">Decline</button> <button type=\"button\" class=\"btn btn-primary\" data-dialog-close=\"accept\">Accept</button> </div> </dialog>"
        },
        {
          "id": "sizes",
          "title": "Sizes",
          "text": "A dialog is max-w-lg wide. .dialog-sm, .dialog-lg and .dialog-xl change that, or use any max-w-* utility. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"size-sm\" command=\"show-modal\">Small</button> <button type=\"button\" class=\"btn btn-outline\" commandfor=\"size-xl\" command=\"show-modal\">Extra large</button> <dialog id=\"size-sm\" class=\"dialog dialog-sm\" data-dialog aria-labelledby=\"size-sm-title\"> <div class=\"dialog-header\"><h2 class=\"dialog-title\" id=\"size-sm-title\">Small dialog</h2></div> <p class=\"muted\">.dialog-sm is max-w-sm.</p> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm dialog-close\" data-dialog-close aria-label=\"Close\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> </dialog> <dialog id=\"size-xl\" class=\"dialog dialog-xl\" data-dialog aria-labelledby=\"size-xl-title\"> <div class=\"dialog-header\"><h2 class=\"dialog-title\" id=\"size-xl-title\">Extra large dialog</h2></div> <p class=\"muted\">.dialog-xl is max-w-4xl.</p> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm dialog-close\" data-dialog-close aria-label=\"Close\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> </dialog>"
        },
        {
          "id": "nested",
          "title": "Dialog over a dialog",
          "text": "A button inside one dialog can open another. It stacks on top with its own backdrop, the first dialog becomes inert, and Esc or a click outside closes the topmost one first. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"nested-outer\" command=\"show-modal\">Open settings</button> <dialog id=\"nested-outer\" class=\"dialog\" data-dialog aria-labelledby=\"nested-outer-title\"> <div class=\"dialog-header\"> <h2 class=\"dialog-title\" id=\"nested-outer-title\">Settings</h2> <p class=\"dialog-description\">Manage your workspace.</p> </div> <div class=\"dialog-footer sm:justify-between\"> <button type=\"button\" class=\"btn btn-danger\" commandfor=\"nested-inner\" command=\"show-modal\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"/></svg> Delete workspace</button> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close>Done</button> </div> </dialog> <dialog id=\"nested-inner\" class=\"dialog dialog-sm\" data-dialog aria-labelledby=\"nested-inner-title\"> <div class=\"dialog-header\"> <h2 class=\"dialog-title\" id=\"nested-inner-title\">Are you sure?</h2> <p class=\"dialog-description\">This is a second dialog, over the first.</p> </div> <div class=\"dialog-footer\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close>Cancel</button> <button type=\"button\" class=\"btn btn-danger\" data-dialog-close>Delete</button> </div> </dialog>"
        },
        {
          "id": "htmx",
          "title": "With htmx",
          "text": ""
        },
        {
          "id": "from-the-server",
          "title": "A dialog from the server",
          "text": "A dialog can come from the server instead of the page. data-dialog-show opens it as a modal as soon as htmx inserts it, and data-dialog-remove takes it out of the page again once it has closed, so every click loads a fresh one. Append it to the body: html <button type=\"button\" class=\"btn btn-outline\" hx-get=\"/api/dialog\" hx-target=\"body\" hx-swap=\"beforeend\"> Rename project </button> <span id=\"server-dialog-result\" class=\"muted\"></span> The server returns: html <dialog class=\"dialog\" data-dialog data-dialog-show data-dialog-remove aria-labelledby=\"rename-title\"> <div class=\"dialog-header\"> <h2 class=\"dialog-title\" id=\"rename-title\">Rename project</h2> </div> <form class=\"contents\" hx-post=\"/api/dialog/save\" hx-target=\"#server-dialog-result\"> <div class=\"field\"> <label class=\"field-label\" for=\"rename-name\">Name</label> <input class=\"input\" id=\"rename-name\" name=\"name\" required autofocus /> </div> <div class=\"dialog-footer\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close>Cancel</button> <button class=\"btn btn-primary\">Save</button> </div> </form> </dialog>"
        },
        {
          "id": "close-from-the-server",
          "title": "Closing it from the server",
          "text": "A dialog:close event dispatched inside a dialog closes it. htmx dispatches the events named in an HX-Trigger response header on the element that made the request, so a server that has saved the form closes the dialog it came from with one header, while the response body goes to the form's hx-target. On a validation error, leave the header off and return the form with its messages instead. text HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 HX-Trigger: dialog:close <span class=\"text-success\">Renamed to htmx-ui</span>"
        },
        {
          "id": "macro",
          "title": "Macro",
          "text": "dialog() writes the <dialog>, its header and the close button, with the ids and ARIA attributes wired up. The call body is everything after the header. jinja {% from \"components/dialog/dialog.html\" import dialog %} <button class=\"btn btn-outline\" commandfor=\"invite\" command=\"show-modal\">Invite</button> {% call dialog(\"invite\", \"Invite people\", \"They will get an email with a link.\", size=\"sm\") %} <div class=\"dialog-footer\"> <button class=\"btn btn-outline\" data-dialog-close>Cancel</button> <button class=\"btn btn-primary\" data-dialog-close=\"send\">Send invites</button> </div> {% endcall %} Parameter Description id Required. The id buttons point commandfor at; also prefixes the title and description ids. title, description Header text. Each sets aria-labelledby / aria-describedby. close=true The corner close button. false leaves it out. alert=false true makes it an alert dialog: role=\"alertdialog\", no corner button, no closing on a click outside. size \"sm\", \"lg\" or \"xl\". media, media_variant An icon name shown above the title, and \"danger\" to tint it. show, remove data-dialog-show and data-dialog-remove, for dialogs returned by htmx. class More classes on the <dialog>."
        },
        {
          "id": "accessibility",
          "title": "Accessibility",
          "text": "Give every dialog a name: aria-labelledby pointing at its title, or aria-label. Focus moves to the first focusable element inside, or to one marked autofocus. Put autofocus on the field the user needs first, or on the safest button. An icon-only close button needs an aria-label. Use an alert dialog when the user must answer before going on."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .dialog The <dialog>: centred, padded, scrolls when taller than the viewport. Fades and zooms in and out. .dialog-header / .dialog-title / .dialog-description Title block, centred on small screens. .dialog-content Body that scrolls on its own, keeping the header and footer in view. .dialog-footer Actions: stacked on small screens, a right-aligned row from sm. .dialog-footer-sticky Tinted, edge-to-edge footer with a top border. .dialog-close Corner close button. .dialog-sm / -lg / -xl Widths: max-w-sm, max-w-2xl, max-w-4xl (default max-w-lg). .dialog-media / .dialog-media-danger An icon or image above the title; see Alert dialog. [data-dialog] Behaviour: closes on a click outside and on [data-dialog-close], opens command buttons in older browsers. [data-dialog-close=\"value\"] Closes the dialog it is in; the value becomes returnValue. [data-dialog-show] / [data-dialog-remove] Open as a modal when initialised / remove from the page once closed. dialog:close event Closes the dialog it is dispatched in, e.g. from an HX-Trigger response header."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/drawer",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/drawer",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/drawer.md",
      "title": "Drawer",
      "description": "A panel that slides in from an edge of the screen and can be swiped away, on the native <dialog> element.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "swipe-direction",
          "text": "Swipe direction"
        },
        {
          "level": 2,
          "id": "swipe-handle",
          "text": "Swipe handle"
        },
        {
          "level": 2,
          "id": "nested",
          "text": "Nested drawers"
        },
        {
          "level": 2,
          "id": "responsive",
          "text": "Responsive dialog"
        },
        {
          "level": 2,
          "id": "scrolling-content",
          "text": "Scrolling content"
        },
        {
          "level": 2,
          "id": "gestures",
          "text": "Keyboard and gestures"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx"
        },
        {
          "level": 3,
          "id": "post-a-form",
          "text": "Posting a form, closing from the server"
        },
        {
          "level": 3,
          "id": "from-the-server",
          "text": "A drawer from the server"
        },
        {
          "level": 2,
          "id": "macro",
          "text": "Macro"
        },
        {
          "level": 2,
          "id": "accessibility",
          "text": "Accessibility"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"drawer-goal\" command=\"show-modal\">Open drawer</button> <dialog id=\"drawer-goal\" class=\"drawer\" data-dialog data-drawer data-swipe-direction=\"down\" aria-labelledby=\"drawer-goal-title\" aria-describedby=\"drawer-goal-description\"> <div class=\"drawer-handle\"></div> <div class=\"mx-auto flex w-full max-w-sm flex-col gap-4\"> <div class=\"drawer-header\"> <h2 class=\"drawer-title\" id=\"drawer-goal-title\">Move goal</h2> <p class=\"drawer-description\" id=\"drawer-goal-description\">Set your daily activity goal.</p> </div> <div class=\"flex items-center justify-center gap-4\"> <button type=\"button\" class=\"btn btn-outline btn-icon btn-rounded\" aria-label=\"Decrease\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14\"/></svg></button> <div class=\"flex-1 text-center\"> <p class=\"text-7xl font-bold tracking-tighter\">350</p> <p class=\"text-xs text-muted-foreground uppercase\">Calories/day</p> </div> <button type=\"button\" class=\"btn btn-outline btn-icon btn-rounded\" aria-label=\"Increase\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5v14\"/></svg></button> </div> <div class=\"flex h-24 items-end gap-1\" role=\"img\" aria-label=\"Calories burned on each of the last 13 days, between 189 and 400\"> <div class=\"flex-1 rounded-sm bg-primary\" style=\"height: 100%\"></div> <div class=\"flex-1 rounded-sm bg-primary\" style=\"height: 75%\"></div> <div class=\"flex-1 rounded-sm bg-primary\" style=\"height: 50%\"></div> <div class=\"flex-1 rounded-sm bg-primary\" style=\"height: 75%\"></div> <div class=\"flex-1 rounded-sm bg-primary\" style=\"height: 50%\"></div> <div class=\"flex-1 rounded-sm bg-primary\""
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "A drawer is a native <dialog> with .drawer, data-dialog and data-drawer. data-dialog makes it a dialog: it opens and closes the same ways, puts the rest of the page out of reach while it is open, and works with htmx the same way. data-drawer adds swiping and stacking. Without JavaScript it still opens (in browsers with invoker commands) and closes with Esc or a <form method=\"dialog\">. .drawer-handle is the bar on the drawer's free edge. It is optional: the whole drawer can be dragged. .drawer-header holds .drawer-title and .drawer-description. Point aria-labelledby and aria-describedby at them. .drawer-content is an optional body that scrolls on its own, keeping the header and footer in view. .drawer-footer holds the actions, stacked with the last one on top. A drawer on the bottom or top edge is full width and as tall as its content, up to 80% of the viewport. To keep its content narrow on wide screens, wrap it in a centred column, as the demo above does with mx-auto w-full max-w-sm."
        },
        {
          "id": "swipe-direction",
          "title": "Swipe direction",
          "text": "data-swipe-direction is the way a swipe dismisses the drawer, and so the edge it is attached to: down (the default) on the bottom edge, up on the top, right on the right and left on the left. Side drawers are full height, three quarters of the screen wide and at most max-w-sm; change that with max-w-* or w-* utilities. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"drawer-dir-up\" command=\"show-modal\">Up</button> <dialog id=\"drawer-dir-up\" class=\"drawer\" data-dialog data-drawer data-swipe-direction=\"up\" aria-labelledby=\"drawer-dir-up-title\" aria-describedby=\"drawer-dir-up-description\"> <div class=\"drawer-handle\"></div> <div class=\"drawer-header\"> <h2 class=\"drawer-title\" id=\"drawer-dir-up-title\">Swipe up</h2> <p class=\"drawer-description\" id=\"drawer-dir-up-description\">Drag this drawer up to dismiss it.</p> </div> <div class=\"drawer-footer\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close>Close</button> </div> </dialog> <button type=\"button\" class=\"btn btn-outline\" commandfor=\"drawer-dir-right\" command=\"show-modal\">Right</button> <dialog id=\"drawer-dir-right\" class=\"drawer\" data-dialog data-drawer data-swipe-direction=\"right\" aria-labelledby=\"drawer-dir-right-title\" aria-describedby=\"drawer-dir-right-description\"> <div class=\"drawer-handle\"></div> <div class=\"drawer-header\"> <h2 class=\"drawer-title\" id=\"drawer-dir-right-title\">Swipe right</h2> <p class=\"drawer-description\" id=\"drawer-dir-right-description\">Drag this drawer right to dismiss it.</p> </div> <div class=\"drawer-footer\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close>Close</button> </div> </dialog> <button type=\"button\" class=\"btn btn-outline\" commandfor=\"drawer-dir-down\" command=\"show-modal\">Down</button> <dialog id=\"drawer-dir-down\" class=\"drawer\" data-dialog data-drawer data-swipe-direction=\"down\" aria-labelledby=\"drawer-dir-down-title\" aria-describedby=\"drawer-dir-down-description\"> <div class=\"drawer-handle\"></div> <div class=\"drawer-header\"> <h2 class="
        },
        {
          "id": "swipe-handle",
          "title": "Swipe handle",
          "text": ".drawer-handle, first inside the drawer, draws a bar on its free edge (the top of a bottom drawer, the left of a right one) and leaves room for it. It tells people the drawer can be dragged. By default a drag can start anywhere on the drawer; data-drawer-handle-only limits it to the handle, so the content stays free for selecting text or for gestures of its own. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"drawer-handle-only\" command=\"show-modal\">Handle only</button> <dialog id=\"drawer-handle-only\" class=\"drawer\" data-dialog data-drawer data-drawer-handle-only aria-labelledby=\"drawer-handle-only-title\"> <div class=\"drawer-handle\"></div> <div class=\"mx-auto flex w-full max-w-sm flex-col gap-4\"> <div class=\"drawer-header\"> <h2 class=\"drawer-title\" id=\"drawer-handle-only-title\">API key</h2> <p class=\"drawer-description\">Only the handle drags this drawer, so the key below can be selected.</p> </div> <p class=\"rounded-md bg-muted p-3 text-center font-mono break-all\">demo-7f3a9c2e-1b4d-4e8a-9c61-2d5f0b8e7a13</p> <div class=\"drawer-footer\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close>Done</button> </div> </div> </dialog>"
        },
        {
          "id": "nested",
          "title": "Nested drawers",
          "text": "A button inside a drawer can open another drawer. The first one stays open behind it, shrinks a little and steps back, so the stack shows; it returns when the one in front closes. Esc, a swipe or a click outside closes only the frontmost. The inner <dialog> can sit inside the outer one's markup or anywhere else on the page, referenced by commandfor; in this demo the second level is inside the first and the third is not. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"drawer-nested-1\" command=\"show-modal\">Open drawer</button> <dialog id=\"drawer-nested-1\" class=\"drawer\" data-dialog data-drawer aria-labelledby=\"drawer-nested-1-title\"> <div class=\"drawer-handle\"></div> <div class=\"mx-auto flex w-full max-w-sm flex-col gap-4\"> <div class=\"drawer-header\"> <h2 class=\"drawer-title\" id=\"drawer-nested-1-title\">Account</h2> <p class=\"drawer-description\">The first drawer.</p> </div> <div class=\"drawer-footer\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close>Close</button> <button type=\"button\" class=\"btn btn-primary\" commandfor=\"drawer-nested-2\" command=\"show-modal\">Security settings</button> </div> </div> <dialog id=\"drawer-nested-2\" class=\"drawer\" data-dialog data-drawer aria-labelledby=\"drawer-nested-2-title\"> <div class=\"drawer-handle\"></div> <div class=\"mx-auto flex w-full max-w-sm flex-col gap-4\"> <div class=\"drawer-header\"> <h2 class=\"drawer-title\" id=\"drawer-nested-2-title\">Security</h2> <p class=\"drawer-description\">The second drawer, inside the first one's markup.</p> </div> <div class=\"drawer-footer\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close>Back</button> <button type=\"button\" class=\"btn btn-primary\" commandfor=\"drawer-nested-3\" command=\"show-modal\">Change password</button> </div> </div> </dialog> </dialog> <dialog id=\"drawer-nested-3\" class=\"drawer\" data-dialog data-drawer aria-labelledby=\"drawer-nested-3-title\"> <div class=\"drawer-handle\"></div> <div class=\"mx-auto flex w-full max-w-sm flex-col gap-4\"> <div clas"
        },
        {
          "id": "responsive",
          "title": "Responsive dialog",
          "text": ".drawer-responsive makes one element serve as a drawer on small screens and as a centred dialog from the md breakpoint up, where the handle is hidden and swiping is off. Resize the window to see both. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"drawer-responsive\" command=\"show-modal\">Edit profile</button> <dialog id=\"drawer-responsive\" class=\"drawer drawer-responsive\" data-dialog data-drawer aria-labelledby=\"drawer-responsive-title\" aria-describedby=\"drawer-responsive-description\"> <div class=\"drawer-handle\"></div> <div class=\"drawer-header\"> <h2 class=\"drawer-title\" id=\"drawer-responsive-title\">Edit profile</h2> <p class=\"drawer-description\" id=\"drawer-responsive-description\">Make changes to your profile here. Click save when you're done.</p> </div> <form class=\"contents\" method=\"dialog\"> <div class=\"grid gap-4\"> <div class=\"field\"> <label class=\"field-label\" for=\"drawer-responsive-email\">Email</label> <input class=\"input\" id=\"drawer-responsive-email\" type=\"email\" name=\"email\" value=\"rin@example.com\" /> </div> <div class=\"field\"> <label class=\"field-label\" for=\"drawer-responsive-username\">Username</label> <input class=\"input\" id=\"drawer-responsive-username\" name=\"username\" value=\"@rin\" /> </div> </div> <div class=\"drawer-footer\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close>Cancel</button> <button class=\"btn btn-primary\" value=\"save\">Save changes</button> </div> </form> </dialog> The switch is the custom property --drawer-swipe: none, which .drawer-responsive sets from md. Set it yourself, in any media query or on any drawer, to turn swiping off."
        },
        {
          "id": "scrolling-content",
          "title": "Scrolling content",
          "text": "Long content goes in .drawer-content, which scrolls between a header and footer that stay in view. A swipe that starts in it scrolls the content until it reaches the end it is moving towards; only then does the drawer move. Here, scroll the list back to the top and keep pulling down to dismiss it. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"drawer-scroll\" command=\"show-modal\">Notifications</button> <dialog id=\"drawer-scroll\" class=\"drawer\" data-dialog data-drawer aria-labelledby=\"drawer-scroll-title\"> <div class=\"drawer-handle\"></div> <div class=\"drawer-header\"> <h2 class=\"drawer-title\" id=\"drawer-scroll-title\">Notifications</h2> <p class=\"drawer-description\">You have 20 unread messages.</p> </div> <ul class=\"drawer-content mx-auto w-full max-w-md divide-y divide-border\"> <li class=\"py-3\"> <p class=\"font-medium\">Deployment 1 finished</p> <p class=\"muted\">htmx-ui · production · 3 minutes ago</p> </li> <li class=\"py-3\"> <p class=\"font-medium\">Deployment 2 finished</p> <p class=\"muted\">htmx-ui · production · 6 minutes ago</p> </li> <li class=\"py-3\"> <p class=\"font-medium\">Deployment 3 finished</p> <p class=\"muted\">htmx-ui · production · 9 minutes ago</p> </li> <li class=\"py-3\"> <p class=\"font-medium\">Deployment 4 finished</p> <p class=\"muted\">htmx-ui · production · 12 minutes ago</p> </li> <li class=\"py-3\"> <p class=\"font-medium\">Deployment 5 finished</p> <p class=\"muted\">htmx-ui · production · 15 minutes ago</p> </li> <li class=\"py-3\"> <p class=\"font-medium\">Deployment 6 finished</p> <p class=\"muted\">htmx-ui · production · 18 minutes ago</p> </li> <li class=\"py-3\"> <p class=\"font-medium\">Deployment 7 finished</p> <p class=\"muted\">htmx-ui · production · 21 minutes ago</p> </li> <li class=\"py-3\"> <p class=\"font-medium\">Deployment 8 finished</p> <p class=\"muted\">htmx-ui · production · 24 minutes ago</p> </li> <li class=\"py-3\"> <p class=\"font-medium\">Deployment 9 finished</p> <p class=\"muted\">htmx-ui · production · 27 minutes ago</p> </li> <li class=\"py"
        },
        {
          "id": "gestures",
          "title": "Keyboard and gestures",
          "text": "Esc closes the frontmost drawer. Tab and Shift + Tab move through the drawer's controls; the page behind it is inert. Dragging the drawer (with a finger, a pen or the mouse) towards its edge moves it. Released more than 30% of the way, or flicked, it closes; otherwise it springs back. A drag doesn't start on inputs, text areas, selects, buttons or links, on anything marked data-drawer-no-drag, or in content that can still scroll the way the pointer moves. A click outside the drawer closes it, as for a dialog. A swipe that dismisses the drawer fires a cancelable cancel event first, as Esc does: call preventDefault() on it to keep the drawer open (it springs back). Then the native close event fires, with an empty returnValue. While a drag is under way the drawer has data-swiping, --drawer-swipe-movement (pixels towards its edge) and --drawer-swipe-progress (0 to 1), which also fades the backdrop."
        },
        {
          "id": "htmx",
          "title": "With htmx",
          "text": ""
        },
        {
          "id": "post-a-form",
          "title": "Posting a form, closing from the server",
          "text": "A form in a drawer posts with htmx like any other. When the server has saved it, the response header HX-Trigger: dialog:close closes the drawer it came from, while the body goes to the form's hx-target. On a validation error, leave the header off and return the form with its messages. This demo posts to the same endpoint as the dialog's. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"drawer-rename\" command=\"show-modal\">Rename project</button> <span id=\"drawer-rename-result\" class=\"muted\"></span> <dialog id=\"drawer-rename\" class=\"drawer drawer-responsive\" data-dialog data-drawer aria-labelledby=\"drawer-rename-title\"> <div class=\"drawer-handle\"></div> <div class=\"drawer-header\"> <h2 class=\"drawer-title\" id=\"drawer-rename-title\">Rename project</h2> <p class=\"drawer-description\">The server closes this drawer once it has saved the name.</p> </div> <form class=\"contents\" hx-post=\"/api/dialog/save\" hx-target=\"#drawer-rename-result\"> <div class=\"field\"> <label class=\"field-label\" for=\"drawer-rename-name\">Name</label> <input class=\"input\" id=\"drawer-rename-name\" name=\"name\" value=\"htmx-ui\" required /> </div> <div class=\"drawer-footer\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close>Cancel</button> <button class=\"btn btn-primary\">Save</button> </div> </form> </dialog> text HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 HX-Trigger: dialog:close <span class=\"text-success\">Renamed to htmx-ui</span>"
        },
        {
          "id": "from-the-server",
          "title": "A drawer from the server",
          "text": "A drawer can also come from the server. data-dialog-show opens it as soon as htmx inserts it, and data-dialog-remove takes it out of the page once it has slid away (swiped or not), so every click loads a fresh one. Append it to the body: html <button class=\"btn btn-outline\" hx-get=\"/api/share\" hx-target=\"body\" hx-swap=\"beforeend\">Share</button> <!-- The response <!-- The response --> <dialog class=\"drawer\" data-dialog data-drawer data-dialog-show data-dialog-remove aria-labelledby=\"share-title\"> <div class=\"drawer-handle\"></div> <div class=\"drawer-header\"> <h2 class=\"drawer-title\" id=\"share-title\">Share</h2> </div> <div class=\"drawer-content\">…</div> </dialog>"
        },
        {
          "id": "macro",
          "title": "Macro",
          "text": "drawer() writes the <dialog>, its handle and header, with the ids and ARIA attributes wired up. The call body is everything after the header. jinja {% from \"components/drawer/drawer.html\" import drawer %} <button class=\"btn btn-outline\" commandfor=\"filters\" command=\"show-modal\">Filters</button> {% call drawer(\"filters\", \"Filters\", \"Narrow down the list.\", responsive=true) %} <div class=\"drawer-content\">…</div> <div class=\"drawer-footer\"> <button class=\"btn btn-outline\" data-dialog-close>Reset</button> <button class=\"btn btn-primary\" data-dialog-close=\"apply\">Apply</button> </div> {% endcall %} Parameter Description id Required. The id buttons point commandfor at; also prefixes the title and description ids. title, description Header text. Each sets aria-labelledby / aria-describedby. direction=\"down\" data-swipe-direction: \"down\", \"up\", \"left\" or \"right\". handle=true The .drawer-handle bar. false leaves it out. handle_only=false true adds data-drawer-handle-only. responsive=false true adds .drawer-responsive: a centred dialog from md. show, remove data-dialog-show and data-dialog-remove, for drawers returned by htmx. class More classes on the <dialog>."
        },
        {
          "id": "accessibility",
          "title": "Accessibility",
          "text": "A drawer is a modal dialog: the browser moves focus into it, makes the page behind it inert and returns focus to the opening button when it closes. Give every drawer a name: aria-labelledby pointing at its title, or aria-label. Swiping is never the only way out. Keep a close or cancel button in the drawer for people who can't drag; Esc and a click outside work too. The handle is decoration for pointers and is not focusable. The slide is skipped for people who ask for reduced motion; dragging still works."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .drawer The <dialog>: attached to an edge, slides in and out, follows a drag. Bottom and top: full width, up to 80dvh tall. Left and right: full height, w-3/4 up to max-w-sm. .drawer-handle The bar on the free edge; the drawer leaves room for it. .drawer-header / .drawer-title / .drawer-description Title block: centred on bottom and top drawers. .drawer-content Body that scrolls on its own. .drawer-footer Actions, stacked with the last on top (a right-aligned row in a responsive drawer from md). .drawer-responsive A centred dialog with no handle and no swiping from md up. [data-dialog] The dialog behaviour: closes on a click outside and on [data-dialog-close], opens command buttons in older browsers. [data-drawer] Swiping and nested-drawer state. [data-swipe-direction] down (default), up, right or left: the way a swipe dismisses it and the edge it is on. [data-drawer-handle-only] Only .drawer-handle starts a drag. [data-drawer-no-drag] On any element inside: a drag can't start there. [data-swiping] Set while it is being dragged. [data-nested-drawer-open] Set while another drawer is open in front of it. [data-drawer-nested] Set on a drawer opened over another; it gets a lighter backdrop. --nested-drawers The number of drawers open in front of it (0 for the frontmost). --drawer-swipe-movement / --drawer-swipe-progress During a drag: pixels towards its edge, and that as a fraction (0 to 1) of its size. --drawer-swipe: none Turns swiping off (set by .drawer-responsive from md). [data-dialog-close=\"value\"] Closes the drawer it is in; the value becomes returnValue. [data-dialog-show] / [data-dialog-remove] Open as a modal when initialised / remove from the page once closed. cancel, close events A dismissing swipe fires cancel (cancelable), then close. dialog:close event Closes the drawer it is dispatched in, e.g. from an HX-Trigger response header."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/hover-card",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/hover-card",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/hover-card.md",
      "title": "Hover card",
      "description": "A card that previews what is behind a link when a mouse rests on it or the link gets keyboard focus, for sighted users.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "opening",
          "text": "Opening and closing"
        },
        {
          "level": 2,
          "id": "side",
          "text": "Side"
        },
        {
          "level": 2,
          "id": "alignment",
          "text": "Alignment"
        },
        {
          "level": 2,
          "id": "size",
          "text": "Size"
        },
        {
          "level": 2,
          "id": "offset",
          "text": "Offset"
        },
        {
          "level": 2,
          "id": "inline",
          "text": "In a paragraph"
        },
        {
          "level": 2,
          "id": "delay",
          "text": "Delays"
        },
        {
          "level": 2,
          "id": "keyboard",
          "text": "Keyboard"
        },
        {
          "level": 2,
          "id": "accessibility",
          "text": "Accessibility"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <span class=\"hover-card\" data-hover-card> <a class=\"btn btn-link\" href=\"#\" data-hover-card-trigger>@htmx-ui</a> <span class=\"hover-card-content hover-card-content-lg\" data-hover-card-content> <span class=\"hover-card-header\"> <span class=\"avatar avatar-lg\" aria-hidden=\"true\"> <img class=\"avatar-image\" src=\"../../../images/avatar-2.svg\" alt=\"\" /> <span class=\"avatar-fallback\">HU</span> </span> <span class=\"hover-card-body\"> <span class=\"hover-card-title\">@htmx-ui</span> <span class=\"hover-card-description\">Components for htmx apps – plain HTML, CSS and a little TypeScript.</span> <span class=\"hover-card-meta\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"4\" rx=\"2\"/><path d=\"M16 2v4\"/><path d=\"M8 2v4\"/><path d=\"M3 10h18\"/></svg> Joined December 2021</span> </span> </span> </span> </span>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "Wrap a link and its card in .hover-card with data-hover-card. The trigger is the element with data-hover-card-trigger, else the first link inside. The card is a .hover-card-content with data-hover-card-content, placed after the trigger. It may hold anything, links included. Write the wrapper, the card and its parts as <span>s, as above: a hover card is often inside a paragraph, and a <div> or <p> there would end the paragraph. For a profile preview, .hover-card-header puts an avatar beside a .hover-card-body, which stacks a .hover-card-title, a .hover-card-description and a muted .hover-card-meta line with an icon. The trigger stays an ordinary link: clicking or tapping it follows it. The card only adds a preview of what is there, so nothing must be reachable through the card alone."
        },
        {
          "id": "opening",
          "title": "Opening and closing",
          "text": "A mouse or pen resting on the trigger opens the card after 700ms, so it doesn't flash while the pointer crosses the page. Leaving before then opens nothing. Once the pointer has left both the trigger and the card, the card closes after 300ms. The pointer can travel from the trigger onto the card, across the gap, to select text or follow a link in it. Keyboard focus on the trigger opens it at once. The card stays open while focus is inside it and closes when focus leaves. A mouse click that focuses the trigger doesn't open it. Touch never opens it: on a phone the link simply navigates. Esc closes it. The behaviour sets data-open on the wrapper while the card is open. Without JavaScript, CSS opens it on hover and keyboard focus, with the same delays."
        },
        {
          "id": "side",
          "title": "Side",
          "text": "Cards open below their trigger. .hover-card-content-top, .hover-card-content-right and .hover-card-content-left put them on another side. html <span class=\"hover-card\" data-hover-card> <a class=\"btn btn-outline btn-sm\" href=\"#\" data-hover-card-trigger>Top</a> <span class=\"hover-card-content hover-card-content-sm hover-card-content-top\" data-hover-card-content>Opens above the trigger.</span> </span> <span class=\"flex gap-24\"> <span class=\"hover-card\" data-hover-card> <a class=\"btn btn-outline btn-sm\" href=\"#\" data-hover-card-trigger>Left</a> <span class=\"hover-card-content hover-card-content-sm hover-card-content-left\" data-hover-card-content>Opens to the left.</span> </span> <span class=\"hover-card\" data-hover-card> <a class=\"btn btn-outline btn-sm\" href=\"#\" data-hover-card-trigger>Right</a> <span class=\"hover-card-content hover-card-content-sm hover-card-content-right\" data-hover-card-content>Opens to the right.</span> </span> </span> <span class=\"hover-card\" data-hover-card> <a class=\"btn btn-outline btn-sm\" href=\"#\" data-hover-card-trigger>Bottom</a> <span class=\"hover-card-content hover-card-content-sm\" data-hover-card-content>Opens below the trigger (the default).</span> </span> If the card would stick out of the window on its side, it opens on the opposite side instead, provided that side has more room: a card set to open above a link at the top of the screen opens below it. data-side on the card says which side it opened on. Like any absolutely positioned element, it is still clipped by an ancestor with overflow: hidden."
        },
        {
          "id": "alignment",
          "title": "Alignment",
          "text": "A card is centred on its trigger. .hover-card-content-start lines it up with the trigger's start edge and .hover-card-content-end with its end edge (the top and bottom edges for cards on the left or right). html <span class=\"hover-card\" data-hover-card> <a class=\"btn btn-outline btn-sm\" href=\"#\" data-hover-card-trigger>Start</a> <span class=\"hover-card-content hover-card-content-sm hover-card-content-start\" data-hover-card-content>Aligned to the start edge of its trigger.</span> </span> <span class=\"hover-card\" data-hover-card> <a class=\"btn btn-outline btn-sm\" href=\"#\" data-hover-card-trigger>Center</a> <span class=\"hover-card-content hover-card-content-sm\" data-hover-card-content>Aligned to the centre of its trigger.</span> </span> <span class=\"hover-card\" data-hover-card> <a class=\"btn btn-outline btn-sm\" href=\"#\" data-hover-card-trigger>End</a> <span class=\"hover-card-content hover-card-content-sm hover-card-content-end\" data-hover-card-content>Aligned to the end edge of its trigger.</span> </span>"
        },
        {
          "id": "size",
          "title": "Size",
          "text": "Cards are 16rem wide. .hover-card-content-sm makes them 12rem and .hover-card-content-lg 20rem; any w-* utility sets another width. They never get wider than the window. html <span class=\"hover-card\" data-hover-card> <a class=\"btn btn-outline btn-sm\" href=\"#\" data-hover-card-trigger>Small</a> <span class=\"hover-card-content hover-card-content-sm\" data-hover-card-content> <span class=\"hover-card-title\">Small</span> <span class=\"hover-card-description text-muted-foreground\">A hover card with .hover-card-content-sm.</span> </span> </span> <span class=\"hover-card\" data-hover-card> <a class=\"btn btn-outline btn-sm\" href=\"#\" data-hover-card-trigger>Default</a> <span class=\"hover-card-content\" data-hover-card-content> <span class=\"hover-card-title\">Default</span> <span class=\"hover-card-description text-muted-foreground\">A hover card with the default width.</span> </span> </span> <span class=\"hover-card\" data-hover-card> <a class=\"btn btn-outline btn-sm\" href=\"#\" data-hover-card-trigger>Large</a> <span class=\"hover-card-content hover-card-content-lg\" data-hover-card-content> <span class=\"hover-card-title\">Large</span> <span class=\"hover-card-description text-muted-foreground\">A hover card with .hover-card-content-lg.</span> </span> </span>"
        },
        {
          "id": "offset",
          "title": "Offset",
          "text": "The gap between trigger and card is --hover-card-offset, 0.25rem by default. Set it on the wrapper or any ancestor; the invisible bridge the pointer crosses grows with it. html <span class=\"hover-card [--hover-card-offset:1rem]\" data-hover-card> <a class=\"btn btn-outline btn-sm\" href=\"#\" data-hover-card-trigger>1rem away</a> <span class=\"hover-card-content hover-card-content-sm\" data-hover-card-content>Opens 1rem below its trigger.</span> </span>"
        },
        {
          "id": "inline",
          "title": "In a paragraph",
          "text": "The usual home of a hover card: names and references in running text, each previewing the page it links to. html <p class=\"max-w-md text-sm leading-relaxed text-muted-foreground\"> This release was written by <span class=\"hover-card\" data-hover-card> <a class=\"link\" href=\"#\" data-hover-card-trigger>Ada Moreno</a> <span class=\"hover-card-content\" data-hover-card-content> <span class=\"hover-card-header\"> <span class=\"avatar\" aria-hidden=\"true\"> <img class=\"avatar-image\" src=\"../../../images/avatar-1.svg\" alt=\"\" /> <span class=\"avatar-fallback\">AM</span> </span> <span class=\"hover-card-body\"> <span class=\"hover-card-title\">Ada Moreno</span> <span class=\"hover-card-description\">Maintainer. Works on the engine and the docs plugin.</span> </span> </span> </span> </span> and reviewed by <span class=\"hover-card\" data-hover-card> <a class=\"link\" href=\"#\" data-hover-card-trigger>Sam Okafor</a> <span class=\"hover-card-content\" data-hover-card-content> <span class=\"hover-card-header\"> <span class=\"avatar\" aria-hidden=\"true\"> <img class=\"avatar-image\" src=\"../../../images/avatar-3.svg\" alt=\"\" /> <span class=\"avatar-fallback\">SO</span> </span> <span class=\"hover-card-body\"> <span class=\"hover-card-title\">Sam Okafor</span> <span class=\"hover-card-description\">Reviews accessibility across the component library.</span> </span> </span> </span> </span>. It closes three issues. </p>"
        },
        {
          "id": "delay",
          "title": "Delays",
          "text": "--hover-card-open-delay (700ms) is how long the pointer must rest on the trigger, and --hover-card-close-delay (300ms) how long the card waits after the pointer has left. Set them on the wrapper or any ancestor, in ms or s. Keyboard focus ignores the open delay. html <span class=\"hover-card [--hover-card-open-delay:100ms]\" data-hover-card> <a class=\"btn btn-outline btn-sm\" href=\"#\" data-hover-card-trigger>Quick</a> <span class=\"hover-card-content hover-card-content-sm\" data-hover-card-content>Opens after 100ms.</span> </span> <span class=\"hover-card [--hover-card-close-delay:1s]\" data-hover-card> <a class=\"btn btn-outline btn-sm\" href=\"#\" data-hover-card-trigger>Lingering</a> <span class=\"hover-card-content hover-card-content-sm\" data-hover-card-content>Stays for a second after the pointer leaves.</span> </span>"
        },
        {
          "id": "keyboard",
          "title": "Keyboard",
          "text": "Key Action Tab Focusing the trigger opens the card. The next Tab moves into the card's links, if it has any; leaving the card closes it. Enter Follows the trigger link, as usual. Esc Closes the card. Focus inside the card returns to the trigger. It opens again when the pointer comes back or focus returns to the trigger."
        },
        {
          "id": "accessibility",
          "title": "Accessibility",
          "text": "A hover card is for sighted mouse and keyboard users. The trigger is a real link that leads to the same information, so everyone else loses nothing. The card has no role and isn't a dialog: it doesn't take focus or trap it, and opening it isn't announced. While closed it is hidden with visibility, so screen readers and Tab skip it. While open it is ordinary content after the link, which keyboard users can tab into. The trigger isn't linked to the card with aria-describedby, as a tooltip is: a profile card is too long to read out after every mention. For a short card, add it yourself: give the card an id and put it in the trigger's aria-describedby. It meets WCAG 1.4.13 (content on hover or focus): it can be dismissed with Esc without moving the pointer, the pointer can move onto it, and it stays until the pointer or focus leaves. With reduced motion, the card fades without sliding or zooming."
        },
        {
          "id": "htmx",
          "title": "With htmx",
          "text": "The card fires hover-card:open and hover-card:close (they bubble) each time it opens and closes. Load the preview from the server the first time it opens with hx-trigger=\"hover-card:open once\" on the card, so a page full of mentions doesn't fetch every profile up front: html <span class=\"hover-card\" data-hover-card> <a class=\"link\" href=\"#\" data-hover-card-trigger>@htmx-ui</a> <span class=\"hover-card-content\" data-hover-card-content hx-get=\"/api/profile?user=htmx-ui\" hx-trigger=\"hover-card:open once\"> <span class=\"flex items-center gap-2 text-muted-foreground\"> <span class=\"spinner\" role=\"status\" aria-label=\"Loading\"></span> Loading profile… </span> </span> </span> The server returns the card's inner markup, which replaces the placeholder. Components in it are initialised as usual, since htmx-ui runs its initialisers on new content."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class / attribute Description .hover-card Wrapper around the trigger and the card. Add data-hover-card. .hover-card-content The card: below the trigger, centred, 16rem wide. Add data-hover-card-content. .hover-card-content-top / -right / -bottom / -left The side it opens on (bottom by default). .hover-card-content-start / -end Align with the trigger's start or end edge (centred by default). .hover-card-content-sm / -lg 12rem / 20rem wide. .hover-card-header An avatar beside the body. .hover-card-body Stacks the title, description and meta line. .hover-card-title The name or title, in bold. .hover-card-description A line or two of text. .hover-card-meta A small muted line with an icon, such as a join date. [data-hover-card] Behaviour: delays, keyboard focus, Escape, flipping, events. [data-hover-card-trigger] Marks the trigger when it isn't the first link inside. [data-hover-card-content] Marks the card. [data-open] Set on the wrapper while the card is open. [data-side] Set on the card when it opens: the side it opened on, after any flip. --hover-card-open-delay How long the pointer rests on the trigger before the card opens (700ms). --hover-card-close-delay How long the card stays after the pointer leaves (300ms). --hover-card-offset Gap between trigger and card (0.25rem). hover-card:open / hover-card:close Events fired on the card (bubbling) when it opens and closes."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/popover",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/popover",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/popover.md",
      "title": "Popover",
      "description": "A floating panel of rich content, such as text or a small form, that opens from a button.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "keyboard",
          "text": "Closing"
        },
        {
          "level": 2,
          "id": "alignment",
          "text": "Alignment"
        },
        {
          "level": 2,
          "id": "form",
          "text": "With a form"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "Content from the server"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"popover\" data-popover> <button class=\"btn btn-outline\" data-popover-trigger aria-expanded=\"false\">Open popover</button> <div class=\"popover-content\" data-popover-content hidden> <div class=\"popover-header\"> <h2 class=\"popover-title\">Title</h2> <p class=\"popover-description\">Description text here.</p> </div> </div> </div>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "Wrap the trigger and panel in .popover with data-popover. The trigger gets data-popover-trigger and aria-expanded=\"false\". The behaviour links it to the panel with aria-controls. The panel gets .popover-content, data-popover-content and starts hidden. A .popover-header stacks a .popover-title (any heading level that fits the page) and a .popover-description, and spaces them from what follows. Focus stays on the trigger when it opens, unless an element in the panel has autofocus. For a list of actions, use a dropdown instead: it adds menu semantics and arrow-key navigation."
        },
        {
          "id": "keyboard",
          "title": "Closing",
          "text": "Esc closes the popover and returns focus to the trigger. Clicking outside or tabbing out of it also closes it, and so does any button inside marked data-popover-close (see With a form)."
        },
        {
          "id": "alignment",
          "title": "Alignment",
          "text": "The panel opens below the trigger, aligned to its left edge (.popover-content-start, the default). .popover-content-center centres it on the trigger and .popover-content-end aligns it to the right edge: pick the one that keeps a panel wider than its trigger on screen. html <div class=\"popover\" data-popover> <button class=\"btn btn-outline btn-sm\" data-popover-trigger aria-expanded=\"false\">Start</button> <div class=\"popover-content popover-content-start w-48\" data-popover-content hidden> <p class=\"popover-description mt-0\">Aligned to the left edge of its trigger.</p> </div> </div> <div class=\"popover\" data-popover> <button class=\"btn btn-outline btn-sm\" data-popover-trigger aria-expanded=\"false\">Center</button> <div class=\"popover-content popover-content-center w-48\" data-popover-content hidden> <p class=\"popover-description mt-0\">Aligned to the centre of its trigger.</p> </div> </div> <div class=\"popover\" data-popover> <button class=\"btn btn-outline btn-sm\" data-popover-trigger aria-expanded=\"false\">End</button> <div class=\"popover-content popover-content-end w-48\" data-popover-content hidden> <p class=\"popover-description mt-0\">Aligned to the right edge of its trigger.</p> </div> </div> .popover-content-up opens it above instead, for a trigger near the bottom of the screen: html <div class=\"popover\" data-popover> <button class=\"btn btn-ghost btn-icon\" data-popover-trigger aria-expanded=\"false\" aria-label=\"About this metric\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4M12 8h.01\"/></svg></button> <div class=\"popover-content popover-content-center popover-content-up w-60\" data-popover-content hidden> <p class=\"popover-description mt-0\">Active users counts everyone who signed in during the last 28 days.</p> </div> </div>"
        },
        {
          "id": "form",
          "title": "With a form",
          "text": "A panel can hold a whole form. Give the panel role=\"dialog\" and point its aria-labelledby at the title so screen readers announce what opened, and put autofocus on the first field. A button with data-popover-close closes the panel and returns focus to the trigger; on the submit button the form still submits, so with htmx the request goes and the panel closes in one click. html <div class=\"popover\" data-popover> <button class=\"btn btn-outline\" data-popover-trigger aria-expanded=\"false\">Dimensions</button> <form class=\"popover-content w-80\" data-popover-content role=\"dialog\" aria-labelledby=\"pop-dim-title\" hidden hx-post=\"/api/echo\" hx-target=\"#pop-dim-result\"> <div class=\"popover-header\"> <h2 class=\"popover-title\" id=\"pop-dim-title\">Dimensions</h2> <p class=\"popover-description\">Set the dimensions for the layer.</p> </div> <div class=\"field-group gap-3\"> <div class=\"field field-horizontal\"> <label class=\"label w-24\" for=\"pop-width\">Width</label> <input class=\"input input-sm\" id=\"pop-width\" name=\"width\" value=\"100%\" autofocus /> </div> <div class=\"field field-horizontal\"> <label class=\"label w-24\" for=\"pop-max-width\">Max. width</label> <input class=\"input input-sm\" id=\"pop-max-width\" name=\"max-width\" value=\"300px\" /> </div> <div class=\"field field-horizontal\"> <label class=\"label w-24\" for=\"pop-height\">Height</label> <input class=\"input input-sm\" id=\"pop-height\" name=\"height\" value=\"25px\" /> </div> <div class=\"field field-horizontal\"> <label class=\"label w-24\" for=\"pop-max-height\">Max. height</label> <input class=\"input input-sm\" id=\"pop-max-height\" name=\"max-height\" value=\"none\" /> </div> </div> <div class=\"popover-footer\"> <button type=\"button\" class=\"btn btn-ghost btn-sm\" data-popover-close>Cancel</button> <button class=\"btn btn-primary btn-sm\" data-popover-close>Apply</button> </div> </form> </div> <p id=\"pop-dim-result\" class=\"muted self-center\" aria-live=\"polite\"></p>"
        },
        {
          "id": "htmx",
          "title": "Content from the server",
          "text": "The panel is an ordinary element, so htmx can fill it. This one loads its content the first time it's opened: html <div class=\"popover\" data-popover> <button class=\"btn btn-outline\" data-popover-trigger aria-expanded=\"false\" hx-get=\"/api/hello\" hx-trigger=\"click once\" hx-target=\"next [data-popover-content]\">Server message</button> <div class=\"popover-content w-80 p-2\" data-popover-content hidden><p class=\"p-2 muted\">Loading…</p></div> </div>"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class / attribute Description .popover Positioning wrapper around the trigger and panel. Add data-popover. .popover-content The floating panel. Add data-popover-content and hidden. .popover-content-start / -center / -end Align the panel to the trigger's left edge (default), centre or right edge. .popover-content-up Open above the trigger. .popover-header Stacks the title and description, spaced from the content below. .popover-title Heading text in the panel. .popover-description Muted supporting text. .popover-footer Right-aligned row of buttons at the bottom of a form. [data-popover-close] Closes the panel and returns focus to the trigger."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/sheet",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/sheet",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/sheet.md",
      "title": "Sheet",
      "description": "A dialog that slides in from an edge of the screen, for content that complements the page: settings, filters, details or navigation.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "open-and-close",
          "text": "Opening and closing"
        },
        {
          "level": 2,
          "id": "sides",
          "text": "Sides"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Sizes"
        },
        {
          "level": 2,
          "id": "no-close-button",
          "text": "No close button"
        },
        {
          "level": 2,
          "id": "scrolling-content",
          "text": "Scrolling content"
        },
        {
          "level": 2,
          "id": "navigation",
          "text": "Navigation"
        },
        {
          "level": 2,
          "id": "over-a-dialog",
          "text": "Over a dialog"
        },
        {
          "level": 2,
          "id": "keyboard",
          "text": "Keyboard"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx"
        },
        {
          "level": 3,
          "id": "post-a-form",
          "text": "Posting a form, closing from the server"
        },
        {
          "level": 3,
          "id": "from-the-server",
          "text": "A sheet from the server"
        },
        {
          "level": 2,
          "id": "macro",
          "text": "Macro"
        },
        {
          "level": 2,
          "id": "accessibility",
          "text": "Accessibility"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"sheet-profile\" command=\"show-modal\">Open</button> <dialog id=\"sheet-profile\" class=\"sheet\" data-dialog aria-labelledby=\"sheet-profile-title\" aria-describedby=\"sheet-profile-description\"> <div class=\"sheet-header\"> <h2 class=\"sheet-title\" id=\"sheet-profile-title\">Edit profile</h2> <p class=\"sheet-description\" id=\"sheet-profile-description\">Make changes to your profile here. Click save when you're done.</p> </div> <form class=\"contents\" method=\"dialog\"> <div class=\"grid gap-4\"> <div class=\"field\"> <label class=\"field-label\" for=\"sheet-profile-name\">Name</label> <input class=\"input\" id=\"sheet-profile-name\" name=\"name\" value=\"Pedro Duarte\" /> </div> <div class=\"field\"> <label class=\"field-label\" for=\"sheet-profile-username\">Username</label> <input class=\"input\" id=\"sheet-profile-username\" name=\"username\" value=\"@peduarte\" /> </div> </div> <div class=\"sheet-footer\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close>Close</button> <button class=\"btn btn-primary\" value=\"save\">Save changes</button> </div> </form> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm sheet-close\" data-dialog-close aria-label=\"Close\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> </dialog>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "A sheet is a native <dialog> with .sheet and data-dialog. It is a dialog in every way but its place on the screen: it opens and closes the same ways, puts the rest of the page out of reach while it is open, and works with htmx the same way. Instead of appearing in the middle, it fills one edge of the screen and slides in from it. .sheet-header holds .sheet-title and .sheet-description. Point aria-labelledby and aria-describedby at them. .sheet-content is an optional body that takes the free height and scrolls on its own, keeping the header and footer in view. .sheet-footer sits at the bottom: its buttons stacked, the last one on top. .sheet-close is the corner close button. It is optional. The sheet has its own .sheet-* parts rather than the dialog's: they are laid out for a tall, narrow panel (left-aligned header, footer pinned to the bottom, wider padding for the scrollbar). Use .sheet on its own, not together with .dialog."
        },
        {
          "id": "open-and-close",
          "title": "Opening and closing",
          "text": "Exactly as for a dialog: a button with commandfor=\"<sheet id>\" and command=\"show-modal\" opens it, or sheet.showModal() from script. Esc, a click outside, any element inside with data-dialog-close, a <form method=\"dialog\"> or command=\"close\" closes it, and the native close event fires on the <dialog> with the answer in returnValue."
        },
        {
          "id": "sides",
          "title": "Sides",
          "text": "A sheet opens on the right. .sheet-left, .sheet-top and .sheet-bottom attach it to another edge (.sheet-right names the default). Left and right sheets are full height, three quarters of the screen wide and at most max-w-sm; top and bottom sheets are full width and as tall as their content, up to the viewport height less a margin. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"sheet-side-top\" command=\"show-modal\">Top</button> <dialog id=\"sheet-side-top\" class=\"sheet sheet-top\" data-dialog aria-labelledby=\"sheet-side-top-title\" aria-describedby=\"sheet-side-top-description\"> <div class=\"sheet-header\"> <h2 class=\"sheet-title\" id=\"sheet-side-top-title\">Edit profile</h2> <p class=\"sheet-description\" id=\"sheet-side-top-description\">This sheet opens from the top.</p> </div> <div class=\"sheet-footer\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close>Cancel</button> <button type=\"button\" class=\"btn btn-primary\" data-dialog-close=\"save\">Save changes</button> </div> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm sheet-close\" data-dialog-close aria-label=\"Close\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> </dialog> <button type=\"button\" class=\"btn btn-outline\" commandfor=\"sheet-side-right\" command=\"show-modal\">Right</button> <dialog id=\"sheet-side-right\" class=\"sheet sheet-right\" data-dialog aria-labelledby=\"sheet-side-right-title\" aria-describedby=\"sheet-side-right-description\"> <div class=\"sheet-header\"> <h2 class=\"sheet-title\" id=\"sheet-side-right-title\">Edit profile</h2> <p class=\"sheet-description\" id=\"sheet-side-right-description\">This sheet opens from the right.</p> </div> <div class=\"sheet-footer\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close>Cancel</button> <button type=\"button\" class=\"btn btn"
        },
        {
          "id": "sizes",
          "title": "Sizes",
          "text": ".sheet-sm, .sheet-lg and .sheet-xl set the width of a left or right sheet: max-w-xs, max-w-lg and max-w-2xl (the default is max-w-sm). Any max-w-* utility works too, and on a top or bottom sheet an h-* utility fixes its height. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"sheet-size-sm\" command=\"show-modal\">Small</button> <button type=\"button\" class=\"btn btn-outline\" commandfor=\"sheet-size-xl\" command=\"show-modal\">Extra large</button> <button type=\"button\" class=\"btn btn-outline\" commandfor=\"sheet-size-half\" command=\"show-modal\">Half-height bottom</button> <dialog id=\"sheet-size-sm\" class=\"sheet sheet-sm\" data-dialog aria-labelledby=\"sheet-size-sm-title\"> <div class=\"sheet-header\"><h2 class=\"sheet-title\" id=\"sheet-size-sm-title\">Small sheet</h2></div> <p class=\"muted\">.sheet-sm is max-w-xs.</p> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm sheet-close\" data-dialog-close aria-label=\"Close\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> </dialog> <dialog id=\"sheet-size-xl\" class=\"sheet sheet-xl\" data-dialog aria-labelledby=\"sheet-size-xl-title\"> <div class=\"sheet-header\"><h2 class=\"sheet-title\" id=\"sheet-size-xl-title\">Extra large sheet</h2></div> <p class=\"muted\">.sheet-xl is max-w-2xl.</p> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm sheet-close\" data-dialog-close aria-label=\"Close\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> </dialog> <dialog id=\"sheet-size-half\" class=\"sheet sheet-bottom h-1/2\" data-dialog aria-labelledby=\"sheet-size-half-title\"> <div class=\"sheet-header\"><h2 class=\"sheet-titl"
        },
        {
          "id": "no-close-button",
          "title": "No close button",
          "text": "Leave out .sheet-close for a sheet that closes with Esc, a click outside or a button of your own. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"sheet-no-close\" command=\"show-modal\">No close button</button> <dialog id=\"sheet-no-close\" class=\"sheet\" data-dialog aria-labelledby=\"sheet-no-close-title\" aria-describedby=\"sheet-no-close-description\"> <div class=\"sheet-header\"> <h2 class=\"sheet-title\" id=\"sheet-no-close-title\">No close button</h2> <p class=\"sheet-description\" id=\"sheet-no-close-description\">Press Escape, click outside or use the button below.</p> </div> <div class=\"sheet-footer\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close>Done</button> </div> </dialog>"
        },
        {
          "id": "scrolling-content",
          "title": "Scrolling content",
          "text": "Long content goes in .sheet-content, which scrolls between a header and footer that stay in view. .sheet-footer-sticky sets the footer apart: tinted, edge to edge, with a border on top. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"sheet-scroll\" command=\"show-modal\">Release notes</button> <dialog id=\"sheet-scroll\" class=\"sheet\" data-dialog aria-labelledby=\"sheet-scroll-title\"> <div class=\"sheet-header\"> <h2 class=\"sheet-title\" id=\"sheet-scroll-title\">Release notes</h2> <p class=\"sheet-description\">What changed in the last ten releases.</p> </div> <div class=\"sheet-content space-y-4 leading-relaxed\"> <div> <p class=\"font-medium\">Version 0.10.0</p> <p class=\"muted\">Components inside fragments initialise as soon as htmx inserts them. Tokens drive every colour, in light and dark. Dialogs, sheets and drawers share one behaviour.</p> </div> <div> <p class=\"font-medium\">Version 0.9.0</p> <p class=\"muted\">Components inside fragments initialise as soon as htmx inserts them. Tokens drive every colour, in light and dark. Dialogs, sheets and drawers share one behaviour.</p> </div> <div> <p class=\"font-medium\">Version 0.8.0</p> <p class=\"muted\">Components inside fragments initialise as soon as htmx inserts them. Tokens drive every colour, in light and dark. Dialogs, sheets and drawers share one behaviour.</p> </div> <div> <p class=\"font-medium\">Version 0.7.0</p> <p class=\"muted\">Components inside fragments initialise as soon as htmx inserts them. Tokens drive every colour, in light and dark. Dialogs, sheets and drawers share one behaviour.</p> </div> <div> <p class=\"font-medium\">Version 0.6.0</p> <p class=\"muted\">Components inside fragments initialise as soon as htmx inserts them. Tokens drive every colour, in light and dark. Dialogs, sheets and drawers share one behaviour.</p> </div> <div> <p class=\"font-medium\">Version 0.5.0</p> <p class=\"muted\">Components inside fragments initialise as soon as htmx inserts them. Tokens drive every colour, in light and dark. D"
        },
        {
          "id": "navigation",
          "title": "Navigation",
          "text": "A left sheet makes a menu for small screens. The links are ordinary links in a <nav>; the sheet closes when the next page loads, or give each link data-dialog-close to close it straight away (for links to sections of the same page, or with hx-boost). html <button type=\"button\" class=\"btn btn-outline btn-icon\" commandfor=\"sheet-nav\" command=\"show-modal\" aria-label=\"Open menu\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M4 6h16M4 12h16M4 18h16\"/></svg></button> <dialog id=\"sheet-nav\" class=\"sheet sheet-left sheet-sm\" data-dialog aria-labelledby=\"sheet-nav-title\"> <div class=\"sheet-header\"> <h2 class=\"sheet-title\" id=\"sheet-nav-title\">Acme Inc.</h2> </div> <nav class=\"sheet-content\" aria-labelledby=\"sheet-nav-title\"> <div class=\"sidebar-group\"> <p class=\"sidebar-group-label\">Workspace</p> <ul class=\"sidebar-menu\"> <li class=\"sidebar-menu-item\"><a class=\"sidebar-menu-button\" href=\"#\" aria-current=\"page\" data-dialog-close>Dashboard</a></li> <li class=\"sidebar-menu-item\"><a class=\"sidebar-menu-button\" href=\"#\" data-dialog-close>Projects</a></li> <li class=\"sidebar-menu-item\"><a class=\"sidebar-menu-button\" href=\"#\" data-dialog-close>Team</a></li> </ul> </div> <div class=\"sidebar-group\"> <p class=\"sidebar-group-label\">Account</p> <ul class=\"sidebar-menu\"> <li class=\"sidebar-menu-item\"><a class=\"sidebar-menu-button\" href=\"#\" data-dialog-close>Settings</a></li> <li class=\"sidebar-menu-item\"><a class=\"sidebar-menu-button\" href=\"#\" data-dialog-close>Billing</a></li> </ul> </div> </nav> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm sheet-close\" data-dialog-close aria-label=\"Close menu\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden"
        },
        {
          "id": "over-a-dialog",
          "title": "Over a dialog",
          "text": "A button inside a dialog (or another sheet) can open a sheet. It stacks on top with its own backdrop, and Esc or a click outside closes the topmost first. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"sheet-over-dialog\" command=\"show-modal\">Invite people</button> <dialog id=\"sheet-over-dialog\" class=\"dialog dialog-sm\" data-dialog aria-labelledby=\"sheet-over-dialog-title\"> <div class=\"dialog-header\"> <h2 class=\"dialog-title\" id=\"sheet-over-dialog-title\">Invite people</h2> <p class=\"dialog-description\">Not sure who has access already?</p> </div> <div class=\"dialog-footer\"> <button type=\"button\" class=\"btn btn-outline\" commandfor=\"sheet-members\" command=\"show-modal\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\"/><circle cx=\"9\" cy=\"7\" r=\"4\"/><path d=\"M22 21v-2a4 4 0 0 0-3-3.87\"/><path d=\"M16 3.13a4 4 0 0 1 0 7.75\"/></svg> Show members</button> <button type=\"button\" class=\"btn btn-primary\" data-dialog-close>Done</button> </div> </dialog> <dialog id=\"sheet-members\" class=\"sheet\" data-dialog aria-labelledby=\"sheet-members-title\"> <div class=\"sheet-header\"> <h2 class=\"sheet-title\" id=\"sheet-members-title\">Members</h2> <p class=\"sheet-description\">A sheet over the dialog.</p> </div> <ul class=\"grid gap-2\"> <li>Olivia Martin</li> <li>Jackson Lee</li> <li>Isabella Nguyen</li> </ul> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm sheet-close\" data-dialog-close aria-label=\"Close\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> </dialog>"
        },
        {
          "id": "keyboard",
          "title": "Keyboard",
          "text": "Esc closes the topmost sheet. Tab and Shift + Tab move through the sheet's controls; the page behind it is inert. Enter or Space on the opening button opens it."
        },
        {
          "id": "htmx",
          "title": "With htmx",
          "text": ""
        },
        {
          "id": "post-a-form",
          "title": "Posting a form, closing from the server",
          "text": "A form in a sheet can post with htmx. When the server has saved it, the response header HX-Trigger: dialog:close closes the sheet (the dialog:close event closes the sheet, dialog or drawer it is dispatched in), while the response body goes to the form's hx-target. On a validation error, leave the header off and return the form with its messages. This demo posts to the same endpoint as the dialog's. html <button type=\"button\" class=\"btn btn-outline\" commandfor=\"sheet-rename\" command=\"show-modal\">Rename project</button> <span id=\"sheet-rename-result\" class=\"muted\"></span> <dialog id=\"sheet-rename\" class=\"sheet\" data-dialog aria-labelledby=\"sheet-rename-title\"> <div class=\"sheet-header\"> <h2 class=\"sheet-title\" id=\"sheet-rename-title\">Rename project</h2> <p class=\"sheet-description\">The server closes this sheet once it has saved the name.</p> </div> <form class=\"contents\" hx-post=\"/api/dialog/save\" hx-target=\"#sheet-rename-result\"> <div class=\"field\"> <label class=\"field-label\" for=\"sheet-rename-name\">Name</label> <input class=\"input\" id=\"sheet-rename-name\" name=\"name\" value=\"htmx-ui\" required /> </div> <div class=\"sheet-footer\"> <button type=\"button\" class=\"btn btn-outline\" data-dialog-close>Cancel</button> <button class=\"btn btn-primary\">Save</button> </div> </form> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm sheet-close\" data-dialog-close aria-label=\"Close\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> </dialog> text HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 HX-Trigger: dialog:close <span class=\"text-success\">Renamed to htmx-ui</span>"
        },
        {
          "id": "from-the-server",
          "title": "A sheet from the server",
          "text": "A sheet can also come from the server. data-dialog-show opens it as soon as htmx inserts it, and data-dialog-remove takes it out of the page once it has slid away, so every click loads a fresh one. Append it to the body: html <button class=\"btn btn-outline\" hx-get=\"/api/order/42\" hx-target=\"body\" hx-swap=\"beforeend\">Order details</button> <!-- The response <!-- The response --> <dialog class=\"sheet\" data-dialog data-dialog-show data-dialog-remove aria-labelledby=\"order-title\"> <div class=\"sheet-header\"> <h2 class=\"sheet-title\" id=\"order-title\">Order #42</h2> <p class=\"sheet-description\">Placed on 3 October.</p> </div> <div class=\"sheet-content\">…</div> <button class=\"btn btn-ghost btn-icon btn-sm sheet-close\" data-dialog-close aria-label=\"Close\">…</button> </dialog>"
        },
        {
          "id": "macro",
          "title": "Macro",
          "text": "sheet() writes the <dialog>, its header and the close button, with the ids and ARIA attributes wired up. The call body is everything after the header. jinja {% from \"components/sheet/sheet.html\" import sheet %} <button class=\"btn btn-outline\" commandfor=\"filters\" command=\"show-modal\">Filters</button> {% call sheet(\"filters\", \"Filters\", \"Narrow down the list.\", side=\"left\", size=\"sm\") %} <div class=\"sheet-content\">…</div> <div class=\"sheet-footer\"> <button class=\"btn btn-outline\" data-dialog-close>Reset</button> <button class=\"btn btn-primary\" data-dialog-close=\"apply\">Apply</button> </div> {% endcall %} Parameter Description id Required. The id buttons point commandfor at; also prefixes the title and description ids. title, description Header text. Each sets aria-labelledby / aria-describedby. side=\"right\" \"top\", \"right\", \"bottom\" or \"left\". close=true The corner close button. false leaves it out. size \"sm\", \"lg\" or \"xl\": the width of a left or right sheet. show, remove data-dialog-show and data-dialog-remove, for sheets returned by htmx. class More classes on the <dialog>."
        },
        {
          "id": "accessibility",
          "title": "Accessibility",
          "text": "A sheet is a modal dialog: the browser moves focus into it, makes the page behind it inert and returns focus to the opening button when it closes. Give every sheet a name: aria-labelledby pointing at its title, or aria-label. Put autofocus on the field the user needs first; otherwise focus goes to the first focusable element. An icon-only button, like the close button or a menu button, needs an aria-label. The slide is skipped for people who ask for reduced motion."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .sheet The <dialog>: attached to the right edge, full height, w-3/4 up to max-w-sm. Slides in and out. .sheet-right / -left / -top / -bottom The edge it is attached to (right is the default). .sheet-sm / -lg / -xl Widths of a left or right sheet: max-w-xs, max-w-lg, max-w-2xl. .sheet-header / .sheet-title / .sheet-description Title block. .sheet-content Body that takes the free height and scrolls on its own. .sheet-footer Actions at the bottom, stacked with the last on top; a right-aligned row in top and bottom sheets from sm. .sheet-footer-sticky Tinted, edge-to-edge footer with a top border. .sheet-close Corner close button. [data-dialog] The dialog behaviour: closes on a click outside and on [data-dialog-close], opens command buttons in older browsers. [data-dialog-close=\"value\"] Closes the sheet it is in; the value becomes returnValue. [data-dialog-show] / [data-dialog-remove] Open as a modal when initialised / remove from the page once closed. dialog:close event Closes the sheet it is dispatched in, e.g. from an HX-Trigger response header."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/tooltip",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/tooltip",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/tooltip.md",
      "title": "Tooltip",
      "description": "A short hint that appears when an element is hovered or receives keyboard focus, on any side, with no JavaScript needed to show it.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "side",
          "text": "Side"
        },
        {
          "level": 2,
          "id": "icon-buttons",
          "text": "Icon buttons and toolbars"
        },
        {
          "level": 2,
          "id": "disabled",
          "text": "Disabled buttons"
        },
        {
          "level": 2,
          "id": "delay",
          "text": "Delay"
        },
        {
          "level": 2,
          "id": "keyboard",
          "text": "Keyboard"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <span class=\"tooltip\" data-tooltip> <button class=\"btn btn-outline\">Hover</button> <span class=\"tooltip-content\">Add to library</span> </span>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "Wrap the trigger and the text in .tooltip with data-tooltip. The trigger is the first focusable element inside: a button, a link, an input. The text is a .tooltip-content placed after the trigger. The behaviour gives it role=\"tooltip\" and an id, and points the trigger's aria-describedby at it, keeping any description the trigger already had. Showing and hiding is CSS: the tooltip appears while the wrapper is hovered, after a short delay, and at once when the trigger gets focus from the keyboard. Clicking a button with the mouse doesn't leave its tooltip open, and on touch screens there is no hover, so a tooltip never sticks after a tap. The pointer can move from the trigger onto the tooltip without it closing, so its text can be selected. A tooltip is a description: keep it short, plain text, and never the only place something is said. An icon-only button still needs an aria-label. For interactive content — links, buttons, a form — use a popover, which opens on click and holds focus."
        },
        {
          "id": "side",
          "title": "Side",
          "text": "Tooltips open above their trigger. .tooltip-content-bottom, .tooltip-content-left and .tooltip-content-right put them on another side; the arrow follows. html <span class=\"tooltip\" data-tooltip> <button class=\"btn btn-outline\">Top</button> <span class=\"tooltip-content\">Opens above</span> </span> <span class=\"tooltip\" data-tooltip> <button class=\"btn btn-outline\">Bottom</button> <span class=\"tooltip-content tooltip-content-bottom\">Opens below</span> </span> <span class=\"tooltip\" data-tooltip> <button class=\"btn btn-outline\">Left</button> <span class=\"tooltip-content tooltip-content-left\">Opens to the left</span> </span> <span class=\"tooltip\" data-tooltip> <button class=\"btn btn-outline\">Right</button> <span class=\"tooltip-content tooltip-content-right\">Opens to the right</span> </span> Tooltips are positioned with CSS only, so they don't flip when they would leave the screen: pick the side with room, such as -bottom for a toolbar at the top of the page. Like any absolutely positioned element they are clipped by an ancestor with overflow: hidden."
        },
        {
          "id": "icon-buttons",
          "title": "Icon buttons and toolbars",
          "text": "The usual home of a tooltip: naming what an icon does, with the shortcut beside it in a kbd. html <div class=\"btn-group\" role=\"toolbar\" aria-label=\"Text formatting\"> <span class=\"tooltip\" data-tooltip> <button class=\"btn btn-outline btn-icon\" aria-label=\"Bold\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8\"/></svg></button> <span class=\"tooltip-content\">Bold <kbd class=\"kbd\">Ctrl B</kbd></span> </span> <span class=\"tooltip\" data-tooltip> <button class=\"btn btn-outline btn-icon\" aria-label=\"Italic\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M19 4h-9M14 20H5M15 4 9 20\"/></svg></button> <span class=\"tooltip-content\">Italic <kbd class=\"kbd\">Ctrl I</kbd></span> </span> <span class=\"tooltip\" data-tooltip> <button class=\"btn btn-outline btn-icon\" aria-label=\"Underline\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M6 4v6a6 6 0 0 0 12 0V4M4 20h16\"/></svg></button> <span class=\"tooltip-content\">Underline <kbd class=\"kbd\">Ctrl U</kbd></span> </span> </div>"
        },
        {
          "id": "disabled",
          "title": "Disabled buttons",
          "text": "A disabled button can't take focus, so keyboard users never reach its tooltip. Use aria-disabled=\"true\" instead, which keeps it focusable and greys it out the same way, and say why it's unavailable in the tooltip: html <span class=\"tooltip\" data-tooltip> <button class=\"btn btn-primary\" aria-disabled=\"true\">Publish</button> <span class=\"tooltip-content\">Add a title before publishing</span> </span>"
        },
        {
          "id": "delay",
          "title": "Delay",
          "text": "A tooltip waits 150ms under the pointer before it shows, so it doesn't flash while the mouse passes over a toolbar. Set --tooltip-delay on the wrapper (or anywhere above it) to change that: html <span class=\"tooltip [--tooltip-delay:0ms]\" data-tooltip> <button class=\"btn btn-outline\">Instant</button> <span class=\"tooltip-content\">No delay</span> </span> <span class=\"tooltip [--tooltip-delay:700ms]\" data-tooltip> <button class=\"btn btn-outline\">Patient</button> <span class=\"tooltip-content\">700ms delay</span> </span>"
        },
        {
          "id": "keyboard",
          "title": "Keyboard",
          "text": "Tab to a trigger shows its tooltip; moving focus away hides it. Esc hides any open tooltip without moving focus or the pointer, so it can be dismissed when it covers something. It shows again once focus moves or the pointer comes back."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class / attribute Description .tooltip Wrapper around the trigger and the text. Add data-tooltip. .tooltip-content The tooltip. Opens above the trigger, centred, with an arrow. .tooltip-content-bottom / -left / -right Open on another side. --tooltip-delay How long the pointer rests before it shows (150ms). Keyboard focus shows it at once. [data-tooltip] Behaviour: role=\"tooltip\", aria-describedby on the trigger, Escape to dismiss. [data-tooltip-trigger] Marks the trigger when it isn't the first focusable element inside."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/alert",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/alert",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/alert.md",
      "title": "Alert",
      "description": "A callout for information the reader should notice: tips, confirmations, warnings and errors.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "variants",
          "text": "Variants with icons"
        },
        {
          "level": 2,
          "id": "dismissible",
          "text": "Dismissible"
        },
        {
          "level": 2,
          "id": "macro",
          "text": "Nunjucks macro"
        },
        {
          "level": 2,
          "id": "accessibility",
          "text": "Accessibility"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"alert\" role=\"status\"> <div class=\"alert-title\">Heads up</div> <div class=\"alert-description\">You can add components to your app using the CLI.</div> </div>"
        },
        {
          "id": "variants",
          "title": "Variants with icons",
          "text": "Put an <svg> first and the text lines up beside it. Variant classes tint the border, background, icon and title. html <div class=\"alert alert-info\" role=\"status\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4M12 8h.01\"/></svg> <div class=\"alert-title\">New version available</div> <div class=\"alert-description\">v0.1.0 adds dark mode and five new components.</div> </div> <div class=\"alert alert-success\" role=\"status\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m9 12 2 2 4-4\"/></svg> <div class=\"alert-title\">Payment received</div> <div class=\"alert-description\">A receipt is on its way to your inbox.</div> </div> <div class=\"alert alert-warning\" role=\"status\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4M12 17h.01\"/></svg> <div class=\"alert-title\">Your trial ends in 3 days</div> <div class=\"alert-description\">Add a payment method to keep your projects online.</div> </div> <div class=\"alert alert-danger\" role=\"alert\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m15 9-6 6M9 9l6 6\"/></svg> <div class=\"alert-title\">Deploy failed</div> <div class=\"alert-description\">The build e"
        },
        {
          "id": "dismissible",
          "title": "Dismissible",
          "text": "Add data-dismissible to the alert and data-dismiss to a button inside it. Clicking the button removes the alert. This also works for alerts that arrive in htmx fragments. html <div class=\"alert alert-info\" role=\"status\" data-dismissible> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4M12 8h.01\"/></svg> <div class=\"alert-title\">Cookies</div> <div class=\"alert-description\">We only use essential cookies.</div> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm alert-close\" data-dismiss aria-label=\"Dismiss\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> </div>"
        },
        {
          "id": "macro",
          "title": "Nunjucks macro",
          "text": "If your templates use Nunjucks, the alert macro picks the icon and ARIA role for you: page.html {% from \"components/alert/alert.html\" import alert %} {% call alert(\"Deploy failed\", variant=\"danger\", dismissible=true) %} The build exited with code 1. {% endcall %}"
        },
        {
          "id": "accessibility",
          "title": "Accessibility",
          "text": "Use role=\"alert\" for urgent, time-sensitive messages (screen readers announce it right away), and role=\"status\" for everything else. Give icon-only close buttons an aria-label."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .alert Container. A leading <svg> becomes the icon column. .alert-title Bold first line. .alert-description Body text in the muted colour. .alert-info / -success / -warning / -danger Status tint for border, background, icon and title. .alert-close Top-right close button. Pair with data-dismiss. [data-dismissible] + [data-dismiss] Behaviour: clicking any [data-dismiss] inside removes the [data-dismissible] element, after a cancelable dismissible:dismiss event and any data-state=\"closing\" transition."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/badge",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/badge",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/badge.md",
      "title": "Badge",
      "description": "A small label for status, counts or categories.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "variants",
          "text": "Variants"
        },
        {
          "level": 2,
          "id": "status",
          "text": "Status"
        },
        {
          "level": 2,
          "id": "dot",
          "text": "With a dot or icon"
        },
        {
          "level": 2,
          "id": "spinner",
          "text": "With a spinner"
        },
        {
          "level": 2,
          "id": "link",
          "text": "Link"
        },
        {
          "level": 2,
          "id": "custom-colors",
          "text": "Custom colours"
        },
        {
          "level": 2,
          "id": "in-context",
          "text": "In context"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <span class=\"badge badge-primary\">New</span>"
        },
        {
          "id": "variants",
          "title": "Variants",
          "text": "html <span class=\"badge badge-primary\">Primary</span> <span class=\"badge badge-secondary\">Secondary</span> <span class=\"badge badge-outline\">Outline</span> <span class=\"badge badge-ghost\">Ghost</span> <span class=\"badge badge-link\">Link</span>"
        },
        {
          "id": "status",
          "title": "Status",
          "text": "Soft, tinted variants for status. They use the --info, --success, --warning and --danger tokens. html <span class=\"badge badge-info\">Info</span> <span class=\"badge badge-success\">Success</span> <span class=\"badge badge-warning\">Warning</span> <span class=\"badge badge-danger\">Danger</span> Add .badge-solid to fill one with its colour, for status that has to stand out, such as a destructive one. html <span class=\"badge badge-info badge-solid\">Info</span> <span class=\"badge badge-success badge-solid\">Success</span> <span class=\"badge badge-warning badge-solid\">Warning</span> <span class=\"badge badge-danger badge-solid\">Destructive</span>"
        },
        {
          "id": "dot",
          "title": "With a dot or icon",
          "text": "Icons go before or after the text and are sized to it. html <span class=\"badge badge-success badge-dot\">Operational</span> <span class=\"badge badge-warning badge-dot\">Degraded</span> <span class=\"badge badge-outline\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z\"/><path d=\"m9 12 2 2 4-4\"/></svg> Verified</span> <span class=\"badge badge-secondary\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z\"/></svg> Featured</span> <span class=\"badge badge-primary\">Next <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5l7 7-7 7\"/></svg></span>"
        },
        {
          "id": "spinner",
          "title": "With a spinner",
          "text": "A spinner inside a badge takes the icon size. With .htmx-indicator, it takes no space until the badge's request starts. html <span class=\"badge badge-secondary\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner\" aria-hidden=\"true\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg> Syncing</span> <span class=\"badge badge-info\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner\" aria-hidden=\"true\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg> Deploying</span> <span class=\"badge badge-outline\"><span class=\"spinner\" aria-hidden=\"true\"></span> Queued</span>"
        },
        {
          "id": "link",
          "title": "Link",
          "text": "A badge can be an <a> (or a <button>): it gets a hover state in its variant and a focus ring. html <a class=\"badge badge-primary\" href=\"#badge\">Open <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5l7 7-7 7\"/></svg></a> <a class=\"badge badge-outline\" href=\"#badge\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></svg> Docs</a> <a class=\"badge badge-ghost\" href=\"#badge\">#htmx</a> <a class=\"badge badge-success\" href=\"#badge\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg> Passing</a> <a class=\"badge badge-link\" href=\"#badge\">View changelog <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/></svg></a>"
        },
        {
          "id": "custom-colors",
          "title": "Custom colours",
          "text": "For a tinted badge in any colour, add .badge-tone and set --badge-tone: it gets the same border, background and hover as the status variants, and works with .badge-solid. For anything else, colour utilities override the badge's own colours. html <span class=\"badge badge-tone [--badge-tone:var(--color-violet-600)] dark:[--badge-tone:var(--color-violet-400)]\">Design</span> <span class=\"badge badge-tone [--badge-tone:var(--color-pink-600)] dark:[--badge-tone:var(--color-pink-400)]\">Research</span> <span class=\"badge badge-tone badge-solid [--badge-tone:var(--color-teal-600)] dark:[--badge-tone:var(--color-teal-400)]\">Shipped</span> <span class=\"badge bg-blue-50 text-blue-700 dark:bg-blue-950 dark:text-blue-300\">Utilities</span> <span class=\"badge bg-linear-to-r from-indigo-500 to-pink-500 text-white\">Gradient</span>"
        },
        {
          "id": "in-context",
          "title": "In context",
          "text": "html <button class=\"btn btn-outline\">Inbox <span class=\"badge badge-primary\">12</span></button> <h3 class=\"h4 flex items-center gap-2\">Billing <span class=\"badge badge-secondary\">Beta</span></h3>"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .badge Base class. On an <a> or <button>: hover state and focus ring. .badge-primary / -secondary / -outline / -ghost / -link Neutral and brand styles. .badge-info / -success / -warning / -danger Soft status colours. .badge-solid With a status or .badge-tone: filled with its colour. .badge-tone Tinted with any colour in --badge-tone. Set --badge-solid-foreground for the text of a solid one. .badge-dot Adds a leading dot in the badge's text colour."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/empty",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/empty",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/empty.md",
      "title": "Empty",
      "description": "Displays an empty state: no data yet, no results, nothing to show, with a way forward.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "outline",
          "text": "Outline"
        },
        {
          "level": 2,
          "id": "background",
          "text": "Background"
        },
        {
          "level": 2,
          "id": "avatar",
          "text": "Avatar"
        },
        {
          "level": 2,
          "id": "avatar-group",
          "text": "Avatar group"
        },
        {
          "level": 2,
          "id": "input-group",
          "text": "Input group"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Sizes"
        },
        {
          "level": 2,
          "id": "card",
          "text": "In a card"
        },
        {
          "level": 2,
          "id": "table",
          "text": "In a table"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx"
        },
        {
          "level": 2,
          "id": "macro",
          "text": "Macro"
        },
        {
          "level": 2,
          "id": "accessibility",
          "text": "Accessibility"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"empty\"> <div class=\"empty-header\"> <div class=\"empty-media empty-media-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12 10v6\"/><path d=\"M9 13h6\"/><path d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\"/></svg></div> <h3 class=\"empty-title\">No projects yet</h3> <p class=\"empty-description\">You haven't created any projects yet. Get started by creating your first project.</p> </div> <div class=\"empty-content\"> <div class=\"flex gap-2\"> <button type=\"button\" class=\"btn btn-primary\">Create project</button> <button type=\"button\" class=\"btn btn-outline\">Import project</button> </div> </div> <a class=\"btn btn-link btn-sm text-muted-foreground\" href=\"#\">Learn more <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M7 7h10v10\"/><path d=\"M7 17 17 7\"/></svg></a> </div>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "html <div class=\"empty\"> <div class=\"empty-header\"> <div class=\"empty-media empty-media-icon\">…icon…</div> <h3 class=\"empty-title\">No projects yet</h3> <p class=\"empty-description\">…</p> </div> <div class=\"empty-content\">…buttons, an input…</div> </div> .empty centres everything in a column and grows to fill its container. It has generous padding, more from the md breakpoint. .empty-header holds the media, title and description, at most 24rem wide. .empty-media holds an icon, an avatar or an avatar group. With .empty-media-icon it is a muted rounded square around an icon. .empty-title says what is missing; .empty-description says why, or what to do. Links in the description are underlined. .empty-content holds the way forward: buttons, a search field, a link."
        },
        {
          "id": "outline",
          "title": "Outline",
          "text": ".empty-outline adds a dashed border, as for a drop zone or a slot waiting to be filled. html <div class=\"empty empty-outline\"> <div class=\"empty-header\"> <div class=\"empty-media empty-media-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z\"/></svg></div> <h3 class=\"empty-title\">Cloud storage empty</h3> <p class=\"empty-description\">Upload files to your cloud storage to access them anywhere.</p> </div> <div class=\"empty-content\"> <button type=\"button\" class=\"btn btn-outline btn-sm\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12\"/></svg> Upload files</button> </div> </div>"
        },
        {
          "id": "background",
          "title": "Background",
          "text": ".empty-muted adds a muted wash that fades out towards the bottom. html <div class=\"empty empty-muted\"> <div class=\"empty-header\"> <div class=\"empty-media empty-media-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9\"/><path d=\"M10.3 21a1.94 1.94 0 0 0 3.4 0\"/></svg></div> <h3 class=\"empty-title\">No notifications</h3> <p class=\"empty-description\">You're all caught up. New notifications will appear here.</p> </div> <div class=\"empty-content\"> <button type=\"button\" class=\"btn btn-outline btn-sm\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\"/><path d=\"M21 3v5h-5\"/><path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\"/><path d=\"M8 16H3v5\"/></svg> Refresh</button> </div> </div>"
        },
        {
          "id": "avatar",
          "title": "Avatar",
          "text": "An avatar in .empty-media (without .empty-media-icon) is shown at 3rem unless it has a size class of its own. It names the person in the title, so it is decorative here. html <div class=\"empty\"> <div class=\"empty-header\"> <div class=\"empty-media\"> <span class=\"avatar\" aria-hidden=\"true\"><img class=\"avatar-image\" src=\"../../../images/avatar-2.svg\" alt=\"\" /><span class=\"avatar-fallback\">KM</span></span> </div> <h3 class=\"empty-title\">Kofi is offline</h3> <p class=\"empty-description\">Kofi is away right now. Leave a message and they'll see it when they're back.</p> </div> <div class=\"empty-content\"> <button type=\"button\" class=\"btn btn-primary btn-sm\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"/></svg> Leave a message</button> </div> </div>"
        },
        {
          "id": "avatar-group",
          "title": "Avatar group",
          "text": "An avatar group is shown large and in grey, for a team that hasn't joined yet. A .avatar-group-count keeps its colour. html <div class=\"empty\"> <div class=\"empty-header\"> <div class=\"empty-media\"> <div class=\"avatar-group\" aria-hidden=\"true\"> <span class=\"avatar\"><img class=\"avatar-image\" src=\"../../../images/avatar-1.svg\" alt=\"\" /><span class=\"avatar-fallback\">AL</span></span> <span class=\"avatar\"><img class=\"avatar-image\" src=\"../../../images/avatar-3.svg\" alt=\"\" /><span class=\"avatar-fallback\">RP</span></span> <span class=\"avatar\"><img class=\"avatar-image\" src=\"../../../images/avatar-4.svg\" alt=\"\" /><span class=\"avatar-fallback\">MC</span></span> </div> </div> <h3 class=\"empty-title\">No team members</h3> <p class=\"empty-description\">Invite your team to collaborate on this project.</p> </div> <div class=\"empty-content\"> <button type=\"button\" class=\"btn btn-outline btn-sm\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5v14\"/></svg> Invite members</button> </div> </div>"
        },
        {
          "id": "input-group",
          "title": "Input group",
          "text": ".empty-content is a column up to 24rem wide, so a field fills it. Here a not-found page offers a search, using an input group, and a link to help. html <div class=\"empty\"> <div class=\"empty-header\"> <h3 class=\"empty-title\">404 – Page not found</h3> <p class=\"empty-description\">The page you're looking for doesn't exist. Try searching for what you need below.</p> </div> <div class=\"empty-content\"> <form class=\"w-full\" role=\"search\" action=\"#\"> <div class=\"input-group\"> <input class=\"input\" type=\"search\" name=\"q\" placeholder=\"Search pages…\" aria-label=\"Search pages\" /> <span class=\"input-group-addon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg></span> <span class=\"input-group-addon input-group-addon-inline-end\"><kbd class=\"kbd\">/</kbd></span> </div> </form> <p class=\"empty-description\">Need help? <a href=\"#\">Contact support</a></p> </div> </div>"
        },
        {
          "id": "sizes",
          "title": "Sizes",
          "text": ".empty-sm is compact, for small panels, sidebars and table bodies: less padding and smaller gaps, a smaller icon square and title. html <div class=\"w-full max-w-xs rounded-[calc(var(--radius)+4px)] border border-border bg-surface\"> <div class=\"border-b border-border px-4 py-3 text-sm font-medium\">Activity</div> <div class=\"empty empty-sm\"> <div class=\"empty-header\"> <div class=\"empty-media empty-media-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12 20V10\"/><path d=\"M18 20V4\"/><path d=\"M6 20v-4\"/></svg></div> <p class=\"empty-title\">No activity this week</p> <p class=\"empty-description\">Activity from your projects shows up here.</p> </div> </div> </div>"
        },
        {
          "id": "card",
          "title": "In a card",
          "text": "Put the empty state in .card-content in place of the content that's missing. Without a variant it has no frame of its own, so it sits in the card's. html <div class=\"card w-full max-w-md\"> <div class=\"card-header\"> <h3 class=\"card-title\">Payment methods</h3> <p class=\"card-description\">Cards and accounts you pay with.</p> </div> <div class=\"card-content\"> <div class=\"empty empty-outline empty-sm\"> <div class=\"empty-header\"> <div class=\"empty-media empty-media-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"20\" height=\"14\" x=\"2\" y=\"5\" rx=\"2\"/><path d=\"M2 10h20\"/></svg></div> <p class=\"empty-title\">No payment methods</p> <p class=\"empty-description\">Add a card to pay for your subscription.</p> </div> <div class=\"empty-content\"> <button type=\"button\" class=\"btn btn-primary btn-sm\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5v14\"/></svg> Add a card</button> </div> </div> </div> </div>"
        },
        {
          "id": "table",
          "title": "In a table",
          "text": "For a table with no rows, keep the header (so the columns still explain the data) and put a compact empty state in a single cell spanning every column. html <div class=\"table-wrap\"> <table class=\"table\"> <thead><tr><th>Invoice</th><th>Status</th><th class=\"text-right\">Amount</th></tr></thead> <tbody> <tr> <td colspan=\"3\"> <div class=\"empty empty-sm\"> <div class=\"empty-header\"> <div class=\"empty-media empty-media-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg></div> <p class=\"empty-title\">No invoices found</p> <p class=\"empty-description\">No invoices match &ldquo;acme&rdquo;. Try another search, or clear the filters.</p> </div> <div class=\"empty-content\"> <button type=\"button\" class=\"btn btn-outline btn-sm\">Clear filters</button> </div> </div> </td> </tr> </tbody> </table> </div>"
        },
        {
          "id": "htmx",
          "title": "With htmx",
          "text": "The empty state is ordinary markup, so a request can replace it with content. Here the button loads rows from the server into the table body with hx-swap=\"innerHTML\", which replaces the empty row with them: html <div class=\"table-wrap\"> <table class=\"table\"> <thead><tr><th>Invoice</th><th>Status</th><th class=\"text-right\">Amount</th></tr></thead> <tbody id=\"empty-invoices\"> <tr> <td colspan=\"3\"> <div class=\"empty empty-sm\"> <div class=\"empty-header\"> <div class=\"empty-media empty-media-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4M10 9H8M16 13H8M16 17H8\"/></svg></div> <p class=\"empty-title\">No invoices yet</p> <p class=\"empty-description\">Invoices you send show up here.</p> </div> <div class=\"empty-content\"> <button type=\"button\" class=\"btn btn-primary btn-sm\" hx-get=\"/api/invoices\" hx-target=\"#empty-invoices\" hx-swap=\"innerHTML\">Load invoices</button> </div> </div> </td> </tr> </tbody> </table> </div> The other way round, have the server return an empty state when a search or a filter finds nothing, and the same swap shows it. An error can return one with a retry button that repeats the request and replaces the whole empty state: html <div class=\"empty empty-outline\" id=\"feed\"> <div class=\"empty-header\"> <div class=\"empty-media empty-media-icon\">…icon…</div> <h3 class=\"empty-title\">Couldn't load the feed</h3> <p class=\"empty-description\">Check your connection and try again.</p> </div> <div class=\"empty-content\"> <button class=\"btn btn-outline btn-sm\" hx-get=\"/feed\" hx-target=\"#feed\" hx-swap=\"outerHTML\">Retry</button> </div> </div>"
        },
        {
          "id": "macro",
          "title": "Macro",
          "text": "The empty() macro writes the header for you. Pass the title, and optionally a description, an icon name, a variant (\"outline\" or \"muted\"), a size (\"sm\"), the title element as heading (\"h3\" by default; use the level that fits the page, or \"p\") and extra classes. Call it with a body to fill .empty-content. html {% from \"components/empty/empty.html\" import empty %} {% call empty(\"No messages\", \"New messages show up here.\", icon=\"inbox\", variant=\"outline\") %} <button class=\"btn btn-primary btn-sm\">Compose</button> {% endcall %} html <div class=\"empty empty-outline\"> <div class=\"empty-header\"> <div class=\"empty-media empty-media-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><polyline points=\"22 12 16 12 14 15 10 15 8 12 2 12\"/><path d=\"M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z\"/></svg></div> <h3 class=\"empty-title\">No messages</h3> <p class=\"empty-description\">New messages show up here.</p> </div> <div class=\"empty-content\"> <button type=\"button\" class=\"btn btn-primary btn-sm\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"20\" height=\"16\" x=\"2\" y=\"4\" rx=\"2\"/><path d=\"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7\"/></svg> Compose</button> </div></div>"
        },
        {
          "id": "accessibility",
          "title": "Accessibility",
          "text": "Make the title a heading at the level that fits where the empty state sits (it stands in for a section's content), or a <p> in small panels and table cells. Media is decorative: the icon is hidden from screen readers, and avatars take aria-hidden=\"true\". Say everything that matters in the title and description. Write button labels that make sense on their own: “Create project”, not “Get started”. When an empty state replaces results that were there a moment ago (after a search), announce it: swap it into a container with aria-live=\"polite\"."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .empty Centred column with padding (more from md) that grows to fill its container. .empty-outline Dashed border. .empty-muted Muted background that fades out towards the bottom. .empty-sm Compact: less padding, smaller gaps, icon square and title. .empty-header Media, title and description; at most 24rem wide. .empty-media Holds an icon, an .avatar (shown at 3rem) or an .avatar-group (large, grey). .empty-media-icon A muted rounded square around an icon. .empty-title What is missing. .empty-description Muted text below the title. Links are underlined. .empty-content Actions and inputs: a centred column, at most 24rem wide."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/loader",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/loader",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/loader.md",
      "title": "Loader",
      "description": "Pure-CSS loading indicators in eight styles, from bouncing dots and waves to shimmering text, tuned with custom properties.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "shapes",
          "text": "Shapes"
        },
        {
          "level": 2,
          "id": "text",
          "text": "Text"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Sizes"
        },
        {
          "level": 2,
          "id": "customisation",
          "text": "Customisation"
        },
        {
          "level": 3,
          "id": "duration",
          "text": "Speed"
        },
        {
          "level": 3,
          "id": "colour",
          "text": "Colour"
        },
        {
          "level": 3,
          "id": "shimmer",
          "text": "Shimmer band"
        },
        {
          "level": 2,
          "id": "button",
          "text": "In a button"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx requests"
        },
        {
          "level": 2,
          "id": "macro",
          "text": "Macro"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"grid w-full grid-cols-2 gap-8 sm:grid-cols-4\"> <div class=\"flex flex-col items-center gap-3\"> <div class=\"flex h-8 items-center\"><span class=\"loader loader-pulse\" role=\"status\" aria-label=\"Loading\"></span></div> <span class=\"text-xs text-muted-foreground\">pulse</span> </div> <div class=\"flex flex-col items-center gap-3\"> <div class=\"flex h-8 items-center\"><span class=\"loader loader-pulse-dot\" role=\"status\" aria-label=\"Loading\"></span></div> <span class=\"text-xs text-muted-foreground\">pulse-dot</span> </div> <div class=\"flex flex-col items-center gap-3\"> <div class=\"flex h-8 items-center\"><span class=\"loader loader-dots\" role=\"status\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span class=\"sr-only\">Loading</span></span></div> <span class=\"text-xs text-muted-foreground\">dots</span> </div> <div class=\"flex flex-col items-center gap-3\"> <div class=\"flex h-8 items-center\"><span class=\"loader loader-typing\" role=\"status\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span class=\"sr-only\">Loading</span></span></div> <span class=\"text-xs text-muted-foreground\">typing</span> </div> <div class=\"flex flex-col items-center gap-3\"> <div class=\"flex h-8 items-center\"><span class=\"loader loader-wave\" role=\"status\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span class=\"sr-only\">Loading</span></span></div> <span class=\"text-xs text-muted-foreground\">wave</span> </div> <div class=\"flex flex-col items-center gap-3\"> <div class=\"flex h-8 items-center\"><span class=\"loader loader-text-blink\" role=\"status\">Loading</span></div> <span class=\"text-xs text-muted-foreground\">text-blink</span> </div> <div class=\"flex flex-col items-center gap-3\"> <div class=\"flex h-8 items-center\"><span class=\"loader loader-text-shimmer\" role=\"status\">Loading</span></div> <span"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "A loader is .loader plus one variant class. It needs no JavaScript: every variant is drawn and animated with CSS. Dots and bars are empty child <span>s (three for .loader-dots and .loader-typing, five for .loader-wave), each aria-hidden=\"true\". The pulse variants have no children: the element itself is the shape. Give the loader role=\"status\" and an accessible name: aria-label, or <span class=\"sr-only\"> text after the dots. The text variants show their own text, which is read out. The loader() macro writes all of this for you. html <span class=\"loader loader-dots\" role=\"status\"> <span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span> <span class=\"sr-only\">Loading</span> </span>"
        },
        {
          "id": "shapes",
          "title": "Shapes",
          "text": ".loader-pulse is a ring that breathes in and out; .loader-pulse-dot a single dot. .loader-dots bounces three dots, .loader-typing fades them in turn like a chat typing indicator, and .loader-wave raises and lowers a row of bars. html <span class=\"loader loader-pulse\" role=\"status\" aria-label=\"Loading\"></span> <span class=\"loader loader-pulse-dot\" role=\"status\" aria-label=\"Loading\"></span> <span class=\"loader loader-dots\" role=\"status\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span class=\"sr-only\">Loading</span></span> <span class=\"loader loader-typing\" role=\"status\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span class=\"sr-only\">Assistant is typing</span></span> <span class=\"loader loader-wave\" role=\"status\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span class=\"sr-only\">Loading</span></span>"
        },
        {
          "id": "text",
          "title": "Text",
          "text": "The text variants animate the words themselves, in the muted text colour at the surrounding font size. .loader-text-blink fades the text in and out, .loader-text-shimmer sweeps a light band across it, and .loader-loading-dots writes an ellipsis one dot at a time. html <span class=\"loader loader-text-blink\" role=\"status\">Fetching results</span> <span class=\"loader loader-text-shimmer\" role=\"status\">Thinking about your question</span> <span class=\"loader loader-loading-dots\" role=\"status\">Generating<span aria-hidden=\"true\">.</span><span aria-hidden=\"true\">.</span><span aria-hidden=\"true\">.</span></span>"
        },
        {
          "id": "sizes",
          "title": "Sizes",
          "text": ".loader-sm and .loader-lg make the shapes 12px and 24px tall instead of 16px, and set the text variants in text-xs and text-base. For any other size set --loader-size; dots and bars are sized from it. html <div class=\"flex items-center gap-8\"> <span class=\"loader loader-dots loader-sm\" role=\"status\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span class=\"sr-only\">Loading</span></span> <span class=\"loader loader-dots\" role=\"status\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span class=\"sr-only\">Loading</span></span> <span class=\"loader loader-dots loader-lg\" role=\"status\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span class=\"sr-only\">Loading</span></span> <span class=\"loader loader-dots [--loader-size:2.5rem]\" role=\"status\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span class=\"sr-only\">Loading</span></span> </div> <div class=\"flex items-center gap-8\"> <span class=\"loader loader-wave loader-sm\" role=\"status\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span class=\"sr-only\">Loading</span></span> <span class=\"loader loader-wave\" role=\"status\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span class=\"sr-only\">Loading</span></span> <span class=\"loader loader-wave loader-lg\" role=\"status\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span class=\"sr-only\">Loading</span></span> <span class=\"loader loader-wave [--loader-size:2.5rem]\" role=\"status\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hi"
        },
        {
          "id": "customisation",
          "title": "Customisation",
          "text": "Every variant reads its look from custom properties on the .loader. Set them in a style attribute, or with Tailwind's arbitrary properties such as [--loader-duration:2s]. --loader-size: the height of the shapes (1rem). --loader-duration: one cycle of the animation (1.2s; the text variants 2s, loading dots 1.4s). The dots' and bars' stagger follows it. --loader-color: the colour. Shapes use the current text colour, so text-* utilities work too; the text variants default to muted-foreground. --loader-highlight and --loader-spread: the colour of the shimmer's light band (foreground) and its half-width, as a share of the sweep (20%)."
        },
        {
          "id": "duration",
          "title": "Speed",
          "text": "html <span class=\"loader loader-wave\" role=\"status\" style=\"--loader-duration: 0.6s\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span class=\"sr-only\">Loading</span></span> <span class=\"loader loader-wave\" role=\"status\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span class=\"sr-only\">Loading</span></span> <span class=\"loader loader-wave\" role=\"status\" style=\"--loader-duration: 2.4s\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span class=\"sr-only\">Loading</span></span>"
        },
        {
          "id": "colour",
          "title": "Colour",
          "text": "html <span class=\"loader loader-pulse text-primary\" role=\"status\" aria-label=\"Loading\"></span> <span class=\"loader loader-pulse-dot text-success\" role=\"status\" aria-label=\"Loading\"></span> <span class=\"loader loader-dots text-warning\" role=\"status\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span class=\"sr-only\">Loading</span></span> <span class=\"loader loader-typing text-muted-foreground\" role=\"status\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span class=\"sr-only\">Loading</span></span> <span class=\"loader loader-wave text-danger\" role=\"status\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span class=\"sr-only\">Loading</span></span> <span class=\"loader loader-text-blink [--loader-color:var(--primary)]\" role=\"status\">Saving</span>"
        },
        {
          "id": "shimmer",
          "title": "Shimmer band",
          "text": "A wider --loader-spread softens the band; a contrasting --loader-highlight tints it. html <span class=\"loader loader-text-shimmer\" role=\"status\" style=\"--loader-spread: 8%\">A narrow, sharp band</span> <span class=\"loader loader-text-shimmer\" role=\"status\" style=\"--loader-spread: 40%\">A wide, soft band</span> <span class=\"loader loader-text-shimmer\" role=\"status\" style=\"--loader-highlight: var(--primary)\">Tinted with the primary colour</span> With prefers-reduced-motion set, nothing moves: every variant fades its opacity slowly instead, so the loader still reads as busy."
        },
        {
          "id": "button",
          "title": "In a button",
          "text": "Shapes sit in a .btn like an icon. When the button text already says what's happening, pass label=none so the loader is hidden from screen readers. html <button class=\"btn btn-primary\" aria-busy=\"true\"><span class=\"loader loader-dots loader-sm\" aria-hidden=\"true\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span></span> Sending</button> <button class=\"btn btn-outline\" disabled><span class=\"loader loader-wave loader-sm\" aria-hidden=\"true\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span></span> Processing</button> <button class=\"btn btn-secondary\" aria-busy=\"true\"><span class=\"loader loader-pulse-dot loader-sm\" aria-hidden=\"true\"></span> Recording</button>"
        },
        {
          "id": "htmx",
          "title": "With htmx requests",
          "text": "Add .htmx-indicator to show a loader only while a request runs. Inside a button it takes no space until then, and .htmx-indicator-hide swaps out an icon for it, as with the spinner. html <div class=\"flex flex-wrap items-center gap-3\"> <button class=\"btn btn-primary\" hx-post=\"/api/slow\" hx-target=\"#loader-result\" hx-disable=\"this\"> <span class=\"loader loader-dots loader-sm htmx-indicator\" aria-hidden=\"true\"><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span></span> Save </button> <button class=\"btn btn-outline\" hx-post=\"/api/slow\" hx-target=\"#loader-result\" hx-disable=\"this\" hx-indicator=\"#loader-status\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\"/><path d=\"M21 3v5h-5\"/><path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\"/><path d=\"M8 16H3v5\"/></svg> Refresh </button> <span class=\"loader loader-loading-dots htmx-indicator\" role=\"status\" id=\"loader-status\">Refreshing<span aria-hidden=\"true\">.</span><span aria-hidden=\"true\">.</span><span aria-hidden=\"true\">.</span></span> </div> <p id=\"loader-result\" class=\"muted\" aria-live=\"polite\"></p> The /api/slow endpoint is a mock that only exists under bun run dev."
        },
        {
          "id": "macro",
          "title": "Macro",
          "text": "loader() writes the variant's children, role=\"status\" and the accessible name. Shapes are read out as label; text variants show text (or the label) and are read as it. jinja {% from \"components/loader/loader.html\" import loader %} {{ loader() }} {# three bouncing dots, \"Loading\" #} {{ loader(\"wave\", size=\"lg\", label=\"Uploading\") }} {{ loader(\"text-shimmer\", text=\"Thinking\") }} {{ loader(\"pulse\", label=none) }} {# decorative #} {{ loader(\"typing\", class=\"htmx-indicator\", attrs={\"id\": \"typing\"}) }}"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .loader The base: an inline flex box that holds the custom properties below. .loader-pulse A ring breathing in and out. .loader-pulse-dot A single pulsing dot. .loader-dots Three bouncing dots (three child spans). .loader-typing Three dots fading in turn, a typing indicator (three child spans). .loader-wave Bars rising and falling (five child spans). .loader-text-blink Text fading in and out. .loader-text-shimmer Text with a light band sweeping across it. .loader-loading-dots Text followed by \"...\" written one dot at a time (three child spans). .loader-sm / .loader-lg 12px or 24px shapes; text-xs or text-base text. --loader-size The height of the shapes (1rem). --loader-duration One animation cycle (1.2s; text 2s, loading dots 1.4s). --loader-color The colour (currentColor; text variants muted-foreground). --loader-highlight / --loader-spread The shimmer band's colour (foreground) and half-width (20%). Macro Description loader(variant=\"dots\", text=none, size=none, label=\"Loading\", class=\"\", attrs=none) Writes a loader. label=none hides a shape from assistive technology; text sets a text variant's words."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/progress",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/progress",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/progress.md",
      "title": "Progress",
      "description": "Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "label-and-value",
          "text": "Label and value"
        },
        {
          "level": 2,
          "id": "indeterminate",
          "text": "Indeterminate"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Sizes"
        },
        {
          "level": 2,
          "id": "colours",
          "text": "Colours"
        },
        {
          "level": 2,
          "id": "macro",
          "text": "Macro"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx"
        },
        {
          "level": 2,
          "id": "accessibility",
          "text": "Accessibility"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <progress class=\"progress w-3/5\" value=\"33\" max=\"100\" aria-label=\"Upload progress\">33%</progress>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "A native <progress class=\"progress\"> with value and max (1 if left out). The browser exposes it as a progress bar with its percentage; no ARIA attributes are needed. Give it an accessible name: a <label for> (see Label and value), or aria-label. The text inside is a fallback for browsers that don't draw progress bars. It fills its container's width; set another with a width utility (w-3/5 above)."
        },
        {
          "id": "label-and-value",
          "title": "Label and value",
          "text": ".progress-field lays out a .progress-label and a .progress-value above the bar, and a .progress-description below it. Each part is optional. The value repeats what the bar already announces, so it is aria-hidden; the description is linked with aria-describedby. html <div class=\"progress-field\"> <label class=\"progress-label\" for=\"upload-files\">Uploading files</label> <span class=\"progress-value\" aria-hidden=\"true\">66%</span> <progress class=\"progress\" id=\"upload-files\" value=\"66\" max=\"100\" aria-describedby=\"upload-files-help\">66%</progress> <p class=\"progress-description\" id=\"upload-files-help\">6 of 9 files, about a minute left.</p> </div> <div class=\"progress-field\"> <label class=\"progress-label\" for=\"storage-used\">Storage</label> <span class=\"progress-value\" aria-hidden=\"true\">7.2 of 10 GB</span> <progress class=\"progress\" id=\"storage-used\" value=\"7.2\" max=\"10\">72%</progress> </div>"
        },
        {
          "id": "indeterminate",
          "title": "Indeterminate",
          "text": "Leave out value when the amount of work isn't known yet: a bar slides along the track. With reduced motion, the track is striped instead, still and full width, so it can't be read as a value. html <progress class=\"progress\" aria-label=\"Loading\"></progress> <div class=\"progress-field\"> <label class=\"progress-label\" for=\"preparing-export\">Preparing export…</label> <progress class=\"progress\" id=\"preparing-export\"></progress> </div>"
        },
        {
          "id": "sizes",
          "title": "Sizes",
          "text": ".progress-sm is 4px high, the default 8px and .progress-lg 12px. html <progress class=\"progress progress-sm\" value=\"40\" max=\"100\" aria-label=\"Small\">40%</progress> <progress class=\"progress\" value=\"60\" max=\"100\" aria-label=\"Default\">60%</progress> <progress class=\"progress progress-lg\" value=\"80\" max=\"100\" aria-label=\"Large\">80%</progress>"
        },
        {
          "id": "colours",
          "title": "Colours",
          "text": ".progress-success, .progress-warning and .progress-danger use the status colours; the track is the same colour, faded. For any other colour, set --progress-tone. Colour alone doesn't say what is wrong, so put that in the label or description too. html <div class=\"progress-field\"> <label class=\"progress-label\" for=\"tests-passed\">Tests passed</label> <span class=\"progress-value\" aria-hidden=\"true\">100%</span> <progress class=\"progress progress-success\" id=\"tests-passed\" value=\"100\" max=\"100\">100%</progress> </div> <div class=\"progress-field\"> <label class=\"progress-label\" for=\"quota-used\">Monthly quota</label> <span class=\"progress-value\" aria-hidden=\"true\">85%</span> <progress class=\"progress progress-warning\" id=\"quota-used\" value=\"85\" max=\"100\" aria-describedby=\"quota-used-help\">85%</progress> <p class=\"progress-description\" id=\"quota-used-help\">Nearly used up. It resets on the 1st.</p> </div> <div class=\"progress-field\"> <label class=\"progress-label\" for=\"disk-full\">Disk</label> <span class=\"progress-value\" aria-hidden=\"true\">97%</span> <progress class=\"progress progress-danger\" id=\"disk-full\" value=\"97\" max=\"100\" aria-describedby=\"disk-full-help\">97%</progress> <p class=\"progress-description\" id=\"disk-full-help\">Almost full: delete old backups to free space.</p> </div> <progress class=\"progress [--progress-tone:var(--color-violet-600)]\" value=\"50\" max=\"100\" aria-label=\"Custom colour\">50%</progress>"
        },
        {
          "id": "macro",
          "title": "Macro",
          "text": "progress() writes the bar, and the field around it when there is a label, value or description, with the ids wired up and the percentage worked out (rounded, and kept within 0–100%). jinja {% from \"components/progress/progress.html\" import progress %} {{ progress(3, max=9, label=\"Uploading files\", show_value=true, description=\"3 of 9 files\") }} {{ progress(aria_label=\"Loading\") }} renders: html <div class=\"progress-field\"> <label class=\"progress-label\" for=\"progress-uploading-files\">Uploading files</label> <span class=\"progress-value\" aria-hidden=\"true\">33%</span> <progress class=\"progress\" id=\"progress-uploading-files\" value=\"3\" max=\"9\" aria-describedby=\"progress-uploading-files-description\">33%</progress> <p class=\"progress-description\" id=\"progress-uploading-files-description\">3 of 9 files</p> </div> <progress class=\"progress\" max=\"100\" aria-label=\"Loading\"></progress> Parameter Description value The current value. Left out (or none): indeterminate. max=100 The value at completion. label A visible <label>. Without one, pass aria_label. aria_label The accessible name when there is no visible label. show_value=false true shows the percentage beside the label (aria-hidden). description Help text under the bar, linked with aria-describedby. id The <progress> id. Defaults to one made from the label (progress-uploading-files); pass it when two bars on a page share a label. size \"sm\" or \"lg\". variant \"success\", \"warning\" or \"danger\". attrs More attributes for the outer element, as a dict, e.g. {\"hx-get\": \"/jobs/42/progress\"}. class More classes on the outer element: the .progress-field, or the <progress> when there is no field."
        },
        {
          "id": "htmx",
          "title": "With htmx",
          "text": "For a job running on the server, let the progress fragment fetch its own replacement. Each response is the field at its new value, polling again after a short delay; the last one leaves the polling attributes out, which stops it. Start it with a button whose hx-post starts the job and returns the first fragment. html <button class=\"btn btn-primary\" hx-post=\"/exports\" hx-target=\"#export\" hx-swap=\"innerHTML\">Export</button> <div id=\"export\"></div> <!-- each response from /exports/42/progress while the job runs <!-- each response from /exports/42/progress while the job runs --> <div class=\"progress-field\" hx-get=\"/exports/42/progress\" hx-trigger=\"load delay:1s\" hx-swap=\"outerHTML\"> <label class=\"progress-label\" for=\"export-progress\">Exporting invoices</label> <span class=\"progress-value\" aria-hidden=\"true\">40%</span> <progress class=\"progress\" id=\"export-progress\" value=\"40\" max=\"100\">40%</progress> </div> <!-- the last one: no hx-trigger, so polling stops <!-- the last one: no hx-trigger, so polling stops --> <div class=\"progress-field\"> <label class=\"progress-label\" for=\"export-progress\">Exporting invoices</label> <span class=\"progress-value\" aria-hidden=\"true\">100%</span> <progress class=\"progress progress-success\" id=\"export-progress\" value=\"100\" max=\"100\">100%</progress> <p class=\"progress-description\" role=\"status\">Done. <a href=\"/exports/42.csv\">Download the CSV</a></p> </div> The bar's width animates from the old value to the new one only when the same element is updated. Swapping in a new <progress> jumps to the new width, which is fine for polling. The Attachment component uses the same polling pattern for uploads."
        },
        {
          "id": "accessibility",
          "title": "Accessibility",
          "text": "<progress> has the progressbar role and reports its value as a percentage, or as busy when indeterminate. Screen readers don't announce every change of value. Announce the end of a task (or a failure) in a role=\"status\" element, as in the htmx example. When the value is more meaningful as text (\"Step 2 of 5\"), set aria-valuetext=\"Step 2 of 5\" on the <progress>. An indeterminate bar standing in for content that is loading needs a name saying what is loading (\"Loading invoices\"), not just \"Loading\"."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .progress On a <progress>: a rounded track; the bar is the value. No value: indeterminate. .progress-sm / .progress-lg 4px / 12px high (8px by default). .progress-success / -warning / -danger Status colours for the bar and track. --progress-tone The bar colour (the track is it at 20%). Defaults to --primary. .progress-field Grid laying out a label and value above the bar and a description below. .progress-label The <label for> naming the bar, on the left. .progress-value The value as text, on the right; aria-hidden when it repeats the percentage. .progress-description Help text under the bar, linked with aria-describedby."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/skeleton",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/skeleton",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/skeleton.md",
      "title": "Skeleton",
      "description": "Placeholders in the shape of content that is still loading: any shape from utilities, ready-made avatars, text, cards, lists, tables and forms.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "shapes",
          "text": "Shapes"
        },
        {
          "level": 2,
          "id": "text",
          "text": "Text"
        },
        {
          "level": 2,
          "id": "animation",
          "text": "Animation"
        },
        {
          "level": 2,
          "id": "layouts",
          "text": "Avatar, card, list, table and form"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"flex items-center gap-4\" role=\"status\" aria-busy=\"true\"> <span class=\"sr-only\">Loading…</span> <div class=\"skeleton skeleton-circle size-12\" aria-hidden=\"true\"></div> <div class=\"flex flex-col gap-2\" aria-hidden=\"true\"> <div class=\"skeleton h-4 w-56\"></div> <div class=\"skeleton h-4 w-40\"></div> </div> </div>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": ".skeleton is a muted, pulsing block. Its shape is yours: size it with utilities (h-4 w-48, size-12, aspect-video), round it with .skeleton-circle or any rounded-*. Mark the loading region aria-busy=\"true\" with a text alternative (<span class=\"sr-only\">Loading…</span>, or role=\"status\" and a label), and the shapes aria-hidden=\"true\": they mean nothing read aloud. No JavaScript: the skeleton is whatever stands in the page until htmx swaps the real content over it."
        },
        {
          "id": "shapes",
          "title": "Shapes",
          "text": "Ready-made shapes take the size of the thing they stand for: .skeleton-avatar (as .avatar-lg), .skeleton-button and .skeleton-input (as .btn and .input), .skeleton-badge, .skeleton-image (16:9). Utilities still resize them. html <div class=\"skeleton-avatar\" aria-hidden=\"true\"></div> <div class=\"skeleton-avatar size-8\" aria-hidden=\"true\"></div> <div class=\"skeleton-badge\" aria-hidden=\"true\"></div> <div class=\"skeleton-button\" aria-hidden=\"true\"></div> <div class=\"skeleton-input w-48\" aria-hidden=\"true\"></div> <div class=\"skeleton size-16 rounded-xl\" aria-hidden=\"true\"></div> <div class=\"skeleton-image w-40\" aria-hidden=\"true\"></div>"
        },
        {
          "id": "text",
          "title": "Text",
          "text": ".skeleton-text is one line, 1em high, so it follows the font size it sits in; .skeleton-heading is a heading-sized line at half width. Stack lines in .skeleton-lines and the last one comes out shorter, like the end of a paragraph. html <div class=\"text-2xl\" aria-hidden=\"true\"><div class=\"skeleton-heading\"></div></div> <div class=\"skeleton-lines text-sm\" aria-hidden=\"true\"> <div class=\"skeleton-text\"></div> <div class=\"skeleton-text\"></div> <div class=\"skeleton-text\"></div> </div>"
        },
        {
          "id": "animation",
          "title": "Animation",
          "text": "Shapes pulse. .skeleton-shimmer, on a shape or on any ancestor, sweeps a highlight across instead; .skeleton-static stops the motion. Both stop for readers who prefer reduced motion. html <div class=\"skeleton-shimmer flex items-center gap-4\" aria-hidden=\"true\"> <div class=\"skeleton-avatar\"></div> <div class=\"skeleton-lines text-sm\"><div class=\"skeleton-text\"></div><div class=\"skeleton-text\"></div></div> </div> <div class=\"skeleton-static flex items-center gap-4\" aria-hidden=\"true\"> <div class=\"skeleton-avatar\"></div> <div class=\"skeleton-lines text-sm\"><div class=\"skeleton-text\"></div><div class=\"skeleton-text\"></div></div> </div>"
        },
        {
          "id": "layouts",
          "title": "Avatar, card, list, table and form",
          "text": "Macros in components/skeleton/skeleton.html build common placeholders from these shapes, each as a loading region (role=\"status\", aria-busy, a hidden \"Loading…\"). They take label, shimmer and class. jinja {% from \"components/skeleton/skeleton.html\" import skeleton_text, skeleton_avatar, skeleton_card, skeleton_list, skeleton_table, skeleton_form %} {{ skeleton_avatar(lines=2) }} {{ skeleton_card(media=true, lines=2, footer=true, shimmer=true) }} {{ skeleton_list(items=3, avatar=true) }} {{ skeleton_table(rows=4, columns=4) }} {{ skeleton_form(fields=2) }} {{ skeleton_text(lines=3, label=\"Loading the article…\") }}"
        },
        {
          "id": "htmx",
          "title": "With htmx",
          "text": "Put the skeleton where the content will go and let htmx replace it: hx-trigger=\"load\" fetches straight away (revealed when it scrolls into view) and hx-swap=\"outerHTML\" swaps the placeholder for the response. html <div class=\"w-full max-w-sm\" hx-get=\"/api/skeleton\" hx-trigger=\"load\" hx-swap=\"outerHTML\"> <div class=\"card w-full overflow-hidden\" role=\"status\" aria-busy=\"true\"> <span class=\"sr-only\">Loading…</span> <div class=\"card-header\" aria-hidden=\"true\"> <div class=\"skeleton-heading w-2/3 text-base\"></div> </div> <div class=\"card-content text-sm\" aria-hidden=\"true\"> <div class=\"skeleton-lines\"><div class=\"skeleton-text\"></div></div> </div> <div class=\"card-footer gap-2\" aria-hidden=\"true\"> <div class=\"skeleton-button\"></div> <div class=\"skeleton-button w-20\"></div> </div> </div> </div> For a reload of content already on the page, show a skeleton as the request's indicator: hx-indicator points at it, and the .htmx-indicator class keeps it hidden until a request is in flight."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .skeleton A muted block that pulses; shape it with utilities. .skeleton-circle Fully rounded. .skeleton-text / .skeleton-heading A line of text (1em high), a heading line (1.5em, half width). .skeleton-lines A paragraph of .skeleton-text; the last line is shorter. .skeleton-avatar / -button / -input / -badge / -image Shapes sized like those components. .skeleton-shimmer On a shape or an ancestor: a sweeping highlight instead of the pulse. .skeleton-static No animation. skeleton_text() … skeleton_form() Macros: placeholders for text, avatars, cards, lists, tables and forms."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/spinner",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/spinner",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/spinner.md",
      "title": "Spinner",
      "description": "An indicator that shows a loading state. Any icon can spin, or use a pure-CSS ring when there is no icon to hand.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "usage",
          "text": "Usage"
        },
        {
          "level": 2,
          "id": "icon",
          "text": "Custom icon"
        },
        {
          "level": 2,
          "id": "css-ring",
          "text": "Without an icon"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Sizes"
        },
        {
          "level": 2,
          "id": "colour",
          "text": "Colour"
        },
        {
          "level": 2,
          "id": "button",
          "text": "In a button"
        },
        {
          "level": 2,
          "id": "badge",
          "text": "In a badge"
        },
        {
          "level": 2,
          "id": "button-group",
          "text": "In a button group"
        },
        {
          "level": 2,
          "id": "input-group",
          "text": "In an input group"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx requests"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner\" role=\"status\" aria-label=\"Loading\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg>"
        },
        {
          "id": "usage",
          "title": "Usage",
          "text": "Add .spinner to an SVG icon and it spins. The spinner() macro inlines an icon from icons/ with the class and accessibility attributes set: role=\"status\" and an aria-label of \"Loading\". html <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner\" role=\"status\" aria-label=\"Saving\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg>"
        },
        {
          "id": "icon",
          "title": "Custom icon",
          "text": "Pass any icon name to the macro, or put .spinner on your own SVG. html <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner\" role=\"status\" aria-label=\"Loading\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner\" role=\"status\" aria-label=\"Loading\"><path d=\"M12 2v4M16.2 7.8l2.9-2.9M18 12h4M16.2 16.2l2.9 2.9M12 18v4M4.9 19.1l2.9-2.9M2 12h4M4.9 4.9l2.9 2.9\"/></svg> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner\" role=\"status\" aria-label=\"Loading\"><path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\"/><path d=\"M21 3v5h-5\"/><path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\"/><path d=\"M8 16H3v5\"/></svg>"
        },
        {
          "id": "css-ring",
          "title": "Without an icon",
          "text": "On any element other than an SVG, .spinner draws a ring with CSS. Use it in markup that can't inline icons, such as HTML fragments built by your server. html <span class=\"spinner\" role=\"status\" aria-label=\"Loading\"></span> <span class=\"spinner spinner-lg text-primary\" role=\"status\" aria-label=\"Loading\"></span>"
        },
        {
          "id": "sizes",
          "title": "Sizes",
          "text": "Use .spinner-sm (12px), the default (16px), .spinner-lg (24px) and .spinner-xl (32px), or any size-* utility. html <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner spinner-sm\" role=\"status\" aria-label=\"Loading\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner\" role=\"status\" aria-label=\"Loading\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner spinner-lg\" role=\"status\" aria-label=\"Loading\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner spinner-xl\" role=\"status\" aria-label=\"Loading\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner size-12\" role=\"status\" aria-label=\"Loading\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg>"
        },
        {
          "id": "colour",
          "title": "Colour",
          "text": "Spinners use the current text colour, so text-* utilities recolour them. html <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner spinner-lg text-primary\" role=\"status\" aria-label=\"Loading\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner spinner-lg text-success\" role=\"status\" aria-label=\"Loading\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner spinner-lg text-warning\" role=\"status\" aria-label=\"Loading\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner spinner-lg text-muted-foreground\" role=\"status\" aria-label=\"Loading\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg>"
        },
        {
          "id": "button",
          "title": "In a button",
          "text": "Inside a .btn, the spinner takes the button's icon size. When the button text already says what's happening, pass label=none so the spinner is hidden from screen readers. aria-busy=\"true\" makes the button ignore clicks without fading it like disabled does. html <button class=\"btn btn-primary\" aria-busy=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner\" aria-hidden=\"true\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg> Saving</button> <button class=\"btn btn-outline\" disabled><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner\" aria-hidden=\"true\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg> Please wait</button> <button class=\"btn btn-secondary btn-sm\" disabled><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner\" aria-hidden=\"true\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg> Loading</button> <button class=\"btn btn-outline btn-icon\" aria-busy=\"true\" aria-label=\"Refresh\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner\" aria-hidden=\"true\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg></button>"
        },
        {
          "id": "badge",
          "title": "In a badge",
          "text": "html <span class=\"badge badge-secondary\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner\" aria-hidden=\"true\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg> Syncing</span> <span class=\"badge badge-outline\"><span class=\"spinner\" aria-hidden=\"true\"></span> Deploying</span> <span class=\"badge badge-info\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner\" aria-hidden=\"true\"><path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\"/><path d=\"M21 3v5h-5\"/><path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\"/><path d=\"M8 16H3v5\"/></svg> Updating</span>"
        },
        {
          "id": "button-group",
          "title": "In a button group",
          "text": "html <div class=\"btn-group\" role=\"group\" aria-label=\"Sync\"> <button class=\"btn btn-outline\" aria-busy=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner\" aria-hidden=\"true\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg> Syncing</button> <button class=\"btn btn-outline btn-icon\" aria-label=\"Cancel\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> </div>"
        },
        {
          "id": "input-group",
          "title": "In an input group",
          "text": "html <div class=\"input-group max-w-sm\"> <input class=\"input\" type=\"search\" value=\"htmx\" aria-label=\"Search\" /> <span class=\"input-group-addon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg></span> <span class=\"input-group-addon input-group-addon-inline-end\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner\" role=\"status\" aria-label=\"Loading\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg></span> </div>"
        },
        {
          "id": "htmx",
          "title": "With htmx requests",
          "text": "Add .htmx-indicator to show a spinner only while a request runs. Inside a button, badge or input-group addon it takes no space until then. Add .htmx-indicator-hide to an icon to swap it for the spinner. html <div class=\"flex flex-wrap items-center gap-3\"> <button class=\"btn btn-primary\" hx-post=\"/api/slow\" hx-target=\"#spinner-result\" hx-disable=\"this\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner htmx-indicator\" aria-hidden=\"true\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg> Save </button> <button class=\"btn btn-outline\" hx-post=\"/api/slow\" hx-target=\"#spinner-result\" hx-disable=\"this\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"htmx-indicator-hide\" aria-hidden=\"true\"><path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\"/><path d=\"M21 3v5h-5\"/><path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\"/><path d=\"M8 16H3v5\"/></svg><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"spinner htmx-indicator\" aria-hidden=\"true\"><path d=\"M21 12a9 9 0 1 1-6.22-8.56\"/></svg> Refresh </button> </div> <p id=\"spinner-result\" class=\"muted\" aria-live=\"polite\"></p> The /api/slow endpoint is a mock that only exists under bun run dev."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .spinner Spins an SVG icon. On any other element, draws a CSS ring. .spinner-sm / -lg / -xl 12px, 24px or 32px instead of 16px. .htmx-indicator-hide Hidden while the htmx request it's inside runs. Macro Description spinner(icon=\"loader\", class=\"\", label=\"Loading\", id=none) Inlines icons/<icon>.svg as a spinner. label=none hides it from assistive technology."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/toast",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/toast",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/toast.md",
      "title": "Toast",
      "description": "A short message that stacks in a corner and leaves on its own: success, info, warning, error and loading types, actions, promises, swipe to dismiss, and toasts sent by the server.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "how-it-works",
          "text": "How it works"
        },
        {
          "level": 2,
          "id": "types",
          "text": "Types"
        },
        {
          "level": 2,
          "id": "action",
          "text": "Action and cancel"
        },
        {
          "level": 2,
          "id": "script",
          "text": "From script"
        },
        {
          "level": 2,
          "id": "htmx-request",
          "text": "An htmx request"
        },
        {
          "level": 2,
          "id": "server",
          "text": "From the server"
        },
        {
          "level": 2,
          "id": "position",
          "text": "Position and the toaster"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <button type=\"button\" class=\"btn btn-outline\" data-toast=\"Event has been created\" data-toast-description=\"Sunday, December 3 at 9:00 AM\" data-toast-action=\"Undo\">Show toast</button>"
        },
        {
          "id": "how-it-works",
          "title": "How it works",
          "text": "Toasts stack in a corner, newest in front, the next two peeking out behind it. Hover or focus the stack to fan it out; that also pauses their timers (as does switching browser tab). Each slides in from its edge and leaves after 4 seconds, or when closed with its × (on hover), swiped towards the edge, or Esc while focused. Alt+T moves focus to the newest one. The toasts sit in a live region, so screen readers read them out; errors are role=\"alert\". Show one from script with toast(), from markup with data-toast, or from the server with an HX-Trigger header. No markup is needed in the page: the toaster is created with the first toast."
        },
        {
          "id": "types",
          "title": "Types",
          "text": "data-toast-type (or toast.success() and friends) sets the icon and its colour: success, info, warning, error and loading, which spins and stays until it's updated or dismissed. html <button type=\"button\" class=\"btn btn-outline\" data-toast=\"Event has been created\">Default</button> <button type=\"button\" class=\"btn btn-outline\" data-toast=\"Changes saved\" data-toast-type=\"success\">Success</button> <button type=\"button\" class=\"btn btn-outline\" data-toast=\"Be at the venue by 8:45\" data-toast-type=\"info\">Info</button> <button type=\"button\" class=\"btn btn-outline\" data-toast=\"Your trial ends in 3 days\" data-toast-type=\"warning\">Warning</button> <button type=\"button\" class=\"btn btn-outline\" data-toast=\"Could not reach the server\" data-toast-type=\"error\" data-toast-description=\"Check your connection and try again.\">Error</button> <button type=\"button\" class=\"btn btn-outline\" data-toast=\"Uploading report.pdf…\" data-toast-type=\"loading\" data-toast-duration=\"3000\">Loading</button>"
        },
        {
          "id": "action",
          "title": "Action and cancel",
          "text": "data-toast-action=\"Undo\" adds a button that dismisses the toast and fires toast:action on the element that showed it, so hx-trigger=\"toast:action\" can send a request; data-toast-cancel adds a quieter one that fires toast:cancel. From script, pass action / cancel with an onClick (call event.preventDefault() to keep the toast open). html <button type=\"button\" class=\"btn btn-outline\" data-toast=\"Message archived\" data-toast-action=\"Undo\" data-toast-duration=\"8000\" hx-post=\"/api/echo\" hx-vals='{\"undo\": \"archive\"}' hx-trigger=\"toast:action\" hx-target=\"#toast-undo-result\">Archive</button> <button type=\"button\" class=\"btn btn-outline\" data-toast=\"Delete 3 files?\" data-toast-type=\"warning\" data-toast-cancel=\"Keep\" data-toast-action=\"Delete\" data-toast-duration=\"infinite\">Delete files</button> <p class=\"text-sm text-muted-foreground\" id=\"toast-undo-result\">Archive, then Undo: the request goes out from the toast.</p>"
        },
        {
          "id": "script",
          "title": "From script",
          "text": "toast(message, options) returns the toast's id. Showing another toast with the same id updates that one in place. app.ts import { toast } from \"htmx-ui\"; toast(\"Event has been created\", { description: \"Sunday, December 3 at 9:00 AM\", action: { label: \"Undo\", onClick: () => undo() }, }); toast.success(\"Saved\"); // also .info, .warning, .error, .loading const id = toast.loading(\"Uploading…\"); toast.success(\"Uploaded\", { id }); // replaces the loading toast toast.dismiss(id); // or toast.dismiss() for all // Loading, then success or error when the promise settles toast.promise(fetch(\"/api/save\", { method: \"POST\" }), { loading: \"Saving…\", success: (res) => `Saved (${res.status})`, error: \"Could not save\", }); Option Description type \"default\" | \"success\" | \"info\" | \"warning\" | \"error\" | \"loading\". description A second, muted line. duration Milliseconds before it leaves (default 4000; loading: Infinity). Infinity keeps it. action / cancel { label, onClick(event) }: buttons that dismiss the toast unless onClick calls preventDefault(). id Update the toast with this id instead of adding one. position top-left, top-center, top-right, bottom-left, bottom-center or bottom-right. dismissible false: no close button, swipe or Escape. closeButton / icon false hides the close button / the type's icon. onDismiss / onAutoClose Called with the id when the reader dismisses it / its timer ends. class More classes on the toast."
        },
        {
          "id": "htmx-request",
          "title": "An htmx request",
          "text": "data-toast-loading on the element that makes a request shows a loading toast while it runs, which becomes data-toast-success or data-toast-error (any status from 400) when it finishes. Leave one out and the toast just goes. html <button type=\"button\" class=\"btn btn-outline\" hx-post=\"/api/toast/save\" hx-swap=\"none\" data-toast-loading=\"Saving changes…\" data-toast-success=\"Changes saved\" data-toast-error=\"Could not save\">Save</button> <button type=\"button\" class=\"btn btn-outline\" hx-post=\"/api/toast/save?fail\" hx-swap=\"none\" data-toast-loading=\"Saving changes…\" data-toast-success=\"Changes saved\" data-toast-error=\"Could not save\" data-toast-description=\"The server answered 500.\">Save (fails)</button>"
        },
        {
          "id": "server",
          "title": "From the server",
          "text": "Send a toast event in the HX-Trigger response header, with the options as its value (a string is taken as the message): http HX-Trigger: {\"toast\": {\"message\": \"Profile updated\", \"type\": \"success\", \"description\": \"Saved at 09:41.\"}} html <button type=\"button\" class=\"btn btn-outline\" hx-post=\"/api/toast\" hx-swap=\"none\">Update profile</button> htmx fires the event on the element that made the request. If the response swaps that element out, it never reaches the page, so add \"target\": \"body\" to the value. Or return an element instead: <div data-toast-show=\"Saved\" data-toast-type=\"success\" hidden></div> shows its toast as it's swapped in and removes itself, which suits an hx-swap-oob fragment."
        },
        {
          "id": "position",
          "title": "Position and the toaster",
          "text": "Place a <section data-toaster> anywhere in your layout to change the defaults: data-position, data-duration, data-visible (toasts shown in the stack, 3), data-expand (always fanned out), data-close-button=\"false\" and data-rich-colors (tint the whole toast by its type). A toast's own position (data-toast-position) overrides the toaster's. html <button type=\"button\" class=\"btn btn-outline btn-sm\" data-toast=\"Toast at top-left\" data-toast-position=\"top-left\">top-left</button> <button type=\"button\" class=\"btn btn-outline btn-sm\" data-toast=\"Toast at top-center\" data-toast-position=\"top-center\">top-center</button> <button type=\"button\" class=\"btn btn-outline btn-sm\" data-toast=\"Toast at top-right\" data-toast-position=\"top-right\">top-right</button> <button type=\"button\" class=\"btn btn-outline btn-sm\" data-toast=\"Toast at bottom-left\" data-toast-position=\"bottom-left\">bottom-left</button> <button type=\"button\" class=\"btn btn-outline btn-sm\" data-toast=\"Toast at bottom-center\" data-toast-position=\"bottom-center\">bottom-center</button> <button type=\"button\" class=\"btn btn-outline btn-sm\" data-toast=\"Toast at bottom-right\" data-toast-position=\"bottom-right\">bottom-right</button> layouts/base.html <section data-toaster data-position=\"top-center\" data-rich-colors data-visible=\"4\"></section> Width, spacing and distance from the edge are CSS variables on .toaster-list: --toast-width (356px), --toast-gap (14px) and --toast-inset (1.5rem). On narrow screens toasts span the width."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description toast(message, options) Show a toast (or update one by id); returns its id. toast.success / info / warning / error / loading toast() with that type. toast.promise(promise, { loading, success, error }) A loading toast that turns into the outcome. toast.dismiss(id?) Dismiss one toast, or all. [data-toast=\"message\"] Shows a toast on click; data-toast-type, -description, -duration, -position, -action, -cancel, -id. [data-toast-loading] / -success / -error On an element making an htmx request: loading toast, then the outcome. [data-toast-show=\"message\"] An element that shows a toast when it's added to the page, then removes itself. HX-Trigger: {\"toast\": …} A toast from a response header. toast:action / toast:cancel Fired on the trigger element when the toast's buttons are clicked. [data-toaster] The toaster's defaults: data-position, -duration, -visible, -expand, -close-button, -rich-colors. .toast, .toast-title, .toast-description, .toast-action … The toast's parts, to restyle; data-type on .toast."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/accordion",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/accordion",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/accordion.md",
      "title": "Accordion",
      "description": "A vertically stacked set of headings that each reveal a section of content. Built on <details>, so it works without JavaScript.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "single",
          "text": "One open at a time"
        },
        {
          "level": 2,
          "id": "multiple",
          "text": "Multiple open"
        },
        {
          "level": 2,
          "id": "disabled",
          "text": "Disabled"
        },
        {
          "level": 2,
          "id": "borders",
          "text": "Borders"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "Loading content with htmx"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"accordion max-w-lg\"> <details class=\"accordion-item\" name=\"faq-basic\" open> <summary class=\"accordion-trigger\">Is it accessible?</summary> <div class=\"accordion-content\"> <p>Yes. It's a native <code>&lt;details&gt;</code> element, so keyboards and screen readers handle it out of the box.</p> </div> </details> <details class=\"accordion-item\" name=\"faq-basic\"> <summary class=\"accordion-trigger\">Does it need JavaScript?</summary> <div class=\"accordion-content\"> <p>No. Opening, closing and \"one at a time\" are all done by the browser. An optional behaviour fully locks disabled items.</p> </div> </details> <details class=\"accordion-item\" name=\"faq-basic\"> <summary class=\"accordion-trigger\">Can it load content from the server?</summary> <div class=\"accordion-content\"> <p>Yes, with htmx. See the example further down.</p> </div> </details> </div>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "Each item is a <details class=\"accordion-item\"> with a <summary class=\"accordion-trigger\"> heading and a .accordion-content body. Wrap the items in .accordion. Add open to an item to show it expanded on load. On older browsers that can't animate to height: auto, items open instantly instead of sliding."
        },
        {
          "id": "single",
          "title": "One open at a time",
          "text": "Give every item the same name. Opening one closes the others, as in the example above. Use a different name for each accordion on the page."
        },
        {
          "id": "multiple",
          "title": "Multiple open",
          "text": "Leave name out, and each item opens and closes on its own. html <div class=\"accordion max-w-lg\"> <details class=\"accordion-item\" open> <summary class=\"accordion-trigger\">Shipping</summary> <div class=\"accordion-content\"><p>Orders ship within two business days.</p></div> </details> <details class=\"accordion-item\" open> <summary class=\"accordion-trigger\">Returns</summary> <div class=\"accordion-content\"><p>Return anything within 30 days for a full refund.</p></div> </details> <details class=\"accordion-item\"> <summary class=\"accordion-trigger\">Warranty</summary> <div class=\"accordion-content\"><p>Two years on every product, parts and labour.</p></div> </details> </div>"
        },
        {
          "id": "disabled",
          "title": "Disabled",
          "text": "Add aria-disabled=\"true\" and tabindex=\"-1\" to the summary: it fades, ignores pointer clicks and drops out of the tab order. Screen readers and scripts can still activate a <summary>, so also put data-accordion on the wrapper. Its one-line behaviour stops a disabled summary from opening its item. html <div class=\"accordion max-w-lg\" data-accordion> <details class=\"accordion-item\" name=\"faq-disabled\"> <summary class=\"accordion-trigger\">Available</summary> <div class=\"accordion-content\"><p>This one opens.</p></div> </details> <details class=\"accordion-item\" name=\"faq-disabled\"> <summary class=\"accordion-trigger\" aria-disabled=\"true\" tabindex=\"-1\">Locked until you upgrade</summary> <div class=\"accordion-content\"><p>Hidden content.</p></div> </details> <details class=\"accordion-item\" name=\"faq-disabled\"> <summary class=\"accordion-trigger\">Also available</summary> <div class=\"accordion-content\"><p>So does this one.</p></div> </details> </div>"
        },
        {
          "id": "borders",
          "title": "Borders",
          "text": ".accordion-bordered draws a box around the accordion. .accordion-separated gives each item its own box. html <div class=\"accordion accordion-bordered max-w-lg\"> <details class=\"accordion-item\" name=\"faq-bordered\" open> <summary class=\"accordion-trigger\">Billing</summary> <div class=\"accordion-content\"><p>Invoices go out on the first of each month.</p></div> </details> <details class=\"accordion-item\" name=\"faq-bordered\"> <summary class=\"accordion-trigger\">Security</summary> <div class=\"accordion-content\"><p>All data is encrypted at rest and in transit.</p></div> </details> <details class=\"accordion-item\" name=\"faq-bordered\"> <summary class=\"accordion-trigger\">Integrations</summary> <div class=\"accordion-content\"><p>Connect over webhooks or the REST API.</p></div> </details> </div> <div class=\"accordion accordion-separated max-w-lg\"> <details class=\"accordion-item\" name=\"faq-separated\"> <summary class=\"accordion-trigger\">Separated item</summary> <div class=\"accordion-content\"><p>Each item is its own box.</p></div> </details> <details class=\"accordion-item\" name=\"faq-separated\"> <summary class=\"accordion-trigger\">Another item</summary> <div class=\"accordion-content\"><p>With space between them.</p></div> </details> </div>"
        },
        {
          "id": "htmx",
          "title": "Loading content with htmx",
          "text": "<details> fires a toggle event when it opens. hx-trigger=\"toggle once\" fetches the body the first time the item is opened. html <div class=\"accordion max-w-lg\"> <details class=\"accordion-item\" hx-get=\"/api/hello\" hx-trigger=\"toggle once\" hx-target=\"find .accordion-content\"> <summary class=\"accordion-trigger\">Server message</summary> <div class=\"accordion-content\"><p>Loading…</p></div> </details> </div>"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .accordion Wrapper around the items. Add data-accordion when any item is disabled. .accordion-item On <details>. Same name on several items: only one open at a time. .accordion-trigger On <summary>. Draws the chevron. aria-disabled=\"true\" + tabindex=\"-1\" disables it. .accordion-content The revealed body. .accordion-bordered Box around the whole accordion, with padded rows. .accordion-separated Each item in its own box."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/card",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/card",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/card.md",
      "title": "Card",
      "description": "Displays a card with a header, content and footer, plus optional media, a header action and scrolling content.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "parts",
          "text": "Parts"
        },
        {
          "level": 2,
          "id": "image",
          "text": "Image"
        },
        {
          "level": 3,
          "id": "horizontal",
          "text": "Horizontal"
        },
        {
          "level": 2,
          "id": "scroll",
          "text": "Scrolling content"
        },
        {
          "level": 2,
          "id": "size",
          "text": "Size"
        },
        {
          "level": 2,
          "id": "link",
          "text": "Clickable card"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "Loading content with htmx"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"card w-full max-w-sm\"> <div class=\"card-header\"> <h3 class=\"card-title\">Log in to your account</h3> <p class=\"card-description\">Enter your email below to log in.</p> <div class=\"card-action\"><a href=\"#signup\" class=\"btn btn-link\">Sign up</a></div> </div> <div class=\"card-content\"> <div class=\"field\"> <label class=\"field-label\" for=\"card-email\">Email</label> <input class=\"input\" id=\"card-email\" type=\"email\" placeholder=\"you@example.com\" /> </div> </div> <div class=\"card-footer flex-col\"> <button class=\"btn btn-primary w-full\">Log in</button> <button class=\"btn btn-outline w-full\">Log in with SSO</button> </div> </div>"
        },
        {
          "id": "parts",
          "title": "Parts",
          "text": "Every part is optional. .card itself has no padding; the header, content and footer each pad themselves, so media, tables and lists can run edge to edge. .card-header holds .card-title and .card-description. .card-action, inside the header, sits in its top-right corner beside the title and description. .card-content is the body. .card-footer is a row for actions, pinned to the bottom of tall cards. html <div class=\"card w-full max-w-sm\"> <div class=\"card-header\"> <h3 class=\"card-title\">Notifications</h3> <p class=\"card-description\">You have 3 unread messages.</p> <div class=\"card-action\"> <button class=\"btn btn-ghost btn-icon btn-sm\" aria-label=\"Settings\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"19\" cy=\"12\" r=\"1\"/><circle cx=\"5\" cy=\"12\" r=\"1\"/></svg></button> </div> </div> <div class=\"card-content\"> <p class=\"muted\">Your call has been confirmed. Your subscription is expiring soon.</p> </div> <div class=\"card-footer justify-end\"> <button class=\"btn btn-ghost\">Dismiss</button> <button class=\"btn btn-primary\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg> Mark all as read</button> </div> </div>"
        },
        {
          "id": "image",
          "title": "Image",
          "text": ".card-image on an <img> (or video, or any media block) runs it edge to edge. As the first or last child, its outer corners follow the card's. html <div class=\"card w-full max-w-xs\"> <img class=\"card-image aspect-video\" src=\"../../../images/landscape.svg\" alt=\"Mountains at dusk\" /> <div class=\"card-header pt-5\"> <h3 class=\"card-title\">Weekend in the mountains</h3> <p class=\"card-description\">Two nights, guided hikes, all meals.</p> <div class=\"card-action\"><span class=\"badge badge-secondary\">New</span></div> </div> <div class=\"card-footer pt-5\"> <button class=\"btn btn-primary w-full\">Book now</button> </div> </div> <div class=\"card w-full max-w-xs\"> <div class=\"card-header pb-5\"> <h3 class=\"card-title\">Image at the bottom</h3> <p class=\"card-description\">Last child, so the bottom corners round.</p> </div> <img class=\"card-image aspect-video\" src=\"../../../images/landscape.svg\" alt=\"Mountains at dusk\" /> </div>"
        },
        {
          "id": "horizontal",
          "title": "Horizontal",
          "text": ".card-horizontal puts the media beside the text from the sm breakpoint. Wrap the other parts in .card-body. html <div class=\"card card-horizontal w-full max-w-lg\"> <img class=\"card-image aspect-video sm:aspect-auto\" src=\"../../../images/landscape.svg\" alt=\"Mountains at dusk\" /> <div class=\"card-body\"> <div class=\"card-header\"> <h3 class=\"card-title\">Alpine route</h3> <p class=\"card-description\">12 km, 800 m of climbing.</p> </div> <div class=\"card-content\"><p class=\"muted\">A full-day loop past three lakes. Start early.</p></div> <div class=\"card-footer\"><button class=\"btn btn-outline btn-sm\">View map</button></div> </div> </div>"
        },
        {
          "id": "scroll",
          "title": "Scrolling content",
          "text": "Add .card-scroll to .card-content to give long content its own scrollbar while the header and footer stay put. Limit the height on the card (h-80, max-h-96…) or on the content (max-h-48). html <div class=\"card h-80 w-full max-w-sm\"> <div class=\"card-header\"> <h3 class=\"card-title\">Terms of service</h3> <p class=\"card-description\">Last updated October 2026.</p> </div> <div class=\"card-content card-scroll space-y-3 text-sm text-muted-foreground\"> <p>By using the service you agree to these terms. Please read them carefully.</p> <p>You are responsible for the content you publish and for keeping your account secure.</p> <p>We may change the service at any time. We'll tell you about significant changes in advance.</p> <p>We can suspend accounts that break these terms, after warning you where we reasonably can.</p> <p>The service is provided as is, without warranties of any kind.</p> <p>These terms are governed by the laws of the place where we are registered.</p> </div> <div class=\"card-footer border-t pt-4 justify-end\"> <button class=\"btn btn-ghost\">Decline</button> <button class=\"btn btn-primary\">Accept</button> </div> </div>"
        },
        {
          "id": "size",
          "title": "Size",
          "text": ".card-sm uses tighter padding. html <div class=\"card card-sm w-full max-w-xs\"> <div class=\"card-header\"> <h3 class=\"card-title\">Small card</h3> <p class=\"card-description\">16px padding instead of 24px.</p> </div> <div class=\"card-content\"><p class=\"muted text-sm\">Good for dense dashboards.</p></div> </div>"
        },
        {
          "id": "link",
          "title": "Clickable card",
          "text": "Make the whole card an <a> and add .card-link for a hover state. html <a href=\"/docs/components\" class=\"card card-link w-full max-w-sm\"> <div class=\"card-header pb-6\"> <h3 class=\"card-title flex items-center justify-between\">All components <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5l7 7-7 7\"/></svg></h3> <p class=\"card-description\">Browse every component in the library.</p> </div> </a>"
        },
        {
          "id": "htmx",
          "title": "Loading content with htmx",
          "text": "Cards make good swap targets. This one fetches its body from the server: html <div class=\"card w-full max-w-sm\"> <div class=\"card-header\"> <h3 class=\"card-title\">Server message</h3> <p class=\"card-description\">Fetched with hx-get.</p> </div> <div class=\"card-content\" id=\"card-body\"> <button class=\"btn btn-outline btn-sm\" hx-get=\"/api/hello\" hx-target=\"#card-body\">Load</button> </div> </div>"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .card Container: border, surface colour, radius and shadow. .card-header Top section, padded on three sides. .card-title Heading text. .card-description Muted supporting text. .card-action Inside the header: top-right slot beside the title and description. .card-content Main body, padded on all sides. .card-scroll On .card-content: scrolls on its own when the card or content has a height limit. .card-footer Bottom row for actions; pinned to the bottom of tall cards. .card-image Edge-to-edge media; outer corners follow the card's. .card-horizontal Media beside the content from the sm breakpoint. Wrap the parts in .card-body. .card-sm Tighter padding. .card-link Hover style for cards that are links."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/carousel",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/carousel",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/carousel.md",
      "title": "Carousel",
      "description": "Slides that drag, swipe and snap with momentum, on Embla Carousel: item sizes, spacing, vertical orientation, options and plugins.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "installation",
          "text": "Installation"
        },
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Item size"
        },
        {
          "level": 2,
          "id": "spacing",
          "text": "Spacing"
        },
        {
          "level": 2,
          "id": "orientation",
          "text": "Orientation"
        },
        {
          "level": 2,
          "id": "options",
          "text": "Options"
        },
        {
          "level": 2,
          "id": "plugins",
          "text": "Plugins"
        },
        {
          "level": 2,
          "id": "api",
          "text": "API"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"carousel w-full max-w-xs\" data-carousel aria-label=\"Numbers\"> <div class=\"carousel-viewport\"> <div class=\"carousel-container\"> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-square text-4xl\">1</div> </div> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-square text-4xl\">2</div> </div> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-square text-4xl\">3</div> </div> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-square text-4xl\">4</div> </div> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-square text-4xl\">5</div> </div> </div> </div> <button type=\"button\" class=\"btn btn-outline btn-icon btn-no-press carousel-prev\" data-carousel-prev aria-label=\"Previous slide\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M19 12H5M12 19l-7-7 7-7\"/></svg></button> <button type=\"button\" class=\"btn btn-outline btn-icon btn-no-press carousel-next\" data-carousel-next aria-label=\"Next slide\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5l7 7-7 7\"/></svg></button> </div>"
        },
        {
          "id": "installation",
          "title": "Installation",
          "text": "The carousel runs on Embla Carousel, a dependency of htmx-ui that is loaded only on pages with a carousel. Plugins are separate packages; add the ones you use: bun add embla-carousel-autoplay npm install embla-carousel-autoplay pnpm add embla-carousel-autoplay yarn add embla-carousel-autoplay"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": ".carousel[data-carousel] holds a .carousel-viewport (it clips), a .carousel-container (it moves) and the .carousel-items. [data-carousel-prev] and [data-carousel-next] buttons scroll, and disable themselves at the ends. Give them an aria-label, and .btn-no-press so they don't nudge when pressed. The behaviour marks the carousel as a region and each item as a slide (\"2 of 5\"); name the region with aria-label. ← → scroll when focus is inside it. Without JavaScript the slides are a clipped row; give the viewport overflow-x-auto if they must stay reachable then."
        },
        {
          "id": "sizes",
          "title": "Item size",
          "text": "Items fill the viewport. Size them with basis-* utilities on the items (responsive ones too), or --carousel-size on the carousel for all of them. html <div class=\"carousel w-full max-w-sm\" data-carousel aria-label=\"Thirds\" data-carousel-options='{\"align\": \"start\"}'> <div class=\"carousel-viewport\"> <div class=\"carousel-container\"> <div class=\"carousel-item basis-1/2 md:basis-1/3\"> <div class=\"card flex aspect-square items-center justify-center text-3xl font-semibold\">1</div> </div> <div class=\"carousel-item basis-1/2 md:basis-1/3\"> <div class=\"card flex aspect-square items-center justify-center text-3xl font-semibold\">2</div> </div> <div class=\"carousel-item basis-1/2 md:basis-1/3\"> <div class=\"card flex aspect-square items-center justify-center text-3xl font-semibold\">3</div> </div> <div class=\"carousel-item basis-1/2 md:basis-1/3\"> <div class=\"card flex aspect-square items-center justify-center text-3xl font-semibold\">4</div> </div> <div class=\"carousel-item basis-1/2 md:basis-1/3\"> <div class=\"card flex aspect-square items-center justify-center text-3xl font-semibold\">5</div> </div> <div class=\"carousel-item basis-1/2 md:basis-1/3\"> <div class=\"card flex aspect-square items-center justify-center text-3xl font-semibold\">6</div> </div> </div> </div> <button type=\"button\" class=\"btn btn-outline btn-icon btn-no-press carousel-prev\" data-carousel-prev aria-label=\"Previous slide\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M19 12H5M12 19l-7-7 7-7\"/></svg></button> <button type=\"button\" class=\"btn btn-outline btn-icon btn-no-press carousel-next\" data-carousel-next aria-label=\"Next slide\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"t"
        },
        {
          "id": "spacing",
          "title": "Spacing",
          "text": "--carousel-gap on the carousel sets the space between items (1rem by default). It is padding on each item, so Embla's snap points stay exact. html <div class=\"carousel w-full max-w-sm [--carousel-gap:0.25rem] [--carousel-size:33.333%]\" data-carousel aria-label=\"Tight\" data-carousel-options='{\"align\": \"start\"}'> <div class=\"carousel-viewport\"> <div class=\"carousel-container\"> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-square text-2xl\">1</div> </div> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-square text-2xl\">2</div> </div> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-square text-2xl\">3</div> </div> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-square text-2xl\">4</div> </div> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-square text-2xl\">5</div> </div> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-square text-2xl\">6</div> </div> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-square text-2xl\">7</div> </div> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-square text-2xl\">8</div> </div> </div> </div> <button type=\"button\" class=\"btn btn-outline btn-icon btn-no-press carousel-prev\" data-carousel-prev aria-label=\"Previous slide\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M19 12H5M12 19l-7-7 7-7\"/></svg></button> <button type=\"button\" class=\"btn btn-outline btn-icon btn-no-press carousel-next\" data-carousel-next aria-label=\"Next slide\"><svg xmlns=\"http://www.w3.org/2000/svg\" "
        },
        {
          "id": "orientation",
          "title": "Orientation",
          "text": "data-orientation=\"vertical\" scrolls up and down (Embla's axis: \"y\"); give the viewport a height. The buttons move above and below and turn, and ↑ ↓ scroll. html <div class=\"carousel w-full max-w-xs\" data-carousel data-orientation=\"vertical\" aria-label=\"Vertical\" data-carousel-options='{\"align\": \"start\"}'> <div class=\"carousel-viewport h-48\"> <div class=\"carousel-container\"> <div class=\"carousel-item basis-1/2\"> <div class=\"card flex h-full items-center justify-center text-3xl font-semibold\">1</div> </div> <div class=\"carousel-item basis-1/2\"> <div class=\"card flex h-full items-center justify-center text-3xl font-semibold\">2</div> </div> <div class=\"carousel-item basis-1/2\"> <div class=\"card flex h-full items-center justify-center text-3xl font-semibold\">3</div> </div> <div class=\"carousel-item basis-1/2\"> <div class=\"card flex h-full items-center justify-center text-3xl font-semibold\">4</div> </div> <div class=\"carousel-item basis-1/2\"> <div class=\"card flex h-full items-center justify-center text-3xl font-semibold\">5</div> </div> </div> </div> <button type=\"button\" class=\"btn btn-outline btn-icon btn-no-press carousel-prev\" data-carousel-prev aria-label=\"Previous slide\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M19 12H5M12 19l-7-7 7-7\"/></svg></button> <button type=\"button\" class=\"btn btn-outline btn-icon btn-no-press carousel-next\" data-carousel-next aria-label=\"Next slide\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5l7 7-7 7\"/></svg></button> </div>"
        },
        {
          "id": "options",
          "title": "Options",
          "text": "data-carousel-options takes Embla's options as JSON and passes them on: loop, align, dragFree, slidesToScroll, startIndex, breakpoints and the rest. Below: a loop that starts on the third slide. .carousel-controls puts the buttons in a row underneath, with an empty [data-carousel-dots] that fills with a dot per snap point. html <div class=\"carousel w-full max-w-xs\" data-carousel aria-label=\"Looping\" data-carousel-options='{\"loop\": true, \"startIndex\": 2}'> <div class=\"carousel-viewport\"> <div class=\"carousel-container\"> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-[4/3] text-4xl\">1</div> </div> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-[4/3] text-4xl\">2</div> </div> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-[4/3] text-4xl\">3</div> </div> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-[4/3] text-4xl\">4</div> </div> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-[4/3] text-4xl\">5</div> </div> </div> </div> <div class=\"carousel-controls\"> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-no-press carousel-prev\" data-carousel-prev aria-label=\"Previous slide\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m15 18-6-6 6-6\"/></svg></button> <div class=\"carousel-dots\" data-carousel-dots></div> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-no-press carousel-next\" data-carousel-next aria-label=\"Next slide\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><pat"
        },
        {
          "id": "plugins",
          "title": "Plugins",
          "text": "Register an Embla plugin under a name once, in your entry script, then ask for it by that name with its options in data-carousel-plugins: app.ts import Autoplay from \"embla-carousel-autoplay\"; import { initComponents, registerCarouselPlugin } from \"htmx-ui\"; registerCarouselPlugin(\"autoplay\", Autoplay); // before initComponents() html <div class=\"carousel w-full max-w-xs\" data-carousel aria-label=\"Autoplay\" data-carousel-options='{\"loop\": true}' data-carousel-plugins='{\"autoplay\": {\"delay\": 2500, \"stopOnInteraction\": false, \"stopOnMouseEnter\": true}}'> <div class=\"carousel-viewport\"> <div class=\"carousel-container\"> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-[4/3] text-4xl\">1</div> </div> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-[4/3] text-4xl\">2</div> </div> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-[4/3] text-4xl\">3</div> </div> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-[4/3] text-4xl\">4</div> </div> <div class=\"carousel-item\"> <div class=\"card flex items-center justify-center font-semibold aspect-[4/3] text-4xl\">5</div> </div> </div> </div> <div class=\"carousel-dots\" data-carousel-dots></div> </div>"
        },
        {
          "id": "api",
          "title": "API",
          "text": "For anything else, take Embla's API from the element once it has loaded: ts import { getCarousel } from \"htmx-ui\"; const el = document.querySelector(\"#gallery\")!; el.addEventListener(\"carousel:init\", (e) => { const api = (e as CustomEvent).detail.api; // same as getCarousel(el) api.scrollTo(3); }); el.addEventListener(\"carousel:select\", (e) => console.log(\"slide\", (e as CustomEvent).detail.index));"
        },
        {
          "id": "htmx",
          "title": "With htmx",
          "text": "A carousel swapped in by htmx initialises like any component. To add slides to one already running, swap them into its .carousel-container: Embla notices new slides and re-measures, and the dots and labels follow."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .carousel The carousel; sets --carousel-gap (1rem) and --carousel-size (100%). .carousel-viewport Clips the slides; give it a height when vertical. .carousel-container The moving row (column when vertical). .carousel-item A slide; basis-* utilities set its size. .carousel-prev / .carousel-next Buttons beside the slides (above and below when vertical). .carousel-controls A row under the slides for buttons and dots. .carousel-dots / .carousel-dot Dot buttons, built in an empty [data-carousel-dots]. [data-carousel] Behaviour: starts Embla, buttons, dots, keys, labels. [data-orientation=\"vertical\"] Scroll vertically (axis \"y\"). [data-carousel-options] Embla options as JSON. [data-carousel-plugins] Plugins by registered name, with their options, as JSON. registerCarouselPlugin(name, factory) Make an Embla plugin available by name. getCarousel(el) The Embla API of a carousel (after carousel:init). carousel:init / carousel:select Events: ready (detail.api), slide changed (detail.index)."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/collapsible",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/collapsible",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/collapsible.md",
      "title": "Collapsible",
      "description": "A button that expands and collapses a panel: more details, extra settings, or a whole file tree.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "settings",
          "text": "Reveal more settings"
        },
        {
          "level": 2,
          "id": "file-tree",
          "text": "File tree"
        },
        {
          "level": 2,
          "id": "details",
          "text": "Without JavaScript"
        },
        {
          "level": 2,
          "id": "find",
          "text": "Find in page"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"collapsible w-full max-w-sm gap-2\" data-collapsible> <div class=\"flex items-center justify-between gap-4 ps-4\"> <h4 class=\"text-sm font-semibold\">Order #4189</h4> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm\" data-collapsible-trigger aria-expanded=\"false\" aria-label=\"Show order details\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m7 15 5 5 5-5\"/><path d=\"m7 9 5-5 5 5\"/></svg> </button> </div> <div class=\"flex items-center justify-between rounded-(--radius) border border-border px-4 py-2 text-sm\"> <span class=\"text-muted-foreground\">Status</span> <span class=\"font-medium\">Shipped</span> </div> <div class=\"collapsible-content gap-2\" data-collapsible-content hidden> <div class=\"rounded-(--radius) border border-border px-4 py-2 text-sm\"> <p class=\"font-medium\">Shipping address</p> <p class=\"text-muted-foreground\">100 Market St, San Francisco</p> </div> <div class=\"rounded-(--radius) border border-border px-4 py-2 text-sm\"> <p class=\"font-medium\">Items</p> <p class=\"text-muted-foreground\">2× Studio Headphones</p> </div> </div> </div>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "Wrap everything in .collapsible with data-collapsible. The trigger is a <button> with data-collapsible-trigger and aria-expanded. It can be anywhere inside — above the panel, below it, in a header — and look like anything: .collapsible-trigger for a full-width row, or any button class. A collapsible may have several triggers; they stay in step. The panel is a .collapsible-content with data-collapsible-content. It starts closed with hidden, or open without it. The behaviour links the triggers to it with aria-controls. A .collapsible-icon in the trigger turns 180° while open (a chevron-down becomes a chevron-up). Set --collapsible-rotate to change the angle, such as 90deg for a chevron-right. For several sections where opening one may close the others, use an accordion."
        },
        {
          "id": "settings",
          "title": "Reveal more settings",
          "text": "A trigger below the panel, worded for both states. The text swaps with Tailwind's group-aria-expanded variant, so no script is needed for it. html <form class=\"card w-full max-w-sm\" onsubmit=\"return false\"> <div class=\"card-header\"> <h3 class=\"card-title\">Radius</h3> <p class=\"card-description\">Set the corner radius of the element.</p> </div> <div class=\"card-content collapsible gap-4\" data-collapsible> <div class=\"field-grid gap-y-4\"> <div class=\"field\"> <label class=\"label\" for=\"radius-x\">Radius X</label> <input class=\"input\" id=\"radius-x\" value=\"0\" inputmode=\"numeric\" /> </div> <div class=\"field\"> <label class=\"label\" for=\"radius-y\">Radius Y</label> <input class=\"input\" id=\"radius-y\" value=\"0\" inputmode=\"numeric\" /> </div> </div> <div class=\"collapsible-content\" data-collapsible-content hidden> <div class=\"field-grid gap-y-4\"> <div class=\"field\"> <label class=\"label\" for=\"radius-tl\">Top left</label> <input class=\"input\" id=\"radius-tl\" value=\"0\" inputmode=\"numeric\" /> </div> <div class=\"field\"> <label class=\"label\" for=\"radius-tr\">Top right</label> <input class=\"input\" id=\"radius-tr\" value=\"0\" inputmode=\"numeric\" /> </div> <div class=\"field\"> <label class=\"label\" for=\"radius-bl\">Bottom left</label> <input class=\"input\" id=\"radius-bl\" value=\"0\" inputmode=\"numeric\" /> </div> <div class=\"field\"> <label class=\"label\" for=\"radius-br\">Bottom right</label> <input class=\"input\" id=\"radius-br\" value=\"0\" inputmode=\"numeric\" /> </div> </div> </div> <button type=\"button\" class=\"group btn btn-outline btn-sm self-start\" data-collapsible-trigger aria-expanded=\"false\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.7"
        },
        {
          "id": "file-tree",
          "title": "File tree",
          "text": "Collapsibles nest: each trigger only opens its own panel. Here every folder is one, with a chevron-right turned by [--collapsible-rotate:90deg] on the tree and the folder icon swapped while open. html <ul class=\"w-64 rounded-(--radius) border border-border p-2 text-sm [--collapsible-rotate:90deg]\" aria-label=\"Files\"> <li class=\"collapsible\" data-collapsible> <button type=\"button\" class=\"group collapsible-trigger justify-start px-2 py-1.5 font-normal hover:bg-accent\" data-collapsible-trigger aria-expanded=\"true\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"collapsible-icon size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4 group-aria-expanded:hidden\" aria-hidden=\"true\"><path d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\"/></svg> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"hidden size-4 group-aria-expanded:block\" aria-hidden=\"true\"><path d=\"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2\"/></svg> src </button> <ul class=\"collapsible-content ms-4 border-s border-border ps-2\" data-collapsible-content> <li class=\"collapsible\" data-collapsible> <button type=\"button\" class=\"group collapsible-trigger justify-start px-2 py-1.5 font-normal hover:bg-accent\" data-collapsible-trigger aria-expanded=\"true\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"no"
        },
        {
          "id": "details",
          "title": "Without JavaScript",
          "text": "The same classes style a native <details>: the <summary> is the trigger and the icon turns while it's open, with no behaviour at all. The trade-off is that the summary must come first, so the trigger can't sit below the panel. html <details class=\"collapsible w-full max-w-sm gap-2\"> <summary class=\"collapsible-trigger rounded-(--radius) border border-border px-4 py-2\"> What does it cost? <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"collapsible-icon size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg> </summary> <p class=\"px-4 text-sm text-muted-foreground\">Nothing: htmx-ui is MIT licensed.</p> </details>"
        },
        {
          "id": "find",
          "title": "Find in page",
          "text": "Closed content is invisible to the browser's find-in-page. Close it with hidden=\"until-found\" instead of hidden, and searching for text inside opens the collapsible; it closes back to until-found. Browsers without support treat it as plain hidden."
        },
        {
          "id": "htmx",
          "title": "With htmx",
          "text": "Every open and close fires a bubbling collapsible:toggle with detail.open, and the wrapper carries data-open while open. To fetch the panel's content the first time it opens, put the request on the trigger: html <div class=\"collapsible w-full max-w-sm gap-3\" data-collapsible> <button type=\"button\" class=\"collapsible-trigger\" data-collapsible-trigger aria-expanded=\"false\" hx-get=\"/api/hello\" hx-trigger=\"click once\" hx-target=\"next [data-collapsible-content]\"> Latest message <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"collapsible-icon size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg> </button> <div class=\"collapsible-content\" data-collapsible-content hidden><p class=\"muted\">Loading…</p></div> </div>"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class / attribute Description .collapsible Wrapper (a flex column). Add data-collapsible, or use it on a <details>. .collapsible-trigger A full-width row button, or a <summary>. Optional: any button works. .collapsible-icon Turns by --collapsible-rotate (180deg) while open. .collapsible-content The panel. Add data-collapsible-content, and hidden to start closed. [data-collapsible-trigger] Toggles the panel; aria-expanded and aria-controls kept in sync. [data-open] On the wrapper while open. collapsible:toggle Event on the wrapper with detail { open }."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/separator",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/separator",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/separator.md",
      "title": "Separator",
      "description": "A line that visually or semantically separates content: horizontal or vertical.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "orientation",
          "text": "Orientation"
        },
        {
          "level": 2,
          "id": "menu",
          "text": "Between menu items"
        },
        {
          "level": 2,
          "id": "list",
          "text": "Between list items"
        },
        {
          "level": 2,
          "id": "with-text",
          "text": "With text"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"w-full max-w-sm\"> <div class=\"space-y-1\"> <p class=\"text-sm leading-none font-medium\">htmx-ui</p> <p class=\"text-sm text-muted-foreground\">Components for htmx, in plain HTML and CSS.</p> </div> <div class=\"separator my-4\" role=\"separator\"></div> <div class=\"flex h-5 items-center gap-4 text-sm\"> <a class=\"link\" href=\"/docs\">Docs</a> <div class=\"separator\" role=\"separator\" aria-orientation=\"vertical\"></div> <a class=\"link\" href=\"/docs/components\">Components</a> <div class=\"separator\" role=\"separator\" aria-orientation=\"vertical\"></div> <a class=\"link\" href=\"/docs/changelog\">Changelog</a> </div> </div>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "A .separator is a one-pixel line in the border colour, as wide as its container. Space it with margin utilities (my-4). role=\"separator\" marks a real change of section for screen readers. A line that is only decoration takes role=\"none\", so it isn't announced. An <hr class=\"separator\"> is a separator already. Content changes for many reasons; a separator is for a break between groups, not between every pair of items. Spacing alone is often enough."
        },
        {
          "id": "orientation",
          "title": "Orientation",
          "text": "aria-orientation=\"vertical\" turns the line upright, for a separator between items in a row. It takes the height of its flex row, so the row sets it (h-5 above); outside a flex row give the separator a height of its own. A decorative vertical line, with role=\"none\" and no aria-orientation, takes .separator-vertical instead. html <div class=\"flex items-center gap-3 text-sm\"> <span>Blog</span> <span class=\"separator separator-vertical h-4\" role=\"none\"></span> <span>Docs</span> <span class=\"separator separator-vertical h-4\" role=\"none\"></span> <span>Source</span> </div>"
        },
        {
          "id": "menu",
          "title": "Between menu items",
          "text": "Vertical separators between items that each carry a description: a separator per gap, stretching to the tallest item. html <div class=\"flex items-stretch gap-4 text-sm sm:gap-6\"> <div class=\"flex flex-col gap-1\"> <span class=\"font-medium\">Settings</span> <span class=\"text-xs text-muted-foreground\">Manage preferences</span> </div> <div class=\"separator\" role=\"separator\" aria-orientation=\"vertical\"></div> <div class=\"flex flex-col gap-1\"> <span class=\"font-medium\">Account</span> <span class=\"text-xs text-muted-foreground\">Profile and security</span> </div> <div class=\"separator\" role=\"separator\" aria-orientation=\"vertical\"></div> <div class=\"flex flex-col gap-1\"> <span class=\"font-medium\">Help</span> <span class=\"text-xs text-muted-foreground\">Support and docs</span> </div> </div>"
        },
        {
          "id": "list",
          "title": "Between list items",
          "text": "Horizontal separators between the rows of a list. The line is decorative here (the list already groups its items), so it is hidden from screen readers. html <dl class=\"w-full max-w-sm text-sm\"> <div class=\"flex justify-between py-2\"> <dt class=\"text-muted-foreground\">Item 1</dt> <dd class=\"font-medium\">Value 1</dd> </div> <div class=\"separator\" role=\"none\"></div> <div class=\"flex justify-between py-2\"> <dt class=\"text-muted-foreground\">Item 2</dt> <dd class=\"font-medium\">Value 2</dd> </div> <div class=\"separator\" role=\"none\"></div> <div class=\"flex justify-between py-2\"> <dt class=\"text-muted-foreground\">Item 3</dt> <dd class=\"font-medium\">Value 3</dd> </div> </dl>"
        },
        {
          "id": "with-text",
          "title": "With text",
          "text": "For a line with a word in it — \"Or\" between two ways of signing in — use .field-separator."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class / attribute Description .separator A horizontal line, as wide as its container. [aria-orientation=\"vertical\"] On a .separator with role=\"separator\": an upright line, as tall as its flex row. .separator-vertical An upright line without aria-orientation, for decorative separators (role=\"none\")."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/avatar",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/avatar",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/avatar.md",
      "title": "Avatar",
      "description": "An image element with a fallback for representing the user.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "image-and-fallback",
          "text": "Image and fallback"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Sizes"
        },
        {
          "level": 2,
          "id": "badge",
          "text": "Badge"
        },
        {
          "level": 3,
          "id": "badge-with-icon",
          "text": "Badge with an icon"
        },
        {
          "level": 2,
          "id": "group",
          "text": "Group"
        },
        {
          "level": 3,
          "id": "group-count",
          "text": "With a count"
        },
        {
          "level": 3,
          "id": "group-icon",
          "text": "With an icon"
        },
        {
          "level": 2,
          "id": "dropdown",
          "text": "As a menu trigger"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <span class=\"avatar\" role=\"img\" aria-label=\"Ana Lima\"> <img class=\"avatar-image\" src=\"../../../images/avatar-1.svg\" alt=\"\" /> <span class=\"avatar-fallback\" aria-hidden=\"true\">AL</span> </span> <span class=\"avatar\" role=\"img\" aria-label=\"Kofi Mensah\"> <span class=\"avatar-fallback\" aria-hidden=\"true\">KM</span> </span> <span class=\"avatar avatar-square\" role=\"img\" aria-label=\"Ravi Patel\"> <img class=\"avatar-image\" src=\"../../../images/avatar-3.svg\" alt=\"\" /> <span class=\"avatar-fallback\" aria-hidden=\"true\">RP</span> </span>"
        },
        {
          "id": "image-and-fallback",
          "title": "Image and fallback",
          "text": ".avatar holds an .avatar-image on top of an .avatar-fallback, usually initials or an icon. The fallback shows while the image loads, when there is no image, and when it fails to load: the avatar behaviour hides an image that errors, so the fallback shows instead of a broken-image icon. It runs on avatars in fragments from the server too. Name the person once, on the avatar: role=\"img\" and aria-label on .avatar, alt=\"\" on the image and aria-hidden=\"true\" on the fallback. Next to the visible name, the avatar is decorative: drop the role and label and give it aria-hidden=\"true\". html <span class=\"avatar avatar-lg\" role=\"img\" aria-label=\"Ana Lima\"> <img class=\"avatar-image\" src=\"../../../images/avatar-1.svg\" alt=\"\" /> <span class=\"avatar-fallback\" aria-hidden=\"true\">AL</span> </span> <span class=\"avatar avatar-lg\" role=\"img\" aria-label=\"Kofi Mensah\"> <span class=\"avatar-fallback\" aria-hidden=\"true\">KM</span> </span> <span class=\"avatar avatar-lg\" role=\"img\" aria-label=\"Unknown user\"> <span class=\"avatar-fallback\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/></svg></span> </span>"
        },
        {
          "id": "sizes",
          "title": "Sizes",
          "text": ".avatar-sm (1.5rem), the default (2rem), .avatar-lg (2.5rem) and .avatar-xl (3.5rem). The fallback text and badge scale with them; any size-* utility works too. html <span class=\"avatar avatar-sm\" role=\"img\" aria-label=\"Mei Chen\"> <img class=\"avatar-image\" src=\"../../../images/avatar-4.svg\" alt=\"\" /> <span class=\"avatar-fallback\" aria-hidden=\"true\">MC</span> </span> <span class=\"avatar \" role=\"img\" aria-label=\"Mei Chen\"> <img class=\"avatar-image\" src=\"../../../images/avatar-4.svg\" alt=\"\" /> <span class=\"avatar-fallback\" aria-hidden=\"true\">MC</span> </span> <span class=\"avatar avatar-lg\" role=\"img\" aria-label=\"Mei Chen\"> <img class=\"avatar-image\" src=\"../../../images/avatar-4.svg\" alt=\"\" /> <span class=\"avatar-fallback\" aria-hidden=\"true\">MC</span> </span> <span class=\"avatar avatar-xl\" role=\"img\" aria-label=\"Mei Chen\"> <img class=\"avatar-image\" src=\"../../../images/avatar-4.svg\" alt=\"\" /> <span class=\"avatar-fallback\" aria-hidden=\"true\">MC</span> </span> <span class=\"avatar avatar-sm\" role=\"img\" aria-label=\"Mei Chen\"> <span class=\"avatar-fallback\" aria-hidden=\"true\">MC</span> </span> <span class=\"avatar \" role=\"img\" aria-label=\"Mei Chen\"> <span class=\"avatar-fallback\" aria-hidden=\"true\">MC</span> </span> <span class=\"avatar avatar-lg\" role=\"img\" aria-label=\"Mei Chen\"> <span class=\"avatar-fallback\" aria-hidden=\"true\">MC</span> </span> <span class=\"avatar avatar-xl\" role=\"img\" aria-label=\"Mei Chen\"> <span class=\"avatar-fallback\" aria-hidden=\"true\">MC</span> </span>"
        },
        {
          "id": "badge",
          "title": "Badge",
          "text": ".avatar-badge adds a dot at the bottom right, ringed in the page colour. It is bg-primary; colour it with any bg-* utility for a status. A dot means nothing to a screen reader, so say the status in the avatar's label. html <span class=\"avatar\" role=\"img\" aria-label=\"Ana Lima, online\"> <img class=\"avatar-image\" src=\"../../../images/avatar-1.svg\" alt=\"\" /> <span class=\"avatar-fallback\" aria-hidden=\"true\">AL</span> <span class=\"avatar-badge bg-success\"></span> </span> <span class=\"avatar avatar-lg\" role=\"img\" aria-label=\"Kofi Mensah, away\"> <img class=\"avatar-image\" src=\"../../../images/avatar-2.svg\" alt=\"\" /> <span class=\"avatar-fallback\" aria-hidden=\"true\">KM</span> <span class=\"avatar-badge bg-warning\"></span> </span> <span class=\"avatar avatar-xl\" role=\"img\" aria-label=\"Ravi Patel, offline\"> <img class=\"avatar-image\" src=\"../../../images/avatar-3.svg\" alt=\"\" /> <span class=\"avatar-fallback\" aria-hidden=\"true\">RP</span> <span class=\"avatar-badge bg-muted-foreground\"></span> </span>"
        },
        {
          "id": "badge-with-icon",
          "title": "Badge with an icon",
          "text": "An icon inside the badge makes it big enough to read. html <span class=\"avatar\" role=\"img\" aria-label=\"Ana Lima, verified\"> <img class=\"avatar-image\" src=\"../../../images/avatar-1.svg\" alt=\"\" /> <span class=\"avatar-fallback\" aria-hidden=\"true\">AL</span> <span class=\"avatar-badge\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span> </span> <span class=\"avatar avatar-lg\" role=\"img\" aria-label=\"Kofi Mensah, admin\"> <img class=\"avatar-image\" src=\"../../../images/avatar-2.svg\" alt=\"\" /> <span class=\"avatar-fallback\" aria-hidden=\"true\">KM</span> <span class=\"avatar-badge bg-warning\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z\"/></svg></span> </span> <span class=\"avatar avatar-xl\" role=\"img\" aria-label=\"Ravi Patel, 2 new messages\"> <img class=\"avatar-image\" src=\"../../../images/avatar-3.svg\" alt=\"\" /> <span class=\"avatar-fallback\" aria-hidden=\"true\">RP</span> <span class=\"avatar-badge bg-danger\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"20\" height=\"16\" x=\"2\" y=\"4\" rx=\"2\"/><path d=\"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7\"/></svg></span> </span>"
        },
        {
          "id": "group",
          "title": "Group",
          "text": ".avatar-group overlaps avatars in a row and rings each one in the page colour. Give it role=\"group\" and a label. html <div class=\"avatar-group\" role=\"group\" aria-label=\"Project members\"> <span class=\"avatar\" role=\"img\" aria-label=\"Ana Lima\"><img class=\"avatar-image\" src=\"../../../images/avatar-1.svg\" alt=\"\" /><span class=\"avatar-fallback\" aria-hidden=\"true\">AL</span></span> <span class=\"avatar\" role=\"img\" aria-label=\"Kofi Mensah\"><img class=\"avatar-image\" src=\"../../../images/avatar-2.svg\" alt=\"\" /><span class=\"avatar-fallback\" aria-hidden=\"true\">KM</span></span> <span class=\"avatar\" role=\"img\" aria-label=\"Ravi Patel\"><img class=\"avatar-image\" src=\"../../../images/avatar-3.svg\" alt=\"\" /><span class=\"avatar-fallback\" aria-hidden=\"true\">RP</span></span> <span class=\"avatar\" role=\"img\" aria-label=\"Mei Chen\"><img class=\"avatar-image\" src=\"../../../images/avatar-4.svg\" alt=\"\" /><span class=\"avatar-fallback\" aria-hidden=\"true\">MC</span></span> </div>"
        },
        {
          "id": "group-count",
          "title": "With a count",
          "text": ".avatar-group-count on an .avatar ends the group with the number of people not shown. It takes the avatar size classes. html <div class=\"avatar-group\" role=\"group\" aria-label=\"Project members\"> <span class=\"avatar\" role=\"img\" aria-label=\"Ana Lima\"><img class=\"avatar-image\" src=\"../../../images/avatar-1.svg\" alt=\"\" /><span class=\"avatar-fallback\" aria-hidden=\"true\">AL</span></span> <span class=\"avatar\" role=\"img\" aria-label=\"Kofi Mensah\"><img class=\"avatar-image\" src=\"../../../images/avatar-2.svg\" alt=\"\" /><span class=\"avatar-fallback\" aria-hidden=\"true\">KM</span></span> <span class=\"avatar\" role=\"img\" aria-label=\"Ravi Patel\"><img class=\"avatar-image\" src=\"../../../images/avatar-3.svg\" alt=\"\" /><span class=\"avatar-fallback\" aria-hidden=\"true\">RP</span></span> <span class=\"avatar avatar-group-count\" role=\"img\" aria-label=\"and 3 more\">+3</span> </div> <div class=\"avatar-group\" role=\"group\" aria-label=\"Reviewers\"> <span class=\"avatar avatar-lg\" role=\"img\" aria-label=\"Mei Chen\"><img class=\"avatar-image\" src=\"../../../images/avatar-4.svg\" alt=\"\" /><span class=\"avatar-fallback\" aria-hidden=\"true\">MC</span></span> <span class=\"avatar avatar-lg\" role=\"img\" aria-label=\"Ana Lima\"><img class=\"avatar-image\" src=\"../../../images/avatar-1.svg\" alt=\"\" /><span class=\"avatar-fallback\" aria-hidden=\"true\">AL</span></span> <span class=\"avatar avatar-lg avatar-group-count\" role=\"img\" aria-label=\"and 12 more\">+12</span> </div>"
        },
        {
          "id": "group-icon",
          "title": "With an icon",
          "text": "An icon in the count, on a button, makes an action at the end of the group, such as inviting someone. html <div class=\"avatar-group\" role=\"group\" aria-label=\"Project members\"> <span class=\"avatar avatar-lg\" role=\"img\" aria-label=\"Ana Lima\"><img class=\"avatar-image\" src=\"../../../images/avatar-1.svg\" alt=\"\" /><span class=\"avatar-fallback\" aria-hidden=\"true\">AL</span></span> <span class=\"avatar avatar-lg\" role=\"img\" aria-label=\"Kofi Mensah\"><img class=\"avatar-image\" src=\"../../../images/avatar-2.svg\" alt=\"\" /><span class=\"avatar-fallback\" aria-hidden=\"true\">KM</span></span> <span class=\"avatar avatar-lg\" role=\"img\" aria-label=\"Ravi Patel\"><img class=\"avatar-image\" src=\"../../../images/avatar-3.svg\" alt=\"\" /><span class=\"avatar-fallback\" aria-hidden=\"true\">RP</span></span> <button type=\"button\" class=\"avatar avatar-lg avatar-group-count\" aria-label=\"Invite people\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5v14\"/></svg></button> </div>"
        },
        {
          "id": "dropdown",
          "title": "As a menu trigger",
          "text": "An avatar can be the button itself, here opening a dropdown of account actions. .avatar on a <button> or <a> gets a pointer and a focus ring. Label the button for what it does. html <div class=\"dropdown\" data-dropdown> <button type=\"button\" class=\"avatar avatar-lg\" data-dropdown-trigger aria-haspopup=\"menu\" aria-expanded=\"false\" aria-label=\"Account menu for Ana Lima\"> <img class=\"avatar-image\" src=\"../../../images/avatar-1.svg\" alt=\"\" /> <span class=\"avatar-fallback\" aria-hidden=\"true\">AL</span> </button> <div class=\"dropdown-menu\" role=\"menu\" hidden> <div class=\"flex items-center gap-2 px-2 py-1.5\"> <span class=\"avatar\" aria-hidden=\"true\"> <img class=\"avatar-image\" src=\"../../../images/avatar-1.svg\" alt=\"\" /> <span class=\"avatar-fallback\">AL</span> </span> <div class=\"grid text-sm leading-tight\"> <span class=\"font-medium\">Ana Lima</span> <span class=\"text-xs text-muted-foreground\">ana@example.com</span> </div> </div> <div class=\"dropdown-separator\" role=\"separator\"></div> <a class=\"dropdown-item\" role=\"menuitem\" href=\"#profile\"><span class=\"flex items-center gap-2\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/></svg> Profile</span></a> <a class=\"dropdown-item\" role=\"menuitem\" href=\"#billing\"><span class=\"flex items-center gap-2\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z\"/><path d=\"m9 12 2 2 4-4\"/></svg> Billing</span></a> <div class=\"dropdown-separator\" role=\"separator\"></div> <butt"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .avatar Round frame, 2rem. On a button or link: pointer and focus ring. .avatar-image The picture, covering the fallback. Give it alt=\"\". Hidden if it fails to load. .avatar-fallback Initials or an icon underneath; shows when there is no image or it fails. .avatar-sm / -lg / -xl 1.5rem, 2.5rem, 3.5rem; fallback text and badge scale. .avatar-square Rounded square instead of a circle. .avatar-badge Bottom-right dot (bg-primary; colour with bg-*). Bigger with an icon inside. .avatar-group Overlapping row, each avatar ringed in the page colour. .avatar-group-count On an .avatar: \"+3\" or an icon at the end of a group."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/item",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/item",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/item.md",
      "title": "Item",
      "description": "Displays a row of content with media, a title, a description and actions, alone or in a list.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "variants",
          "text": "Variants"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Sizes"
        },
        {
          "level": 2,
          "id": "icon",
          "text": "Icon"
        },
        {
          "level": 2,
          "id": "avatar",
          "text": "Avatar"
        },
        {
          "level": 2,
          "id": "image",
          "text": "Image"
        },
        {
          "level": 2,
          "id": "group",
          "text": "Group"
        },
        {
          "level": 2,
          "id": "header",
          "text": "Header"
        },
        {
          "level": 2,
          "id": "link",
          "text": "Link"
        },
        {
          "level": 3,
          "id": "link-with-actions",
          "text": "A link with actions"
        },
        {
          "level": 2,
          "id": "dropdown",
          "text": "Dropdown"
        },
        {
          "level": 3,
          "id": "dropdown-in-actions",
          "text": "A menu in the actions"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "Actions with htmx"
        },
        {
          "level": 2,
          "id": "accessibility",
          "text": "Accessibility"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"flex w-full max-w-md flex-col gap-4\"> <div class=\"item item-outline\"> <div class=\"item-content\"> <p class=\"item-title\">Basic item</p> <p class=\"item-description\">A simple item with a title and a description.</p> </div> <div class=\"item-actions\"> <button type=\"button\" class=\"btn btn-outline btn-sm\">Action</button> </div> </div> <a class=\"item item-outline item-sm\" href=\"#\"> <div class=\"item-media\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-5\" aria-hidden=\"true\"><path d=\"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z\"/><path d=\"m9 12 2 2 4-4\"/></svg></div> <div class=\"item-content\"> <p class=\"item-title\">Your profile has been verified.</p> </div> <div class=\"item-actions\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg></div> </a> </div>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "An item is a flex row. Every part is optional except the content: html <div class=\"item\"> <div class=\"item-header\">…</div> <!-- optional, full width, above <!-- optional, full width, above --> <div class=\"item-media\">…</div> <!-- an icon, image or avatar <!-- an icon, image or avatar --> <div class=\"item-content\"> <p class=\"item-title\">…</p> <p class=\"item-description\">…</p> </div> <div class=\"item-actions\">…</div> <!-- buttons, a menu, a badge <!-- buttons, a menu, a badge --> <div class=\"item-footer\">…</div> <!-- optional, full width, below <!-- optional, full width, below --> </div> .item-media sits at the start. Next to a title and a description it aligns with the title rather than the middle. .item-content takes the free space. A second .item-content (a date, an amount) only takes the room it needs. .item-description is clamped to two lines. Links inside it are underlined. .item-actions sits at the end. .item-header and .item-footer take a full line each, above and below the rest. Items wrap. When the media, the content and the actions don't fit side by side, the actions move to a line of their own, still at the end, so an item works on a phone with no extra classes."
        },
        {
          "id": "variants",
          "title": "Variants",
          "text": "The default item has no border or background, for lists inside a card or a panel. .item-outline adds a border and .item-muted a muted background. html <div class=\"flex w-full max-w-md flex-col gap-4\"> <div class=\"item\"> <div class=\"item-content\"> <p class=\"item-title\">Default</p> <p class=\"item-description\">Transparent, with no border.</p> </div> <div class=\"item-actions\"><button type=\"button\" class=\"btn btn-outline btn-sm\">Open</button></div> </div> <div class=\"item item-outline\"> <div class=\"item-content\"> <p class=\"item-title\">Outline</p> <p class=\"item-description\">A border around the item.</p> </div> <div class=\"item-actions\"><button type=\"button\" class=\"btn btn-outline btn-sm\">Open</button></div> </div> <div class=\"item item-muted\"> <div class=\"item-content\"> <p class=\"item-title\">Muted</p> <p class=\"item-description\">A muted background, for secondary content.</p> </div> <div class=\"item-actions\"><button type=\"button\" class=\"btn btn-outline btn-sm\">Open</button></div> </div> </div>"
        },
        {
          "id": "sizes",
          "title": "Sizes",
          "text": "The default size, .item-sm and .item-xs reduce the padding and gaps. The extra small size also shrinks icon and image media and the description. html <div class=\"flex w-full max-w-md flex-col gap-4\"> <a class=\"item item-outline\" href=\"#\"> <div class=\"item-media item-media-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><polyline points=\"22 12 16 12 14 15 10 15 8 12 2 12\"/><path d=\"M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z\"/></svg></div> <div class=\"item-content\"> <p class=\"item-title\">Inbox</p> <p class=\"item-description\">12 unread messages</p> </div> <div class=\"item-actions\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg></div> </a> <a class=\"item item-outline item-sm\" href=\"#\"> <div class=\"item-media item-media-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><polyline points=\"22 12 16 12 14 15 10 15 8 12 2 12\"/><path d=\"M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z\"/></svg></div> <div class=\"item-content\"> <p class=\"item-title\">Inbox</p> <p class=\"item-description\">12 unread messages</p> </div> <div class=\"item-actions\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg></div> </a> <a class=\"item item-outline item"
        },
        {
          "id": "icon",
          "title": "Icon",
          "text": ".item-media-icon puts an icon in a small bordered, muted square. A bare icon in .item-media works too. html <div class=\"item item-outline w-full max-w-md\"> <div class=\"item-media item-media-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z\"/><path d=\"m9 12 2 2 4-4\"/></svg></div> <div class=\"item-content\"> <p class=\"item-title\">Security alert</p> <p class=\"item-description\">New sign-in from an unknown device in Lisbon, Portugal.</p> </div> <div class=\"item-actions\"> <button type=\"button\" class=\"btn btn-outline btn-sm\">Review</button> </div> </div>"
        },
        {
          "id": "avatar",
          "title": "Avatar",
          "text": "Put an avatar or an avatar group in .item-media. The person's name is in the title, so the avatar is decorative: aria-hidden=\"true\", with no role or label. html <div class=\"flex w-full max-w-md flex-col gap-4\"> <div class=\"item item-outline\"> <div class=\"item-media\"><span class=\"avatar avatar-lg\" aria-hidden=\"true\"><img class=\"avatar-image\" src=\"../../../images/avatar-1.svg\" alt=\"\" /><span class=\"avatar-fallback\">AL</span></span></div> <div class=\"item-content\"> <p class=\"item-title\">Ana Lima</p> <p class=\"item-description\">Last seen 5 months ago</p> </div> <div class=\"item-actions\"> <button type=\"button\" class=\"btn btn-outline btn-icon btn-sm btn-rounded\" aria-label=\"Invite Ana Lima\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5v14\"/></svg></button> </div> </div> <div class=\"item item-outline\"> <div class=\"item-media\"> <div class=\"avatar-group\" aria-hidden=\"true\"> <span class=\"avatar\" aria-hidden=\"true\"><img class=\"avatar-image\" src=\"../../../images/avatar-2.svg\" alt=\"\" /><span class=\"avatar-fallback\">KM</span></span> <span class=\"avatar\" aria-hidden=\"true\"><img class=\"avatar-image\" src=\"../../../images/avatar-3.svg\" alt=\"\" /><span class=\"avatar-fallback\">RP</span></span> <span class=\"avatar\" aria-hidden=\"true\"><img class=\"avatar-image\" src=\"../../../images/avatar-4.svg\" alt=\"\" /><span class=\"avatar-fallback\">MC</span></span> </div> </div> <div class=\"item-content\"> <p class=\"item-title\">No team members</p> <p class=\"item-description\">Invite your team to collaborate on this project.</p> </div> <div class=\"item-actions\"> <button type=\"button\" class=\"btn btn-outline btn-sm\">Invite</button> </div> </div> </div>"
        },
        {
          "id": "image",
          "title": "Image",
          "text": ".item-media-image crops an image to a rounded square: put an <img> inside it, or use it on the <img> itself. Images next to a title are decorative: alt=\"\". html <div class=\"item-group w-full max-w-md gap-2\" role=\"list\"> <div role=\"listitem\"> <a class=\"item item-outline\" href=\"#\"> <div class=\"item-media item-media-image\"><img src=\"../../../images/landscape.svg\" alt=\"\" /></div> <div class=\"item-content\"> <p class=\"item-title\">Mountain Echoes <span class=\"text-muted-foreground\">· The Ridgeline</span></p> <p class=\"item-description\">High Country</p> </div> <div class=\"item-content text-muted-foreground tabular-nums\">3:42</div> </a> </div> <div role=\"listitem\"> <a class=\"item item-outline\" href=\"#\"> <div class=\"item-media item-media-image\"><img src=\"../../../images/ocean.svg\" alt=\"\" /></div> <div class=\"item-content\"> <p class=\"item-title\">Low Tide <span class=\"text-muted-foreground\">· Harbour Lights</span></p> <p class=\"item-description\">Saltwater</p> </div> <div class=\"item-content text-muted-foreground tabular-nums\">4:15</div> </a> </div> <div role=\"listitem\"> <a class=\"item item-outline\" href=\"#\"> <div class=\"item-media item-media-image\"><img src=\"../../../images/forest.svg\" alt=\"\" /></div> <div class=\"item-content\"> <p class=\"item-title\">Under the Canopy <span class=\"text-muted-foreground\">· Moss &amp; Fern</span></p> <p class=\"item-description\">Evergreen</p> </div> <div class=\"item-content text-muted-foreground tabular-nums\">5:03</div> </a> </div> </div>"
        },
        {
          "id": "group",
          "title": "Group",
          "text": ".item-group stacks items in a list. Give it role=\"list\" and each item role=\"listitem\" (or use a <ul> and <li>). Put an a .separator between items for a line, hidden from screen readers with aria-hidden=\"true\" so the list only holds items; or space them with a gap-* utility. html <div class=\"item-group w-full max-w-md rounded-[calc(var(--radius)+4px)] border border-border bg-surface\" role=\"list\"> <div class=\"item\" role=\"listitem\"> <div class=\"item-media\"><span class=\"avatar\" aria-hidden=\"true\"><img class=\"avatar-image\" src=\"../../../images/avatar-1.svg\" alt=\"\" /><span class=\"avatar-fallback\">AL</span></span></div> <div class=\"item-content\"> <p class=\"item-title\">Ana Lima</p> <p class=\"item-description\">ana@example.com</p> </div> <div class=\"item-actions\"> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm btn-rounded\" aria-label=\"Invite Ana Lima\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5v14\"/></svg></button> </div> </div> <div class=\"separator\" aria-hidden=\"true\"></div> <div class=\"item\" role=\"listitem\"> <div class=\"item-media\"><span class=\"avatar\" aria-hidden=\"true\"><img class=\"avatar-image\" src=\"../../../images/avatar-2.svg\" alt=\"\" /><span class=\"avatar-fallback\">KM</span></span></div> <div class=\"item-content\"> <p class=\"item-title\">Kofi Mensah</p> <p class=\"item-description\">kofi@example.com</p> </div> <div class=\"item-actions\"> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm btn-rounded\" aria-label=\"Invite Kofi Mensah\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M5 12h14M12 5v14\"/></svg></button> </div> </div> <div class=\"separator\" aria-hidden=\"true\"></div> <div class=\"item"
        },
        {
          "id": "header",
          "title": "Header",
          "text": ".item-header is a full-width row above the media and content. An image or video directly inside it fills the width with rounded corners; set its shape with an aspect-* utility. Laid out in a grid, such items make a set of cards. .item-footer is the same below. html <div class=\"item-group grid w-full max-w-2xl grid-cols-1 gap-4 sm:grid-cols-3\" role=\"list\"> <div class=\"item item-outline\" role=\"listitem\"> <div class=\"item-header\"><img class=\"aspect-square\" src=\"../../../images/landscape.svg\" alt=\"\" /></div> <div class=\"item-content\"> <p class=\"item-title\">Summit-1</p> <p class=\"item-description\">Everyday tasks, fast and inexpensive.</p> </div> <div class=\"item-footer\"> <span class=\"badge badge-secondary\">Available</span> <button type=\"button\" class=\"btn btn-ghost btn-sm\">Select</button> </div> </div> <div class=\"item item-outline\" role=\"listitem\"> <div class=\"item-header\"><img class=\"aspect-square\" src=\"../../../images/ocean.svg\" alt=\"\" /></div> <div class=\"item-content\"> <p class=\"item-title\">Tide-2</p> <p class=\"item-description\">Long documents and careful reasoning.</p> </div> <div class=\"item-footer\"> <span class=\"badge badge-secondary\">Available</span> <button type=\"button\" class=\"btn btn-ghost btn-sm\">Select</button> </div> </div> <div class=\"item item-outline\" role=\"listitem\"> <div class=\"item-header\"><img class=\"aspect-square\" src=\"../../../images/forest.svg\" alt=\"\" /></div> <div class=\"item-content\"> <p class=\"item-title\">Canopy-3</p> <p class=\"item-description\">Images and charts alongside text.</p> </div> <div class=\"item-footer\"> <span class=\"badge badge-secondary\">Available</span> <button type=\"button\" class=\"btn btn-ghost btn-sm\">Select</button> </div> </div> </div>"
        },
        {
          "id": "link",
          "title": "Link",
          "text": "When the whole item goes somewhere and has nothing else to click, make the item the link: <a class=\"item\" href=\"…\">. It gets a hover background and a focus outline. The same works on a <button> (which takes the full width), though a button may only contain phrasing content, so use <span>s for its parts. html <div class=\"flex w-full max-w-md flex-col gap-4\"> <a class=\"item\" href=\"#\"> <div class=\"item-content\"> <p class=\"item-title\">Visit our documentation</p> <p class=\"item-description\">Learn how to get started with our components.</p> </div> <div class=\"item-actions\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m9 18 6-6-6-6\"/></svg></div> </a> <a class=\"item item-outline\" href=\"#\" target=\"_blank\" rel=\"noopener noreferrer\"> <div class=\"item-content\"> <p class=\"item-title\">External resource</p> <p class=\"item-description\">Opens in a new tab with security attributes.</p> </div> <div class=\"item-actions\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/></svg></div> </a> </div>"
        },
        {
          "id": "link-with-actions",
          "title": "A link with actions",
          "text": "A link can't contain buttons or other links, so an item that is a link and has actions keeps the item a <div> and puts a link with .item-link around the title. Its ::after stretches over the whole item, so a click anywhere follows the link and the item shows the link's hover and focus. The actions and the footer come after it in the source and stay clickable on top. Put anything interactive there, not in the header. html <div class=\"item item-outline w-full max-w-md\"> <div class=\"item-media item-media-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4M10 9H8M16 13H8M16 17H8\"/></svg></div> <div class=\"item-content\"> <p class=\"item-title\"><a class=\"item-link\" href=\"#\">Quarterly report</a></p> <p class=\"item-description\">Updated 2 hours ago by Ana Lima</p> </div> <div class=\"item-actions\"> <a class=\"btn btn-ghost btn-icon btn-sm\" href=\"#\" aria-label=\"Download Quarterly report\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3\"/></svg></a> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm\" aria-label=\"Share Quarterly report\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z\"/><path d=\"m21.854 2.147-10.94 10.939\"/></svg></button> </div> </div>"
        },
        {
          "id": "dropdown",
          "title": "Dropdown",
          "text": "Items work as entries in a dropdown menu: give each one role=\"menuitem\". Inside .dropdown-menu they take the menu's padding and highlight, and the description shrinks to one line. Use <button> items with <span> parts. html <div class=\"dropdown\" data-dropdown> <button type=\"button\" class=\"btn btn-outline\" data-dropdown-trigger aria-haspopup=\"menu\" aria-expanded=\"false\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\"/><circle cx=\"9\" cy=\"7\" r=\"4\"/><path d=\"M22 21v-2a4 4 0 0 0-3-3.87\"/><path d=\"M16 3.13a4 4 0 0 1 0 7.75\"/></svg> Assign to <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg> </button> <div class=\"dropdown-menu w-72\" role=\"menu\" hidden> <p class=\"dropdown-label\">Team</p> <button type=\"button\" class=\"item item-xs\" role=\"menuitem\"> <span class=\"item-media\"><span class=\"avatar avatar-sm\" aria-hidden=\"true\"><img class=\"avatar-image\" src=\"../../../images/avatar-1.svg\" alt=\"\" /><span class=\"avatar-fallback\">AL</span></span></span> <span class=\"item-content\"> <span class=\"item-title\">Ana Lima</span> <span class=\"item-description\">ana@example.com</span> </span> </button> <button type=\"button\" class=\"item item-xs\" role=\"menuitem\"> <span class=\"item-media\"><span class=\"avatar avatar-sm\" aria-hidden=\"true\"><img class=\"avatar-image\" src=\"../../../images/avatar-2.svg\" alt=\"\" /><span class=\"avatar-fallback\">KM</span></span></span> <span class=\"item-content\"> <span class=\"item-title\">Kofi Mensah</span> <span class=\"item-description\">kofi@example.com</span> </span> </button> <button type=\"button\" class=\"item item-xs\" role=\"menuitem\"> <span class=\"item-media\"><span c"
        },
        {
          "id": "dropdown-in-actions",
          "title": "A menu in the actions",
          "text": "Put a dropdown in .item-actions for the actions that don't fit as buttons. Align its menu to the end with .dropdown-menu-end. html <div class=\"item item-outline w-full max-w-md\"> <div class=\"item-media item-media-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\"/></svg></div> <div class=\"item-content\"> <p class=\"item-title\">Project Atlas</p> <p class=\"item-description\">8 members · Updated yesterday</p> </div> <div class=\"item-actions\"> <div class=\"dropdown\" data-dropdown> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm\" data-dropdown-trigger aria-haspopup=\"menu\" aria-expanded=\"false\" aria-label=\"More actions for Project Atlas\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"12\" cy=\"5\" r=\"1\"/><circle cx=\"12\" cy=\"19\" r=\"1\"/></svg></button> <div class=\"dropdown-menu dropdown-menu-end\" role=\"menu\" hidden> <a class=\"dropdown-item\" role=\"menuitem\" href=\"#\">Open</a> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">Rename</button> <button type=\"button\" class=\"dropdown-item\" role=\"menuitem\">Duplicate</button> <div class=\"dropdown-separator\" role=\"separator\"></div> <button type=\"button\" class=\"dropdown-item dropdown-item-destructive\" role=\"menuitem\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\""
        },
        {
          "id": "htmx",
          "title": "Actions with htmx",
          "text": "Actions are ordinary buttons, so they take hx-* attributes. Here the button posts a setting and the server's reply replaces the status in the description. A server can just as well return whole items, to append to an .item-group or to replace one item with hx-swap=\"outerHTML\". html <div class=\"item item-outline w-full max-w-md\"> <div class=\"item-media item-media-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9\"/><path d=\"M10.3 21a1.94 1.94 0 0 0 3.4 0\"/></svg></div> <div class=\"item-content\"> <p class=\"item-title\">Email notifications</p> <p class=\"item-description\" id=\"item-notify-status\">Off. Turn them on to hear about new comments.</p> </div> <div class=\"item-actions\"> <button type=\"button\" class=\"btn btn-outline btn-sm\" hx-post=\"/api/echo\" hx-vals='{\"notifications\": \"on\"}' hx-target=\"#item-notify-status\">Turn on</button> </div> </div>"
        },
        {
          "id": "accessibility",
          "title": "Accessibility",
          "text": "Group items with role=\"list\" and role=\"listitem\", or <ul> and <li>. Don't put role=\"listitem\" on an <a class=\"item\">: it replaces the link role. Wrap the link in an element with the role instead. Separators inside a list get aria-hidden=\"true\", so the list holds only items; outside a list, use role=\"separator\". Never nest a link or button inside <a class=\"item\">. For a link item with actions, use .item-link on a link around the title. Icon-only actions need an aria-label that names the item: “Invite Ana Lima”, not “Invite”. Media next to a title is decorative: alt=\"\" on images, aria-hidden=\"true\" on avatars. The title is a <p> here. Make it a heading when the items are sections of the page someone would navigate by."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .item A flex row of media, content and actions that wraps on narrow widths. On an <a> or <button>: hover and focus styles. .item-outline / .item-muted A border / a muted background. The default has neither. .item-sm / .item-xs Smaller padding and gaps; -xs also shrinks the media and description. .item-media Media at the start: an icon, an .avatar or an .avatar-group. Aligns with the title when there is a description. .item-media-icon An icon in a bordered, muted square. .item-media-image A rounded square image: an <img> inside it, or the <img> itself. .item-content Title and description; takes the free space. A second one only takes the room it needs. .item-title The title, in medium weight. Holds text, a badge or an .item-link. .item-description Muted text clamped to two lines (one in a menu). Links are underlined. .item-actions Buttons, menus or an icon at the end. .item-header / .item-footer Full-width rows above / below. An image or video directly inside fills the width. .item-link A link whose ::after covers the item, for a link item that also has actions. .item-group A list of items (flex column). Add role=\"list\". .dropdown-menu .item Items with role=\"menuitem\" inside a dropdown take the menu's padding and highlight."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/table",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/table",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/table.md",
      "title": "Table",
      "description": "Readable data tables with a bordered, horizontally scrollable wrapper.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "usage",
          "text": "Usage"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "Rows from the server"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"table-wrap\"> <table class=\"table table-hover\"> <caption>Recent invoices</caption> <thead> <tr><th>Invoice</th><th>Status</th><th>Method</th><th class=\"text-right\">Amount</th></tr> </thead> <tbody> <tr> <td class=\"font-medium\">INV-001</td> <td><span class=\"badge badge-success\">Paid</span></td> <td>Credit card</td> <td class=\"text-right tabular-nums\">$250.00</td> </tr> <tr> <td class=\"font-medium\">INV-002</td> <td><span class=\"badge badge-warning\">Pending</span></td> <td>Bank transfer</td> <td class=\"text-right tabular-nums\">$150.00</td> </tr> <tr> <td class=\"font-medium\">INV-003</td> <td><span class=\"badge badge-danger\">Overdue</span></td> <td>Credit card</td> <td class=\"text-right tabular-nums\">$350.00</td> </tr> <tr> <td class=\"font-medium\">INV-004</td> <td><span class=\"badge badge-success\">Paid</span></td> <td>PayPal</td> <td class=\"text-right tabular-nums\">$450.00</td> </tr> </tbody> </table> </div>"
        },
        {
          "id": "usage",
          "title": "Usage",
          "text": "Wrap the table in .table-wrap for the border, rounded corners and horizontal scrolling on narrow screens. Add .table-hover to highlight rows under the pointer. Align numbers with text-right tabular-nums. A <caption> renders below the table as muted text."
        },
        {
          "id": "htmx",
          "title": "Rows from the server",
          "text": "Have the server return <tr> fragments and append them to the <tbody> with hx-swap=\"beforeend\". Badges in the new rows are plain classes, so they need no initialisation. html <div class=\"table-wrap\"> <table class=\"table\"> <thead><tr><th>Invoice</th><th>Status</th><th class=\"text-right\">Amount</th></tr></thead> <tbody id=\"invoice-rows\"> <tr><td class=\"font-medium\">INV-001</td><td><span class=\"badge badge-success\">Paid</span></td><td class=\"text-right tabular-nums\">$250.00</td></tr> </tbody> </table> </div> <button class=\"btn btn-outline btn-sm mt-4\" hx-get=\"/api/invoices\" hx-target=\"#invoice-rows\" hx-swap=\"beforeend\">Load more</button>"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .table-wrap Bordered, rounded, horizontally scrollable wrapper. .table Base table styles: header row, cell padding, row dividers. .table-hover Highlights the row under the pointer."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/timeline",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/timeline",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/timeline.md",
      "title": "Timeline",
      "description": "A list of events in chronological order along a line: dots, icons or avatars, colours and sizes, vertical or horizontal, on one side or alternating.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "markers",
          "text": "Markers"
        },
        {
          "level": 2,
          "id": "steps",
          "text": "Progress and the current step"
        },
        {
          "level": 2,
          "id": "content",
          "text": "Rich content"
        },
        {
          "level": 2,
          "id": "opposite",
          "text": "Opposite content"
        },
        {
          "level": 2,
          "id": "alternate",
          "text": "Alternating"
        },
        {
          "level": 2,
          "id": "horizontal",
          "text": "Horizontal"
        },
        {
          "level": 2,
          "id": "line",
          "text": "Line style"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Sizes"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx"
        },
        {
          "level": 2,
          "id": "customise",
          "text": "Customising"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <ol class=\"timeline max-w-md\"> <li class=\"timeline-item\"> <span class=\"timeline-marker timeline-marker-primary\"></span> <div class=\"timeline-content\"> <time class=\"timeline-time\" datetime=\"2026-10-07\">7 October 2026</time> <h3 class=\"timeline-title\">Version 1.0 released</h3> <p class=\"timeline-description\">Stable APIs for every component, the engine and the plugins.</p> </div> </li> <li class=\"timeline-item\"> <span class=\"timeline-marker\"></span> <div class=\"timeline-content\"> <time class=\"timeline-time\" datetime=\"2026-08-14\">14 August 2026</time> <h3 class=\"timeline-title\">Release candidate</h3> <p class=\"timeline-description\">Feature freeze, and a public call for testing.</p> </div> </li> <li class=\"timeline-item\"> <span class=\"timeline-marker\"></span> <div class=\"timeline-content\"> <time class=\"timeline-time\" datetime=\"2026-05-02\">2 May 2026</time> <h3 class=\"timeline-title\">First public beta</h3> <p class=\"timeline-description\">Forty components and the docs site.</p> </div> </li> </ol>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "An <ol class=\"timeline\"> of <li class=\"timeline-item\">: a list in order, which is what screen readers announce. It is CSS only, with no behaviour. Each item has a .timeline-marker and a .timeline-content. Inside the content, .timeline-time (a <time datetime>), .timeline-title and .timeline-description are all optional; any other markup works too. The line to the next item is drawn for you, and stops at the last item. Newest first or oldest first is up to you: the timeline shows items in the order they are written."
        },
        {
          "id": "markers",
          "title": "Markers",
          "text": "An empty .timeline-marker is a dot. .timeline-marker-primary, -success, -warning, -danger and -info colour it, and .timeline-marker-outline makes it hollow. html <ol class=\"timeline\"> <li class=\"timeline-item\"><span class=\"timeline-marker\"></span><div class=\"timeline-content\"><p class=\"timeline-title\">Default</p></div></li> <li class=\"timeline-item\"><span class=\"timeline-marker timeline-marker-primary\"></span><div class=\"timeline-content\"><p class=\"timeline-title\">Primary</p></div></li> <li class=\"timeline-item\"><span class=\"timeline-marker timeline-marker-success\"></span><div class=\"timeline-content\"><p class=\"timeline-title\">Success</p></div></li> <li class=\"timeline-item\"><span class=\"timeline-marker timeline-marker-warning\"></span><div class=\"timeline-content\"><p class=\"timeline-title\">Warning</p></div></li> <li class=\"timeline-item\"><span class=\"timeline-marker timeline-marker-danger\"></span><div class=\"timeline-content\"><p class=\"timeline-title\">Danger</p></div></li> <li class=\"timeline-item\"><span class=\"timeline-marker timeline-marker-info\"></span><div class=\"timeline-content\"><p class=\"timeline-title\">Info</p></div></li> </ol> <ol class=\"timeline\"> <li class=\"timeline-item\"><span class=\"timeline-marker timeline-marker-outline\"></span><div class=\"timeline-content\"><p class=\"timeline-title\">Default</p></div></li> <li class=\"timeline-item\"><span class=\"timeline-marker timeline-marker-outline timeline-marker-primary\"></span><div class=\"timeline-content\"><p class=\"timeline-title\">Primary</p></div></li> <li class=\"timeline-item\"><span class=\"timeline-marker timeline-marker-outline timeline-marker-success\"></span><div class=\"timeline-content\"><p class=\"timeline-title\">Success</p></div></li> <li class=\"timeline-item\"><span class=\"timeline-marker timeline-marker-outline timeline-marker-warning\"></span><div class=\"timeline-content\"><p class=\"timeline-title\">Warning</p></div></li> <li class=\"timeline-item\"><span class=\"timeline-marker timeline-marker-outline timeline-"
        },
        {
          "id": "steps",
          "title": "Progress and the current step",
          "text": "For a process (an order, a deployment, an application), colour the steps that are done, mark the current item with aria-current=\"step\", which adds a ring and tells screen readers where it is, and leave the rest as outline markers. Numbers work as markers too. html <ol class=\"timeline\"> <li class=\"timeline-item\"> <span class=\"timeline-marker timeline-marker-success\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span> <div class=\"timeline-content\"> <h3 class=\"timeline-title\">Order placed</h3> <time class=\"timeline-time\" datetime=\"2026-10-03T09:12\">3 Oct, 9:12 am</time> </div> </li> <li class=\"timeline-item\"> <span class=\"timeline-marker timeline-marker-success\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z\"/><path d=\"m3.3 7 8.7 5 8.7-5M12 22V12\"/></svg></span> <div class=\"timeline-content\"> <h3 class=\"timeline-title\">Packed</h3> <time class=\"timeline-time\" datetime=\"2026-10-04T14:40\">4 Oct, 2:40 pm</time> </div> </li> <li class=\"timeline-item\" aria-current=\"step\"> <span class=\"timeline-marker timeline-marker-primary\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2\"/><path d=\"M15 18H9\"/><path d=\"M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.624l-3.48-4.35A1 1 0 0 0 17.52 8H14\"/><circle cx=\"17\" cy=\"18\" r=\"2\"/><circle cx=\"7\" cy=\"18\" r=\"2\"/></svg></span"
        },
        {
          "id": "content",
          "title": "Rich content",
          "text": "The content takes any markup: a card, an alert, buttons. html <ol class=\"timeline w-full max-w-lg\"> <li class=\"timeline-item\"> <span class=\"timeline-marker timeline-marker-danger\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4M12 17h.01\"/></svg></span> <div class=\"timeline-content\"> <h3 class=\"timeline-title\">Deploy failed</h3> <time class=\"timeline-time\" datetime=\"2026-10-07T10:05\">10:05 am</time> <div class=\"alert alert-danger mt-2\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m15 9-6 6M9 9l6 6\"/></svg> <div class=\"alert-title\">Build step exited with code 1</div> <div class=\"alert-description\">Cannot find module \"htmx-ui/timeline\". <a href=\"#content\">View logs</a></div> </div> </div> </li> <li class=\"timeline-item\"> <span class=\"timeline-marker\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12\"/></svg></span> <div class=\"timeline-content\"> <h3 class=\"timeline-title\">Deploy started</h3> <time class=\"timeline-time\" datetime=\"2026-10-07T10:03\">10:03 am</time> <div class=\"card mt-2 gap-2 p-4 text-sm\"> <p class=\"m-0 font-mono text-xs text-muted-foreground\">main · 02e12f4</p> <p class=\"m-0\">docs: 0.2.0 changelog and contributor guides</p> </div> </div> </li> </ol>"
        },
        {
          "id": "opposite",
          "title": "Opposite content",
          "text": "A .timeline-opposite in every item goes on the other side of the line, right-aligned in a column --timeline-opposite wide (6rem): dates, times or amounts that read down one edge. html <ol class=\"timeline max-w-md\"> <li class=\"timeline-item\"> <span class=\"timeline-marker timeline-marker-primary\"></span> <div class=\"timeline-content\"><h3 class=\"timeline-title\">Keynote</h3><p class=\"timeline-description\">Main hall</p></div> <time class=\"timeline-opposite\" datetime=\"09:00\">9:00 am</time> </li> <li class=\"timeline-item\"> <span class=\"timeline-marker\"></span> <div class=\"timeline-content\"><h3 class=\"timeline-title\">Building with htmx 4</h3><p class=\"timeline-description\">Room 2</p></div> <time class=\"timeline-opposite\" datetime=\"10:30\">10:30 am</time> </li> <li class=\"timeline-item\"> <span class=\"timeline-marker\"></span> <div class=\"timeline-content\"><h3 class=\"timeline-title\">Lunch</h3></div> <time class=\"timeline-opposite\" datetime=\"12:00\">12:00 pm</time> </li> </ol>"
        },
        {
          "id": "alternate",
          "title": "Alternating",
          "text": ".timeline-alternate puts the line in the middle and the items on either side of it in turn, with the .timeline-opposite, if any, across from each one. On narrow screens (below the sm breakpoint) it falls back to one side, so nothing gets squeezed. html <ol class=\"timeline timeline-alternate w-full max-w-2xl\"> <li class=\"timeline-item\"> <span class=\"timeline-marker timeline-marker-primary\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12 3l1.9 5.8L20 11l-6.1 2.2L12 19l-1.9-5.8L4 11l6.1-2.2z\"/><path d=\"M19 3v4M17 5h4\"/></svg></span> <div class=\"timeline-content\"> <h3 class=\"timeline-title\">Idea</h3> <p class=\"timeline-description\">A component library for htmx with no React and no build step for users.</p> </div> <time class=\"timeline-opposite\" datetime=\"2025-11\">November 2025</time> </li> <li class=\"timeline-item\"> <span class=\"timeline-marker\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m16 18 6-6-6-6M8 6l-6 6 6 6\"/></svg></span> <div class=\"timeline-content\"> <h3 class=\"timeline-title\">First components</h3> <p class=\"timeline-description\">Buttons, inputs, dialogs and tabs, styled with Tailwind v4.</p> </div> <time class=\"timeline-opposite\" datetime=\"2026-01\">January 2026</time> </li> <li class=\"timeline-item\"> <span class=\"timeline-marker\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2zM22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z\"/></svg></span> <div class=\"timeline-content\"> <h3 class=\"timeline-title\">"
        },
        {
          "id": "horizontal",
          "title": "Horizontal",
          "text": ".timeline-horizontal lays the items out in a row, each at least --timeline-min (10rem) wide, scrolling sideways when they don't fit. A .timeline-opposite goes above the line. html <ol class=\"timeline timeline-horizontal w-full\"> <li class=\"timeline-item\"> <span class=\"timeline-marker timeline-marker-success\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span> <div class=\"timeline-content\"><h3 class=\"timeline-title\">Plan</h3><p class=\"timeline-description\">Scope and estimates</p></div> <span class=\"timeline-opposite\">Week 1</span> </li> <li class=\"timeline-item\"> <span class=\"timeline-marker timeline-marker-success\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span> <div class=\"timeline-content\"><h3 class=\"timeline-title\">Design</h3><p class=\"timeline-description\">Mockups signed off</p></div> <span class=\"timeline-opposite\">Week 2</span> </li> <li class=\"timeline-item\" aria-current=\"step\"> <span class=\"timeline-marker timeline-marker-primary\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m16 18 6-6-6-6M8 6l-6 6 6 6\"/></svg></span> <div class=\"timeline-content\"><h3 class=\"timeline-title\">Build</h3><p class=\"timeline-description\">In progress</p></div> <span class=\"timeline-opposite\">Weeks 3–5</span> </li> <li class=\"timeline-item\"> <span class=\"timeline-marker timeline-marker-outline\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"current"
        },
        {
          "id": "line",
          "title": "Line style",
          "text": ".timeline-dashed and .timeline-dotted change the line, vertical or horizontal. html <ol class=\"timeline timeline-dashed\"> <li class=\"timeline-item\"><span class=\"timeline-marker timeline-marker-outline\"></span><div class=\"timeline-content\"><p class=\"timeline-title\">Dashed</p></div></li> <li class=\"timeline-item\"><span class=\"timeline-marker timeline-marker-outline\"></span><div class=\"timeline-content\"><p class=\"timeline-title\">Second</p></div></li> <li class=\"timeline-item\"><span class=\"timeline-marker timeline-marker-outline\"></span><div class=\"timeline-content\"><p class=\"timeline-title\">Third</p></div></li> </ol> <ol class=\"timeline timeline-dotted\"> <li class=\"timeline-item\"><span class=\"timeline-marker\"></span><div class=\"timeline-content\"><p class=\"timeline-title\">Dotted</p></div></li> <li class=\"timeline-item\"><span class=\"timeline-marker\"></span><div class=\"timeline-content\"><p class=\"timeline-title\">Second</p></div></li> <li class=\"timeline-item\"><span class=\"timeline-marker\"></span><div class=\"timeline-content\"><p class=\"timeline-title\">Third</p></div></li> </ol>"
        },
        {
          "id": "sizes",
          "title": "Sizes",
          "text": ".timeline-sm and .timeline-lg scale the markers, the text and the spacing together. html <ol class=\"timeline timeline-sm\"> <li class=\"timeline-item\"> <span class=\"timeline-marker timeline-marker-primary\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span> <div class=\"timeline-content\"><h3 class=\"timeline-title\">Small</h3><p class=\"timeline-description\">Signed up</p></div> </li> <li class=\"timeline-item\"> <span class=\"timeline-marker\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"20\" height=\"16\" x=\"2\" y=\"4\" rx=\"2\"/><path d=\"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7\"/></svg></span> <div class=\"timeline-content\"><h3 class=\"timeline-title\">Email sent</h3><p class=\"timeline-description\">Welcome</p></div> </li> <li class=\"timeline-item\"> <span class=\"timeline-marker\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/></svg></span> <div class=\"timeline-content\"><h3 class=\"timeline-title\">Profile</h3><p class=\"timeline-description\">Completed</p></div> </li> </ol> <ol class=\"timeline\"> <li class=\"timeline-item\"> <span class=\"timeline-marker timeline-marker-primary\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span> <div class=\"timeline-content\"><h3 clas"
        },
        {
          "id": "htmx",
          "title": "With htmx",
          "text": "A timeline is a list, so loading more events is appending items. Here a button at the end fetches the next page and replaces itself with it (hx-swap=\"outerHTML\"), the response being more <li class=\"timeline-item\">s and a new button: activity.html <ol class=\"timeline\"> <li class=\"timeline-item\">…</li> <li class=\"timeline-item\"> <span class=\"timeline-marker\"></span> <div class=\"timeline-content\"> <button class=\"btn btn-outline btn-sm\" hx-get=\"/activity?page=2\" hx-target=\"closest li\" hx-swap=\"outerHTML\">Load older</button> </div> </li> </ol>"
        },
        {
          "id": "customise",
          "title": "Customising",
          "text": "Every measurement is a custom property on .timeline; set them with Tailwind's arbitrary properties or your own CSS. Property Description --timeline-dot Dot size (0.75rem). --timeline-icon Circle size for icon, text and image markers (2rem). --timeline-line Line thickness (2px). --timeline-gap Between the marker and the content (0.75rem). --timeline-space Between items (1.5rem). --timeline-opposite Width of the .timeline-opposite column, vertical (6rem). --timeline-min Narrowest item, horizontal (10rem)."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class / attribute Description .timeline The list: an <ol>. .timeline-item One event: an <li>. aria-current=\"step\" rings its marker. .timeline-marker Empty: a dot. With an icon, text or image: a circle. .timeline-marker-primary / -success / -warning / -danger / -info Marker colours. .timeline-marker-outline A hollow marker, in its colour. .timeline-content The event: .timeline-time, .timeline-title, .timeline-description, or anything. .timeline-opposite Across the line from the content. .timeline-alternate Items on both sides of the line in turn. .timeline-horizontal Items in a row. .timeline-dashed / .timeline-dotted Line style. .timeline-sm / .timeline-lg Sizes."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/code-block",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/code-block",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/code-block.md",
      "title": "Code block",
      "description": "Syntax-highlighted source with a copy button, file tabs, and package-manager tabs that remember the reader's choice. Every snippet in these docs uses it.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "highlighting",
          "text": "Syntax highlighting"
        },
        {
          "level": 2,
          "id": "package-managers",
          "text": "Package-manager commands"
        },
        {
          "level": 2,
          "id": "file-tabs",
          "text": "File tabs"
        },
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "macros",
          "text": "Nunjucks macros"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "src/app.ts import { initComponents } from \"htmx-ui\"; // Wire up components inside content htmx inserts document.addEventListener(\"htmx:after:process\", (e) => { initComponents((e.target as ParentNode) ?? document); });"
        },
        {
          "id": "highlighting",
          "title": "Syntax highlighting",
          "text": "Code is highlighted at build time by Shiki, so pages ship coloured HTML and no highlighting JavaScript. Supported languages: html, css, ts/js, json, bash, toml, md, xml/svg and jinja (Nunjucks). Anything else is shown escaped, uncoloured. Colours are CSS variables, --code-token-keyword, --code-token-string and so on, defined for light and dark next to the other tokens. Override them to restyle every code block. src/styles.css :root { --code-token-keyword: var(--color-fuchsia-600); --code-token-string: var(--color-teal-700); }"
        },
        {
          "id": "package-managers",
          "title": "Package-manager commands",
          "text": "Install commands come in bun, npm, pnpm and yarn flavours. Switch tabs here and every command block on every page follows. The choice is saved in localStorage and restored on your next visit. bun add htmx-ui htmx.org bun add -d tailwindcss npm install htmx-ui htmx.org npm install -D tailwindcss pnpm add htmx-ui htmx.org pnpm add -D tailwindcss yarn add htmx-ui htmx.org yarn add -D tailwindcss With the Nunjucks macro, write each line with an action and it's translated for each manager: page.html {% from \"components/code/code.html\" import cli %} {{ cli(\"add htmx-ui htmx.org add-dev tailwindcss run dev\") }} Action Description init npm init -y · pnpm init · yarn init -y · bun init -y install npm install · pnpm install · yarn install · bun install add npm install · pnpm add · yarn add · bun add add-dev npm install -D · pnpm add -D · yarn add -D · bun add -d run npm run · pnpm run · yarn run · bun run exec npx · pnpm dlx · yarn dlx · bunx create npm create · pnpm create · yarn create · bun create anything else Shown unchanged on every tab, e.g. mkdir my-app && cd my-app The commands live in htmx-ui/components/code/package-managers.json; add a manager or change the default there."
        },
        {
          "id": "file-tabs",
          "title": "File tabs",
          "text": "Show related files side by side: <button class=\"btn btn-primary\" hx-post=\"/save\">Save</button> @layer components { .btn { @apply font-semibold; } }"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "The copy button is an icon-only clipboard button with data-clipboard-target=\"pre:not([hidden]) code\": the clipboard copies the nearest match, which is the visible <code> in its own block, and shows a green tick for two seconds. .code-copy mutes it until hovered; without a header it floats over the top-right corner of the code. html <div class=\"code-block\" data-code> <div class=\"code-block-header\"> <span class=\"font-mono\">app.ts</span> <button type=\"button\" class=\"btn btn-ghost btn-xs btn-icon clipboard code-copy\" data-clipboard data-clipboard-target=\"pre:not([hidden]) code\" aria-label=\"Copy code\"> <span class=\"clipboard-idle\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg></span> <span class=\"clipboard-done\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span> </button> </div> <pre><code>initComponents(document);</code></pre> </div> For tabs, put data-tabs on the block (plus data-tabs-sync=\"key\" to sync), .tabs-trigger buttons with data-tab in a .tabs-list.tabs-line.code-tabs strip in the header (underlined tabs, fitted to it), and one <pre data-tab-panel> per tab. See Tabs."
        },
        {
          "id": "macros",
          "title": "Nunjucks macros",
          "text": "page.html {% from \"components/code/code.html\" import code, code_block, code_tabs, cli %} {% call code(\"html\", title=\"index.html\") %} <button class=\"btn btn-primary\">Save</button> {% endcall %} {{ code_block(some_string, \"ts\") }} {{ code_tabs([{ id: \"a\", label: \"a.ts\", lang: \"ts\", source: a }], sync=\"files\") }} Bodies are de-indented and escaped. Wrap Nunjucks syntax in {% raw %} so it's shown, not run."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .code-block Container: border, code background and the <pre> inside. .code-block-header Top bar for a file name, tabs and the copy button. .code-copy On the copy button (an icon-only .clipboard): muted until hovered. .code-tabs On a .tabs-list.tabs-line of .tabs-trigger buttons: the tab strip, fitted to the header. data-clipboard-target=\"pre:not([hidden]) code\" On the copy button: copies the visible <code> of its block. --code-background / --code-foreground / --code-token-* Highlighting colours (CSS variables)."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/icon",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/icon",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/icon.md",
      "title": "Icon",
      "description": "SVG icons stored as files in htmx-ui/icons and inlined into the HTML at build time, so they inherit colour and need no requests. They can also be linked as images.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "usage",
          "text": "Usage"
        },
        {
          "level": 2,
          "id": "inline-vs-img",
          "text": "Inline or image"
        },
        {
          "level": 2,
          "id": "any-svg",
          "text": "Any SVG"
        },
        {
          "level": 2,
          "id": "adding",
          "text": "Adding an icon"
        },
        {
          "level": 2,
          "id": "all",
          "text": "All icons"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-6\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"4\"/><path d=\"M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41\"/></svg> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-6\" aria-hidden=\"true\"><path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\"/></svg> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-6 text-primary\" aria-hidden=\"true\"><path d=\"M13 2 3 14h9l-1 8 10-12h-9l1-8z\"/></svg> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-6 text-success\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m9 12 2 2 4-4\"/></svg>"
        },
        {
          "id": "usage",
          "title": "Usage",
          "text": "Icons are 24×24 Lucide-style SVGs drawn with stroke=\"currentColor\", so they take the text colour. Size them with Tailwind (size-4, size-5…). The macro inlines the file's <svg> and adds your class: page.html {% from \"components/icon/icon.html\" import icon %} {{ icon(\"sun\") }} {# decorative: aria-hidden #} {{ icon(\"info\", \"size-5\", label=\"Note\") }} {# meaningful: role=\"img\" + aria-label #} {{ icon(\"sun\", mode=\"img\") }} {# <img> pointing at a copy of the file #}"
        },
        {
          "id": "inline-vs-img",
          "title": "Inline or image",
          "text": "Inline (default) mode=\"img\" Colour Inherits text colour (text-primary works) Fixed black stroke; the macro adds dark:invert Requests None, part of the HTML One per icon; the file is content-hashed, so it caches forever Use when Almost always Many repeats of a large SVG, or linking from elsewhere html <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-6 text-primary\" aria-hidden=\"true\"><path d=\"M12 22a10 10 0 1 1 10-10c0 2.76-2.24 4-5 4h-1.5a1.5 1.5 0 0 0-1 2.6A1.5 1.5 0 0 1 12 22Z\"/><circle cx=\"7.5\" cy=\"11.5\" r=\"1\"/><circle cx=\"10.5\" cy=\"7.5\" r=\"1\"/><circle cx=\"15.5\" cy=\"8.5\" r=\"1\"/></svg> <img src=\"../../../../packages/ui/src/icons/palette.svg\" class=\"size-6 dark:invert\" width=\"24\" height=\"24\" alt=\"\" aria-hidden=\"true\" /> In img mode the bundler copies the SVG next to the page with a content hash in its name (sun-a1b2c3.svg), so a changed icon is never served stale. Separately, this site's build copies the package's icons/ to dist/assets/icons/ with unchanged names. /assets/icons/<name>.svg is therefore a stable URL for linking from other sites, emails or docs, e.g. /assets/icons/sun.svg."
        },
        {
          "id": "any-svg",
          "title": "Any SVG",
          "text": "The svg() template function inlines any SVG file under a template root (your project, then htmx-ui), setting or overriding attributes on its root element: jinja {{ svg(\"images/logo.svg\", { class: \"h-8 w-auto\", \"aria-label\": \"HTMX UI\", role: \"img\" }) }}"
        },
        {
          "id": "adding",
          "title": "Adding an icon",
          "text": "Save a 24×24 SVG with stroke=\"currentColor\" (or fill=\"currentColor\") as icons/<name>.svg in your project; your file wins over a package icon of the same name. Lucide icons drop straight in. Use it: {{ icon(\"<name>\") }}. A misspelt name fails the build."
        },
        {
          "id": "all",
          "title": "All icons",
          "text": "94 icons, listed straight from the package's icons/ with glob(): Icon names: alert-triangle, align-center, align-left, align-right, arrow-down, arrow-left, arrow-right, arrow-up-right, arrow-up, asterisk, badge-check, bar-chart, bell, bold, book, bookmark, bot, box, calendar, check-circle, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-left, chevrons-right, chevrons-up-down, circle-alert, circle, cloud, code, command, copy, corner-down-left, credit-card, download, ellipsis-vertical, ellipsis, external-link, eye-off, eye, file-text, file, folder-open, folder-plus, folder, git-commit, history, house, image, inbox, info, italic, layers, link, loader-spokes, loader, log-in, log-out, mail, menu, message-square, minus, monitor, moon, music, palette, panel-left, panel-right, paperclip, play, plus, refresh-cw, search, send, server, settings, shield-check, slash, sparkles, star, sun, terminal, trash, truck, underline, upload, user, users, video, x-circle, x, zap"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Macro / function Description icon(name, class=\"size-4\") Inline icons/<name>.svg with a class. aria-hidden by default. icon(name, label=\"…\") Meaningful icon: role=\"img\" and aria-label instead of aria-hidden. icon(name, mode=\"img\") <img> of a hashed copy of the file instead of inlining. svg(path, attrs) Inline any SVG under a template root, setting attributes on the root <svg>."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/kbd",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/kbd",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/kbd.md",
      "title": "Kbd",
      "description": "Shows keyboard input: a single key or a shortcut, on its own or inside buttons, tooltips and input groups.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "group",
          "text": "Group"
        },
        {
          "level": 2,
          "id": "button",
          "text": "Button"
        },
        {
          "level": 2,
          "id": "tooltip",
          "text": "Tooltip"
        },
        {
          "level": 2,
          "id": "input-group",
          "text": "Input group"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <kbd class=\"kbd-group\"> <kbd class=\"kbd\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3\"/></svg></kbd> <kbd class=\"kbd\">Shift</kbd> <kbd class=\"kbd\">Alt</kbd> <kbd class=\"kbd\">Ctrl</kbd> </kbd> <kbd class=\"kbd-group\"> <kbd class=\"kbd\">Ctrl</kbd> <span>+</span> <kbd class=\"kbd\">B</kbd> </kbd>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "A key is a <kbd class=\"kbd\">. It holds text (Esc, K, ↵) or an icon, which is sized to the key. A shortcut of several keys is a .kbd-group around them, with any text between: +, then. Make the group a <kbd> too, as HTML nests <kbd> for a key combination. Give an icon key a name for screen readers when the symbol alone could be misread: icon(\"command\", label=\"Command\"). Inside .prose, a bare <kbd> is already styled by the text styles; use .kbd in UI."
        },
        {
          "id": "group",
          "title": "Group",
          "text": "Keys in a sentence: .kbd-group keeps a shortcut together and spaces its keys evenly. html <p class=\"text-sm text-muted-foreground\"> Use <kbd class=\"kbd-group\"><kbd class=\"kbd\">Ctrl</kbd><span>+</span><kbd class=\"kbd\">B</kbd></kbd> or <kbd class=\"kbd-group\"><kbd class=\"kbd\">Ctrl</kbd><span>+</span><kbd class=\"kbd\">K</kbd></kbd> to open the command palette. </p>"
        },
        {
          "id": "button",
          "title": "Button",
          "text": "A key at the end of a button shows its shortcut. On filled buttons it becomes a translucent chip of the button's text colour; in small buttons it shrinks with them. html <button class=\"btn btn-outline\">Accept <kbd class=\"kbd\">↵</kbd></button> <button class=\"btn btn-outline btn-sm\">Cancel <kbd class=\"kbd\">Esc</kbd></button> <button class=\"btn btn-primary\">Save <kbd class=\"kbd-group\"><kbd class=\"kbd\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3\"/></svg></kbd><kbd class=\"kbd\">S</kbd></kbd></button> <button class=\"btn btn-danger btn-sm\">Delete <kbd class=\"kbd\">Del</kbd></button>"
        },
        {
          "id": "tooltip",
          "title": "Tooltip",
          "text": "In a tooltip the key inverts with it, so it reads in both themes. html <div class=\"btn-group\"> <span class=\"tooltip\" data-tooltip> <button class=\"btn btn-outline btn-sm\">Save</button> <span class=\"tooltip-content\">Save changes <kbd class=\"kbd\">S</kbd></span> </span> <span class=\"tooltip\" data-tooltip> <button class=\"btn btn-outline btn-sm\">Print</button> <span class=\"tooltip-content\">Print document <kbd class=\"kbd-group\"><kbd class=\"kbd\">Ctrl</kbd><kbd class=\"kbd\">P</kbd></kbd></span> </span> </div>"
        },
        {
          "id": "input-group",
          "title": "Input group",
          "text": "A key in an input group addon hints at the shortcut that focuses the field, like the search box at the top of this page. html <div class=\"input-group max-w-xs\"> <input class=\"input\" type=\"search\" placeholder=\"Search…\" aria-label=\"Search\" /> <span class=\"input-group-addon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg></span> <span class=\"input-group-addon input-group-addon-inline-end\"> <kbd class=\"kbd-group\"><kbd class=\"kbd\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3\"/></svg></kbd><kbd class=\"kbd\">K</kbd></kbd> </span> </div>"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .kbd One key: text or an icon. .kbd-group Keys of one shortcut, with optional text between them."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/text",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/text",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/text.md",
      "title": "Text",
      "description": "Style long-form content without a class on every element: wrap it in .prose from @tailwindcss/typography, tuned to htmx-ui's tokens. Small helper classes cover single elements in UI chrome.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "prose",
          "text": "Prose"
        },
        {
          "level": 2,
          "id": "customisation",
          "text": "What htmx-ui changes"
        },
        {
          "level": 2,
          "id": "components",
          "text": "Components inside prose"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Sizes"
        },
        {
          "level": 2,
          "id": "custom",
          "text": "Customising further"
        },
        {
          "level": 2,
          "id": "helpers",
          "text": "Single elements"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "prose",
          "title": "Prose",
          "text": "Wrap plain HTML (an article, Markdown output, CMS content, a server-rendered fragment) in .prose. Every heading, paragraph, list, link, quote, table and code element inside is styled. No per-element classes. html <article class=\"prose\"> <h2>Shipping hypermedia</h2> <p class=\"lead\">Your server already renders HTML. htmx swaps it in; htmx-ui makes it look good.</p> <p>Return a fragment from any endpoint, point <code>hx-target</code> at it, and you're done. See <a href=\"/docs/installation\">installation</a>.</p> <ul> <li>Plain <strong>HTML</strong> in, styled content out</li> <li>Works with Markdown renderers and CMS output</li> </ul> <blockquote><p>Hypermedia is the engine of application state.</p></blockquote> <p>Press <kbd>Ctrl</kbd> + <kbd>K</kbd> to search.</p> </article> .prose comes from @tailwindcss/typography, which htmx-ui/styles.css loads for you. Everything in its docs works: sizes (prose-sm, prose-lg), element modifiers (prose-a:text-primary, prose-headings:tracking-tight), and max-w-none to drop the default line-length limit."
        },
        {
          "id": "customisation",
          "title": "What htmx-ui changes",
          "text": "Colours come from the tokens: body and headings use --foreground, links --primary, rules and table borders --border, code blocks --code-background. Prose therefore follows custom themes and dark mode on its own. Don't add dark:prose-invert. Inline code is a pill on a muted background instead of being wrapped in backticks. Links get a soft underline that firms up on hover. Headings get scroll-margin-top so #anchor links clear a sticky header. Components placed inside prose with not-prose (alerts, code blocks, tables) get the same vertical spacing as paragraphs."
        },
        {
          "id": "components",
          "title": "Components inside prose",
          "text": "Prose styles every element inside it, which would also restyle a component's markup. Add not-prose to a component to keep its own styles. htmx-ui's Nunjucks macros (code, alert) already do. The flip side: .prose placed inside a not-prose element gets no styles at all, so don't nest them that way. html <article class=\"prose\"> <p>Before you deploy:</p> <div class=\"alert alert-warning not-prose\" role=\"status\"> <div class=\"alert-title\">Back up your database</div> </div> <p>Then run the migration.</p> </article>"
        },
        {
          "id": "sizes",
          "title": "Sizes",
          "text": "html <div class=\"prose prose-sm\"><p><strong>prose-sm</strong>: for sidebars, cards and dense UIs.</p></div> <div class=\"prose\"><p><strong>prose</strong>: the default, for articles and docs.</p></div> <div class=\"prose prose-lg\"><p><strong>prose-lg</strong>: for landing-page copy.</p></div>"
        },
        {
          "id": "custom",
          "title": "Customising further",
          "text": "Tweak one block with element modifiers, or change prose everywhere by adding to the prose utility: html <article class=\"prose prose-headings:font-bold prose-a:no-underline\">…</article> src/styles.css @utility prose { --tw-prose-bullets: var(--primary); & :where(h2):not(:where([class~=\"not-prose\"] *)) { border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; } }"
        },
        {
          "id": "helpers",
          "title": "Single elements",
          "text": "UI chrome (a page title, a hint under a form field) isn't long-form content. For one-off elements outside prose, use these helpers: html <p class=\"eyebrow\">Settings</p> <h1 class=\"h1\">Account</h1> <p class=\"lead\">Manage your profile and preferences.</p> <h3 class=\"h3\">Danger zone</h3> <p class=\"muted\">Deleting your account can't be undone.</p> <p>Read the <a class=\"link\" href=\"/docs/theming\">theming guide</a>, run <code class=\"code\">bun run dev</code>, press <kbd class=\"kbd\">Esc</kbd>.</p> Which one? More than a couple of text elements in a row is content: use .prose. A single label or title in a layout: use a helper."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .prose Long-form content (@tailwindcss/typography), mapped to htmx-ui tokens. .prose-sm / .prose-lg Smaller / larger prose. .not-prose Opt a subtree (usually a component) out of prose styles. .h1 / .h2 / .h3 / .h4 Heading scale for single titles outside prose. .lead Intro paragraph: larger, muted. .muted Small secondary text. .eyebrow Small primary-coloured label above a heading. .link Underlined link in the primary colour. .code Inline code outside prose. .kbd Keyboard key outside prose; see Kbd for groups and keys in buttons and tooltips."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/attachment",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/attachment",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/attachment.md",
      "title": "Attachment",
      "description": "Displays a file or image attachment with media, metadata, upload state and actions.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "parts",
          "text": "Parts"
        },
        {
          "level": 2,
          "id": "states",
          "text": "Upload states"
        },
        {
          "level": 2,
          "id": "sizes",
          "text": "Sizes"
        },
        {
          "level": 2,
          "id": "orientation",
          "text": "Orientation"
        },
        {
          "level": 2,
          "id": "variants",
          "text": "Image, video, audio and file"
        },
        {
          "level": 2,
          "id": "group",
          "text": "Group"
        },
        {
          "level": 2,
          "id": "trigger",
          "text": "Opening an attachment"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "Upload progress with htmx"
        },
        {
          "level": 2,
          "id": "accessibility",
          "text": "Accessibility"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"attachment\"> <div class=\"attachment-media\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4M10 9H8M16 13H8M16 17H8\"/></svg></div> <div class=\"attachment-content\"> <p class=\"attachment-title\">quarterly-report.pdf</p> <p class=\"attachment-description\">2.4 MB · PDF</p> </div> <div class=\"attachment-actions\"> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-xs\" aria-label=\"Remove quarterly-report.pdf\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> </div> </div> <div class=\"attachment\" data-state=\"uploading\"> <img class=\"attachment-media\" src=\"../../../images/landscape.svg\" alt=\"\" /> <div class=\"attachment-content\"> <p class=\"attachment-title\">mountains-at-dusk.svg</p> <p class=\"attachment-description\">Uploading · 64%</p> </div> <div class=\"attachment-actions\"> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-xs\" aria-label=\"Cancel upload of mountains-at-dusk.svg\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> </div> <progress class=\"attachment-progress\" value=\"64\" max=\"100\" aria-label=\"Upload progress\"></progress> </div>"
        },
        {
          "id": "parts",
          "title": "Parts",
          "text": ".attachment is the frame: media, content and actions in a row. .attachment-media holds an icon, or an image. It can also be the <img> or <video> itself. .attachment-content holds .attachment-title (the file name) and .attachment-description (size, type, progress or an error). Both truncate. .attachment-actions holds small icon buttons: remove, download, retry. .attachment-progress is an optional native <progress> along the bottom edge. .attachment-trigger is an optional link or button covering the whole attachment."
        },
        {
          "id": "states",
          "title": "Upload states",
          "text": "Set data-state to idle, uploading, processing, error or done (the default). Idle attachments have a dashed border. While uploading or processing, the title shimmers and the media pulses. An error turns the border, media and description red; write what went wrong in the description, so it isn't told by colour alone. html <div class=\"attachment\" data-state=\"idle\"> <div class=\"attachment-media\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"/></svg></div> <div class=\"attachment-content\"> <p class=\"attachment-title\">contract-draft.docx</p> <p class=\"attachment-description\">Waiting to upload</p> </div> <div class=\"attachment-actions\"> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-xs\" aria-label=\"Remove contract-draft.docx\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> </div> </div> <div class=\"attachment\" data-state=\"uploading\" aria-busy=\"true\"> <div class=\"attachment-media\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4M10 9H8M16 13H8M16 17H8\"/></svg></div> <div class=\"attachment-content\"> <p class=\"attachment-title\">quarterly-report.pdf</p> <p class=\"attachment-description\">Uploading · 42%</p> </div> <div class=\"attachment-actions\"> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-xs\" aria-label=\"Cancel uplo"
        },
        {
          "id": "sizes",
          "title": "Sizes",
          "text": ".attachment-sm and .attachment-xs shrink the frame, media and text. The extra small size drops the description. html <div class=\"attachment\"> <div class=\"attachment-media\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4M10 9H8M16 13H8M16 17H8\"/></svg></div> <div class=\"attachment-content\"> <p class=\"attachment-title\">design-brief.pdf</p> <p class=\"attachment-description\">840 KB · PDF</p> </div> </div> <div class=\"attachment attachment-sm\"> <div class=\"attachment-media\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4M10 9H8M16 13H8M16 17H8\"/></svg></div> <div class=\"attachment-content\"> <p class=\"attachment-title\">design-brief.pdf</p> <p class=\"attachment-description\">840 KB · PDF</p> </div> </div> <div class=\"attachment attachment-xs\"> <div class=\"attachment-media\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4M10 9H8M16 13H8M16 17H8\"/></svg></div> <div class=\"attachment-content\"> <p class=\"attachment-title\">design-brief.pdf</p> </div> </div>"
        },
        {
          "id": "orientation",
          "title": "Orientation",
          "text": ".attachment-vertical puts the media on top at full width, for previews. The actions float over the media's corner. It combines with the sizes. html <div class=\"attachment attachment-vertical\"> <img class=\"attachment-media\" src=\"../../../images/forest.svg\" alt=\"\" /> <div class=\"attachment-content\"> <p class=\"attachment-title\">forest.svg</p> <p class=\"attachment-description\">1.2 MB</p> </div> <div class=\"attachment-actions\"> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-xs\" aria-label=\"Remove forest.svg\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> </div> </div> <div class=\"attachment attachment-vertical attachment-sm\"> <img class=\"attachment-media\" src=\"../../../images/forest.svg\" alt=\"\" /> <div class=\"attachment-content\"> <p class=\"attachment-title\">forest.svg</p> <p class=\"attachment-description\">1.2 MB</p> </div> </div> <div class=\"attachment attachment-vertical attachment-xs\"> <img class=\"attachment-media\" src=\"../../../images/forest.svg\" alt=\"\" /> <div class=\"attachment-content\"> <p class=\"attachment-title\">forest.svg</p> </div> </div>"
        },
        {
          "id": "variants",
          "title": "Image, video, audio and file",
          "text": "The media says what kind of file it is: an image shows itself, a video its thumbnail with a play icon on top, audio and other files an icon. An icon placed over an image or video is centred and drawn in white. Each works in both orientations. html <div class=\"flex flex-wrap justify-center gap-3\"> <div class=\"attachment\"> <img class=\"attachment-media\" src=\"../../../images/ocean.svg\" alt=\"\" /> <div class=\"attachment-content\"> <p class=\"attachment-title\">sunset.svg</p> <p class=\"attachment-description\">Image · 3.1 MB</p> </div> </div> <div class=\"attachment\"> <div class=\"attachment-media\"><img src=\"../../../images/landscape.svg\" alt=\"\" /><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M6 4.5a1 1 0 0 1 1.5-.87l12 7.5a1 1 0 0 1 0 1.74l-12 7.5A1 1 0 0 1 6 19.5Z\"/></svg></div> <div class=\"attachment-content\"> <p class=\"attachment-title\">timelapse.mp4</p> <p class=\"attachment-description\">Video · 0:48</p> </div> </div> <div class=\"attachment\"> <div class=\"attachment-media\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M9 18V5l12-2v13\"/><circle cx=\"6\" cy=\"18\" r=\"3\"/><circle cx=\"18\" cy=\"16\" r=\"3\"/></svg></div> <div class=\"attachment-content\"> <p class=\"attachment-title\">interview.mp3</p> <p class=\"attachment-description\">Audio · 12:04</p> </div> </div> <div class=\"attachment\"> <div class=\"attachment-media\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"/></svg></"
        },
        {
          "id": "group",
          "title": "Group",
          "text": ".attachment-group lays attachments out in a row that scrolls sideways, snapping to each one, with its edges fading out. Give it role=\"group\" and an aria-label. Each attachment keeps its own width, so mix sizes and orientations freely. html <div class=\"attachment-group mx-auto w-full max-w-xl\" role=\"group\" aria-label=\"Attachments\"> <div class=\"attachment attachment-vertical attachment-sm\"> <img class=\"attachment-media\" src=\"../../../images/landscape.svg\" alt=\"\" /> <div class=\"attachment-content\"><p class=\"attachment-title\">mountains.svg</p><p class=\"attachment-description\">2.2 MB</p></div> </div> <div class=\"attachment attachment-vertical attachment-sm\"> <img class=\"attachment-media\" src=\"../../../images/ocean.svg\" alt=\"\" /> <div class=\"attachment-content\"><p class=\"attachment-title\">sunset.svg</p><p class=\"attachment-description\">3.1 MB</p></div> </div> <div class=\"attachment attachment-vertical attachment-sm\" data-state=\"uploading\"> <img class=\"attachment-media\" src=\"../../../images/forest.svg\" alt=\"\" /> <div class=\"attachment-content\"><p class=\"attachment-title\">forest.svg</p><p class=\"attachment-description\">Uploading · 30%</p></div> <progress class=\"attachment-progress\" value=\"30\" max=\"100\" aria-label=\"Upload progress\"></progress> </div> <div class=\"attachment attachment-vertical attachment-sm\"> <div class=\"attachment-media\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M9 18V5l12-2v13\"/><circle cx=\"6\" cy=\"18\" r=\"3\"/><circle cx=\"18\" cy=\"16\" r=\"3\"/></svg></div> <div class=\"attachment-content\"><p class=\"attachment-title\">interview.mp3</p><p class=\"attachment-description\">12:04</p></div> </div> <div class=\"attachment attachment-vertical attachment-sm\"> <div class=\"attachment-media\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"current"
        },
        {
          "id": "trigger",
          "title": "Opening an attachment",
          "text": ".attachment-trigger on an empty link or button stretches it over the whole attachment, so a click anywhere opens or previews the file, while the actions stay separately clickable above it. It has no text, so give it an aria-label saying what it does. html <div class=\"attachment\"> <div class=\"attachment-media\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4M10 9H8M16 13H8M16 17H8\"/></svg></div> <div class=\"attachment-content\"> <p class=\"attachment-title\">quarterly-report.pdf</p> <p class=\"attachment-description\">2.4 MB · PDF</p> </div> <div class=\"attachment-actions\"> <a class=\"btn btn-ghost btn-icon btn-xs\" href=\"#download\" aria-label=\"Download quarterly-report.pdf\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3\"/></svg></a> </div> <a class=\"attachment-trigger\" href=\"#preview\" aria-label=\"Open quarterly-report.pdf\"></a> </div>"
        },
        {
          "id": "htmx",
          "title": "Upload progress with htmx",
          "text": "The server can own the state. Return the attachment as a fragment that asks for its next state with hx-trigger=\"load delay:…\" and hx-swap=\"outerHTML\", and stop polling once it is done or in error. Each click below adds one that moves from uploading to processing to done: html <button type=\"button\" class=\"btn btn-outline\" hx-get=\"/api/attachment\" hx-target=\"#upload-list\" hx-swap=\"beforeend\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12\"/></svg> Upload a file</button> <div id=\"upload-list\" class=\"flex flex-col gap-2\"></div> One step of the server's response: html <div class=\"attachment\" data-state=\"uploading\" hx-get=\"/uploads/42?step=3\" hx-trigger=\"load delay:600ms\" hx-swap=\"outerHTML\"> <div class=\"attachment-media\">…icon…</div> <div class=\"attachment-content\"> <p class=\"attachment-title\">quarterly-report.pdf</p> <p class=\"attachment-description\">Uploading · 60%</p> </div> <progress class=\"attachment-progress\" value=\"60\" max=\"100\" aria-label=\"Upload progress\"></progress> </div>"
        },
        {
          "id": "accessibility",
          "title": "Accessibility",
          "text": "Icon-only actions need an aria-label naming the file: “Remove quarterly-report.pdf”, not just “Remove”. So does .attachment-trigger, which has no text of its own. Media images are decorative next to the file name: give them alt=\"\". Mark attachments in progress with aria-busy=\"true\", and put errors in words in the description. A group of attachments gets role=\"group\" and an aria-label."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .attachment The frame: media, content and actions in a row, 16rem wide (override with w-*). .attachment-media Icon box, or an image/video (inside it or as it). An icon over an image is centred and white. .attachment-content / -title / -description File name and details; both truncate. .attachment-actions Icon buttons, kept clickable above the trigger. .attachment-trigger Empty link or button covering the attachment. .attachment-progress Native <progress> along the bottom edge. [data-state] idle (dashed), uploading / processing (shimmer), error (red), done (default). .attachment-sm / -xs Smaller sizes; -xs hides the description. .attachment-vertical Media on top at full width; actions over its corner. .attachment-group Sideways-scrolling row with snapping and faded edges."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/chain-of-thought",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/chain-of-thought",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/chain-of-thought.md",
      "title": "Chain of thought",
      "description": "A sequence of steps on a connecting line, each folding its details away, for reasoning traces, tool calls and process logs.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "status",
          "text": "Steps and status"
        },
        {
          "level": 2,
          "id": "plain",
          "text": "Without the line"
        },
        {
          "level": 2,
          "id": "collapsible",
          "text": "Collapsible chain"
        },
        {
          "level": 2,
          "id": "tools",
          "text": "With tool calls and search results"
        },
        {
          "level": 2,
          "id": "message",
          "text": "In a message"
        },
        {
          "level": 2,
          "id": "macro",
          "text": "Macro"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"chain-of-thought mx-auto max-w-lg\"> <details class=\"chain-of-thought-step\" data-status=\"complete\" open> <summary class=\"chain-of-thought-step-header\"> <span class=\"chain-of-thought-step-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg></span> <span class=\"chain-of-thought-step-label\">Searching for htmx 4 event names</span> </summary> <div class=\"chain-of-thought-step-content\"> <ul class=\"chain-of-thought-step-items\"> <li class=\"chain-of-thought-step-item\">Events are colon-delimited: <code>htmx:after:swap</code>.</li> <li class=\"chain-of-thought-step-item\"><code>htmx:after:process</code> fires on each new element.</li> </ul> <div class=\"chain-of-thought-results\"> <span class=\"badge badge-secondary\">htmx.org</span> <span class=\"badge badge-secondary\">github.com</span> </div> </div> </details> <details class=\"chain-of-thought-step\" data-status=\"complete\"> <summary class=\"chain-of-thought-step-header\"> <span class=\"chain-of-thought-step-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4M10 9H8M16 13H8M16 17H8\"/></svg></span> <span class=\"chain-of-thought-step-label\">Reading the migration guide</span> </summary> <div class=\"chain-of-thought-step-content\"><p>Three renamed events affect the components.</p></div> </details> <div class=\"chain-of-thought-step\" data-status=\"active\" aria-current=\"step\"> <div class=\"chain-of-thought-step-header\"> <span class=\"chain-of-thought-step-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": ".chain-of-thought stacks the steps. Pure CSS: each step that has details is a <details class=\"chain-of-thought-step\">, so it opens with no JavaScript. A step's .chain-of-thought-step-header (its <summary>) holds a .chain-of-thought-step-icon and a .chain-of-thought-step-label; the chevron is drawn for you. An empty icon is a dot. .chain-of-thought-step-content holds the details: a .chain-of-thought-step-items list of .chain-of-thought-step-items, .chain-of-thought-results (a row of badges), a .chain-of-thought-image, or any text. A step without details is a plain <div class=\"chain-of-thought-step\"> with a <div class=\"chain-of-thought-step-header\">: a row that doesn't open. A line runs from each step's icon, past its details, to the next step."
        },
        {
          "id": "status",
          "title": "Steps and status",
          "text": "data-status on a step: complete (or none) is muted, active is in the text colour with a pulsing dot (or put a spinner in its icon), pending is faded and error is in the danger colour. Mark the active step aria-current=\"step\". To update a running chain, swap the steps from the server with htmx as they change. html <div class=\"chain-of-thought mx-auto max-w-lg\"> <div class=\"chain-of-thought-step\" data-status=\"complete\"> <div class=\"chain-of-thought-step-header\"><span class=\"chain-of-thought-step-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></span><span class=\"chain-of-thought-step-label\">Fetched the invoices</span></div> </div> <div class=\"chain-of-thought-step\" data-status=\"error\"> <div class=\"chain-of-thought-step-header\"><span class=\"chain-of-thought-step-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m15 9-6 6M9 9l6 6\"/></svg></span><span class=\"chain-of-thought-step-label\">Couldn't reach the payments API</span></div> </div> <div class=\"chain-of-thought-step\" data-status=\"active\" aria-current=\"step\"> <div class=\"chain-of-thought-step-header\"><span class=\"chain-of-thought-step-icon\"></span><span class=\"chain-of-thought-step-label\">Retrying with the backup endpoint</span></div> </div> <div class=\"chain-of-thought-step\" data-status=\"pending\"> <div class=\"chain-of-thought-step-header\"><span class=\"chain-of-thought-step-icon\"></span><span class=\"chain-of-thought-step-label\">Reconcile the totals</span></div> </div> </div>"
        },
        {
          "id": "plain",
          "title": "Without the line",
          "text": ".chain-of-thought-plain on the chain leaves out the line between the steps. html <div class=\"chain-of-thought chain-of-thought-plain mx-auto max-w-lg\"> <div class=\"chain-of-thought-step\"><div class=\"chain-of-thought-step-header\"><span class=\"chain-of-thought-step-icon\"></span><span class=\"chain-of-thought-step-label\">Parsed the request</span></div></div> <div class=\"chain-of-thought-step\"><div class=\"chain-of-thought-step-header\"><span class=\"chain-of-thought-step-icon\"></span><span class=\"chain-of-thought-step-label\">Looked up the account</span></div></div> <div class=\"chain-of-thought-step\"><div class=\"chain-of-thought-step-header\"><span class=\"chain-of-thought-step-icon\"></span><span class=\"chain-of-thought-step-label\">Drafted the reply</span></div></div> </div>"
        },
        {
          "id": "collapsible",
          "title": "Collapsible chain",
          "text": "The whole chain can fold away under one line: make it a <details class=\"chain-of-thought\"> with a <summary class=\"chain-of-thought-header\">, and put the steps in a .chain-of-thought-content. html <details class=\"chain-of-thought mx-auto max-w-lg\"> <summary class=\"chain-of-thought-header\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12 3l1.9 5.8L20 11l-6.1 2.2L12 19l-1.9-5.8L4 11l6.1-2.2z\"/><path d=\"M19 3v4M17 5h4\"/></svg><span>Worked for 12 seconds</span></summary> <div class=\"chain-of-thought-content\"> <div class=\"chain-of-thought-step\"><div class=\"chain-of-thought-step-header\"><span class=\"chain-of-thought-step-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg></span><span class=\"chain-of-thought-step-label\">Searched 4 sources</span></div></div> <div class=\"chain-of-thought-step\"><div class=\"chain-of-thought-step-header\"><span class=\"chain-of-thought-step-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m4 17 6-6-6-6M12 19h8\"/></svg></span><span class=\"chain-of-thought-step-label\">Ran the test suite</span></div></div> <div class=\"chain-of-thought-step\"><div class=\"chain-of-thought-step-header\"><span class=\"chain-of-thought-step-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg></s"
        },
        {
          "id": "tools",
          "title": "With tool calls and search results",
          "text": "A step's details can hold anything: the sources a search found, a tool's input and output, or an image it looked at. html <div class=\"chain-of-thought mx-auto max-w-lg\"> <details class=\"chain-of-thought-step\" data-status=\"complete\" open> <summary class=\"chain-of-thought-step-header\"> <span class=\"chain-of-thought-step-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg></span> <span class=\"chain-of-thought-step-label\">Searched \"Sydney weather this weekend\"</span> </summary> <div class=\"chain-of-thought-step-content\"> <div class=\"chain-of-thought-results\"> <a class=\"badge badge-outline\" href=\"https://www.bom.gov.au\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/></svg> bom.gov.au</a> <a class=\"badge badge-outline\" href=\"https://www.weatherzone.com.au\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/></svg> weatherzone.com.au</a> </div> </div> </details> <details class=\"chain-of-thought-step\" data-status=\"complete\" open> <summary class=\"chain-of-thought-step-header\"> <span class=\"chain-of-thought-step-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M17.5 19H9a7 "
        },
        {
          "id": "message",
          "title": "In a message",
          "text": "Above an assistant's reply in a .message-plain body, folded away once the work is done. html <div class=\"message-list mx-auto w-full max-w-lg\"> <div class=\"message\" data-align=\"end\"> <div class=\"message-body\"><div class=\"message-content\">Why did last night's deploy fail?</div></div> </div> <div class=\"message message-plain\"> <span class=\"avatar message-avatar\" role=\"img\" aria-label=\"Assistant\"><span class=\"avatar-fallback\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12 8V4H8\"/><rect width=\"16\" height=\"12\" x=\"4\" y=\"8\" rx=\"2\"/><path d=\"M2 14h2M20 14h2M15 13v2M9 13v2\"/></svg></span></span> <div class=\"message-body\"> <details class=\"chain-of-thought\"> <summary class=\"chain-of-thought-header\"><span>3 steps</span></summary> <div class=\"chain-of-thought-content\"> <div class=\"chain-of-thought-step\"><div class=\"chain-of-thought-step-header\"><span class=\"chain-of-thought-step-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m4 17 6-6-6-6M12 19h8\"/></svg></span><span class=\"chain-of-thought-step-label\">Read the deploy log</span></div></div> <div class=\"chain-of-thought-step\"><div class=\"chain-of-thought-step-header\"><span class=\"chain-of-thought-step-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg></span><span class=\"chain-of-thought-step-label\">Found \"DATABASE_URL is not set\"</span></div></div> <div class=\"chain-of-thought-step\"><div class=\"chain-of-thought-step-header\">"
        },
        {
          "id": "macro",
          "title": "Macro",
          "text": "chain_of_thought() writes the chain (a <details> when it has a label) and chain_of_thought_step() a step: a <details> when it has a call body, else a plain row. Without an icon, a step's status picks one: a check (complete), a spinner (active), a cross (error), else a dot; icon=false always gives the dot. jinja {% from \"components/chain-of-thought/chain-of-thought.html\" import chain_of_thought, chain_of_thought_step %} {% call chain_of_thought() %} {% call chain_of_thought_step(\"Searching the docs\", status=\"complete\", icon=\"search\", open=true) %} <ul class=\"chain-of-thought-step-items\"><li class=\"chain-of-thought-step-item\">Found 3 pages</li></ul> {% endcall %} {{ chain_of_thought_step(\"Writing the answer\", status=\"active\") }} {% endcall %} {% call chain_of_thought(\"Worked for 12 seconds\", open=false, plain=true) %}…{% endcall %} Macro Description chain_of_thought(label=none, open=true, icon=none, plain=false, attrs=none, class=\"\") label folds the chain under a summary; plain drops the line. chain_of_thought_step(label, status=none, icon=none, open=false, attrs=none, class=\"\") status: complete | active | pending | error; open only with a call body."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .chain-of-thought The steps, stacked. A <div>, or a <details> to fold them away. .chain-of-thought-header / -content A folding chain's <summary>, and the steps under it. .chain-of-thought-plain On the chain: no line between the steps. .chain-of-thought-step One step: a <details> with details, else a <div>. [data-status] On a step: complete | active | pending | error. .chain-of-thought-step-header The step's <summary> (or <div>): icon, label, chevron. .chain-of-thought-step-icon An icon, a spinner, or empty for a dot. .chain-of-thought-step-label The step's text. .chain-of-thought-step-content The step's details, indented under its label. .chain-of-thought-step-items / -item A list of findings. .chain-of-thought-results A wrapping row of badges, e.g. search results. .chain-of-thought-image An image in a step."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/message",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/message",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/message.md",
      "title": "Message",
      "description": "A message in a conversation, with an avatar, a header, a footer, attachments and start or end alignment; groups for consecutive messages, and reasoning, tool calls and typing for AI chat.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "alignment",
          "text": "Alignment"
        },
        {
          "level": 2,
          "id": "variants",
          "text": "Variants"
        },
        {
          "level": 2,
          "id": "header-footer",
          "text": "Header and footer"
        },
        {
          "level": 2,
          "id": "group",
          "text": "Group"
        },
        {
          "level": 2,
          "id": "attachments",
          "text": "Attachments"
        },
        {
          "level": 2,
          "id": "ai",
          "text": "AI chat"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"message-list mx-auto w-full max-w-lg\"> <div class=\"message\"> <span class=\"avatar message-avatar\" role=\"img\" aria-label=\"Ada\"><span class=\"avatar-fallback\" aria-hidden=\"true\">AL</span></span> <div class=\"message-body\"> <div class=\"message-header\"><span class=\"message-author\">Ada</span><time class=\"message-time\" datetime=\"09:41\">09:41</time></div> <div class=\"message-content\">Is the release still on for Friday?</div> </div> </div> <div class=\"message\" data-align=\"end\"> <div class=\"message-body\"> <div class=\"message-content\">Yes! Docs are done, just waiting on the last review.</div> <div class=\"message-footer\"><span class=\"message-status\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"/></svg> Read 09:42</span></div> </div> </div> </div>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": ".message lays out one message: an optional .message-avatar (any avatar) beside a .message-body. The body stacks a .message-header (.message-author, .message-time), the .message-content itself, .message-attachments and a .message-footer. Every part but the content is optional. Pure CSS. Wrap a conversation in .message-list to space it; give it role=\"log\" and aria-live=\"polite\" when new messages arrive while it's on screen."
        },
        {
          "id": "alignment",
          "title": "Alignment",
          "text": "data-align=\"end\" puts a message on the right, for the reader's own: the avatar, header, attachments and footer all move to that side, and the default bubble turns primary. data-align=\"start\" (or nothing) is everyone else. html <div class=\"message-list mx-auto w-full max-w-lg\"> <div class=\"message\"> <span class=\"avatar message-avatar\" role=\"img\" aria-label=\"Jane\"><span class=\"avatar-fallback\" aria-hidden=\"true\">JC</span></span> <div class=\"message-body\"> <div class=\"message-header\"><span class=\"message-author\">Jane</span><time class=\"message-time\">10:02</time></div> <div class=\"message-content\">Lunch at 12:30?</div> </div> </div> <div class=\"message\" data-align=\"end\"> <span class=\"avatar message-avatar\" role=\"img\" aria-label=\"You\"><span class=\"avatar-fallback\" aria-hidden=\"true\">ME</span></span> <div class=\"message-body\"> <div class=\"message-header\"><span class=\"message-author\">You</span><time class=\"message-time\">10:03</time></div> <div class=\"message-content\">Perfect, see you at the usual place.</div> </div> </div> </div>"
        },
        {
          "id": "variants",
          "title": "Variants",
          "text": "On .message: the default filled bubble, .message-secondary, .message-outline, .message-soft (tinted primary) and .message-plain (no bubble, full width: for assistant replies and long text). html <div class=\"message-list mx-auto w-full max-w-lg\"> <div class=\"message\" data-align=\"start\"> <div class=\"message-body\"> <div class=\"message-content\">default: the other side.</div> </div> </div> <div class=\"message message-secondary\" data-align=\"end\"> <div class=\"message-body\"> <div class=\"message-content\">message-secondary: the reader&#39;s side.</div> </div> </div> <div class=\"message message-outline\" data-align=\"start\"> <div class=\"message-body\"> <div class=\"message-content\">message-outline: the other side.</div> </div> </div> <div class=\"message message-soft\" data-align=\"end\"> <div class=\"message-body\"> <div class=\"message-content\">message-soft: the reader&#39;s side.</div> </div> </div> <div class=\"message message-plain\"> <span class=\"avatar message-avatar\" role=\"img\" aria-label=\"Assistant\"><span class=\"avatar-fallback\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12 8V4H8\"/><rect width=\"16\" height=\"12\" x=\"4\" y=\"8\" rx=\"2\"/><path d=\"M2 14h2M20 14h2M15 13v2M9 13v2\"/></svg></span></span> <div class=\"message-body\"> <div class=\"message-content\"> <p>message-plain: no bubble, the full width of the row. Suits assistant replies that run to paragraphs, lists and code.</p> <p>Put <code class=\"code\">.prose</code> inside for long-form formatting.</p> </div> </div> </div> </div>"
        },
        {
          "id": "header-footer",
          "title": "Header and footer",
          "text": "The header carries the sender's name and the time; the footer a status and .message-actions. Both follow the message's side, and on an data-align=\"end\" row the actions stay at the end. .message-actions-hover shows them only on hover or focus (always on touch screens). html <div class=\"message-list mx-auto w-full max-w-lg\"> <div class=\"message\"> <span class=\"avatar message-avatar\" role=\"img\" aria-label=\"Ada\"><span class=\"avatar-fallback\" aria-hidden=\"true\">AL</span></span> <div class=\"message-body\"> <div class=\"message-header\"><span class=\"message-author\">Ada Lovelace</span><span class=\"badge badge-secondary\">Admin</span><time class=\"message-time\">Yesterday</time></div> <div class=\"message-content\">I've pushed the fix for the flaky test.</div> <div class=\"message-footer\"> <span class=\"message-status\">Edited</span> <div class=\"message-actions message-actions-hover\"> <button type=\"button\" class=\"btn btn-ghost btn-icon\" aria-label=\"Reply\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M10 9V5l-7 7 7 7v-4.1c5 0 8.5 1.6 11 5.1-1-5-4-10-11-11z\"/></svg></button> <button type=\"button\" class=\"btn btn-ghost btn-icon clipboard\" data-clipboard data-clipboard-target=\".message-content\" aria-label=\"Copy\"><span class=\"clipboard-idle\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg></span><span class=\"clipboard-done\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true"
        },
        {
          "id": "group",
          "title": "Group",
          "text": ".message-group stacks consecutive messages from one sender close together: the avatar and header show on the first only (the avatar keeps its space), and the bubbles tuck together on the sender's side. html <div class=\"message-list mx-auto w-full max-w-lg\"> <div class=\"message-group\"> <div class=\"message\"> <span class=\"avatar message-avatar\" role=\"img\" aria-label=\"Jane\"><span class=\"avatar-fallback\" aria-hidden=\"true\">JC</span></span> <div class=\"message-body\"> <div class=\"message-header\"><span class=\"message-author\">Jane</span><time class=\"message-time\">14:10</time></div> <div class=\"message-content\">Heads up: the staging deploy is red.</div> </div> </div> <div class=\"message\"> <span class=\"avatar message-avatar\" role=\"img\" aria-label=\"Jane\"><span class=\"avatar-fallback\" aria-hidden=\"true\">JC</span></span> <div class=\"message-body\"><div class=\"message-content\">Looks like a missing env var.</div></div> </div> <div class=\"message\"> <span class=\"avatar message-avatar\" role=\"img\" aria-label=\"Jane\"><span class=\"avatar-fallback\" aria-hidden=\"true\">JC</span></span> <div class=\"message-body\"><div class=\"message-content\">I'll add it to the config.</div></div> </div> </div> <div class=\"message-group\"> <div class=\"message\" data-align=\"end\"><div class=\"message-body\"><div class=\"message-content\">Good catch.</div></div></div> <div class=\"message\" data-align=\"end\"><div class=\"message-body\"><div class=\"message-content\">Ping me when it's green.</div></div></div> </div> </div>"
        },
        {
          "id": "attachments",
          "title": "Attachments",
          "text": ".message-attachments holds attachments and lines them up on the message's side. A lone picture can be a .message-image. html <div class=\"message-list mx-auto w-full max-w-lg\"> <div class=\"message\" data-align=\"end\"> <div class=\"message-body\"> <div class=\"message-attachments\"> <div class=\"attachment attachment-sm\"> <div class=\"attachment-media\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4M10 9H8M16 13H8M16 17H8\"/></svg></div> <div class=\"attachment-content\"><p class=\"attachment-title\">q3-report.pdf</p><p class=\"attachment-description\">PDF · 2.4 MB</p></div> </div> <div class=\"attachment attachment-sm\" data-state=\"uploading\"> <div class=\"attachment-media\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><circle cx=\"9\" cy=\"9\" r=\"2\"/><path d=\"m21 15-3.09-3.09a2 2 0 0 0-2.82 0L6 21\"/></svg></div> <div class=\"attachment-content\"><p class=\"attachment-title\">chart.png</p><p class=\"attachment-description\">Uploading · 60%</p></div> <progress class=\"attachment-progress\" value=\"60\" max=\"100\" aria-label=\"Upload progress\"></progress> </div> </div> <div class=\"message-content\">Here's the report and the chart for the slides.</div> </div> </div> </div>"
        },
        {
          "id": "ai",
          "title": "AI chat",
          "text": "For assistants, .message-tool is a tool call with its input and output, its state in data-state (running, done, error). The rest comes from other components, placed in .message-body: Reasoning for the model's thinking (open while it streams, then folded away with how long it took), a typing loader (loader(\"typing\")) for a reply on its way, and Chain of thought for steps, searches and tool calls on a line. Stream them in with htmx, swapping or appending to the conversation. html <div class=\"message-list mx-auto w-full max-w-lg\" role=\"log\" aria-live=\"polite\" aria-label=\"Conversation\"> <div class=\"message\" data-align=\"end\"> <div class=\"message-body\"><div class=\"message-content\">What's the weather in Sydney tomorrow?</div></div> </div> <div class=\"message message-plain\"> <span class=\"avatar message-avatar\" role=\"img\" aria-label=\"Assistant\"><span class=\"avatar-fallback\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12 8V4H8\"/><rect width=\"16\" height=\"12\" x=\"4\" y=\"8\" rx=\"2\"/><path d=\"M2 14h2M20 14h2M15 13v2M9 13v2\"/></svg></span></span> <div class=\"message-body\"> <details class=\"reasoning\" data-reasoning data-reasoning-duration=\"3\"> <summary class=\"reasoning-trigger\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12 3l1.9 5.8L20 11l-6.1 2.2L12 19l-1.9-5.8L4 11l6.1-2.2z\"/><path d=\"M19 3v4M17 5h4\"/></svg><span class=\"reasoning-label\">Thought for 3 seconds</span> </summary> <div class=\"reasoning-content\"> <ol> <li>The user wants tomorrow's forecast for Sydney, Australia.</li> <li>Call the weather tool with the city and date.</li> </ol> </div> </details> <details class=\"message-tool\" data-state=\"done\"> <sum"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .message One message: avatar and body, start-aligned. [data-align=\"end\"] The reader's own message, on the right; everything follows. .message-avatar On an .avatar beside the body. .message-body Header, content, attachments and footer, stacked. .message-header / .message-author / .message-time Name and time above the message. .message-content The message: a bubble (default) or plain text. .message-secondary / -outline / -soft / -plain Variants, on .message. .message-attachments / .message-image Attachments, or a single picture. .message-footer / .message-status Status line under the message. .message-actions / .message-actions-hover Small icon buttons; the second shows on hover or focus. .message-group Consecutive messages from one sender. .message-list A conversation's spacing. .message-tool (-header, -status, -content, -label) On a <details>: a tool call; data-state running | done | error."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/message-scroller",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/message-scroller",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/message-scroller.md",
      "title": "Message scroller",
      "description": "Scrolling for a chat transcript that streams: new questions rise to the top, replies grow beneath them, and the view only moves when the reader moves it.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "behaviour",
          "text": "How it scrolls"
        },
        {
          "level": 2,
          "id": "demo",
          "text": "Try it"
        },
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "following",
          "text": "Following the reply"
        },
        {
          "level": 2,
          "id": "new-turns",
          "text": "New turns"
        },
        {
          "level": 2,
          "id": "opening",
          "text": "Opening position"
        },
        {
          "level": 2,
          "id": "history",
          "text": "Loading history"
        },
        {
          "level": 2,
          "id": "jumping",
          "text": "Jumping to messages"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "Streaming with htmx"
        },
        {
          "level": 2,
          "id": "accessibility",
          "text": "Accessibility"
        },
        {
          "level": 2,
          "id": "long-threads",
          "text": "Long threads"
        },
        {
          "level": 2,
          "id": "api",
          "text": "API and events"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "The message scroller is the scrolling part of a chat window. Your messages, the prompt box and whatever produces the replies stay in your own markup and on your server; the scroller looks after one thing, the scroll position of the transcript while it changes underneath the person reading it. In a chat that streams, the reader and the reply being written pull on the same scrollbar. The scroller settles that in the reader's favour: the view moves when they do something (send a message, press the jump button, follow a link), and otherwise what's on screen stays put."
        },
        {
          "id": "behaviour",
          "title": "How it scrolls",
          "text": "Most of what it does comes down to one question, is the reader at the bottom?, asked again whenever the transcript or the reader changes something. When… the scroller… the page opens shows the reader's last question near the top, or the message named in the URL, or an unread marker. No animation. the reader sends a message lifts it to just under the top, with a sliver of the previous reply above it, and leaves empty room below for the answer. a reply streams in and the reader is at the bottom keeps the newest line in view. The empty room fills first, so the question doesn't move until the answer is taller than the view. a reply streams in and the reader is further up stays still. The jump button appears, showing that more is being written below. the reader scrolls up, selects text, presses Page Up or clicks a link stops following at once. Scrolling back to the bottom, or pressing the jump button, picks it up again. something above the reader changes height: an image loads, code is highlighted, older messages load, a reply is stopped, regenerated or removed corrects the scroll position in the same frame, so the message being read doesn't shift. the reader follows a link to a message scrolls the transcript, not the page, to that message and flashes it. a reply finishes tells screen readers once (\"Response complete\") rather than reading the reply out word by word. It measures at most once per frame and listens passively, so long conversations with heavy Markdown stay smooth; rows out of view can also skip rendering (see long threads)."
        },
        {
          "id": "demo",
          "title": "Try it",
          "text": "Send a message: your turn moves near the top and the reply streams in below it. Scroll up while it's writing, or select some text, and it leaves you alone; the button at the bottom shows it's still going. The demo's reply is simulated in the browser, so it works on this static site. A real app gets these rows from its server (see streaming with htmx)."
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "One [data-message-scroller] element is both the state and the frame. Inside it: the .message-scroller-viewport that scrolls, the .message-scroller-content that holds the rows, and an optional jump button. Give the scroller a height, or flex-1 min-h-0 in a column with the prompt input below it. html <div class=\"flex h-dvh flex-col\"> <div class=\"message-scroller min-h-0 flex-1\" data-message-scroller aria-label=\"Conversation\"> <div class=\"message-scroller-viewport\"> <div class=\"message-scroller-content message-list\" id=\"transcript\"> <div class=\"message\" data-align=\"end\" id=\"m1\">…</div> <div class=\"message message-plain\" id=\"m2\">…</div> </div> </div> <button type=\"button\" class=\"btn btn-outline btn-sm btn-rounded message-scroller-jump\" data-message-scroller-jump aria-label=\"Jump to latest\"> {{ icon(\"arrow-down\") }} <span class=\"message-scroller-jump-unread\">New messages</span> <span class=\"message-scroller-jump-streaming loader loader-typing\" aria-hidden=\"true\"><span></span><span></span><span></span></span> </button> </div> <form>…prompt input…</form> </div> The rows are the content's children: .message rows, .message-groups, markers, anything. Give a row an id to link to it. The script adds a spacer after the content (not inside it, so rows you append land before it) and a visually hidden status for announcements. The viewport becomes a focusable region labelled with the scroller's aria-label (default \"Conversation\")."
        },
        {
          "id": "following",
          "title": "Following the reply",
          "text": "The reader is at the bottom when they are within data-message-scroller-threshold pixels of it (32 by default). While the reader is there, the scroller is following: as content grows, it keeps the newest line in view. Following stops when they scroll up by any means (wheel, touch, Page Up, Arrow Up, Home, Shift+Space, the scrollbar, find-in-page), select text in the transcript, or press a link. It starts again when they scroll back to the edge or press the jump button. Content arriving never decides this, only what the reader does. The scroller can tell its own scrolling from the reader's, so following a stream doesn't count as the reader scrolling. The scroller carries data-following while following, data-at-bottom while at the bottom, and data-unread when rows arrived below the reader. To never follow, so a long reply stops at the top of the view and the reader scrolls on when they choose, set data-message-scroller-follow=\"false\"."
        },
        {
          "id": "new-turns",
          "title": "New turns",
          "text": "When an anchor row is appended to the end of the transcript, the scroller moves it near the top of the viewport. Anchors are the reader's own turns: rows with data-message-anchor, data-role=\"user\", or a .message[data-align=\"end\"]. data-message-anchor=\"false\" opts a row out, data-message-scroller-anchor=\"selector\" sets your own selector (we recommend an explicit data-message-anchor), and \"none\" turns anchoring off. --message-scroller-anchor-offset (3rem) is how much of the previous turn stays visible above the new one. Below it, the spacer leaves exactly enough room for the turn to sit there. As the reply grows into that room the spacer shrinks, so nothing moves, and it reaches zero once the reply fills the view. The offset is also each row's scroll-margin-top, so jumps and in-page links land the same way. css .chat { --message-scroller-anchor-offset: 25%; } An anchor appended while the reader is scrolled away still moves into view, because it's their own message. Rows from anyone else arrive without moving the view: the jump button shows \"New messages\" instead."
        },
        {
          "id": "opening",
          "title": "Opening position",
          "text": "On load, without animation, the scroller opens at data-message-scroller-open: last-anchor (default): the last question near the top, with its answer below it. Without one, the bottom. bottom or top. #id: a row, for example an unread marker (see jumping). A location.hash that names a row wins over all of these, so a shared link to a message opens on that message, highlighted."
        },
        {
          "id": "history",
          "title": "Loading history",
          "text": "Older messages can load in above the reader without moving what they see. The scroller remembers the first visible row and its distance from the top of the viewport. When anything changes size (rows prepended, images loading, code rendering, Markdown expanding, a reply regenerated in place), it puts that row back before the next paint. It turns off the browser's own overflow-anchor so the position isn't corrected twice. With htmx, put a sentinel at the top that swaps itself for the previous page of rows (and a new sentinel) when it scrolls into view: html <div class=\"message-scroller mx-auto h-72 w-full max-w-lg rounded-(--radius) border border-border bg-background\" data-message-scroller data-message-scroller-open=\"bottom\" aria-label=\"Conversation history\"> <div class=\"message-scroller-viewport\"> <div class=\"message-scroller-content message-list\"> <div class=\"flex items-center justify-center gap-2 py-2 text-xs text-muted-foreground\" hx-get=\"/api/chat/history?before=10\" hx-trigger=\"intersect once\" hx-swap=\"outerHTML\"> <span class=\"spinner spinner-sm\" role=\"status\" aria-label=\"Loading\"></span> Loading older messages… </div> <div class=\"message\" data-align=\"end\" id=\"history-10\"><div class=\"message-body\"><div class=\"message-content\">Scroll up to load older messages.</div></div></div> <div class=\"message message-plain\" id=\"history-11\"><div class=\"message-body\"><div class=\"message-content\"><p>They load above this one, and this one stays where it is.</p></div></div></div> <div class=\"message\" data-align=\"end\" id=\"history-12\"><div class=\"message-body\"><div class=\"message-content\">Nice.</div></div></div> </div> </div> </div> The demo's /api/chat/history is a mock route that only the dev server serves (site/server/api.ts). On the static site the sentinel stays as it is."
        },
        {
          "id": "jumping",
          "title": "Jumping to messages",
          "text": "Links to rows scroll the transcript, not the page, and briefly highlight the row they land on (data-highlighted). They can be inside the transcript (a citation, \"see above\") or anywhere on the page: try the first question or the second answer in the demo above. html <a href=\"#m42\">Jump to the decision</a> From script: ts import { getMessageScroller } from \"htmx-ui\"; const chat = getMessageScroller(document.querySelector(\"#chat\")); chat?.scrollToMessage(\"m42\"); // near the top, highlighted chat?.scrollToMessage(\"m42\", { align: \"center\", highlight: false, smooth: true }); chat?.scrollToBottom({ smooth: true }); // and follow from there An unread marker is a row you insert yourself before the first message the reader hasn't seen. Open on it with data-message-scroller-open=\"#unread\": html <div class=\"message-list mx-auto w-full max-w-lg\"> <div class=\"message\"><div class=\"message-body\"><div class=\"message-content\">See you tomorrow!</div></div></div> <div class=\"message-scroller-marker\" id=\"unread\" role=\"separator\">New</div> <div class=\"message\"><div class=\"message-body\"><div class=\"message-content\">Morning! Did the deploy go out?</div></div></div> </div>"
        },
        {
          "id": "htmx",
          "title": "Streaming with htmx",
          "text": "The scroller watches the transcript; it doesn't care how rows get there. A typical flow: the prompt form posts with hx-swap=\"beforeend\" into the content, and the server answers with the user's turn (an anchor) and an empty assistant row marked data-streaming. The reply then streams into that row, for example over SSE or by polling, and the last update leaves the row without data-streaming. html <form hx-post=\"/chat\" hx-target=\"#transcript\" hx-swap=\"beforeend\"> <textarea class=\"textarea\" name=\"prompt\"></textarea> <button class=\"btn btn-primary\">Send</button> </form> html <!-- the response <!-- the response --> <div class=\"message\" data-align=\"end\">…the prompt…</div> <div class=\"message message-plain\" id=\"reply-7\" data-streaming hx-get=\"/chat/reply/7\" hx-trigger=\"load delay:300ms\" hx-swap=\"outerHTML\"> <div class=\"message-body\"><div class=\"message-content\"> <span class=\"loader loader-typing\" aria-hidden=\"true\"><span></span><span></span><span></span></span> </div></div> </div> Each poll returns the row with the reply so far (and the same trigger), and the final one drops data-streaming and the trigger. Replacing a row, whether to stop it, retry, regenerate or show an error, goes through the same position-keeping, so it doesn't move the conversation. Put data-streaming on the scroller itself instead if the stream doesn't map to one row."
        },
        {
          "id": "accessibility",
          "title": "Accessibility",
          "text": "The viewport is a focusable region (tabindex=\"0\"), so keyboard users can scroll it, labelled by the scroller's aria-label. The transcript is deliberately not a live region: a streamed reply would be read out token by token. Instead a visually hidden polite status says data-message-scroller-announce-new (\"New message\") when a row arrives from someone else, and data-message-scroller-announce-done (\"Response complete\") when the last data-streaming goes away. Announcements are throttled. Set either attribute to \"\" to stay quiet, or call announce(text). The scroller never moves focus. The jump button is inert while hidden; if it had focus when pressed, focus moves to the viewport rather than being lost. Jumps respect prefers-reduced-motion."
        },
        {
          "id": "long-threads",
          "title": "Long threads",
          "text": "Work is batched into one pass per frame, listeners are passive and the reading position is found by binary search, so the cost of a stream doesn't grow with the length of the thread. Add .message-scroller-lazy to skip rendering rows that are out of view (content-visibility: auto). Position-keeping absorbs the size changes as rows come into view. It clips absolutely positioned popups inside rows, so leave it off if your rows carry dropdown menus."
        },
        {
          "id": "api",
          "title": "API and events",
          "text": "ts import { getMessageScroller } from \"htmx-ui\"; const el = document.querySelector(\"#chat\")!; el.addEventListener(\"message-scroller:init\", (e) => { const chat = (e as CustomEvent).detail.api; // same as getMessageScroller(el) }); el.addEventListener(\"message-scroller:unread\", (e) => console.log((e as CustomEvent).detail.rows)); API Description getMessageScroller(el) The API of the scroller containing el (once initialised). scrollToBottom({ smooth }) Scroll to the end and follow. scrollToMessage(idOrEl, { align, highlight, smooth }) Scroll a row into view: align start (default) | center | end; highlight defaults to true. False if it isn't in the transcript. anchor(row, { smooth }) Treat a row as the newest turn: near the top, with room below. unfollow() Stop following, as if the reader scrolled away. update() Re-measure now, after a change the observers can't see. announce(text) Say something through the polite status. following / atBottom / unread Read-only state. message-scroller:init Ready; detail.api. message-scroller:follow / :unfollow Following started or stopped; detail.reason (scroll, wheel, touch, keyboard, selection, link, jump, api…). message-scroller:unread Rows arrived below the reader; detail.rows. message-scroller:anchor A turn was anchored near the top; detail.row."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .message-scroller The scroller: a column; give it a height. Sets --message-scroller-anchor-offset (3rem). .message-scroller-viewport The scrolling region (overflow-anchor: none). .message-scroller-content The rows; padded. Add .message-list for spacing. .message-scroller-jump On a .btn.btn-outline.btn-sm.btn-rounded: the jump button, over the bottom of the viewport; data-state hidden | latest | unread | streaming. .message-scroller-jump-unread / -streaming Inside the jump button: shown only in that state. .message-scroller-marker A divider row, e.g. \"New\" before the first unread message. .message-scroller-lazy On the scroller: rows out of view aren't rendered (content-visibility: auto). [data-message-scroller] Behaviour: following, anchoring, position-keeping, jumps, announcements. [data-message-scroller-open] last-anchor (default) | bottom | top | #id. [data-message-scroller-threshold] Pixels from the bottom that still count as at the bottom (32). [data-message-scroller-follow=\"false\"] Never follow growing content. [data-message-scroller-anchor] Selector for anchor turns, or \"none\". [data-message-scroller-announce-new / -done] Announcement texts; \"\" for none. [data-message-scroller-jump] On the jump button. [data-message-anchor] On a row: a turn to anchor (=\"false\" opts out). [data-streaming] On a row or the scroller: a reply is being written. [data-highlighted] Set briefly on a row you jumped to. [data-following] / [data-at-bottom] / [data-unread] State, set on the scroller."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/prompt-input",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/prompt-input",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/prompt-input.md",
      "title": "Prompt input",
      "description": "The text box of an AI chat: a growing textarea with attachments, tools and a send button that turns into stop, plus @ mentions and / commands.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "actions",
          "text": "Actions"
        },
        {
          "level": 2,
          "id": "mentions-and-commands",
          "text": "Mentions and commands"
        },
        {
          "level": 2,
          "id": "server",
          "text": "Suggestions from the server"
        },
        {
          "level": 2,
          "id": "htmx",
          "text": "With htmx"
        },
        {
          "level": 2,
          "id": "keyboard",
          "text": "Keyboard"
        },
        {
          "level": 2,
          "id": "events",
          "text": "Events"
        },
        {
          "level": 2,
          "id": "macro",
          "text": "Nunjucks macro"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div id=\"pi-messages\" class=\"message-list\" aria-live=\"polite\"></div> <form class=\"prompt-input\" data-prompt-input hx-post=\"/api/prompt-input/reply\" hx-target=\"#pi-messages\" hx-swap=\"beforeend\"> <textarea class=\"prompt-input-textarea\" name=\"prompt\" rows=\"1\" data-textarea aria-label=\"Prompt\" placeholder=\"Ask anything. Type @ to mention someone, / for commands.\"></textarea> <div class=\"prompt-input-actions\"> <div class=\"prompt-input-tools\"> <span class=\"tooltip\" data-tooltip> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm\" aria-label=\"Attach files\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48\"/></svg></button> <span class=\"tooltip-content\" role=\"tooltip\">Attach files</span> </span> <button type=\"button\" class=\"btn btn-ghost btn-sm\" data-toggle aria-pressed=\"false\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg> Search</button> <div class=\"select prompt-input-select\" data-select> <button type=\"button\" id=\"pi-model\" class=\"select-trigger select-sm\" data-select-trigger aria-haspopup=\"listbox\" aria-expanded=\"false\" aria-label=\"Model\"> <span class=\"select-value\" data-placeholder=\"Select…\">Claude Opus</span> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg> </button> <div class=\"select-content select-content-up\" id=\"pi-model-listbox\" role=\"listbox\" aria-"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": "The box is a <form class=\"prompt-input\" data-prompt-input>. It shows one focus ring for everything inside it, and a click on its empty space puts the caret in the textarea. The text goes in a <textarea class=\"prompt-input-textarea\"> with a name and an accessible name (aria-label). It grows with its text up to max-h-48 (put another max-h-* on it to change that), then scrolls. rows=\"1\" starts it one line tall. Add data-textarea so the textarea behaviour grows it in browsers without CSS field-sizing; leave out data-textarea-submit, as the prompt input already handles Enter. Below it, .prompt-input-actions is the toolbar: a .prompt-input-tools group at the start, and the submit button at the end. The submit button has data-prompt-input-submit, an aria-label, the .prompt-input-submit class and two icons: the send arrow, and a .prompt-input-stop-icon square that replaces it while a request runs. Optional: a .prompt-input-attachments row before the textarea, and one .prompt-input-menu per trigger character (see Mentions and commands). Minimal <form class=\"prompt-input\" data-prompt-input hx-post=\"/api/chat\" hx-target=\"#messages\" hx-swap=\"beforeend\"> <textarea class=\"prompt-input-textarea\" name=\"prompt\" rows=\"1\" data-textarea aria-label=\"Prompt\" placeholder=\"Ask anything…\"></textarea> <div class=\"prompt-input-actions\"> <button class=\"btn btn-primary btn-icon btn-sm btn-rounded prompt-input-submit\" data-prompt-input-submit aria-label=\"Send\"> <svg>…arrow-up…</svg><span class=\"prompt-input-stop-icon\" aria-hidden=\"true\"></span> </button> </div> </form>"
        },
        {
          "id": "actions",
          "title": "Actions",
          "text": "Anything can go in .prompt-input-tools: small ghost buttons (btn btn-ghost btn-icon btn-sm), toggles with aria-pressed for tools the model may use, a select for the model with class=\"prompt-input-select\", size=\"sm\" and side=\"top\" (it looks like a ghost button until hovered, and opens above the box), tooltips on icon buttons, or a dropdown. Give them type=\"button\" so they don't send the form. The submit button is disabled while the box is empty: no text (spaces don't count) and no attachment inside. Put attachments in .prompt-input-attachments, above the text; the button follows as they are added and removed. html <form class=\"prompt-input\" data-prompt-input hx-boost=\"false\" onsubmit=\"event.preventDefault()\"> <div class=\"prompt-input-attachments\"> <div class=\"attachment attachment-xs\"> <div class=\"attachment-media\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4M10 9H8M16 13H8M16 17H8\"/></svg></div> <div class=\"attachment-content\"><p class=\"attachment-title\">roadmap.pdf</p></div> <div class=\"attachment-actions\"> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-xs\" aria-label=\"Remove roadmap.pdf\" onclick=\"this.closest('.attachment').remove()\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button> </div> </div> </div> <textarea class=\"prompt-input-textarea\" name=\"prompt\" rows=\"1\" data-textarea aria-label=\"Prompt\" placeholder=\"Ask about the attachment…\"></textarea> <div class=\"prompt-input-actions\"> <div class=\"prompt-input-tools\"> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm\" ari"
        },
        {
          "id": "mentions-and-commands",
          "title": "Mentions and commands",
          "text": "A .prompt-input-menu with data-prompt-input-menu=\"@\" and role=\"listbox\" (and an aria-label) holds suggestions for one trigger character. Typing the trigger opens it above the box, and the text typed after the trigger filters it: an option matches on its data-value, its text and its data-keywords, ignoring case and accents. Each option is a .prompt-input-option with role=\"option\" and a data-value; inside, anything goes: an icon or an avatar, a .prompt-input-option-label and a .prompt-input-option-description. A .prompt-input-empty with data-prompt-input-empty shows when nothing matches; without one the menu closes instead. Where a trigger counts. @ (and any other character) opens its menu at the start of the text or after a space, so an email address doesn't. / only counts as the very first character of the text, as in most chat apps, so a URL or a fraction never opens it. Set data-prompt-input-position=\"start\" or \"word\" on a menu to choose. Picking replaces the trigger and the query with the trigger, the option's data-value and a space (@grace , /summarize ). Give an option data-insert to insert other text. Groups: wrap options in a .prompt-input-group with role=\"group\", headed by a .prompt-input-label it names through aria-labelledby. A group with nothing left in it hides. Menus open above the box, as wide as it, or below it when there is no room above. Add .prompt-input-menu-down to always open below. html <form class=\"prompt-input\" data-prompt-input hx-boost=\"false\" onsubmit=\"event.preventDefault()\"> <textarea class=\"prompt-input-textarea\" name=\"prompt\" rows=\"1\" data-textarea aria-label=\"Prompt\" placeholder=\"Type @ for files, / for commands\"></textarea> <div class=\"prompt-input-actions\"> <button class=\"btn btn-primary btn-icon btn-sm btn-rounded prompt-input-submit\" data-prompt-input-submit aria-label=\"Send\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\""
        },
        {
          "id": "server",
          "title": "Suggestions from the server",
          "text": "For a list too long to put in the page, add data-prompt-input-src to the menu and leave it empty. As the user types, the behaviour fetches src?q=<query>&trigger=@ (debounced: 150 ms, or data-prompt-input-delay; older answers are dropped), and the returned option markup replaces the menu's content. The server does the filtering. Return a .prompt-input-empty with data-prompt-input-empty to say nothing matched, or nothing to close the menu. The request carries HX-Request: true, so a route shared with htmx can return fragments as usual. html <form class=\"prompt-input\" data-prompt-input hx-boost=\"false\" onsubmit=\"event.preventDefault()\"> <textarea class=\"prompt-input-textarea\" name=\"prompt\" rows=\"1\" data-textarea aria-label=\"Prompt\" placeholder=\"Type @ and a name: Ada, Grace, Tim…\"></textarea> <div class=\"prompt-input-actions\"> <button class=\"btn btn-primary btn-icon btn-sm btn-rounded prompt-input-submit\" data-prompt-input-submit aria-label=\"Send\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12 19V5M5 12l7-7 7 7\"/></svg><span class=\"prompt-input-stop-icon\" aria-hidden=\"true\"></span> </button> </div> <div class=\"prompt-input-menu\" data-prompt-input-menu=\"@\" data-prompt-input-src=\"/api/prompt-input/mentions\" role=\"listbox\" aria-label=\"People\" hidden></div> </form> The endpoint answers with plain option markup, the same as inline options: <div class=\"prompt-input-option\" role=\"option\" data-value=\"ada\">…</div>. This demo's endpoint is part of the dev server, so it only answers on a running site, not on a static host."
        },
        {
          "id": "htmx",
          "title": "With htmx",
          "text": "Put hx-post on the form. Enter sends it with requestSubmit(), so htmx, native validation (required, maxlength) and your own submit listeners all run, and every field in the form goes along: the prompt, the model select, hidden inputs for toggles or attachment ids. While the form's request runs (from htmx's htmx:before:request to htmx:finally:request), the form has data-state=\"loading\". htmx:finally:request is used rather than htmx:after:request because htmx 4 also fires it after an error or an abort. In the loading state the submit button shows the stop square, is labelled \"Stop\" (data-prompt-input-stop-label on the button changes that), and Enter sends nothing. Clicking stop fires prompt-input:stop on the form, then sends htmx an htmx:abort, which cancels the request. Call preventDefault() on the event to stop the answer your own way instead (say, by telling a streaming server to stop). When the response is under 400 the textarea is cleared. An error keeps the text, so the user can try again. Add data-prompt-input-clear=\"false\" to keep it always. Streaming the answer (server-sent events, or a request that appends as it goes) is the server's business; the prompt input only needs the request's start and end. An app that sends prompts without htmx sets and removes data-state=\"loading\" on the form itself; the button follows, and stop then clears the state after firing its event. A stop that tells the server <form class=\"prompt-input\" data-prompt-input hx-post=\"/chat\" hx-target=\"#messages\" hx-swap=\"beforeend\" hx-on:prompt-input:stop=\"fetch('/chat/stop', { method: 'POST' })\"> … </form>"
        },
        {
          "id": "keyboard",
          "title": "Keyboard",
          "text": "Key Action Enter Send the prompt (nothing while empty or loading) Shift Enter New line @, / Open that trigger's menu; the text after it filters the menu ↓ ↑ Menu open: move the highlight (wrapping round) Enter, Tab Menu open: insert the highlighted suggestion Esc Close the menu; it stays closed until the next trigger The first suggestion is highlighted as soon as a menu opens, so Enter takes it. While an IME composition is in progress, Enter is left to the IME."
        },
        {
          "id": "events",
          "title": "Events",
          "text": "Both events bubble from the form. prompt-input:select, after a suggestion is inserted, with detail { trigger, value, option, query }: the trigger character, the option's value, the option element and the text that was typed after the trigger. prompt-input:stop, when the stop button is clicked. Cancelable: preventDefault() keeps the request running and the loading state on. Picks are inserted as plain text, which the server can read straight from the prompt. To show them as chips instead, or to send the picked ids, listen for prompt-input:select: app.js document.addEventListener(\"prompt-input:select\", (e) => { const { trigger, value } = e.detail; if (trigger !== \"@\") return; // Send the mentioned ids with the prompt, as hidden fields in the form. const input = Object.assign(document.createElement(\"input\"), { type: \"hidden\", name: \"mentions\", value }); e.target.append(input); });"
        },
        {
          "id": "macro",
          "title": "Nunjucks macro",
          "text": "The prompt_input macro writes the form, the textarea, the toolbar with its submit button and the menus. The call body goes in the toolbar. mentions and commands take options as {value, label, description, keywords, icon}; mentions_src and commands_src fetch them from the server instead. html <form class=\"prompt-input\" data-prompt-input id=\"pi-macro\" hx-boost=\"false\" onsubmit=\"event.preventDefault()\"> <textarea class=\"prompt-input-textarea\" name=\"prompt\" rows=\"1\" placeholder=\"Send a message\" aria-label=\"Prompt\" data-textarea id=\"pi-macro-textarea\"></textarea> <div class=\"prompt-input-actions\"> <div class=\"prompt-input-tools\"> <button type=\"button\" class=\"btn btn-ghost btn-icon btn-sm\" aria-label=\"Attach files\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48\"/></svg></button> </div> <button class=\"btn btn-primary btn-icon btn-sm btn-rounded prompt-input-submit\" data-prompt-input-submit aria-label=\"Send\" data-prompt-input-stop-label=\"Stop\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12 19V5M5 12l7-7 7 7\"/></svg><span class=\"prompt-input-stop-icon\" aria-hidden=\"true\"></span> </button> </div> <div class=\"prompt-input-menu\" data-prompt-input-menu=\"@\" role=\"listbox\" aria-label=\"Mentions\" hidden> <div class=\"prompt-input-option\" role=\"option\" data-value=\"ada\"> <span class=\"prompt-input-option-label\">Ada Lovelace</span> <span class=\"prompt-input-option-description\">Analyst</span> </div> <div class=\"prompt-input-option\" role=\"option\" data-value=\"alan\" data-keywords=\"enigma\"> <span class=\"prompt-input-option-label\">"
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class / attribute Description .prompt-input The box: a <form> with data-prompt-input. One focus ring for all of it. .prompt-input-textarea The text. Grows with its content up to max-h-48. Add data-textarea for older browsers. .prompt-input-attachments A row of attachments above the text; takes no room while empty. .prompt-input-actions The toolbar under the text. .prompt-input-tools Buttons, toggles and selects at the start of the toolbar. .prompt-input-select On a .select (or a native select.select.input-sm): the model picker, looking like a ghost button. .prompt-input-submit The send button, at the end of the toolbar. Add data-prompt-input-submit. .prompt-input-stop-icon The square shown in the submit button while loading. .prompt-input-menu A listbox of suggestions for one trigger, above the box. data-prompt-input-menu=\"@\". .prompt-input-menu-down Always open the menu below the box (otherwise it flips below on its own when there is no room above: data-side=\"bottom\"). .prompt-input-option One suggestion (role=\"option\", data-value). data-keywords: extra words to match; data-insert: the text to insert. .prompt-input-option-label / -description The option's name, and muted text at its end. .prompt-input-group / .prompt-input-label A group of options (role=\"group\") and its heading. .prompt-input-empty Shown when nothing matches. Add data-prompt-input-empty. data-state=\"loading\" On the form while its request runs: the submit button becomes stop. data-prompt-input-clear=\"false\" Keep the text after a successful send. data-prompt-input-stop-label On the submit button: its label while loading (default \"Stop\"). data-prompt-input-position On a menu: \"start\" (only as the first character; the default for /) or \"word\" (after a space). data-prompt-input-src / -delay On a menu: fetch options from src?q=…&trigger=…, after delay ms (150). prompt-input:select Event with detail { trigger, value, option, query }. prompt-input:stop Cancelable event when stop is clicked; then htmx:abort"
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/components/reasoning",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/components/reasoning",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/components/reasoning.md",
      "title": "Reasoning",
      "description": "A collapsible for an AI model's reasoning: open and shimmering while it streams, closed with how long it thought once it's done.",
      "section": "Components",
      "headings": [
        {
          "level": 2,
          "id": "markup",
          "text": "Markup"
        },
        {
          "level": 2,
          "id": "streaming",
          "text": "Streaming and auto-close"
        },
        {
          "level": 3,
          "id": "htmx",
          "text": "From the server with htmx"
        },
        {
          "level": 2,
          "id": "manual",
          "text": "Manual control"
        },
        {
          "level": 2,
          "id": "message",
          "text": "With a message"
        },
        {
          "level": 2,
          "id": "macro",
          "text": "Macro"
        },
        {
          "level": 2,
          "id": "reference",
          "text": "Reference"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "html <div class=\"mx-auto w-full max-w-lg\"> <details class=\"reasoning\" data-reasoning data-reasoning-duration=\"4\" open> <summary class=\"reasoning-trigger\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12 3l1.9 5.8L20 11l-6.1 2.2L12 19l-1.9-5.8L4 11l6.1-2.2z\"/><path d=\"M19 3v4M17 5h4\"/></svg><span class=\"reasoning-label\">Thought for 4 seconds</span></summary> <div class=\"reasoning-content\"> <p>The user wants to know why the build is slower since Tuesday.</p> <ol> <li>Compare the two CI runs: the <code>install</code> step went from 20s to 2m.</li> <li>The lockfile changed on Tuesday, so the dependency cache missed.</li> <li>Suggest keying the cache on the lockfile's hash.</li> </ol> </div> </details> </div>"
        },
        {
          "id": "markup",
          "title": "Markup",
          "text": ".reasoning goes on a <details>, so it opens and closes with no JavaScript. data-reasoning adds the streaming behaviour. Its <summary class=\"reasoning-trigger\"> holds an optional icon and the .reasoning-label, the text the behaviour rewrites. The chevron is drawn for you and turns while open. .reasoning-content holds the reasoning: paragraphs, lists and code are styled, with a line down its side (.reasoning-plain on the <details> drops it). It opens and closes smoothly where the browser can animate to height: auto, and snaps for readers who prefer reduced motion."
        },
        {
          "id": "streaming",
          "title": "Streaming and auto-close",
          "text": "While data-streaming is on the <details> it stays open, is aria-busy, and its label is a shimmering \"Thinking…\". When the attribute goes, the label becomes \"Thought for N seconds\", timed from when it started streaming, and a second later it closes. The attribute is watched, so whatever removes it (a script, an htmx swap) ends the stream. html <div class=\"mx-auto flex w-full max-w-lg flex-col items-start gap-4\"> <button type=\"button\" class=\"btn btn-outline btn-sm\" onclick=\"streamReasoning(this)\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M6 4.5a1 1 0 0 1 1.5-.87l12 7.5a1 1 0 0 1 0 1.74l-12 7.5A1 1 0 0 1 6 19.5Z\"/></svg> Ask</button> <details class=\"reasoning\" id=\"reasoning-live\" data-reasoning> <summary class=\"reasoning-trigger\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12 3l1.9 5.8L20 11l-6.1 2.2L12 19l-1.9-5.8L4 11l6.1-2.2z\"/><path d=\"M19 3v4M17 5h4\"/></svg><span class=\"reasoning-label\">Reasoning</span></summary> <div class=\"reasoning-content\"><p>Press Ask to stream some reasoning.</p></div> </details> </div> <script> function streamReasoning(button) { const reasoning = document.getElementById(\"reasoning-live\"); const content = reasoning.querySelector(\".reasoning-content\"); const steps = [\"Reading the question.\", \"Checking the release notes for 0.2.\", \"Writing a short answer.\"]; button.disabled = true; content.replaceChildren(); reasoning.setAttribute(\"data-streaming\", \"\"); steps.forEach((step, i) => setTimeout(() => content.insertAdjacentHTML(\"beforeend\", `<p>${step}</p>`), 800 * (i + 1)), ); setTimeout(() => { reasoning.removeAttribute(\"data-streaming\"); button.disabled = false; }, 800 * (steps.length + 1)"
        },
        {
          "id": "htmx",
          "title": "From the server with htmx",
          "text": "Poll for the reasoning so far: each response is the whole <details> again, swapped with hx-swap=\"outerHTML\", still streaming and asking for the next one. Give it an id: the replacement then carries on the first one's timer and whether the reader opened or closed it. The last response leaves out data-streaming and the polling, and the reasoning closes as above. html <!-- each response from /chat/7/reasoning while the model thinks <!-- each response from /chat/7/reasoning while the model thinks --> <details class=\"reasoning\" id=\"reasoning-7\" data-reasoning data-streaming open hx-get=\"/chat/7/reasoning\" hx-trigger=\"load delay:500ms\" hx-swap=\"outerHTML\"> <summary class=\"reasoning-trigger\"> <span class=\"reasoning-label\"><span class=\"loader loader-text-shimmer\">Thinking…</span></span> </summary> <div class=\"reasoning-content\"><p>The user wants…</p></div> </details> <!-- the last one: done, so no data-streaming and no polling <!-- the last one: done, so no data-streaming and no polling --> <details class=\"reasoning\" id=\"reasoning-7\" data-reasoning data-reasoning-duration=\"6\"> <summary class=\"reasoning-trigger\"><span class=\"reasoning-label\">Thought for 6 seconds</span></summary> <div class=\"reasoning-content\"><p>The user wants…</p><p>So the answer is…</p></div> </details> Streaming over SSE or a WebSocket instead, append the text to .reasoning-content as it arrives and remove data-streaming when the reasoning ends."
        },
        {
          "id": "manual",
          "title": "Manual control",
          "text": "Leave out data-reasoning (or data-streaming) and it is a plain disclosure: open starts it open, and the reader toggles it. Script it like any <details>, with el.open = true and the toggle event. With the behaviour, reasoning:start and reasoning:end bubble from it, the second with detail.duration in seconds. html <div class=\"mx-auto flex w-full max-w-lg flex-col gap-4\"> <details class=\"reasoning\"> <summary class=\"reasoning-trigger\"><span class=\"reasoning-label\">Show reasoning</span></summary> <div class=\"reasoning-content\"><p>Closed until the reader opens it.</p></div> </details> <details class=\"reasoning reasoning-plain\" open> <summary class=\"reasoning-trigger\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12 3l1.9 5.8L20 11l-6.1 2.2L12 19l-1.9-5.8L4 11l6.1-2.2z\"/><path d=\"M19 3v4M17 5h4\"/></svg><span class=\"reasoning-label\">Explanation</span></summary> <div class=\"reasoning-content\"><p>Open from the start, with no line beside it.</p></div> </details> </div>"
        },
        {
          "id": "message",
          "title": "With a message",
          "text": "Put it in a .message-plain body, above the reply, as in the Message component's AI chat. html <div class=\"message-list mx-auto w-full max-w-lg\"> <div class=\"message\" data-align=\"end\"> <div class=\"message-body\"><div class=\"message-content\">Is 1013 prime?</div></div> </div> <div class=\"message message-plain\"> <span class=\"avatar message-avatar\" role=\"img\" aria-label=\"Assistant\"><span class=\"avatar-fallback\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12 8V4H8\"/><rect width=\"16\" height=\"12\" x=\"4\" y=\"8\" rx=\"2\"/><path d=\"M2 14h2M20 14h2M15 13v2M9 13v2\"/></svg></span></span> <div class=\"message-body\"> <details class=\"reasoning\" data-reasoning data-reasoning-duration=\"2\"> <summary class=\"reasoning-trigger\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"size-4\" aria-hidden=\"true\"><path d=\"M12 3l1.9 5.8L20 11l-6.1 2.2L12 19l-1.9-5.8L4 11l6.1-2.2z\"/><path d=\"M19 3v4M17 5h4\"/></svg><span class=\"reasoning-label\">Thought for 2 seconds</span></summary> <div class=\"reasoning-content\"> <p>√1013 ≈ 31.8, so try the primes up to 31.</p> <p>None of 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 or 31 divides it.</p> </div> </details> <div class=\"message-content\">Yes, 1013 is prime: no prime up to its square root divides it.</div> </div> </div> </div>"
        },
        {
          "id": "macro",
          "title": "Macro",
          "text": "reasoning() writes the markup, with the label worked out from duration and the options as data attributes. The call body is the content. jinja {% from \"components/reasoning/reasoning.html\" import reasoning %} {% call reasoning(duration=4) %}<p>The user wants…</p>{% endcall %} {% call reasoning(streaming=true, id=\"reasoning-7\") %}<p>The user wants…</p>{% endcall %} {% call reasoning(\"Explanation\", open=true, plain=true, icon=none) %}…{% endcall %} Argument Description label=none The summary text. Defaults to \"Thought for N seconds\" with a duration, else \"Reasoning\". streaming=false Adds data-streaming: open, with the shimmering streaming_label. open=false Start open. duration=none Seconds it took (data-reasoning-duration). streaming_label / done_label The labels while streaming and once done ({s} = seconds). auto_close=true / close_delay=none Whether, and after how many ms, it closes when the stream ends. icon=\"sparkles\" An icon before the label; none for none. plain=false No line beside the content. id / attrs / class An id (needed when htmx replaces it mid-stream), more attributes as a dict, more classes."
        },
        {
          "id": "reference",
          "title": "Reference",
          "text": "Class Description .reasoning On a <details>: the whole component. [data-reasoning] Adds the behaviour: open while streaming, label and close at the end. [data-streaming] Still streaming: open, aria-busy, shimmering label. .reasoning-trigger On the <summary>: icon, label and chevron. .reasoning-label The summary text, rewritten by the behaviour. .reasoning-content The reasoning, with a line beside it. .reasoning-plain On .reasoning: no line beside the content. reasoning:start / reasoning:end Events, bubbling; reasoning:end has detail.duration (seconds)."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/upgrading",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/upgrading",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/upgrading.md",
      "title": "Upgrading",
      "description": "Move a project to a newer htmx-ui with one command: htmx-ui-upgrade rewrites what changed and lists what it couldn't.",
      "section": "Project",
      "headings": [
        {
          "level": 2,
          "id": "what-it-does",
          "text": "What it does"
        },
        {
          "level": 2,
          "id": "options",
          "text": "Options"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "Before 1.0, a minor release can rename a class or change a component's markup; every one is listed under Breaking changes in the changelog. htmx-ui-upgrade makes those edits for you. Run it in your project, with your work committed: bunx htmx-ui-upgrade@latest npx htmx-ui-upgrade@latest pnpm dlx htmx-ui-upgrade@latest yarn dlx htmx-ui-upgrade@latest Then install the new versions and build the site. Always run @latest: each release of htmx-ui ships an htmx-ui-upgrade of the same version that knows every migration up to it, so a project several releases behind upgrades in one go."
        },
        {
          "id": "what-it-does",
          "title": "What it does",
          "text": "Reads the version your code is on from package.json (htmx-ui, else htmx-ui-engine, else a plugin). --from sets it instead. Runs each release's migration after that version, oldest first, over your templates, scripts, stylesheets and htmx-ui.config.*: renamed classes and attributes in class=\"…\" values, selectors and strings; markup that changed shape; classes that became required; removed functions; renamed config keys. Sets htmx-ui, htmx-ui-engine and the plugins in package.json to the new version. Prints what it changed, file by file, and every place it left for you as file:line: markup it doesn't recognise, classes with no one-to-one replacement, old names in prose or comments. text Upgrading 0.2.0 → 0.3.0 (htmx-ui ^0.2.0) Migrations: 0.3.0 Changed 3 files: layouts/docs.html .code-tab → .tabs-trigger .dialog-close gets .btn .btn-ghost .btn-icon .btn-sm (2) app.ts initCode() call removed (initComponents() runs the clipboard) package.json htmx-ui ^0.2.0 → ^0.3.0 Check by hand (1): partials/chat.html:14 .message-typing is gone: use loader(\"typing\") (/docs/components/loader) It never touches node_modules, dist, build, lib, archive (frozen docs versions) or dot-directories, and it refuses to run on a git working tree with uncommitted changes, so the upgrade is one diff to review, or undo."
        },
        {
          "id": "options",
          "title": "Options",
          "text": "Option Description [directory] The project to upgrade. Default: the current directory. --from <version> The version the code is written for. Default: read from package.json. --to <version> Upgrade to this version. Default: the newest this release knows. --dry-run Show what would change and write nothing. --force Run even though there are uncommitted changes. --list List every migration and what it does. bunx htmx-ui-upgrade@latest ./my-site --dry-run bunx htmx-ui-upgrade@latest --from 0.1.0 --to 0.2.0 npx htmx-ui-upgrade@latest ./my-site --dry-run npx htmx-ui-upgrade@latest --from 0.1.0 --to 0.2.0 pnpm dlx htmx-ui-upgrade@latest ./my-site --dry-run pnpm dlx htmx-ui-upgrade@latest --from 0.1.0 --to 0.2.0 yarn dlx htmx-ui-upgrade@latest ./my-site --dry-run yarn dlx htmx-ui-upgrade@latest --from 0.1.0 --to 0.2.0"
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/changelog",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/changelog",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/changelog.md",
      "title": "Changelog",
      "description": "Every release of HTMX UI, newest first. The project follows semantic versioning.",
      "section": "Project",
      "headings": [
        {
          "level": 2,
          "id": "versioning",
          "text": "Versioning"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "v0.3.0 Latest2026-10-08 Six built-in themes with a switcher; htmx-ui-upgrade, one command that migrates a project between versions; clipboard, timeline and password input components; copy buttons that work everywhere; scroll-preserving htmx extensions; a dev server that reflects every change; and components rebuilt on shared utilities. v0.2.0 2026-10-06 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. v0.1.0 2026-10-02 First release: design tokens, light and dark themes, twenty components including forms, toggles and loading states, syntax highlighting, versioned and agent-ready docs; plus htmx-ui-engine (dev server and builds on Bun or Node) and create-htmx-ui."
        },
        {
          "id": "versioning",
          "title": "Versioning",
          "text": "Versions follow semantic versioning. Before 1.0, minor versions (0.x.0) may rename classes or tokens; each release page lists what to change, and htmx-ui-upgrade applies those changes to your project in one command — see Upgrading."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/changelog/0.1.0",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/changelog/0.1.0",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/changelog/0.1.0.md",
      "title": "v0.1.0",
      "description": "First release: design tokens, light and dark themes, twenty components including forms, toggles and loading states, syntax highlighting, versioned and agent-ready docs; plus htmx-ui-engine (dev server and builds on Bun or Node) and create-htmx-ui.",
      "section": "Project",
      "headings": [
        {
          "level": 2,
          "id": "added",
          "text": "Added"
        },
        {
          "level": 3,
          "id": "components",
          "text": "Components"
        },
        {
          "level": 3,
          "id": "theming-and-dark-mode",
          "text": "Theming and dark mode"
        },
        {
          "level": 3,
          "id": "templates-and-data",
          "text": "Templates and data"
        },
        {
          "level": 3,
          "id": "docs",
          "text": "Docs"
        },
        {
          "level": 3,
          "id": "package",
          "text": "Package"
        },
        {
          "level": 3,
          "id": "engine-and-project-starter",
          "text": "Engine and project starter"
        },
        {
          "level": 3,
          "id": "for-ai-agents",
          "text": "For AI agents"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "All releases Latest 2026-10-02"
        },
        {
          "id": "added",
          "title": "Added",
          "text": ""
        },
        {
          "id": "components",
          "title": "Components",
          "text": "Accordion: built on <details>: one or several open, disabled items, bordered and separated styles, lazy-loaded content with htmx. Alert: neutral, info, success, warning and danger, with an optional dismiss button. Badge: solid, outline and soft status variants, plus a status dot. Button: primary, secondary, outline, ghost, danger and link variants; extra small to large sizes; icon, rounded, disabled and loading states. Button group: joins buttons, inputs, dropdowns and popovers: vertical, sized, separated, nested toolbars and split buttons. Card: header, title, description, action, content and footer parts; edge-to-edge images, horizontal layout, scrolling content, compact size and clickable cards. Code block: build-time syntax highlighting, copy button, file tabs and npm/pnpm/yarn/bun command tabs. Dropdown: accessible menu with keyboard navigation; powers the docs version switcher. Field: labels, descriptions and error, warning and success messages; an automatic required asterisk; inline, responsive and grid form layouts; fieldsets and separators. Icon: SVG files inlined at build time, or linked as images. Input: text inputs, textareas, selects and file inputs, with sizes and disabled, error, warning and success states. Input group: icons, text, badges, buttons, dropdowns and spinners inside an input, at the inline start or end, above or below. Popover: a floating panel of rich content or a small form that opens from a button. Sidebar: sticky on desktop, off-canvas with a backdrop on mobile. Spinner: any icon or a pure-CSS ring, in four sizes; works in buttons, badges, button groups and input groups, and as an htmx request indicator. Table: bordered wrapper, muted header and hover rows. Tabs: accessible tab groups that can sync with each other and remember the reader's choice. Text: @tailwindcss/typography (.prose) mapped onto the design tokens, plus heading, lead, muted, link, inline code and kbd helpers. Toggle: any button becomes an on/off toggle with aria-pres"
        },
        {
          "id": "theming-and-dark-mode",
          "title": "Theming and dark mode",
          "text": "Design tokens as CSS variables, exposed as Tailwind colours with @theme inline. See Theming. Thin, theme-tinted scrollbars everywhere (--scrollbar-thumb). See Theming. Class-based dark mode with a theme switcher that follows the OS until the user picks. See Dark mode."
        },
        {
          "id": "templates-and-data",
          "title": "Templates and data",
          "text": "Component behaviours initialise inside swapped content via htmx:after:process, including when the swapped element is the component itself. Optional Nunjucks macros for alerts, code blocks, command tabs, icons and spinners. json(), svg() and glob() template functions. See Templates & data."
        },
        {
          "id": "docs",
          "title": "Docs",
          "text": "Versioned docs: a version switcher, frozen snapshots of older versions under /docs/v<version>, and /docs/versions.json. See Versions. Site search (Ctrl / ⌘ + K): fuzzy-finds pages and sections in the docs version you're reading, and opens the exact section."
        },
        {
          "id": "package",
          "title": "Package",
          "text": "ES modules with TypeScript types for htmx-ui and htmx-ui/theme; CSS, icons and macros ship as source. htmx.org 4 and tailwindcss 4 are peer dependencies; @tailwindcss/typography is installed with the package. Nunjucks macros for alerts, code blocks, command tabs, icons and spinners, resolved by name in engine sites."
        },
        {
          "id": "engine-and-project-starter",
          "title": "Engine and project starter",
          "text": "htmx-ui-engine: htmx-ui dev, build and preview for sites made of Nunjucks pages, with file-based routing, Tailwind, hot reload (including layouts, partials and data) and mock htmx endpoints. Runs on Bun (Bun.serve, Bun.build) or Node (Vite), picked from whatever started it. See Engine. A Bun plugin (htmx-ui-engine/bun) and a Vite plugin (htmx-ui-engine/vite) for existing build setups. create-htmx-ui: npm create htmx-ui, pnpm create htmx-ui, yarn create htmx-ui or bun create htmx-ui scaffolds a site with pages, a layout, a theme toggle and an htmx demo."
        },
        {
          "id": "for-ai-agents",
          "title": "For AI agents",
          "text": "Every docs page as Markdown at <url>.md, plus /llms.txt, /llms-full.txt, /sitemap.json (with section text for search, and per-version sitemaps) and /sitemap.xml. See AI agents. Every docs heading has a # link, so sections can be shared and opened directly. Prompt examples and a ready-made agent skill that point at /llms.txt."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/changelog/0.2.0",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/changelog/0.2.0",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/changelog/0.2.0.md",
      "title": "v0.2.0",
      "description": "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.",
      "section": "Project",
      "headings": [
        {
          "level": 2,
          "id": "breaking",
          "text": "Breaking changes"
        },
        {
          "level": 3,
          "id": "templates-is-now-roots",
          "text": "templates is now roots"
        },
        {
          "level": 3,
          "id": "create-htmx-ui-defaults-to-bun",
          "text": "create-htmx-ui defaults to bun"
        },
        {
          "level": 3,
          "id": "sidebar-is-a-composable-app-sidebar",
          "text": "Sidebar is a composable app sidebar"
        },
        {
          "level": 2,
          "id": "added",
          "text": "Added"
        },
        {
          "level": 3,
          "id": "components",
          "text": "Components"
        },
        {
          "level": 3,
          "id": "templates-and-data",
          "text": "Templates and data"
        },
        {
          "level": 3,
          "id": "plugins",
          "text": "Plugins"
        },
        {
          "level": 3,
          "id": "engine",
          "text": "Engine"
        },
        {
          "level": 3,
          "id": "package",
          "text": "Package"
        },
        {
          "level": 2,
          "id": "changed",
          "text": "Changed"
        },
        {
          "level": 2,
          "id": "fixed",
          "text": "Fixed"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "All releases Latest 2026-10-06"
        },
        {
          "id": "breaking",
          "title": "Breaking changes",
          "text": "Three changes need an edit in your config, your command line or your sidebar markup."
        },
        {
          "id": "templates-is-now-roots",
          "title": "templates is now roots",
          "text": "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."
        },
        {
          "id": "create-htmx-ui-defaults-to-bun",
          "title": "create-htmx-ui defaults to bun",
          "text": "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."
        },
        {
          "id": "sidebar-is-a-composable-app-sidebar",
          "title": "Sidebar is a composable app sidebar",
          "text": "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-link becomes .sidebar-menu-button, in a <li class=\"sidebar-menu-item\"> of a <ul class=\"sidebar-menu\">. .sidebar-label becomes .sidebar-group-label. .sidebar-backdrop is 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."
        },
        {
          "id": "added",
          "title": "Added",
          "text": ""
        },
        {
          "id": "components",
          "title": "Components",
          "text": "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, and HX-Trigger: dialog:close closes 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-describedby and dismissing with Esc. Label: .label for any control, around checkboxes, radios and switches, or as a .label-card choice card. It works as .field-label ins"
        },
        {
          "id": "templates-and-data",
          "title": "Templates and data",
          "text": "Named template roots: roots in htmx-ui.config.ts is 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() and asset() — 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 point roots at the copy."
        },
        {
          "id": "plugins",
          "title": "Plugins",
          "text": "Plugins: optional features as separate packages, listed under plugins in htmx-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 in htmx-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:name and htmx-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) and contentType() from htmx-ui-engine, plugin commands listed by htmx-ui --help, and an api for plugins that build on each other."
        },
        {
          "id": "engine",
          "title": "Engine",
          "text": "Serving a site from your own backend: createSite() is the framework-free core of the dev servers, wrapped by htmx-ui-engine/elysia (recommended), htmx-ui-engine/fastify, htmx-ui-engine/koa, htmx-ui-engine/express and htmx-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, and site.handle() answers a request from config routes, the built dist/, the config's fetch, then a 404 page. Outside production it renders a page from its template when nothing is built yet, with the context you pass. A default 404 page: a project without pages/404.html gets htmx-ui's own, from the adapters, htmx-ui dev, htmx-ui preview and as dist/404.html in every build. Add pages/404.html to 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's routes intact. Code blocks highlight docker and nginx. render: true in 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 }) with warm() to compile ahead of the first request (createSite() does both), and markup() for a global that returns HTML. debug in the config logs what answered each request, every render with its duration, what the template cache compiled, and builds: debug: true for everything or a list of topics (requests, render, templates, build). Off by default."
        },
        {
          "id": "package",
          "title": "Package",
          "text": "htmx-ui exports the component APIs for script — toast and its types, registerCarouselPlugin, getCarousel, matchesHotkey, commandMatches, getMessageScroller, scrollToEdge, distanceFromEdge and scrollTargetOf — plus queryAll (the helper components use to include the root in their scan) and the Theme type. It now depends on embla-carousel for the carousel. htmx-ui-engine exports createSite, Site, SiteOptions, normalizeRoots, tryLocate, locate, TemplateRoot, RootSpec, warm, markup, logger, TOPICS and the plugin writer's kit: definePlugin, editHtml and contentType. 37 new icons, from arrow-up and bell to folder-plus, paperclip, shield-check and users. bun run version:set <x.y.z> names the docs version in development, dates it, and sets the version on every package at once."
        },
        {
          "id": "changed",
          "title": "Changed",
          "text": "Components animate with shared motion tokens (--motion-duration-*, --motion-ease-*), which collapse to 0s for 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 .gitignore and skips node_modules, dist, public and .git whatever the roots say, so a root of . no longer adds a file watcher for every directory in node_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. .tabs is 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-remove waits for the dialog's own transition before removing it, so sheets and drawers finish sliding out."
        },
        {
          "id": "fixed",
          "title": "Fixed",
          "text": "Links in a .field-description are underlined again; the minified build dropped the underline. htmx-ui build on 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-4 class icon() 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 a data-ver attribute before the bundler sees the page and moved onto the finished URL after it. .select no longer restyles the select component's trigger as a native dropdown; the rule is scoped to select.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."
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/changelog/0.3.0",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/changelog/0.3.0",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/changelog/0.3.0.md",
      "title": "v0.3.0",
      "description": "Six built-in themes with a switcher; htmx-ui-upgrade, one command that migrates a project between versions; clipboard, timeline and password input components; copy buttons that work everywhere; scroll-preserving htmx extensions; a dev server that reflects every change; and components rebuilt on shared utilities.",
      "section": "Project",
      "headings": [
        {
          "level": 2,
          "id": "breaking",
          "text": "Breaking changes"
        },
        {
          "level": 3,
          "id": "code-block-copy-buttons-are-clipboard-buttons",
          "text": "Code block copy buttons are clipboard buttons"
        },
        {
          "level": 2,
          "id": "added",
          "text": "Added"
        },
        {
          "level": 3,
          "id": "components",
          "text": "Components"
        },
        {
          "level": 3,
          "id": "scroll-position-across-swaps",
          "text": "Scroll position across swaps"
        },
        {
          "level": 3,
          "id": "copying-everywhere",
          "text": "Copying everywhere"
        },
        {
          "level": 3,
          "id": "themes",
          "text": "Themes"
        },
        {
          "level": 3,
          "id": "docs",
          "text": "Docs"
        },
        {
          "level": 2,
          "id": "changed",
          "text": "Changed"
        },
        {
          "level": 3,
          "id": "the-select-macro-covers-what-the-component-does",
          "text": "The select() macro covers what the component does"
        },
        {
          "level": 3,
          "id": "reuse",
          "text": "Components reuse each other"
        },
        {
          "level": 3,
          "id": "shared-behaviour-helpers",
          "text": "Shared behaviour helpers"
        },
        {
          "level": 3,
          "id": "also-changed",
          "text": "Also changed"
        },
        {
          "level": 2,
          "id": "fixed",
          "text": "Fixed"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "All releases Latest 2026-10-08"
        },
        {
          "id": "breaking",
          "title": "Breaking changes",
          "text": "npx htmx-ui-upgrade@latest makes these edits for you, and lists what it couldn't. One change needs an edit, and only in hand-written code blocks; several component classes also moved onto shared button markup (see Components reuse each other)."
        },
        {
          "id": "code-block-copy-buttons-are-clipboard-buttons",
          "title": "Code block copy buttons are clipboard buttons",
          "text": "initCode() is gone. The code(), code_block(), code_tabs() and cli() macros render the new button for you. In hand-written blocks, replace the old button with an icon-only clipboard button holding .clipboard-idle and .clipboard-done icons: Before: <button class=\"btn btn-ghost btn-xs code-copy\" data-copy>…</button> After: <button type=\"button\" class=\"btn btn-ghost btn-xs btn-icon clipboard code-copy\" data-clipboard data-clipboard-target=\"pre:not([hidden]) code\" aria-label=\"Copy code\">…</button> .code-copy-idle, .code-copied and data-copy-label are gone. initComponents() runs the clipboard, so drop any direct initCode() call. See Code block for the full markup."
        },
        {
          "id": "added",
          "title": "Added",
          "text": ""
        },
        {
          "id": "components",
          "title": "Components",
          "text": "Clipboard: copy buttons on any button variant and size, with an icon, a label or both, that swap to a green tick for two seconds without changing width, and copy tags. It copies a fixed text, the nearest element matching data-clipboard-target (its value or text), or a URL's text (data-clipboard-url). It uses the Clipboard API with an execCommand fallback for plain http, older Safari and iframes, is announced to screen readers, fires clipboard:copy and clipboard:error, and comes with clipboard() and clipboard_tag() macros. Timeline: CSS only, with dot, icon, number or avatar markers in five colours and outline, a current step, opposite content, alternating sides, vertical or horizontal, dashed or dotted lines, and three sizes. Password input: a show / hide toggle on an input group (an eye icon or Show / Hide text, or a checkbox for several fields). It keeps the caret, hides the password again on submit so password managers save it, hides Edge's own reveal button, has an optional Caps Lock warning, and a password_input() macro."
        },
        {
          "id": "scroll-position-across-swaps",
          "title": "Scroll position across swaps",
          "text": "preserve-scroll, an htmx extension: registerPreserveScroll(), then hx-preserve-scroll=\"<key>\" on any number of scroll containers keeps their position when htmx replaces them, boosted navigations included. Repeated keys match in order; snapshotScroll() and restoreScroll() do the same for DOM you replace yourself. sidebar-active-scroll, for navigation sidebars: hx-sidebar-active-scroll keeps the position across swaps and the current page's link in view, centred on a fresh page load (a link opened in a new tab, a reload) and scrolled just into view after a swap that hid it. The docs sidebar uses it."
        },
        {
          "id": "copying-everywhere",
          "title": "Copying everywhere",
          "text": "Every copy button is now a clipboard button that shows a green tick for two seconds: icon only on code blocks and the docs demos, icon and text on the docs plugin's \"Copy Markdown\" button. \"Copy Markdown\" reads \"Copied\" after copying and fetches the page's .md through data-clipboard-url, so it needs no plugin client. initMarkdownCopy() from htmx-ui-plugin-docs/client stays, deprecated, for pages built with the older markup. Copying from a code block now works over plain http too."
        },
        {
          "id": "themes",
          "title": "Themes",
          "text": "Six built-in themes as htmx-ui/themes/<name>.css: aurora (violet→indigo→cyan — the one this site runs on), ember (amber→orange→rose on warm paper), lagoon (teal→cyan→sky), dusk (pink→fuchsia→purple), jade (emerald→teal→lime) and onyx (monochrome graphite and silver). Each redefines the brand tokens — --radius, --primary, --ring, --brand-gradient — plus dark --background, --surface and --border. Opt-in both ways: no import, no CSS; no attribute, no styles. A switcher ships with them — buttons carrying data-theme-palette apply their palette and remember the choice (initTheme() wires them; getPalette()/setPalette() are exported) and the theme-swatch dot draws the current one. See Theming for the mechanism and Themes for the catalogue — every theme previewed, its exact palette, a one-click apply."
        },
        {
          "id": "docs",
          "title": "Docs",
          "text": "Input OTP is documented for visible 4-digit authenticator codes and PINs, next to 6-digit codes."
        },
        {
          "id": "changed",
          "title": "Changed",
          "text": ""
        },
        {
          "id": "the-select-macro-covers-what-the-component-does",
          "title": "The select() macro covers what the component does",
          "text": "The docs use it wherever they had a native <select> (field, button group, pagination, prompt input). New options: id on the trigger (for <label for>), labelledby, describedby, size, align, side, invalid, required and class. Option groups ({label, options}) and per-option disabled, icon and keywords. attrs also takes a dict. The preselected value is written into the trigger and the hidden field, so it shows and submits before JavaScript runs. The select's search now filters like the combobox's (accents, every word, data-keywords, empty groups hidden), and typing a letter in a list without a search box jumps to the next option starting with it. Button groups and .prompt-input-select accept the select component as well as a native select."
        },
        {
          "id": "reuse",
          "title": "Components reuse each other",
          "text": "Shared CSS: shared looks are Tailwind utilities in the new utilities.css: menu-item, menu-label, menu-separator, menu-empty, popup-surface, focus-outline, input-bare, label-text, description-text, choice-card, panel-title, chevron-mask, summary-trigger and details-animate. Select, combobox, command, dropdown, prompt input, popover, hover card, navigation menu, label and field, the radio card, accordion, reasoning and chain of thought are built on them. Button markup: close buttons (dialog, sheet, alert), the code block's copy button, the sidebar trigger and the message scroller's jump button are .btn buttons. .dialog-close, .sheet-close, .alert-close, .code-copy, .sidebar-trigger and .message-scroller-jump now only position them. Add btn btn-ghost btn-icon btn-sm (copy: btn btn-ghost btn-xs; jump: btn btn-outline btn-sm btn-rounded) to hand-written markup. This also gives the alert's close and the copy button a focus ring. Select trigger: .select-trigger shares the .input rules, including .input-warning and .input-success. Code tabs: code block tabs are .tabs-list.tabs-line.code-tabs with .tabs-trigger buttons. .code-tab is gone. Item separator: .item-separator is gone; use .separator. Reasoning and typing: .message-reasoning and .message-typing are gone; use the reasoning component and loader(\"typing\"). reasoning() renders its streaming label with loader(), and loader(…, label=none) on a text variant drops role=\"status\". Toast icons come from src/icons."
        },
        {
          "id": "shared-behaviour-helpers",
          "title": "Shared behaviour helpers",
          "text": "New modules in packages/ui/src/utils/: listbox.ts: option filtering and the activeDescendant() highlight, used by combobox, command and prompt input, and filtering in select. dismiss.ts: closing on an outside pointer down or when focus leaves. position.ts: place(), which flips a popup that would leave the viewport. shared.ts: ids, arrow-key indexes, small checks. hotkey.ts: matchesHotkey, still exported from htmx-ui. As a result: Select, combobox and popover lists flip when they would leave the viewport, and so does the dropdown's own menu (before, only submenus did). Tabs and toggle groups swap ← and → in right-to-left layouts. The command menu's filter ignores accents. The listboxes scroll their own list, never the page."
        },
        {
          "id": "also-changed",
          "title": "Also changed",
          "text": "dismissible reacts to any [data-dismiss] inside it, including buttons added later, and fires a cancelable dismissible:dismiss. It sets data-state=\"closing\" and waits for a transition before removing the element. Macro options are consistent. Every component macro takes class=\"\" and attrs as a dict, through macros/attrs.html; string attrs still work. alert, dialog, drawer, sheet, empty, code_block, code_tabs, cli, checkbox and input_otp gained the options they lacked, and checkbox gained invalid. A new --overlay token (bg-overlay) dims the page behind modals, sheets, drawers and the mobile sidebar. Accordion, reasoning, chain of thought, collapsible, progress, toast and sidebar transitions use the motion tokens, so they stop under reduced motion. htmx-ui-plugin-search's palette is htmx-ui's command menu in a dialog (.dialog.command-dialog, .command-*); its stylesheet only sizes it. .search-field, .search-input, .search-group and .search-message are gone. htmx-ui-plugin-versions' old-version banner shows the alert's warning icon."
        },
        {
          "id": "fixed",
          "title": "Fixed",
          "text": "Switching docs versions loads the target page instead of swapping the body under the version you are leaving. Version links carry hx-boost=\"false\" — the switcher's items, the ones its client rebuilds from the manifest, the old-version banner, and the links on the versions page — because every version brings its own theme, styles and scripts. Archiving bakes it into a snapshot, whose <body> carries hx-boost=\"false\" as well: nothing inside a frozen page (its logo, footer, sidebar or content links) may boost the live site's code into it. The published v0.1 and v0.2 snapshots carry it all, and htmx-ui-upgrade adds the attribute to a hand-written switcher. Pages your server renders get their scripts and styles. A page from site.render() (or from handle() in development) linked its sources, such as /app.ts, which nothing served. In development they are now bundled from source on request, kept in memory and reloaded on change, never written to dist/; in production they link the build's bundles, from the manifest htmx-ui build now writes. See Development and production. NODE_ENV=production works on Node: the engine's compiled code had the value it was built with baked in, so the adapters on Node always behaved as in development. The Express, Fastify and Koa adapters stream responses, so one that never ends (server-sent events) reaches the client. Server adapters in development show template edits on the next request, find pages added since the server started, and serve public/ before a build has copied it. htmx-ui dev on Bun shows every change without a restart. Edits to a linked package (a workspace package, file: or link: dependency) were picked up once and then missed, and saves that replace the file were always missed; stylesheets that a Tailwind stylesheet @imports now reload the page; changes to htmx-ui.config.ts, plugins, the mock API or the engine now reach rendered pages; and a new page gets its route without a restart. If Bun's dev server crashes, htmx-ui dev star"
        }
      ]
    },
    {
      "url": "https://susonwaiba.github.io/htmx-ui/docs/versions",
      "absoluteUrl": "https://susonwaiba.github.io/htmx-ui/docs/versions",
      "markdown": "https://susonwaiba.github.io/htmx-ui/docs/versions.md",
      "title": "Versions",
      "description": "Docs for every release stay online. Use the version switcher at the top of the sidebar, or the links below.",
      "section": "Project",
      "headings": [
        {
          "level": 2,
          "id": "urls",
          "text": "URLs"
        },
        {
          "level": 2,
          "id": "switching",
          "text": "Switching versions"
        },
        {
          "level": 2,
          "id": "policy",
          "text": "Versioning policy"
        },
        {
          "level": 2,
          "id": "how",
          "text": "How it works"
        },
        {
          "level": 2,
          "id": "agents",
          "text": "For tools and agents"
        }
      ],
      "sections": [
        {
          "id": "",
          "title": "",
          "text": "Version Released Status Docs v0.3 2026-10-08 Latest /docs v0.2 2026-10-06 Archived /docs/v0.2 v0.1 2026-10-02 Archived /docs/v0.1"
        },
        {
          "id": "urls",
          "title": "URLs",
          "text": "The latest docs live at /docs. Each older version keeps its own copy under /docs/v<version>: text /docs/components/button latest /docs/v0.1/components/button v0.1 /docs/v0.1/components/button.md v0.1 as Markdown Older versions are frozen exactly as they were published, including their styles and scripts, so their examples keep working after later releases change the components. They show a notice linking to the same page in the latest version, and ask search engines not to index them."
        },
        {
          "id": "switching",
          "title": "Switching versions",
          "text": "The switcher keeps you on the same page when it exists in the version you pick, and goes to that version's introduction otherwise. Older versions list newer releases too, because the list is read from /docs/versions.json when the page loads. Version links are never htmx-boosted: each version carries its own theme, styles and scripts, so a switch is a full page load (hx-boost=\"false\") rather than a body swap under the stylesheet you are already wearing."
        },
        {
          "id": "policy",
          "title": "Versioning policy",
          "text": "Releases follow semantic versioning. Docs are versioned per minor release (0.1, 0.2, …) before 1.0, and per major release after. Patch releases update the docs of their version in place. The version under development has no number yet and is listed as next until it is released. Every release's changes and migration notes are in the changelog."
        },
        {
          "id": "how",
          "title": "How it works",
          "text": "The versions plugin (htmx-ui-plugin-versions) does all of this: it serves the frozen versions, publishes the manifest the switcher reads, and freezes a version with htmx-ui versions:archive when the next one starts. Add it to your own site to version its docs the same way."
        },
        {
          "id": "agents",
          "title": "For tools and agents",
          "text": "/docs/versions.json lists every version with its URL prefix, its own sitemap (with section text, for search) and the pages it contains: /docs/versions.json { \"latest\": \"0.2\", \"versions\": [ { \"id\": \"0.2\", \"label\": \"v0.2\", \"path\": \"/docs\", \"latest\": true, \"sitemap\": \"/docs/sitemap.json\", \"pages\": [\"\", \"/installation\", \"…\"] }, { \"id\": \"0.1\", \"label\": \"v0.1\", \"path\": \"/docs/v0.1\", \"latest\": false, \"sitemap\": \"/docs/v0.1/sitemap.json\", \"pages\": [\"\", \"…\"] } ] }"
        }
      ]
    }
  ]
}