{
  "package": "bootstrap-html",
  "version": "0.3.0",
  "bootstrap": "^5.3.8",
  "bootstrapBundleGzipKb": 23.3,
  "components": [
    {
      "id": "accordion",
      "name": "Accordion",
      "route": "/components/accordion",
      "docs": "/components/accordion/",
      "summary": "Exclusive groups with details/summary and name.",
      "needsScript": false,
      "baseline": "newly",
      "element": {
        "name": "pe-focus-group",
        "module": "src/behaviors/focus-group.js",
        "required": false,
        "adds": "Arrow, Home, and End keys across the summaries. Tab works without it."
      },
      "polyfills": [],
      "javascript": {
        "bootstrap": "5.6 KB gzip",
        "bootstrapHtml": "0 KB"
      },
      "platform": {
        "bootstrap": "Collapse plugin. Accordion is that plugin; there is no accordion.js.",
        "bootstrapHtml": "details and summary, with name for an exclusive group"
      },
      "support": {
        "noScript": "`details` / `summary`. Exclusive `name`: Chrome 120+, Firefox 130+, Safari 17.2+.",
        "polyfill": "None",
        "noFallback": "The height transition needs `interpolate-size: allow-keywords` to animate `block-size` from `0` to `auto`. That is Chromium-only (Chrome 129+), so Firefox and Safari snap the panel open. `::details-content` itself is Chrome 131+, Firefox 143+, Safari 18.4+, and only styles the panel."
      },
      "replaces": [
        {
          "from": "Accordion buttons",
          "to": "`<details class=\"accordion-item\">` with `<summary class=\"accordion-button\">`"
        },
        {
          "from": "`data-bs-parent`",
          "to": "The same `name` on every `<details>` in the group"
        }
      ],
      "avoid": [
        "`.accordion-button` has to be the `<summary>`, a direct child of the `<details>`.",
        "No `data-bs-toggle`. Opening state is the `open` attribute.",
        "Do not put `hidden` or `hidden=\"until-found\"` on `.accordion-body`. An open item would stay hidden. Accordion panels are `<details>`; `hidden=\"until-found\"` is for custom regions."
      ],
      "accessibility": [
        "`<details>` / `<summary>` is a disclosure. The browser exposes expanded and collapsed; do not add `aria-expanded` on the summary.",
        "Do not put a heading inside `<summary>`. Screen-reader heading lists skip that text.",
        "Tab reaches every summary without script. Optional `<pe-focus-group>` adds Arrow, Home, and End. Enter and Space still open the item.",
        "Closed panels stay in the document. Find in page can match them. Do not add `display: none` on the body."
      ],
      "example": "<div class=\"accordion\">\n  <details class=\"accordion-item\" name=\"accordionExample\" open>\n    <summary class=\"accordion-button\">Accordion Item #1</summary>\n    <div class=\"accordion-body\">\n      <strong>This is the first item’s accordion body.</strong> It is shown by default. You can modify any of this with custom CSS or overriding our default variables. It’s also worth noting that just about any HTML can go within the <code>.accordion-body</code>, though the transition does limit overflow.\n    </div>\n  </details>\n  <details class=\"accordion-item\" name=\"accordionExample\">\n    <summary class=\"accordion-button\">Accordion Item #2</summary>\n    <div class=\"accordion-body\">\n      <strong>This is the second item’s accordion body.</strong> It is hidden by default. Find in page can still match this panel: the token is <strong>quetzalcoatlus</strong>.\n    </div>\n  </details>\n  <details class=\"accordion-item\" name=\"accordionExample\">\n    <summary class=\"accordion-button\">Accordion Item #3</summary>\n    <div class=\"accordion-body\">\n      <strong>This is the third item’s accordion body.</strong> It is hidden by default. You can modify any of this with custom CSS or overriding our default variables. It’s also worth noting that just about any HTML can go within the <code>.accordion-body</code>, though the transition does limit overflow.\n    </div>\n  </details>\n</div>"
    },
    {
      "id": "alerts",
      "name": "Alerts",
      "route": "/components/alerts",
      "docs": "/components/alerts/",
      "summary": "Static alerts are HTML. Dismiss needs pe-alert.",
      "needsScript": true,
      "baseline": "newly",
      "element": {
        "name": "pe-alert",
        "module": "src/behaviors/alert.js",
        "required": true,
        "adds": "Removes the alert on `command=\"--dismiss\"`. A static alert does not use the element."
      },
      "polyfills": [
        {
          "module": "src/polyfills/invoker-commands.js",
          "gzipKb": 0.8,
          "loadedWhen": "commandForElement is missing and the page has [commandfor]."
        }
      ],
      "javascript": {
        "bootstrap": "5.0 KB gzip",
        "bootstrapHtml": "0.5 KB gzip"
      },
      "platform": {
        "bootstrap": "Alert plugin",
        "bootstrapHtml": "pe-alert. command=--dismiss and commandfor. Required to remove the node."
      },
      "support": {
        "noScript": "A static `.alert` needs no script. Without `alert.js` a dismissible alert stays.",
        "polyfill": "[`command` / `commandfor`](/polyfills#invoker-commands) when that API is missing. `pe-alert` is still required to remove the node.",
        "noFallback": "No native dismiss for an inline alert."
      },
      "replaces": [
        {
          "from": "`data-bs-dismiss=\"alert\"`",
          "to": "`<pe-alert class=\"alert\">` with `command=\"--dismiss\"` and `commandfor` set to the alert id. Load `src/behaviors/alert.js`."
        }
      ],
      "avoid": [
        "`commandfor` is the alert id with no `#`.",
        "The close button has to sit inside the `<pe-alert>`; the `command` event does not bubble."
      ],
      "accessibility": [
        "Keep `role=\"alert\"` on an important message so assistive technology announces it. A static informational banner can be a `div` without that role.",
        "The close button needs an accessible name (`aria-label=\"Close\"` on `.btn-close`). Removing the node is the dismiss; there is no `aria-hidden` step."
      ],
      "example": "<script type=\"module\" src=\"./node_modules/bootstrap-html/src/behaviors/alert.js\"></script>\n\n<pe-alert class=\"alert alert-warning alert-dismissible fade show\" id=\"dismiss-alert\" role=\"alert\">\n  <div><strong>Holy guacamole!</strong> You should check in on some of those fields below.</div>\n  <button type=\"button\" class=\"btn-close\" command=\"--dismiss\" commandfor=\"dismiss-alert\" aria-label=\"Close\"></button>\n</pe-alert>"
    },
    {
      "id": "buttons",
      "name": "Buttons",
      "route": "/components/buttons",
      "docs": "/components/buttons/",
      "summary": "Toggle buttons are checkbox or radio plus .btn-check. No button plugin.",
      "needsScript": false,
      "baseline": "newly",
      "element": null,
      "polyfills": [],
      "javascript": {
        "bootstrap": "1.4 KB gzip",
        "bootstrapHtml": "0 KB"
      },
      "platform": {
        "bootstrap": "Button plugin. It toggles .active and aria-pressed on a button.",
        "bootstrapHtml": "checkbox or radio with .btn-check and a label.btn"
      },
      "support": {
        "noScript": "`.btn-check` is CSS on a native checkbox or radio. Chrome, Firefox, and Safari have shipped it with Bootstrap’s stylesheet.",
        "polyfill": "None",
        "noFallback": "None. Without this stylesheet the control is still a checkbox or radio. The label just looks like a button when `.btn-check` is present."
      },
      "replaces": [
        {
          "from": "`data-bs-toggle=\"button\"` and `aria-pressed` set from script",
          "to": "`<input type=\"checkbox\" class=\"btn-check\">` or `type=\"radio\"` with a `<label class=\"btn\">`"
        }
      ],
      "avoid": [
        "Do not add `data-bs-toggle=\"button\"`. State is the input’s `checked` attribute.",
        "Do not set `aria-pressed` on the label. The checkbox or radio is the accessible control.",
        "A lone `<button>` is a command, not a toggle. Use an input when the choice submits or persists."
      ],
      "accessibility": [
        "The input stays in the tree. `.btn-check` visually hides it; the label is the hit target. Do not add `display: none` on the input.",
        "Group radios with the same `name`. A checkbox group can sit in a `<fieldset>` with a `<legend>`.",
        "Do not put `role=\"button\"` on the label. It is already a label for the input."
      ],
      "example": "<fieldset>\n  <legend class=\"visually-hidden\">Notifications</legend>\n  <input type=\"checkbox\" class=\"btn-check\" id=\"buttons-email\" autocomplete=\"off\" checked>\n  <label class=\"btn btn-outline-primary\" for=\"buttons-email\">Email</label>\n  <input type=\"checkbox\" class=\"btn-check\" id=\"buttons-sms\" autocomplete=\"off\">\n  <label class=\"btn btn-outline-primary\" for=\"buttons-sms\">SMS</label>\n</fieldset>"
    },
    {
      "id": "collapse",
      "name": "Collapse",
      "route": "/components/collapse",
      "docs": "/components/collapse/",
      "summary": "Single panel; trigger and content must be parent/child.",
      "needsScript": false,
      "baseline": "newly",
      "element": null,
      "polyfills": [],
      "javascript": {
        "bootstrap": "5.6 KB gzip",
        "bootstrapHtml": "0 KB"
      },
      "platform": {
        "bootstrap": "Collapse plugin",
        "bootstrapHtml": "details and summary"
      },
      "support": {
        "noScript": "`details` / `summary`. Trigger and panel must be parent/child.",
        "polyfill": "None",
        "noFallback": "Same `interpolate-size` limit as the accordion. `.collapse-horizontal` animates to `max-content` and depends on it too. The panel opens and closes everywhere."
      },
      "replaces": [
        {
          "from": "`data-bs-toggle=\"collapse\"`",
          "to": "`<details>` / `<summary>`. The trigger must wrap the panel."
        }
      ],
      "avoid": [
        "No `data-bs-target` or `href` pointing at a distant id. A remote toggle needs your own script.",
        "Do not put padding on `.collapse` itself; keep it on an inner wrapper such as `.card-body`.",
        "Do not put `hidden` or `hidden=\"until-found\"` on the panel inside `<details>`. Collapse is the disclosure; `hidden=\"until-found\"` is for custom regions."
      ],
      "accessibility": [
        "`<summary>` is the control. The browser maps it to a disclosure, including Enter and Space. A hidden checkbox plus label is a checkbox, not a disclosure; that pattern is documented under Checkbox hack and is not used here.",
        "Closed panels stay in the document. Find in page can match them. Do not add `display: none` on the inner wrapper."
      ],
      "example": "<details class=\"collapse\">\n  <summary class=\"btn btn-primary mb-2\">Toggle collapse</summary>\n  <div class=\"card card-body\">\n    Some placeholder content for the collapse component. This panel is hidden by default but revealed when the user activates the relevant trigger. Find in page can still match it: the token is <strong>archaeopteryx</strong>.\n  </div>\n</details>"
    },
    {
      "id": "modal",
      "name": "Modal",
      "route": "/components/modal",
      "docs": "/components/modal/",
      "summary": "dialog with command/commandfor. Polyfill if command or closedby is missing.",
      "needsScript": false,
      "baseline": "newly",
      "element": null,
      "polyfills": [
        {
          "module": "src/polyfills/invoker-commands.js",
          "gzipKb": 0.8,
          "loadedWhen": "commandForElement is missing and the page has [commandfor]."
        },
        {
          "module": "src/polyfills/dialog-closedby.js",
          "gzipKb": 0.5,
          "loadedWhen": "closedBy is missing and the page has dialog[closedby]."
        }
      ],
      "javascript": {
        "bootstrap": "7.3 KB gzip",
        "bootstrapHtml": "0 KB"
      },
      "platform": {
        "bootstrap": "Modal plugin (backdrop, focus trap, scrollbar)",
        "bootstrapHtml": "dialog with command and commandfor"
      },
      "support": {
        "noScript": "`dialog`",
        "polyfill": "[`command` / `commandfor`](/polyfills#invoker-commands) (Chrome 135+, Firefox 144+, Safari 26.2+). [`closedby`](/polyfills#closedby) is missing in Safari.",
        "noFallback": "None"
      },
      "replaces": [
        {
          "from": "`data-bs-toggle=\"modal\"` and `data-bs-target`",
          "to": "`command=\"show-modal\"` and `commandfor` on a button. The dialog uses `closedby`."
        },
        {
          "from": "`data-bs-dismiss=\"modal\"`",
          "to": "`command=\"close\"` and the same `commandfor`"
        },
        {
          "from": "A confirm or prompt plugin, or click listeners on footer buttons",
          "to": "`<form method=\"dialog\">` inside the dialog. Submit buttons set `dialog.returnValue` to their `value` and close without script."
        },
        {
          "from": "`data-bs-backdrop=\"static\"`",
          "to": "`closedby=\"closerequest\"` on the `<dialog>`"
        }
      ],
      "avoid": [
        "`command` works on `<button>`, not `<a href>`.",
        "No `.modal-backdrop` element. The `<dialog>` has `::backdrop`.",
        "Do not mix `command=\"close\"` with `<form method=\"dialog\">` on the same control. A submit button’s `value` is the returnValue."
      ],
      "accessibility": [
        "`showModal()` (from `command=\"show-modal\"`) puts the dialog in the top layer, traps focus, and handles Escape. Name it with `aria-labelledby` pointing at `.modal-title`.",
        "`closedby=\"any\"` also closes on a backdrop click. `closedby=\"none\"` keeps Escape and backdrop from closing; provide a close button.",
        "`<form method=\"dialog\">` submit buttons close the dialog and set `returnValue`. Backdrop or Escape leaves that string empty."
      ],
      "example": "<button type=\"button\" class=\"btn btn-primary\" command=\"show-modal\" commandfor=\"exampleModal\">\n  Launch demo modal\n</button>\n\n<dialog class=\"modal\" closedby=\"any\" id=\"exampleModal\" aria-labelledby=\"exampleModalLabel\">\n  <div class=\"modal-dialog\">\n    <div class=\"modal-content\">\n      <div class=\"modal-header\">\n        <h1 class=\"modal-title fs-5\" id=\"exampleModalLabel\">Modal title</h1>\n        <button type=\"button\" class=\"btn-close\" command=\"close\" commandfor=\"exampleModal\" aria-label=\"Close\"></button>\n      </div>\n      <div class=\"modal-body\">\n        Woohoo, you’re reading this text in a modal!\n      </div>\n      <div class=\"modal-footer\">\n        <button type=\"button\" class=\"btn btn-secondary\" command=\"close\" commandfor=\"exampleModal\">Close</button>\n        <button type=\"button\" class=\"btn btn-primary\">Save changes</button>\n      </div>\n    </div>\n  </div>\n</dialog>"
    },
    {
      "id": "offcanvas",
      "name": "Offcanvas",
      "route": "/components/offcanvas",
      "docs": "/components/offcanvas/",
      "summary": "dialog placements. Polyfill if command or closedby is missing.",
      "needsScript": false,
      "baseline": "newly",
      "element": null,
      "polyfills": [
        {
          "module": "src/polyfills/invoker-commands.js",
          "gzipKb": 0.8,
          "loadedWhen": "commandForElement is missing and the page has [commandfor]."
        },
        {
          "module": "src/polyfills/dialog-closedby.js",
          "gzipKb": 0.5,
          "loadedWhen": "closedBy is missing and the page has dialog[closedby]."
        }
      ],
      "javascript": {
        "bootstrap": "6.9 KB gzip",
        "bootstrapHtml": "0 KB"
      },
      "platform": {
        "bootstrap": "Offcanvas plugin (same helpers as Modal)",
        "bootstrapHtml": "dialog with offcanvas placement"
      },
      "support": {
        "noScript": "`dialog` with `.offcanvas-start`, `-end`, `-top`, or `-bottom`.",
        "polyfill": "The same [`command`](/polyfills#invoker-commands) and [`closedby`](/polyfills#closedby) polyfills as the modal.",
        "noFallback": "None"
      },
      "replaces": [
        {
          "from": "`data-bs-toggle=\"offcanvas\"`",
          "to": "The same commands on `<dialog class=\"offcanvas\">`"
        },
        {
          "from": "`data-bs-scroll=\"true\"`",
          "to": "`popover=\"manual\"` instead of a modal `<dialog>`"
        }
      ],
      "avoid": [
        "`command` works on `<button>`, not `<a href>`.",
        "A responsive `.offcanvas-{sm,md,lg,xl,xxl}` stays in the flow above its breakpoint; do not also toggle it there."
      ],
      "accessibility": [
        "A modal `<dialog class=\"offcanvas\">` traps focus like a modal. Name it with `aria-labelledby` on the title.",
        "`popover=\"manual\"` is not modal: the page still scrolls and focus is not trapped. Give that panel a close control. `popover=\"auto\"` light-dismisses and still does not trap focus."
      ],
      "example": "<button type=\"button\" class=\"btn btn-primary\" command=\"show-modal\" commandfor=\"offcanvasExample\">\n  Toggle offcanvas\n</button>\n\n<dialog class=\"offcanvas offcanvas-start\" id=\"offcanvasExample\" closedby=\"any\" aria-labelledby=\"offcanvasExampleLabel\">\n  <div class=\"offcanvas-header\">\n    <h5 class=\"offcanvas-title\" id=\"offcanvasExampleLabel\">Offcanvas</h5>\n    <button type=\"button\" class=\"btn-close\" command=\"close\" commandfor=\"offcanvasExample\" aria-label=\"Close\"></button>\n  </div>\n  <div class=\"offcanvas-body\">\n    Some text as placeholder. In real life you can have the elements you have chosen. Like, text, images, lists, etc.\n  </div>\n</dialog>"
    },
    {
      "id": "navbar",
      "name": "Navbar",
      "route": "/components/navbar",
      "docs": "/components/navbar/",
      "summary": "details/summary, or a dialog offcanvas. Polyfill on the dialog toggler.",
      "needsScript": false,
      "baseline": "newly",
      "element": null,
      "polyfills": [
        {
          "module": "src/polyfills/invoker-commands.js",
          "gzipKb": 0.8,
          "loadedWhen": "commandForElement is missing and the page has [commandfor]."
        },
        {
          "module": "src/polyfills/dialog-closedby.js",
          "gzipKb": 0.5,
          "loadedWhen": "closedBy is missing and the page has dialog[closedby]."
        }
      ],
      "javascript": {
        "bootstrap": "5.6 KB gzip",
        "bootstrapHtml": "0 KB"
      },
      "platform": {
        "bootstrap": "Collapse plugin. Navbar is that plugin plus the navbar CSS.",
        "bootstrapHtml": "details and summary, or dialog for the offcanvas navbar"
      },
      "support": {
        "noScript": "The same `details` menu. `.navbar-expand-*` shows it from that breakpoint. The offcanvas variant is a `dialog`.",
        "polyfill": "The offcanvas toggler uses the same [`command` polyfill](/polyfills#invoker-commands) as the modal.",
        "noFallback": "The hamburger cannot sit outside the `<details>`. Use the offcanvas navbar for that."
      },
      "replaces": [
        {
          "from": "`data-bs-toggle=\"collapse\"` on `.navbar-toggler`",
          "to": "`<summary class=\"navbar-toggler\">` inside `<details class=\"navbar-collapse\">`"
        }
      ],
      "avoid": [
        "The brand stays outside the `<details>`, not inside the collapsing panel.",
        "`.navbar-toggler` has to be the `<summary>`.",
        "Do not put `role=\"search\"` on the form inside `<search>`. The element is already that landmark."
      ],
      "accessibility": [
        "The hamburger is a `<summary>`. The browser exposes expanded and collapsed on that disclosure; do not add `aria-expanded` there.",
        "Dropdowns in the bar are the same popover menus as elsewhere. Invoker Commands keep `aria-expanded` in sync on the toggle.",
        "The offcanvas navbar is a modal `<dialog>`. Name it with `aria-labelledby`.",
        "Wrap the search form in `<search>`. Name the input with `aria-label` or a visible label."
      ],
      "example": "<nav class=\"navbar navbar-expand-lg bg-body-tertiary\">\n  <div class=\"container-fluid\">\n    <a class=\"navbar-brand\" href=\"#\">Navbar</a>\n    <details class=\"navbar-collapse\">\n      <summary class=\"navbar-toggler\" aria-label=\"Toggle navigation\">\n        <span class=\"navbar-toggler-icon\"></span>\n      </summary>\n      <ul class=\"navbar-nav me-auto mb-2 mb-lg-0\">\n      <li class=\"nav-item\">\n        <a class=\"nav-link active\" aria-current=\"page\" href=\"#\">Home</a>\n      </li>\n      <li class=\"nav-item\">\n        <a class=\"nav-link\" href=\"#\">Link</a>\n      </li>\n      <li class=\"nav-item dropdown\">\n        <button class=\"nav-link dropdown-toggle\" type=\"button\" command=\"toggle-popover\" commandfor=\"navbarDropdown\">\n          Dropdown\n        </button>\n        <ul class=\"dropdown-menu\" popover=\"auto\" id=\"navbarDropdown\">\n          <li><a class=\"dropdown-item\" href=\"#\">Action</a></li>\n          <li><a class=\"dropdown-item\" href=\"#\">Another action</a></li>\n          <li><hr class=\"dropdown-divider\"></li>\n          <li><a class=\"dropdown-item\" href=\"#\">Something else here</a></li>\n        </ul>\n      </li>\n      <li class=\"nav-item\">\n        <a class=\"nav-link disabled\" aria-disabled=\"true\">Disabled</a>\n      </li>\n    </ul>\n    <search>\n      <form class=\"d-flex\">\n        <input class=\"form-control me-2\" type=\"search\" name=\"q\" placeholder=\"Search\" aria-label=\"Search\">\n        <button class=\"btn btn-outline-success\" type=\"submit\">Search</button>\n      </form>\n    </search>\n    </details>\n  </div>\n</nav>"
    },
    {
      "id": "dropdown",
      "name": "Dropdown",
      "route": "/components/dropdown",
      "docs": "/components/dropdown/",
      "summary": "Popover API menus. Polyfill if command is missing.",
      "needsScript": false,
      "baseline": "newly",
      "element": {
        "name": "pe-focus-group",
        "module": "src/behaviors/focus-group.js",
        "required": false,
        "adds": "Arrow, Home, and End keys across `.dropdown-item`. Tab works without it."
      },
      "polyfills": [
        {
          "module": "src/polyfills/invoker-commands.js",
          "gzipKb": 0.8,
          "loadedWhen": "commandForElement is missing and the page has [commandfor]."
        }
      ],
      "javascript": {
        "bootstrap": "13.7 KB gzip",
        "bootstrapHtml": "0 KB"
      },
      "platform": {
        "bootstrap": "Dropdown plugin, including Popper",
        "bootstrapHtml": "popover=\"auto\" and CSS Anchor Positioning. No Popper."
      },
      "support": {
        "noScript": "`popover=\"auto\"`: Chrome 114+, Firefox 125+, Safari 17+.",
        "polyfill": "[`command`](/polyfills#invoker-commands) on the toggle, the same polyfill as the modal.",
        "noFallback": "CSS Anchor Positioning: Chrome 125+, Safari 26+, Firefox 147+. Without it the menu is not attached to the button. There is no Popper fallback."
      },
      "replaces": [
        {
          "from": "`data-bs-toggle=\"dropdown\"`",
          "to": "`command=\"toggle-popover\"` and `popover=\"auto\"` on `.dropdown-menu`"
        },
        {
          "from": "`data-bs-offset` and `data-bs-reference`",
          "to": "`anchor()` and `position-try-fallbacks` in CSS"
        },
        {
          "from": "Submenus (Bootstrap 5 dropped them; Popper could not keep the parent open)",
          "to": "`.dropdown-submenu` with a nested `popover=\"auto\"` inside the parent menu"
        }
      ],
      "avoid": [
        "`command` works on `<button>`, not `<a href>`.",
        "Sibling `popover=\"auto\"` menus replace each other. Nest the submenu popover (or its invoker) inside the parent popover, or the parent closes.",
        "These are generic overlays, not ARIA `role=\"menu\"` widgets. Add that ARIA yourself if you need an application menu."
      ],
      "accessibility": [
        "Bootstrap’s dropdowns are generic overlays, not ARIA `role=\"menu\"` widgets. The same applies here: add more specific ARIA yourself if you need an application menu.",
        "Native popovers close with Escape and move to the top layer. Invoker Commands keep `aria-expanded` in sync on the toggle. They do not move through `.dropdown-item` with arrow keys. Tab through items, or wrap the dropdown in optional `<pe-focus-group>`.",
        "A `.dropdown-submenu` toggle is a button with `.dropdown-item`. Nested `popover=\"auto\"` keeps the ancestor open; Escape closes the topmost menu first."
      ],
      "example": "<div class=\"dropdown\">\n  <button class=\"btn btn-secondary dropdown-toggle\" type=\"button\" command=\"toggle-popover\" commandfor=\"dropdownMenuButton\">\n    Dropdown button\n  </button>\n  <ul class=\"dropdown-menu\" popover=\"auto\" id=\"dropdownMenuButton\">\n    <li><a class=\"dropdown-item\" href=\"#\">Action</a></li>\n    <li><a class=\"dropdown-item\" href=\"#\">Another action</a></li>\n    <li><a class=\"dropdown-item\" href=\"#\">Something else here</a></li>\n  </ul>\n</div>"
    },
    {
      "id": "popover",
      "name": "Popover",
      "route": "/components/popover",
      "docs": "/components/popover/",
      "summary": "Titled bubbles, four directions. Polyfill if command is missing.",
      "needsScript": false,
      "baseline": "newly",
      "element": null,
      "polyfills": [
        {
          "module": "src/polyfills/invoker-commands.js",
          "gzipKb": 0.8,
          "loadedWhen": "commandForElement is missing and the page has [commandfor]."
        }
      ],
      "javascript": {
        "bootstrap": "15.5 KB gzip",
        "bootstrapHtml": "0 KB"
      },
      "platform": {
        "bootstrap": "Popover plugin, which includes Tooltip and Popper",
        "bootstrapHtml": "Popover API and CSS Anchor Positioning. No Popper."
      },
      "support": {
        "noScript": "`popover=\"auto\"`: Chrome 114+, Firefox 125+, Safari 17+.",
        "polyfill": "[`command`](/polyfills#invoker-commands) on the trigger, the same polyfill as the modal.",
        "noFallback": "CSS Anchor Positioning: Chrome 125+, Safari 26+, Firefox 147+. Without it the bubble is not attached to the trigger. `.bs-popover-auto` does not reorient; it matches `.bs-popover-end`."
      },
      "replaces": [
        {
          "from": "`data-bs-toggle=\"popover\"`",
          "to": "`popover=\"auto\"` on the bubble, opened with `command=\"toggle-popover\"`"
        },
        {
          "from": "`data-bs-content` and `data-bs-placement`",
          "to": "Real markup inside `.popover-body`, and `.bs-popover-top` / `-end` / `-bottom` / `-start`"
        }
      ],
      "avoid": [
        "`command` works on `<button>`, not `<a href>`.",
        "No `data-bs-html=\"true\"`. The bubble is markup already."
      ],
      "accessibility": [
        "The bubble is a popover, not a dialog. Escape and a click outside close it. Invoker Commands keep `aria-expanded` in sync on the trigger.",
        "Put real heading text in `.popover-header`. Do not rely on `data-bs-title`."
      ],
      "example": "<span class=\"d-inline-block\">\n  <button type=\"button\" class=\"btn btn-lg btn-danger\" command=\"toggle-popover\" commandfor=\"popoverLive\">\n    Click to toggle popover\n  </button>\n  <div class=\"popover bs-popover-end\" popover=\"auto\" id=\"popoverLive\">\n    <div class=\"popover-arrow\"></div>\n    <h3 class=\"popover-header\">Popover title</h3>\n    <div class=\"popover-body\">And here’s some amazing content. It’s very engaging. Right?</div>\n  </div>\n</span>"
    },
    {
      "id": "tooltip",
      "name": "Tooltip",
      "route": "/components/tooltip",
      "docs": "/components/tooltip/",
      "summary": "Hover/focus tips. aria-description is the no-script fallback. popover=hint plus interestfor is experimental.",
      "needsScript": false,
      "baseline": "newly",
      "element": null,
      "polyfills": [
        {
          "module": "src/polyfills/interestfor.js",
          "gzipKb": 2.8,
          "loadedWhen": "interestForElement is missing and the page has [interestfor]."
        }
      ],
      "javascript": {
        "bootstrap": "15.3 KB gzip",
        "bootstrapHtml": "0 KB"
      },
      "platform": {
        "bootstrap": "Tooltip plugin, including Popper",
        "bootstrapHtml": "aria-description and CSS generated content, or popover=hint. No Popper."
      },
      "support": {
        "noScript": "`aria-description` and `content: attr()`. Hover and focus. Plain text, not in the top layer.",
        "polyfill": "[`interestfor`](/polyfills#interestfor) when `interestForElement` is missing and the page has `[interestfor]`. It does not polyfill `popover=\"hint\"`.",
        "noFallback": "`popover=\"hint\"` is Chrome 151+, Firefox 153+, Safari preview. Without hint the polyfill cannot open the top-layer bubble. `.bs-tooltip-auto` on a hint popover flips with `position-try-fallbacks`. The arrow stays in the default (top) orientation after a flip."
      },
      "replaces": [
        {
          "from": "`title` / `data-bs-title`",
          "to": "`aria-description` plus `.bs-tooltip-top` (or end, bottom, start), or a `.tooltip` with `popover=\"hint\"`"
        },
        {
          "from": "`data-bs-html`",
          "to": "Real markup in `.tooltip-inner` on a hint popover"
        },
        {
          "from": "`data-bs-delay`",
          "to": "`interest-delay-start` / `interest-delay-end` and `--interest-delay-start` / `--interest-delay-end` (or `--bs-tooltip-delay-start` / `--bs-tooltip-delay-end`) on the trigger"
        }
      ],
      "avoid": [
        "Do not set `role=\"tooltip\"` or `aria-describedby` on an `interestfor` trigger. Native interestfor associates the target; the polyfill sets `aria-describedby` (plain hint) or `aria-details` (rich).",
        "`interestfor` is valid on `<a>`, `<button>`, and `<area>` only.",
        "Do not put `aria-description` and `interestfor` on the same control (Chromium would announce both).",
        "The CSS fallback is plain text. Markup needs a hint popover."
      ],
      "accessibility": [
        "`aria-description` is a description, not the accessible name. Give icon-only controls a name (`aria-label` or visible text) and use the description for extra help.",
        "The CSS fallback bubble is generated content with empty alternative text, so assistive technology is not read a second copy. Hover and `:focus-within` show it. It is not in the top layer.",
        "Native `interestfor` gives a hint popover an implicit tooltip role. The polyfill sets `aria-describedby` on a plain hint. Escape dismisses interest. The pointer can move onto the bubble (WCAG 1.4.13). Do not add `role=\"tooltip\"` yourself on the native path."
      ],
      "example": "<div class=\"d-flex flex-wrap justify-content-center gap-2 py-4\">\n  <button type=\"button\" class=\"btn btn-secondary bs-tooltip-top\" aria-description=\"Tooltip on top\">\n    Tooltip on top\n  </button>\n  <button type=\"button\" class=\"btn btn-secondary bs-tooltip-end\" aria-description=\"Tooltip on right\">\n    Tooltip on right\n  </button>\n  <button type=\"button\" class=\"btn btn-secondary bs-tooltip-bottom\" aria-description=\"Tooltip on bottom\">\n    Tooltip on bottom\n  </button>\n  <button type=\"button\" class=\"btn btn-secondary bs-tooltip-start\" aria-description=\"Tooltip on left\">\n    Tooltip on left\n  </button>\n</div>"
    },
    {
      "id": "scrollspy",
      "name": "Scrollspy",
      "route": "/components/scrollspy",
      "docs": "/components/scrollspy/",
      "summary": "scroll-target-group and :target-current. Polyfill when that CSS is missing.",
      "needsScript": false,
      "baseline": "experimental",
      "element": null,
      "polyfills": [
        {
          "module": "src/polyfills/scroll-target-group.js",
          "gzipKb": 0.8,
          "loadedWhen": "scroll-target-group is missing and the page has [data-polyfill-scrollspy]."
        }
      ],
      "javascript": {
        "bootstrap": "5.7 KB gzip",
        "bootstrapHtml": "0 KB"
      },
      "platform": {
        "bootstrap": "Scrollspy plugin",
        "bootstrapHtml": "scroll-target-group and :target-current"
      },
      "support": {
        "noScript": "`scroll-target-group`: Chrome 140+.",
        "polyfill": "[IntersectionObserver](/polyfills#scroll-target-group) when the CSS is missing. It does not set `aria-current`.",
        "noFallback": "Marked experimental on the component page. The polyfill covers browsers without the CSS."
      },
      "replaces": [
        {
          "from": "`data-bs-spy=\"scroll\"` and `data-bs-target`",
          "to": "`data-polyfill-scrollspy` on the link group, which applies `scroll-target-group: auto`. `:target-current` marks the current link."
        }
      ],
      "avoid": [
        "Links in a closed `[popover]` menu are not scroll markers. Keep a `.scroll-marker-alias` copy next to the toggle.",
        "The polyfill does not set `aria-current`. Add it yourself if you need it announced."
      ],
      "accessibility": [
        "`:target-current` is visual only. Assistive technology still needs `aria-current=\"true\"` on the in-view link, which requires script. This demo does not set it. The links remain ordinary in-page links."
      ],
      "example": "<nav id=\"navbar-example2\" class=\"navbar bg-body-tertiary px-3 mb-3\" data-polyfill-scrollspy>\n  <a class=\"navbar-brand\" href=\"#\">Navbar</a>\n  <ul class=\"nav nav-pills\">\n    <li class=\"nav-item\">\n      <a class=\"nav-link\" href=\"#scrollspyHeading1\">First</a>\n    </li>\n    <li class=\"nav-item\">\n      <a class=\"nav-link\" href=\"#scrollspyHeading2\">Second</a>\n    </li>\n    <li class=\"nav-item dropdown\">\n      <button class=\"nav-link dropdown-toggle\" type=\"button\" command=\"toggle-popover\" commandfor=\"scrollspyDropdown\">Dropdown</button>\n      <ul class=\"dropdown-menu\" popover=\"auto\" id=\"scrollspyDropdown\">\n        <li><a class=\"dropdown-item\" href=\"#scrollspyHeading3\">Third</a></li>\n        <li><a class=\"dropdown-item\" href=\"#scrollspyHeading4\">Fourth</a></li>\n        <li><hr class=\"dropdown-divider\"></li>\n        <li><a class=\"dropdown-item\" href=\"#scrollspyHeading5\">Fifth</a></li>\n      </ul>\n      <span class=\"scroll-marker-alias\" aria-hidden=\"true\">\n        <a href=\"#scrollspyHeading3\" tabindex=\"-1\">Third</a>\n        <a href=\"#scrollspyHeading4\" tabindex=\"-1\">Fourth</a>\n        <a href=\"#scrollspyHeading5\" tabindex=\"-1\">Fifth</a>\n      </span>\n    </li>\n  </ul>\n</nav>\n<div class=\"scrollspy-example bg-body-tertiary p-3 rounded-2\" tabindex=\"0\">\n  <h4 id=\"scrollspyHeading1\">First heading</h4>\n  <p>This is some placeholder content for the scrollspy page. As the area scrolls, the matching navigation link is highlighted. The same paragraph is repeated so the scroller is long enough to move the highlight from heading to heading.</p>\n  <h4 id=\"scrollspyHeading2\">Second heading</h4>\n  <p>This is some placeholder content for the scrollspy page. As the area scrolls, the matching navigation link is highlighted. The same paragraph is repeated so the scroller is long enough to move the highlight from heading to heading.</p>\n  <h4 id=\"scrollspyHeading3\">Third heading</h4>\n  <p>This is some placeholder content for the scrollspy page. As the area scrolls, the matching navigation link is highlighted. The same paragraph is repeated so the scroller is long enough to move the highlight from heading to heading.</p>\n  <h4 id=\"scrollspyHeading4\">Fourth heading</h4>\n  <p>This is some placeholder content for the scrollspy page. As the area scrolls, the matching navigation link is highlighted. The same paragraph is repeated so the scroller is long enough to move the highlight from heading to heading.</p>\n  <h4 id=\"scrollspyHeading5\">Fifth heading</h4>\n  <p>This is some placeholder content for the scrollspy page. As the area scrolls, the matching navigation link is highlighted. The same paragraph is repeated so the scroller is long enough to move the highlight from heading to heading.</p>\n</div>"
    },
    {
      "id": "tabs",
      "name": "Tabs",
      "route": "/components/tabs",
      "docs": "/components/tabs/",
      "summary": "pe-tabs is required. Panes do not switch without it.",
      "needsScript": true,
      "baseline": "newly",
      "element": {
        "name": "pe-tabs",
        "module": "src/behaviors/tabs.js",
        "required": true,
        "adds": "Keeps the selection, the roving `tabindex`, and the `.active` / `.show` panes in sync."
      },
      "polyfills": [],
      "javascript": {
        "bootstrap": "5.8 KB gzip",
        "bootstrapHtml": "1.3 KB gzip"
      },
      "platform": {
        "bootstrap": "Tab plugin",
        "bootstrapHtml": "pe-tabs. role=tab and aria-controls. Required behavior."
      },
      "support": {
        "noScript": "The `.active` pane stays visible. Other tabs do not switch.",
        "polyfill": "None. `pe-tabs` is required to switch panes. It is not a polyfill.",
        "noFallback": "No native tab control yet. Dropdowns inside a tab list are not supported."
      },
      "replaces": [
        {
          "from": "`data-bs-toggle=\"tab\"` and `data-bs-target`",
          "to": "`<pe-tabs>` with `role=\"tab\"` and `aria-controls`. Load `src/behaviors/tabs.js`."
        }
      ],
      "avoid": [
        "Do not wrap the tabs in `<pe-focus-group>`. `<pe-tabs>` already owns the arrow keys.",
        "A `.nav-tabs` list of page links is a different thing and needs no tab roles."
      ],
      "accessibility": [
        "This follows the WAI-ARIA tabs pattern: `role=\"tablist\"`, `role=\"tab\"`, `role=\"tabpanel\"`, `aria-controls`, and `aria-selected`. The selected tab is `tabindex=\"0\"`; the others are `-1`.",
        "Left and Right move in a horizontal list. `aria-orientation=\"vertical\"` uses Up and Down. Home and End jump to the ends. Choosing a tab also selects it."
      ],
      "example": "<script type=\"module\" src=\"./node_modules/bootstrap-html/src/behaviors/tabs.js\"></script>\n\n<pe-tabs>\n  <ul class=\"nav nav-tabs\" role=\"tablist\">\n    <li class=\"nav-item\" role=\"presentation\">\n      <button class=\"nav-link active\" id=\"home-tab\" type=\"button\" role=\"tab\" aria-controls=\"home-tab-pane\" aria-selected=\"true\">Home</button>\n    </li>\n    <li class=\"nav-item\" role=\"presentation\">\n      <button class=\"nav-link\" id=\"profile-tab\" type=\"button\" role=\"tab\" aria-controls=\"profile-tab-pane\" aria-selected=\"false\" tabindex=\"-1\">Profile</button>\n    </li>\n    <li class=\"nav-item\" role=\"presentation\">\n      <button class=\"nav-link\" id=\"contact-tab\" type=\"button\" role=\"tab\" aria-controls=\"contact-tab-pane\" aria-selected=\"false\" tabindex=\"-1\">Contact</button>\n    </li>\n  </ul>\n  <div class=\"tab-content\">\n    <div class=\"tab-pane active show\" id=\"home-tab-pane\" role=\"tabpanel\" aria-labelledby=\"home-tab\" tabindex=\"0\">\n      <p class=\"mt-3 mb-0\">This is the home pane. Arrow keys in the field below stay in the field.</p>\n      <input class=\"form-control mt-3\" type=\"text\" value=\"Edit me\" aria-label=\"Note\">\n    </div>\n    <div class=\"tab-pane\" id=\"profile-tab-pane\" role=\"tabpanel\" aria-labelledby=\"profile-tab\" tabindex=\"0\">\n      <p class=\"mt-3 mb-0\">This is the profile pane.</p>\n    </div>\n    <div class=\"tab-pane\" id=\"contact-tab-pane\" role=\"tabpanel\" aria-labelledby=\"contact-tab\" tabindex=\"0\">\n      <p class=\"mt-3 mb-0\">This is the contact pane.</p>\n    </div>\n  </div>\n</pe-tabs>"
    },
    {
      "id": "toasts",
      "name": "Toasts",
      "route": "/components/toasts",
      "docs": "/components/toasts/",
      "summary": "popover=manual. Optional pe-toast adds data-delay and stacking.",
      "needsScript": false,
      "baseline": "newly",
      "element": {
        "name": "pe-toast",
        "module": "src/behaviors/toast.js",
        "required": false,
        "adds": "`data-delay` autohide and corner stacking. Show and close work without it."
      },
      "polyfills": [],
      "javascript": {
        "bootstrap": "5.4 KB gzip",
        "bootstrapHtml": "0 KB. Optional behavior for autohide."
      },
      "platform": {
        "bootstrap": "Toast plugin",
        "bootstrapHtml": "popover=manual. Optional pe-toast for data-delay and stacking."
      },
      "support": {
        "noScript": "`popover=\"manual\"` with `command` to show and close. No light-dismiss.",
        "polyfill": "None for show and close. Optional `pe-toast` for `data-delay` and stacking.",
        "noFallback": "Without `pe-toast`, open toasts share one corner."
      },
      "replaces": [
        {
          "from": "`data-bs-autohide` and `data-bs-delay`",
          "to": "`data-delay` on `<pe-toast>`. Load `src/behaviors/toast.js`."
        },
        {
          "from": "`data-bs-dismiss=\"toast\"`",
          "to": "`command=\"hide-popover\"` and `commandfor`"
        }
      ],
      "avoid": [
        "`popover=\"manual\"` has no light-dismiss. Give every toast a close control.",
        "The toast does not take focus. Use `role=\"status\"` or `role=\"alert\"` so it is announced."
      ],
      "accessibility": [
        "Use `role=\"status\"` with `aria-live=\"polite\"` and `aria-atomic=\"true\"` for an ordinary message. Use `role=\"alert\"` with `aria-live=\"assertive\"` for an important one. The toast does not take focus.",
        "`popover=\"manual\"` does not light-dismiss. Hover and focus pause optional `data-delay` autohide so a reader can finish the message."
      ],
      "example": "<button type=\"button\" class=\"btn btn-primary\" command=\"show-popover\" commandfor=\"liveToast\">\n  Show live toast\n</button>\n\n<div class=\"toast-container position-fixed bottom-0 end-0 p-3\">\n  <div class=\"toast\" id=\"liveToast\" popover=\"manual\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n    <div class=\"toast-header\">\n      <strong class=\"me-auto\">Bootstrap</strong>\n      <small>11 mins ago</small>\n      <button type=\"button\" class=\"btn-close\" command=\"hide-popover\" commandfor=\"liveToast\" aria-label=\"Close\"></button>\n    </div>\n    <div class=\"toast-body\">\n      Hello, world! This is a toast message.\n    </div>\n  </div>\n</div>"
    },
    {
      "id": "carousel",
      "name": "Carousel",
      "route": "/components/carousel",
      "docs": "/components/carousel/",
      "summary": "CSS scroll snap and swipe. Buttons outside Chrome are an optional module.",
      "needsScript": false,
      "baseline": "experimental",
      "element": {
        "name": "pe-carousel",
        "module": "src/behaviors/carousel.js",
        "required": false,
        "adds": "Prev, next, and pip controls where `::scroll-button` is missing. Inert where it is not."
      },
      "polyfills": [
        {
          "module": "src/polyfills/invoker-commands.js",
          "gzipKb": 0.8,
          "loadedWhen": "commandForElement is missing and the page has [commandfor]."
        }
      ],
      "javascript": {
        "bootstrap": "6.7 KB gzip",
        "bootstrapHtml": "0 KB. Optional behavior is 1.4 KB gzip."
      },
      "platform": {
        "bootstrap": "Carousel plugin (slide, swipe, indicators, keyboard)",
        "bootstrapHtml": "CSS scroll snap. Optional pe-carousel when ::scroll-button is missing."
      },
      "support": {
        "noScript": "`scroll-snap-type` is widely available. Swipe works.",
        "polyfill": "None",
        "noFallback": "`::scroll-button` and `::scroll-marker`: Chrome 135+ only. Optional `pe-carousel` covers buttons and pips elsewhere. No autoplay, fade, or wrap."
      },
      "replaces": [
        {
          "from": "`data-bs-ride` and `data-bs-slide`",
          "to": "`::scroll-button()` and `::scroll-marker` in Chromium, or `command=\"--prev\"` / `--next` / `--slide` on `<pe-carousel>`"
        },
        {
          "from": "`.carousel-control-prev-icon` markup",
          "to": "`content` on `::scroll-button()`"
        }
      ],
      "avoid": [
        "No autoplay, no `.carousel-fade` crossfade, and no wrap from the last slide to the first.",
        "`commandfor` is the `.carousel-inner` id with no `#`."
      ],
      "accessibility": [
        "Put `data-label` on each `.carousel-item` so `::scroll-marker` has an accessible name. Native scroll buttons use `content: \"\" / \"Previous\"` and `\"Next\"`.",
        "On `<pe-carousel>`, the current pip is `aria-current=\"true\"`. Previous and next disable at the ends. `scroll-behavior: smooth` follows `prefers-reduced-motion`."
      ],
      "example": "<script type=\"module\" src=\"./node_modules/bootstrap-html/src/behaviors/carousel.js\"></script>\n\n<pe-carousel class=\"carousel slide\">\n  <button type=\"button\" class=\"carousel-control-prev\" command=\"--prev\" commandfor=\"fallback-carousel\">\n    <span class=\"carousel-control-prev-icon\" aria-hidden=\"true\"></span>\n    <span class=\"visually-hidden\">Previous</span>\n  </button>\n  <div class=\"carousel-inner\" id=\"fallback-carousel\">\n    <div class=\"carousel-item\" data-label=\"Slide 1\">\n    <div class=\"d-flex w-100 align-items-center justify-content-center bg-secondary text-white\" style=\"min-height: 16rem\">Fallback first slide</div>\n  </div>\n  <div class=\"carousel-item\" data-label=\"Slide 2\">\n    <div class=\"d-flex w-100 align-items-center justify-content-center bg-dark text-white\" style=\"min-height: 16rem\">Fallback second slide</div>\n  </div>\n  <div class=\"carousel-item\" data-label=\"Slide 3\">\n    <div class=\"d-flex w-100 align-items-center justify-content-center bg-secondary text-white\" style=\"min-height: 16rem\">Fallback third slide</div>\n  </div>\n  </div>\n  <button type=\"button\" class=\"carousel-control-next\" command=\"--next\" commandfor=\"fallback-carousel\">\n    <span class=\"carousel-control-next-icon\" aria-hidden=\"true\"></span>\n    <span class=\"visually-hidden\">Next</span>\n  </button>\n  <div class=\"carousel-indicators\">\n    <button type=\"button\" command=\"--slide\" commandfor=\"fallback-carousel\" aria-label=\"Slide 1\"></button>\n    <button type=\"button\" command=\"--slide\" commandfor=\"fallback-carousel\" aria-label=\"Slide 2\"></button>\n    <button type=\"button\" command=\"--slide\" commandfor=\"fallback-carousel\" aria-label=\"Slide 3\"></button>\n  </div>\n</pe-carousel>"
    },
    {
      "id": "progress",
      "name": "Progress",
      "route": "/components/progress",
      "docs": "/components/progress/",
      "summary": "Native progress and meter. Omit value for the indeterminate stripe.",
      "needsScript": false,
      "baseline": "newly",
      "element": null,
      "polyfills": [],
      "javascript": {
        "bootstrap": "Not in bootstrap.min.js",
        "bootstrapHtml": "0 KB"
      },
      "platform": {
        "bootstrap": "No Progress plugin. Markup is div.progress > .progress-bar with ARIA.",
        "bootstrapHtml": "progress and meter"
      },
      "support": {
        "noScript": "`<progress>` is Baseline Widely available (2015). `<meter>` is too (2017). The fill follows `value` / `max` with no script.",
        "polyfill": "None",
        "noFallback": "Without this CSS the native control still works. Token colors, radius, and the striped animation need `appearance: none` and the vendor bar pseudos."
      },
      "replaces": [
        {
          "from": "`<div class=\"progress\"><div class=\"progress-bar\" style=\"width: 25%\">`",
          "to": "`<progress class=\"progress\" value=\"25\" max=\"100\">25%</progress>`"
        },
        {
          "from": "`role=\"progressbar\"` and `aria-valuenow` / `min` / `max`",
          "to": "The `value` and `max` attributes. Do not add a role."
        },
        {
          "from": "`.progress-bar.bg-success`",
          "to": "`.text-success` on the `<progress>`. The fill is `currentcolor`."
        }
      ],
      "avoid": [
        "No inner `.progress-bar` and no `style=\"width: …%\"`. `value` and `max` are the fill.",
        "Stacked bars are several values in one track. A `<progress>` has one value."
      ],
      "accessibility": [
        "Label with `<label for>` or wrap the control. Text between the tags is a fallback for old browsers, not the accessible name.",
        "Omit `value` only when the duration is unknown. Assistive technology then announces busy, not a percent. Do not add `role=\"progressbar\"`."
      ],
      "example": "<label class=\"form-label\" for=\"file\">File progress</label>\n<progress id=\"file\" class=\"progress\" value=\"70\" max=\"100\">70%</progress>"
    },
    {
      "id": "spinner",
      "name": "Spinner",
      "route": "/components/spinner",
      "docs": "/components/spinner/",
      "summary": "aria-busy paints a spinner on the host. No extra spinner element.",
      "needsScript": false,
      "baseline": "newly",
      "element": null,
      "polyfills": [],
      "javascript": {
        "bootstrap": "Not in bootstrap.min.js",
        "bootstrapHtml": "0 KB"
      },
      "platform": {
        "bootstrap": "No Spinner plugin. Markup is .spinner-border plus visually-hidden text.",
        "bootstrapHtml": "`aria-busy=\"true\"` on the host. CSS paints the disc."
      },
      "support": {
        "noScript": "`aria-busy` is a global ARIA state. The disc is CSS on `[aria-busy=\"true\"]`. Removing the attribute hides it.",
        "polyfill": "None",
        "noFallback": "Without this CSS the attribute still tells assistive technology. There is no disc. `.spinner-border` stays in the sheet as a standalone glyph."
      },
      "replaces": [
        {
          "from": "`<span class=\"spinner-border\" role=\"status\"><span class=\"visually-hidden\">Loading…</span></span>`",
          "to": "`aria-busy=\"true\"` on the host that is loading"
        },
        {
          "from": "A `.spinner-border` child inside a `disabled` button",
          "to": "`aria-busy=\"true\"` and `disabled` on the button. The name stays."
        }
      ],
      "avoid": [
        "Do not put a `.spinner-border` child inside a host that already has `aria-busy`. The disc is a pseudo-element.",
        "A known percent is a `<progress>`, not a spinner. See Progress.",
        "Do not set `aria-busy` on `body` for one widget. Put it on the host that is changing."
      ],
      "accessibility": [
        "The attribute is the state. The disc is decorative (`::before` / `::after`). Do not add `role=\"status\"` on a spinner element.",
        "A nameless host needs a role that can take a name (`role=\"status\"`) and `aria-label` (or a more specific name) while it is busy. A generic `<div>` with only `aria-label` is ignored.",
        "A button keeps its accessible name. `disabled` blocks a second submit.",
        "On a live region, `aria-busy=\"true\"` tells assistive technology to wait until you set it to `false` (or remove it) before announcing the update.",
        "`prefers-reduced-motion` slows the turn. It does not freeze the disc, so the busy state stays visible."
      ],
      "example": "<div class=\"d-flex flex-wrap gap-2\">\n  <button type=\"button\" class=\"btn btn-primary\">Publish</button>\n  <button type=\"button\" class=\"btn btn-primary\" aria-busy=\"true\" disabled>Publish</button>\n  <button type=\"button\" class=\"btn btn-outline-secondary\" aria-busy=\"true\" disabled>Export CSS</button>\n  <button type=\"button\" class=\"btn btn-sm btn-primary\" aria-busy=\"true\" disabled>Publish</button>\n</div>"
    },
    {
      "id": "validation",
      "name": "Validation",
      "route": "/forms/validation",
      "docs": "/forms/validation/",
      "summary": ":user-valid / :user-invalid after type, blur, or submit. No wrapper class.",
      "needsScript": false,
      "baseline": "newly",
      "element": null,
      "polyfills": [],
      "javascript": {
        "bootstrap": "Not in bootstrap.min.js",
        "bootstrapHtml": "0 KB"
      },
      "platform": {
        "bootstrap": "No validation plugin. The Bootstrap docs add .was-validated from a submit listener.",
        "bootstrapHtml": ":user-valid and :user-invalid"
      },
      "support": {
        "noScript": "`:user-valid` / `:user-invalid`: Chrome 119+, Firefox 88+, Safari 16.5+.",
        "polyfill": "None",
        "noFallback": "None"
      },
      "replaces": [
        {
          "from": "`.was-validated`",
          "to": "`:user-valid` / `:user-invalid`. No wrapper class."
        },
        {
          "from": "The submit listener that adds it",
          "to": "Nothing. The browser decides when the user has interacted."
        }
      ],
      "avoid": [
        "No `.was-validated` wrapper and no `novalidate` plus a hand-rolled check.",
        "`.is-valid` / `.is-invalid` stay, but only for state the server decided.",
        "Do not expect a non-required `.form-check-input` to turn green after a click. `:user-valid` success on checks is for `required` only."
      ],
      "accessibility": [
        "Native constraints (`required`, `type`, `pattern`) are exposed by the control. Associate `.invalid-feedback` with the field through `aria-describedby`. Put that feedback after the control so `:user-invalid` can show it.",
        "Do not add `novalidate` unless you replace the browser’s submit check with your own. Empty required fields stay unstyled until the user interacts or submits."
      ],
      "example": "<form class=\"row g-3\">\n  <div class=\"col-md-4\">\n    <label for=\"validationCustom01\" class=\"form-label\">First name</label>\n    <input type=\"text\" class=\"form-control\" id=\"validationCustom01\" value=\"Mark\" autocomplete=\"given-name\" aria-describedby=\"validationCustom01Feedback\" required>\n    <div id=\"validationCustom01Feedback\" class=\"valid-feedback\">\n      Looks good!\n    </div>\n  </div>\n  <div class=\"col-md-4\">\n    <label for=\"validationCustom02\" class=\"form-label\">Last name</label>\n    <input type=\"text\" class=\"form-control\" id=\"validationCustom02\" value=\"Otto\" autocomplete=\"family-name\" aria-describedby=\"validationCustom02Feedback\" required>\n    <div id=\"validationCustom02Feedback\" class=\"valid-feedback\">\n      Looks good!\n    </div>\n  </div>\n  <div class=\"col-md-4\">\n    <label for=\"validationCustomUsername\" class=\"form-label\">Username</label>\n    <div class=\"input-group has-validation\">\n      <span class=\"input-group-text\" id=\"inputGroupPrepend\">@</span>\n      <input type=\"text\" class=\"form-control\" id=\"validationCustomUsername\" aria-describedby=\"inputGroupPrepend validationCustomUsernameFeedback\" required>\n      <div id=\"validationCustomUsernameFeedback\" class=\"invalid-feedback\">\n        Please choose a username.\n      </div>\n    </div>\n  </div>\n  <div class=\"col-md-6\">\n    <label for=\"validationCustom03\" class=\"form-label\">City</label>\n    <input type=\"text\" class=\"form-control\" id=\"validationCustom03\" autocomplete=\"address-level2\" aria-describedby=\"validationCustom03Feedback\" required>\n    <div id=\"validationCustom03Feedback\" class=\"invalid-feedback\">\n      Please provide a valid city.\n    </div>\n  </div>\n  <div class=\"col-md-3\">\n    <label for=\"validationCustom04\" class=\"form-label\">State</label>\n    <select class=\"form-select\" id=\"validationCustom04\" aria-describedby=\"validationCustom04Feedback\" required>\n      <option selected disabled value=\"\">Choose...</option>\n      <option>California</option>\n      <option>Texas</option>\n    </select>\n    <div id=\"validationCustom04Feedback\" class=\"invalid-feedback\">\n      Please select a valid state.\n    </div>\n  </div>\n  <div class=\"col-md-3\">\n    <label for=\"validationCustom05\" class=\"form-label\">Zip</label>\n    <input type=\"text\" class=\"form-control\" id=\"validationCustom05\" autocomplete=\"postal-code\" aria-describedby=\"validationCustom05Feedback\" required>\n    <div id=\"validationCustom05Feedback\" class=\"invalid-feedback\">\n      Please provide a valid zip.\n    </div>\n  </div>\n  <div class=\"col-12\">\n    <div class=\"form-check\">\n      <input class=\"form-check-input\" type=\"checkbox\" value=\"\" id=\"invalidCheck\" aria-describedby=\"invalidCheckFeedback\" required>\n      <label class=\"form-check-label\" for=\"invalidCheck\">\n        Agree to terms and conditions\n      </label>\n      <div id=\"invalidCheckFeedback\" class=\"invalid-feedback\">\n        You must agree before submitting.\n      </div>\n    </div>\n  </div>\n  <div class=\"col-12\">\n    <button class=\"btn btn-primary\" type=\"submit\">Submit form</button>\n  </div>\n</form>"
    },
    {
      "id": "field-sizing",
      "name": "Field sizing",
      "route": "/forms/field-sizing",
      "docs": "/forms/field-sizing/",
      "summary": "Opt-in field-sizing: content. The control grows to its value.",
      "needsScript": false,
      "baseline": "newly",
      "element": null,
      "polyfills": [],
      "javascript": {
        "bootstrap": "Not in bootstrap.min.js",
        "bootstrapHtml": "0 KB"
      },
      "platform": {
        "bootstrap": "No equivalent. Autosize is a third-party script.",
        "bootstrapHtml": "`field-sizing: content` on `.field-sizing`"
      },
      "support": {
        "noScript": "`field-sizing: content`: Chrome 123+, Firefox 152+, Safari 26.2+.",
        "polyfill": "None",
        "noFallback": "None. Without `field-sizing` the control keeps Bootstrap’s fixed box."
      },
      "replaces": [
        {
          "from": "Autosize scripts that grow a textarea on `input`",
          "to": "`.field-sizing` on `.form-control` or `.form-select`"
        }
      ],
      "avoid": [
        "`.form-control` / `.form-select` alone stay `width: 100%`. Add `.field-sizing`.",
        "An explicit `width` or `height` overrides `field-sizing`. Do not set those on the same control."
      ],
      "accessibility": [
        "Label with `<label for>` or wrap the control. Do not use `placeholder` as the only name.",
        "`max-inline-size` / `max-block-size` cap growth so the page does not jump without bound. The size change itself is not announced; do not rely on it as the only status."
      ],
      "example": "<label class=\"form-label\" for=\"note\">Note</label>\n<textarea class=\"form-control field-sizing\" id=\"note\" name=\"note\">Type more lines to grow the box.</textarea>"
    },
    {
      "id": "datalist",
      "name": "Datalist",
      "route": "/forms/datalist",
      "docs": "/forms/datalist/",
      "summary": "Native typeahead with input list and datalist. No dropdown script.",
      "needsScript": false,
      "baseline": "newly",
      "element": null,
      "polyfills": [],
      "javascript": {
        "bootstrap": "Not in bootstrap.min.js",
        "bootstrapHtml": "0 KB"
      },
      "platform": {
        "bootstrap": "No equivalent. Typeahead is a third-party script or a dropdown.",
        "bootstrapHtml": "`<input list>` and `<datalist>`"
      },
      "support": {
        "noScript": "`<datalist>`: Chrome 20+, Firefox 4+, Safari 12.1+. Firefox on Android does not show the suggestion menu.",
        "polyfill": "None",
        "noFallback": "None. Without a suggestion menu the input still accepts a typed value."
      },
      "replaces": [
        {
          "from": "A dropdown or typeahead plugin wired to an input",
          "to": "`list` on the input pointing at a `<datalist>` id"
        }
      ],
      "avoid": [
        "`list` is the datalist id with no `#`. The datalist is not a `.dropdown-menu`.",
        "Do not put `role=\"listbox\"` on the datalist. The browser exposes the suggestions."
      ],
      "accessibility": [
        "Label with `<label for>` or wrap the control. Do not use `placeholder` as the only name.",
        "A datalist is a suggestion list, not a required choice. The user can submit a value that is not in the options."
      ],
      "example": "<label class=\"form-label\" for=\"ice-cream\">Flavor</label>\n<input class=\"form-control\" list=\"ice-cream-flavors\" id=\"ice-cream\" name=\"ice-cream\" placeholder=\"Type to filter\">\n<datalist id=\"ice-cream-flavors\">\n  <option value=\"Chocolate\"></option>\n  <option value=\"Coconut\"></option>\n  <option value=\"Mint\"></option>\n  <option value=\"Strawberry\"></option>\n  <option value=\"Vanilla\"></option>\n</datalist>"
    },
    {
      "id": "output",
      "name": "Output",
      "route": "/forms/output",
      "docs": "/forms/output/",
      "summary": "Native output for a calculated result. Optional count.js for checked boxes.",
      "needsScript": false,
      "baseline": "newly",
      "element": null,
      "polyfills": [],
      "javascript": {
        "bootstrap": "Not in bootstrap.min.js",
        "bootstrapHtml": "0 KB. Optional src/behaviors/count.js"
      },
      "platform": {
        "bootstrap": "No equivalent. A badge or span updated from a change listener.",
        "bootstrapHtml": "`<output>`. Optional `src/behaviors/count.js` for `data-controls`"
      },
      "support": {
        "noScript": "`<output>` is Baseline Widely available. Without `count.js` the text stays at whatever was in the markup.",
        "polyfill": "None",
        "noFallback": "A closed popover is `display: none`, so a CSS counter inside that menu stays 0. `count.js` is the exception for that case. Visible checkboxes can use `:has()` and `counter()` without it."
      },
      "replaces": [
        {
          "from": "A span or badge rewritten from a change listener you wrote",
          "to": "`<output data-controls>` with the container id, no hash, and `src/behaviors/count.js`"
        }
      ],
      "avoid": [
        "Do not add a `pe-count` or `pe-checked` element. The output is already a status live region.",
        "`data-controls` is a container id, with no `#` and no CSS selector. `:checked` does not belong in the attribute.",
        "Do not watch the `checked` attribute with a MutationObserver. Listen to `change`.",
        "Do not lift `.form-check-input` out of a closed popover to feed a CSS counter. Closed popovers are not in the tab order.",
        "Do not put the output inside a toggle button. The count would be announced twice."
      ],
      "accessibility": [
        "`<output>` is a status live region. Keep it beside the control, not inside a button name.",
        "`for` lists the contributing control ids. `data-controls` is the live group `count.js` queries.",
        "On a dropdown toggle, `aria-describedby` points at the output so the button name stays the action."
      ],
      "example": "<script type=\"module\" src=\"./node_modules/bootstrap-html/src/behaviors/count.js\"></script>\n\n<fieldset>\n  <legend>\n    Notify me\n    <output id=\"output-notify-count\" class=\"ms-1 badge rounded-pill text-bg-secondary\" data-controls=\"output-notify\" for=\"output-notify-email output-notify-sms\">1<span class=\"visually-hidden\"> selected</span></output>\n  </legend>\n  <div id=\"output-notify\">\n    <div class=\"form-check\">\n      <input class=\"form-check-input\" type=\"checkbox\" value=\"email\" id=\"output-notify-email\" checked>\n      <label class=\"form-check-label\" for=\"output-notify-email\">Email</label>\n    </div>\n    <div class=\"form-check\">\n      <input class=\"form-check-input\" type=\"checkbox\" value=\"sms\" id=\"output-notify-sms\">\n      <label class=\"form-check-label\" for=\"output-notify-sms\">SMS</label>\n    </div>\n  </div>\n</fieldset>"
    },
    {
      "id": "spin-button",
      "name": "Spin button",
      "route": "/forms/spin-button",
      "docs": "/forms/spin-button/",
      "summary": "Plus and minus buttons step a native input. commandfor is the input id.",
      "needsScript": true,
      "baseline": "newly",
      "element": null,
      "polyfills": [
        {
          "module": "src/polyfills/invoker-commands.js",
          "gzipKb": 0.8,
          "loadedWhen": "commandForElement is missing and the page has [commandfor]."
        }
      ],
      "javascript": {
        "bootstrap": "Not in bootstrap.min.js",
        "bootstrapHtml": "Optional src/behaviors/step.js"
      },
      "platform": {
        "bootstrap": "No spin-button plugin. Quantity steppers are custom markup plus script.",
        "bootstrapHtml": "`stepUp` / `stepDown` from `command=\"--step-up\"` / `--step-down`. Load `src/behaviors/step.js`"
      },
      "support": {
        "noScript": "The input still accepts a typed or native-stepped value. Without `step.js` the plus and minus buttons do nothing.",
        "polyfill": "[`command` / `commandfor`](/polyfills#invoker-commands) when that API is missing. `step.js` is still required to change the value.",
        "noFallback": "`stepUp()` throws on `step=\"any\"` and on types that have no step. An empty date, time, week, or month field without `min` or `value` jumps to 1970-01-01 or 00:00."
      },
      "replaces": [
        {
          "from": "`role=\"spinbutton\"` and `aria-valuenow` / `min` / `max` on an `<input type=\"text\">`",
          "to": "A native `<input type=\"number\">` (or date, time, range). `min`, `max`, and `step` are the constraints."
        },
        {
          "from": "`aria-controls` and `data-*` increment buttons wired in page script",
          "to": "`command=\"--step-up\"` / `--step-down` and `commandfor` set to the input id. Load `src/behaviors/step.js`."
        }
      ],
      "avoid": [
        "`commandfor` is the input id, with no `#`. Do not point it at a wrapper.",
        "Do not add a `pe-step` or `pe-spin` element. The buttons already invoke the input.",
        "Do not add `role=\"spinbutton\"` or `aria-valuemin` / `aria-valuemax` / `aria-valuenow` on the input. `type=\"number\"` already has that role.",
        "`step=\"any\"` is ignored. `stepUp()` throws `InvalidStateError`; the command does nothing.",
        "Give date, time, week, month, and datetime-local a `min` or a `value`. An empty field steps from epoch."
      ],
      "accessibility": [
        "`<input type=\"number\">` is an implicit spinbutton. Arrow keys, Home, and End, and `min` / `max` as valuemin / valuemax, come from the browser. Do not restyle a text input into that role.",
        "Name the buttons (`aria-label`). Hide the `+` / `−` glyphs with `aria-hidden`. `tabindex=\"-1\"` follows the APG: the keys on the input already step, and touch and voice can still activate the button.",
        "Focus stays on the button. A visually hidden `<output for>` announces the new value. The module writes it only on a button press.",
        "The buttons stay enabled at min and max. `stepUp()` already clamps. `disabled` would drop focus."
      ],
      "example": "<script type=\"module\" src=\"./node_modules/bootstrap-html/src/behaviors/step.js\"></script>\n\n<label class=\"form-label\" for=\"qty\">Quantity</label>\n<div class=\"input-group\">\n  <button type=\"button\" class=\"btn btn-outline-secondary\" command=\"--step-down\" commandfor=\"qty\" aria-label=\"Decrease quantity\" tabindex=\"-1\">\n    <span aria-hidden=\"true\">−</span>\n  </button>\n  <input type=\"number\" class=\"form-control\" id=\"qty\" name=\"qty\" value=\"1\" min=\"1\" max=\"8\">\n  <button type=\"button\" class=\"btn btn-outline-secondary\" command=\"--step-up\" commandfor=\"qty\" aria-label=\"Increase quantity\" tabindex=\"-1\">\n    <span aria-hidden=\"true\">+</span>\n  </button>\n</div>\n<output class=\"visually-hidden\" for=\"qty\"></output>"
    },
    {
      "id": "range-group",
      "name": "Range group",
      "route": "/forms/range-group",
      "docs": "/forms/range-group/",
      "summary": "Two native range inputs stacked on one track. Optional range-group.js clamps and paints the fill.",
      "needsScript": false,
      "baseline": "newly",
      "element": null,
      "polyfills": [],
      "javascript": {
        "bootstrap": "Not in bootstrap.min.js",
        "bootstrapHtml": "0 KB. Optional src/behaviors/range-group.js"
      },
      "platform": {
        "bootstrap": "No range-group plugin. Dual sliders are a third-party widget or custom ARIA sliders.",
        "bootstrapHtml": "Two `.form-range` in `.form-range-group`. Optional `src/behaviors/range-group.js` for clamp and fill."
      },
      "support": {
        "noScript": "Two native `type=\"range\"` on one track. Keyboard, submit, and focus work. Without `range-group.js` thumbs can cross and there is no fill. `data-range-group=\"off\"` skips the module when it is loaded.",
        "polyfill": "None",
        "noFallback": "The track is not a click target: `pointer-events: none` on the inputs, so only the thumbs receive pointer events. The later thumb in the DOM wins when they overlap. Fill needs `range-group.js`."
      },
      "replaces": [
        {
          "from": "noUiSlider, or a custom `role=\"slider\"` multi-thumb widget",
          "to": "Two `<input type=\"range\" class=\"form-range\">` in `.form-range-track`, inside `<fieldset class=\"form-range-group\">`"
        },
        {
          "from": "`aria-valuemin` / `aria-valuemax` updated as the other thumb moves",
          "to": "Value clamp in `src/behaviors/range-group.js`. Do not rewrite `min` / `max` on the inputs; that breaks the shared track."
        }
      ],
      "avoid": [
        "Do not point `aria-labelledby` at an `<output>`. That replaces the label and the slider is named by its value.",
        "Do not put `role=\"tooltip\"` on the `<output>`. It is a readout, not a tooltip.",
        "Do not add a `pe-range-group` element. The fieldset is already the group.",
        "`data-range-group=\"off\"` skips clamp and fill. Omit it when you load the module and want that behavior.",
        "Give both inputs the same `min`, `max`, and `step`. Different bounds break the shared geometry.",
        "Do not add `role=\"slider\"` or `aria-valuemin` / `aria-valuemax` / `aria-valuenow` on the inputs. `type=\"range\"` already has that role."
      ],
      "accessibility": [
        "Each thumb is a native slider. Arrow keys, Home, End, and Page Up / Page Down come from the browser. Tab order is DOM order and does not change when thumbs overlap.",
        "Bootstrap’s focus ring is on the thumb, so each stacked input still shows which handle is focused.",
        "Name the group with `<legend>`. Name each thumb with `<label for>`. Do not use `aria-labelledby` on a value readout.",
        "The readout is `<output for aria-live=\"off\">`. Focus is on the slider, which already announces the value; a live region would speak it twice. That is the opposite of Spin button, where focus stays on the button.",
        "Thumbs are 1.5rem (24px) because the track is not a pointer target. That meets WCAG 2.5.8 Target Size (Minimum)."
      ],
      "example": "<script type=\"module\" src=\"./node_modules/bootstrap-html/src/behaviors/range-group.js\"></script>\n\n<fieldset class=\"form-range-group\">\n  <legend class=\"form-label\">Price</legend>\n  <label class=\"visually-hidden\" for=\"price-min\">Minimum price</label>\n  <label class=\"visually-hidden\" for=\"price-max\">Maximum price</label>\n  <div class=\"form-range-track\">\n    <input type=\"range\" class=\"form-range\" id=\"price-min\" name=\"price-min\" min=\"0\" max=\"1000\" step=\"10\" value=\"200\">\n    <input type=\"range\" class=\"form-range\" id=\"price-max\" name=\"price-max\" min=\"0\" max=\"1000\" step=\"10\" value=\"800\">\n  </div>\n  <p class=\"form-text mb-0\">\n    <output for=\"price-min\" aria-live=\"off\">200</output> – <output for=\"price-max\" aria-live=\"off\">800</output>\n  </p>\n</fieldset>"
    },
    {
      "id": "customizable-select",
      "name": "Customizable select",
      "route": "/forms/customizable-select",
      "docs": "/forms/customizable-select/",
      "summary": "appearance: base-select on .form-select. Picker, icon, and checkmark take Bootstrap tokens.",
      "needsScript": false,
      "baseline": "experimental",
      "element": null,
      "polyfills": [],
      "javascript": {
        "bootstrap": "Not in bootstrap.min.js",
        "bootstrapHtml": "0 KB"
      },
      "platform": {
        "bootstrap": "No equivalent. A dropdown or a third-party select plugin.",
        "bootstrapHtml": "`appearance: base-select` on `.form-select`"
      },
      "support": {
        "noScript": "`appearance: base-select`: Chrome 135+, Safari 27+. Firefox 149 behind `dom.select.customizable_select.enabled` and `layout.css.appearance-base.enabled`.",
        "polyfill": "None",
        "noFallback": "Marked experimental on the forms page. Without `base-select` the closed `.form-select` and the OS picker stay. Rich markup inside `<option>` is stripped to text. `<button>` / `<selectedcontent>` inside the select are ignored."
      },
      "replaces": [
        {
          "from": "A dropdown or Choices/Select2 widget used as a select",
          "to": "`<select class=\"form-select\">`. `::picker(select)` is the menu."
        }
      ],
      "avoid": [
        "Do not rebuild a select with `.dropdown-menu`. That is a menu, not a form control.",
        "`<select multiple>` and `size` other than `1` stay on the OS listbox. This CSS does not opt those in.",
        "Do not put links or buttons inside `<option>`. The select’s button is inert."
      ],
      "accessibility": [
        "Label with `<label for>` or wrap the control. Keep a `name` so the value submits.",
        "Decorative icons inside options take `aria-hidden=\"true\"`. If the mirrored rich content would read badly, set `aria-label` on the option.",
        "`::picker-icon` and `::checkmark` are not in the accessibility tree. Do not put the only meaning of the control in generated content.",
        "The checked option uses background and font-weight, not color alone. Arrow keys still move in one dimension even if the picker is laid out as a grid."
      ],
      "example": "<label class=\"form-label\" for=\"plan\">Plan</label>\n<select class=\"form-select\" id=\"plan\" name=\"plan\">\n  <option value=\"free\">Free</option>\n  <option value=\"pro\" selected>Pro</option>\n  <option value=\"team\">Team</option>\n</select>"
    },
    {
      "id": "composed",
      "name": "Composed examples",
      "route": "/components/composed",
      "docs": "/components/composed/",
      "summary": "Mixed examples that compose existing primitives. No new pe-* element.",
      "needsScript": false,
      "baseline": "experimental",
      "element": null,
      "polyfills": [],
      "javascript": {
        "bootstrap": "Varies by recipe",
        "bootstrapHtml": "0 KB. Optional pe-toast, pe-tabs, and src/behaviors/count.js for output[data-controls]."
      },
      "platform": {
        "bootstrap": "Not one plugin. These are recipes on Dropdown, Offcanvas, Toast, Tabs, Progress, Buttons, and Output.",
        "bootstrapHtml": "Popover, dialog, details, progress, form checks, search, and btn-check. Optional pe-toast and pe-tabs as on those pages."
      },
      "support": {
        "noScript": "Drawer, context menu, and the progress ring work without a component script. The multi-select menu opens without script. `<output data-controls>` needs `src/behaviors/count.js` to stay in sync, and a closed popover cannot increment a CSS counter. `command` still uses the invoker polyfill when missing.",
        "polyfill": "The same [`command`](/polyfills#invoker-commands) polyfill as dropdown and offcanvas.",
        "noFallback": "Context-menu `position-area` and the tab underline need CSS Anchor Positioning. The shrinking header and scroll bar need `animation-timeline: scroll()` (not in Firefox). The ring’s `attr()` into `--bs-progress-value` is Chrome 133+; set the custom property in markup elsewhere. No scroll-listener and no pointer-coordinate script. `src/behaviors/count.js` is the one optional script on this page that is not a `pe-*` behavior."
      },
      "replaces": [
        {
          "from": "A context-menu plugin that measures `clientX` / `clientY`",
          "to": "`popover=\"auto\"` with `.dropdown-menu-context` and `position-area` on `.context-host`"
        },
        {
          "from": "A JS drawer that scroll-snaps nested panels",
          "to": "`<dialog class=\"offcanvas\">` with nested `<details>` in `.nav-tree`"
        },
        {
          "from": "`<select multiple>` restyled as a dropdown",
          "to": "`.dropdown-multiselect` with `.form-check-input` in the menu"
        }
      ],
      "avoid": [
        "Do not add a `pe-*` element to place the menu at the pointer. There is no CSS path from last-pointer coordinates, and this package does not measure for layout.",
        "Do not put `role=\"menu\"` on these items unless you also implement that keyboard contract. They are buttons and links in a popover.",
        "Do not put `.header-shrink` or `.scroll-progress` on the docs or site chrome. They belong on a nested `.scroll-demo` (or your own scroller).",
        "Do not style every `details` in `.offcanvas-body`. Put the tree in `.nav-tree` so it can live in a drawer or on the page.",
        "Do not lift `.form-check-input` out of a closed `.dropdown-multiselect` popover to feed a CSS counter. Closed popovers are not in the tab order. Do not add a `pe-*` for the count: use `<output data-controls>` with the container id, no hash, and load `src/behaviors/count.js`.",
        "Do not add `.progress` on a `.progress-ring`. The linear class is a full-width bar.",
        "The command palette list is static. Do not filter it with script. `datalist` is the typeahead.",
        "Do not set `aria-pressed` on a price-toggle label. The radio is the control; `:has(:checked)` switches the price."
      ],
      "accessibility": [
        "Context-menu items are buttons (or links), not `role=\"menuitem\"`. Escape and light-dismiss come from `popover=\"auto\"`. Right-click (and the `contextmenu` key) is the surface gesture. A visible Actions button is a separate left-click invoker; the menu must tether to that button, not the whole host.",
        "Nested `details` in `.nav-tree` are disclosures. Do not add `aria-expanded` on the summary. A catalog of links can sit in a `<nav>`.",
        "The toast countdown bar is decorative. `role=\"status\"` or `role=\"alert\"` on the toast is still the announcement. Hover and focus pause `data-delay` so the message can be read.",
        "The tab underline follows `aria-selected`. It is not a substitute for those roles.",
        "Label a `.progress-ring` with `<label for>` or `aria-label`. Center text is optional decoration.",
        "A purely visual `.scroll-progress` takes `aria-hidden=\"true\"`.",
        "The checked count is an `<output data-controls>` status live region, not a `pe-*` element. Keep it out of the toggle button so the count is not announced twice. On a dropdown, `aria-describedby` on the button points at that output. Keep menu checkboxes in the closed popover so they are not tab stops. The contract lives on Output.",
        "A lightbox image needs an `alt` that names the photo. The dialog title can repeat it.",
        "Confirm-then-toast: the dialog submit is the decision; the toast is the status. Do not announce the toast if the user cancelled.",
        "An interest preview is not a live region. `aria-description` on the trigger is the no-script name. The hint popover is extra, not a second announcement."
      ],
      "example": "<div\n  class=\"card context-host\"\n  oncontextmenu=\"event.preventDefault(); this.querySelector('[popover]').showPopover({source: event.target})\"\n>\n  <div class=\"card-body\">\n    <p class=\"mb-2\">Right-click this card. Actions is a separate left-click control.</p>\n    <button type=\"button\" class=\"btn btn-sm btn-secondary\" command=\"toggle-popover\" commandfor=\"composed-context-host\">\n      Actions\n    </button>\n  </div>\n  <ul class=\"dropdown-menu dropdown-menu-context\" popover=\"auto\" id=\"composed-context-host\">\n    <li><button type=\"button\" class=\"dropdown-item\">Edit</button></li>\n    <li><button type=\"button\" class=\"dropdown-item\">Duplicate</button></li>\n    <li><hr class=\"dropdown-divider\"></li>\n    <li><button type=\"button\" class=\"dropdown-item text-danger\">Delete</button></li>\n  </ul>\n</div>"
    }
  ]
}