Skip to content

Documentation

On this page

Composed examples

These are mixed examples of composed elements, not one widget. Different kinds of recipes share this page for now. Each one stacks markup and CSS you already have: popovers, offcanvas, toasts, tabs, progress, buttons, and form checks. No extra pe-* element, and no script that measures or repositions layout. Checked counts use the <output> contract.

Sizes are minified and gzip. A plugin figure includes the Bootstrap runtime that plugin imports. Shipping every plugin is bootstrap.bundle.min.js at 23.3 KB gzip, not the sum of these rows.
What runs it JavaScript
Bootstrap 5.3 Not one plugin. These are recipes on Dropdown, Offcanvas, Toast, Tabs, Progress, Buttons, and Output. Varies by recipe
This repo Popover, dialog, details, progress, form checks, search, and btn-check. Optional pe-toast and pe-tabs as on those pages. 0 KB. Optional pe-toast, pe-tabs, and src/behaviors/count.js for output[data-controls].
Markup this component accepts. The full inventory is /components.json.
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.

Context menu

Right-click is the context menu. The contextmenu event also fires from the Menu key or Shift+F10, so that path needs no extra script. Left-click Actions is a command menu on a button — useful as a visible control, not a second way to click the same pixel. Both can share one popover="auto"; the invoker is the anchor. Items are buttons, not role="menu". There is no CSS path from the last pointer coordinates, so a right-click still tethers to the clicked element rather than the pixel. oncontextmenu calls showPopover({ source: event.target }) after preventDefault().

On a host

Right-click the card (or the paragraph). Left-click Actions to open the same menu from that button.

Right-click this card. Actions is a separate left-click control.

HTML
<div
  class="card context-host"
  oncontextmenu="event.preventDefault(); this.querySelector('[popover]').showPopover({source: event.target})"
>
  <div class="card-body">
    <p class="mb-2">Right-click this card. Actions is a separate left-click control.</p>
    <button type="button" class="btn btn-sm btn-secondary" command="toggle-popover" commandfor="composed-context-host">
      Actions
    </button>
  </div>
  <ul class="dropdown-menu dropdown-menu-context" popover="auto" id="composed-context-host">
    <li><button type="button" class="dropdown-item">Edit</button></li>
    <li><button type="button" class="dropdown-item">Duplicate</button></li>
    <li><hr class="dropdown-divider"></li>
    <li><button type="button" class="dropdown-item text-danger">Delete</button></li>
  </ul>
</div>

From a button

Left-click only. The menu is a command menu, not a context menu.

HTML
<div class="dropdown">
  <button class="btn btn-secondary dropdown-toggle" type="button" command="toggle-popover" commandfor="composed-context-btn">
    Actions
  </button>
  <ul class="dropdown-menu dropdown-menu-context" popover="auto" id="composed-context-btn">
    <li><button type="button" class="dropdown-item">Edit</button></li>
    <li><button type="button" class="dropdown-item">Duplicate</button></li>
    <li><hr class="dropdown-divider"></li>
    <li><button type="button" class="dropdown-item text-danger">Delete</button></li>
  </ul>
</div>

Drawer with drill-down

A dialog.offcanvas opened with command. Nested details in .nav-tree are the levels. The tree is not tied to the drawer: the same class works on the page. This is not a JavaScript scroll-snap drawer.

Offcanvas tree

Open the drawer, then expand a branch. Summaries are the controls.

Catalog
HTML
<button type="button" class="btn btn-primary" command="show-modal" commandfor="composed-drawer">
  Open drawer
</button>

<dialog class="offcanvas offcanvas-start" id="composed-drawer" closedby="any" aria-labelledby="composed-drawer-label">
  <div class="offcanvas-header">
    <h5 class="offcanvas-title" id="composed-drawer-label">Catalog</h5>
    <button type="button" class="btn-close" command="close" commandfor="composed-drawer" aria-label="Close"></button>
  </div>
  <div class="offcanvas-body">
    <nav class="nav-tree" aria-label="Catalog">
      <details open>
        <summary>Products</summary>
        <details open>
          <summary>Shoes</summary>
          <a href="#">Runners</a>
          <a href="#">Boots</a>
        </details>
        <details>
          <summary>Jackets</summary>
          <a href="#">Rain</a>
          <a href="#">Fleece</a>
        </details>
      </details>
      <details open>
        <summary>Account</summary>
        <a href="#">Orders</a>
        <a href="#">Settings</a>
      </details>
    </nav>
  </div>
</dialog>

On the page

Same .nav-tree without a dialog. Links indent under their parent.

HTML
<div class="card" style="max-width: 22rem">
  <div class="card-body">
    <nav class="nav-tree" aria-label="On-page catalog">
      <details open>
        <summary>Products</summary>
        <details open>
          <summary>Shoes</summary>
          <a href="#">Runners</a>
          <a href="#">Boots</a>
        </details>
        <details>
          <summary>Jackets</summary>
          <a href="#">Rain</a>
          <a href="#">Fleece</a>
        </details>
      </details>
      <details open>
        <summary>Account</summary>
        <a href="#">Orders</a>
        <a href="#">Settings</a>
      </details>
    </nav>
  </div>
</div>

Toast countdown

pe-toast already hides after data-delay. It also sets --pe-toast-delay so a CSS bar can match that time. Hover and focus pause the timer and the bar; leaving starts a full delay again.

Autohide bar

Show the toast. The bar empties with the delay. Hover to pause.

Bootstrap HTML just now
Hover the toast to pause the bar. Leaving starts the delay again.
HTML
<script type="module" src="./node_modules/bootstrap-html/src/behaviors/toast.js"></script>

<button type="button" class="btn btn-primary" command="show-popover" commandfor="composed-toast">
  Show toast
</button>

<div class="toast-container position-fixed bottom-0 end-0 p-3">
  <pe-toast class="toast" id="composed-toast" popover="manual" data-delay="5000" role="status" aria-live="polite" aria-atomic="true">
    <div class="toast-header">
      <strong class="me-auto">Bootstrap HTML</strong>
      <small>just now</small>
      <button type="button" class="btn-close" command="hide-popover" commandfor="composed-toast" aria-label="Close"></button>
    </div>
    <div class="toast-body">Hover the toast to pause the bar. Leaving starts the delay again.</div>
  </pe-toast>
</div>

Sliding tab underline

Inside pe-tabs, the selected .nav-link[aria-selected="true"] is an anchor. The tablist paints one ::after. Ordinary .nav-tabs page links keep Bootstrap’s static border.

Selected tab

Choose a tab. The underline moves with inset, not position-area.

The underline follows the selected tab.

This is the profile pane.

This is the contact pane.

HTML
<script type="module" src="./node_modules/bootstrap-html/src/behaviors/tabs.js"></script>

<pe-tabs>
  <ul class="nav nav-tabs" role="tablist">
    <li class="nav-item" role="presentation">
      <button class="nav-link active" id="composed-home-tab" type="button" role="tab" aria-controls="composed-home-pane" aria-selected="true">Home</button>
    </li>
    <li class="nav-item" role="presentation">
      <button class="nav-link" id="composed-profile-tab" type="button" role="tab" aria-controls="composed-profile-pane" aria-selected="false" tabindex="-1">Profile</button>
    </li>
    <li class="nav-item" role="presentation">
      <button class="nav-link" id="composed-contact-tab" type="button" role="tab" aria-controls="composed-contact-pane" aria-selected="false" tabindex="-1">Contact</button>
    </li>
  </ul>
  <div class="tab-content">
    <div class="tab-pane active show" id="composed-home-pane" role="tabpanel" aria-labelledby="composed-home-tab" tabindex="0">
      <p class="mt-3 mb-0">The underline follows the selected tab.</p>
    </div>
    <div class="tab-pane" id="composed-profile-pane" role="tabpanel" aria-labelledby="composed-profile-tab" tabindex="0">
      <p class="mt-3 mb-0">This is the profile pane.</p>
    </div>
    <div class="tab-pane" id="composed-contact-pane" role="tabpanel" aria-labelledby="composed-contact-tab" tabindex="0">
      <p class="mt-3 mb-0">This is the contact pane.</p>
    </div>
  </div>
</pe-tabs>

Progress ring

<progress class="progress-ring"> is still a progress element. A conic gradient is the fill. Set --bs-progress-value to the same number as value when max is 100. Do not also add .progress; that class is the linear bar.

Determinate rings

25, 70, and 100. .text-info recolors the fill. 100 uses the success token.

25% 25%
70% 70%
100% 100%
HTML
<div class="d-flex flex-wrap gap-4 align-items-center">
  <div class="progress-ring-wrapper">
    <progress class="progress-ring" value="25" max="100" style="--bs-progress-value: 25" aria-label="Upload 25 percent">25%</progress>
    <span class="progress-ring-content">25%</span>
  </div>
  <div class="progress-ring-wrapper">
    <progress class="progress-ring text-info" value="70" max="100" style="--bs-progress-value: 70" aria-label="Sync 70 percent">70%</progress>
    <span class="progress-ring-content">70%</span>
  </div>
  <div class="progress-ring-wrapper">
    <progress class="progress-ring" value="100" max="100" style="--bs-progress-value: 100" aria-label="Complete">100%</progress>
    <span class="progress-ring-content">100%</span>
  </div>
</div>

Scroll progress and shrinking header

Opt-in classes on a nested .scroll-demo scroller, not the site header. The bar is decorative and aria-hidden="true". There is no scroll-listener fallback.

Nested scroller

Scroll inside the panel. The header shortens over the first 8 rem.

Brand

Scroll this panel. The header shrinks and the bar tracks how far you have read. These classes stay on a nested scroller so they do not move the docs chrome.

Anchor positioning, popovers, and native widgets on this site are unchanged. The animation is decorative: without scroll-driven animations the header stays tall and the bar is omitted.

Keep going. The range is the first 8 rem of this scroller, then the header stays compact.

Copy below the fold is here so you can finish the shrink and still have distance left. The bar should not hit 100% the moment the header goes compact.

Recipes on this page stay independent. Nested details, tabs, and toasts do not share this timeline. Nothing here measures scroll position.

Prefer a nested scroller like this when you try the pattern in an app. Putting the shrink class on the site header would pin and shrink the docs chrome.

The bar is one sticky strip at the top of the panel. It only mirrors how far you have scrolled. The heading in the header is the visible landmark.

Firefox does not run scroll-driven animations yet. There the header stays tall and the bar is not shown. That is the documented fallback, not a second script.

If you reduce motion, both animations stay off. The panel still scrolls; the header just does not change height.

A little more height so the shrink can finish before you run out of scroll. Keep moving until the bar is most of the way across.

The progress bar is the full scroll of this panel. The header only uses the first 8 rem. After that the compact height holds while you read the rest.

More filler so the last stretch is obvious. The named timeline still drives the bar after the header range has ended.

Weeknotes, invoices, and a long changelog would look like this in a real card: enough copy that you actually scroll, not a one-screen stub.

One more block for the last part of the travel. If the bar is near the end, the header should already be compact and stay there.

Almost there. Scroll a bit further so the thumb reaches the bottom of this panel.

End of the demo column. Scroll back up to restore the tall header and empty the bar.

HTML
<div class="scroll-demo">
  <div class="scroll-progress" aria-hidden="true"></div>
  <header class="header-shrink">Brand</header>
  <div class="p-3">
    <p>Scroll this panel. The header shrinks and the bar tracks how far you have read. These classes stay on a nested scroller so they do not move the docs chrome.</p>
    <p>Anchor positioning, popovers, and native widgets on this site are unchanged. The animation is decorative: without scroll-driven animations the header stays tall and the bar is omitted.</p>
    <p>Keep going. The range is the first 8 rem of this scroller, then the header stays compact.</p>
    <p>Copy below the fold is here so you can finish the shrink and still have distance left. The bar should not hit 100% the moment the header goes compact.</p>
    <p>Recipes on this page stay independent. Nested details, tabs, and toasts do not share this timeline. Nothing here measures scroll position.</p>
    <p>Prefer a nested scroller like this when you try the pattern in an app. Putting the shrink class on the site header would pin and shrink the docs chrome.</p>
    <p>The bar is one sticky strip at the top of the panel. It only mirrors how far you have scrolled. The heading in the header is the visible landmark.</p>
    <p>Firefox does not run scroll-driven animations yet. There the header stays tall and the bar is not shown. That is the documented fallback, not a second script.</p>
    <p>If you reduce motion, both animations stay off. The panel still scrolls; the header just does not change height.</p>
    <p>A little more height so the shrink can finish before you run out of scroll. Keep moving until the bar is most of the way across.</p>
    <p>The progress bar is the full scroll of this panel. The header only uses the first 8 rem. After that the compact height holds while you read the rest.</p>
    <p>More filler so the last stretch is obvious. The named timeline still drives the bar after the header range has ended.</p>
    <p>Weeknotes, invoices, and a long changelog would look like this in a real card: enough copy that you actually scroll, not a one-screen stub.</p>
    <p>One more block for the last part of the travel. If the bar is near the end, the header should already be compact and stay there.</p>
    <p>Almost there. Scroll a bit further so the thumb reaches the bottom of this panel.</p>
    <p>End of the demo column. Scroll back up to restore the tall header and empty the bar.</p>
  </div>
</div>

Checked count

The contract lives on Output: <output data-controls> and src/behaviors/count.js. This page only shows recipes that use it.

Multi-select dropdown

Checkboxes in a popover="auto" menu. They stay there so a closed popover is not in the tab order. The same <output data-controls> sits beside the toggle as a positioned badge. The button name stays “Tags”. :has(:checked) hides the badge when nothing is selected. This is not <select multiple>.

Counted checks

Open the menu and toggle tags. The pill tracks how many are checked, including when the menu is closed.

HTML
<script type="module" src="./node_modules/bootstrap-html/src/behaviors/count.js"></script>

<div class="dropdown dropdown-multiselect">
  <button class="btn btn-primary dropdown-toggle" type="button" command="toggle-popover" commandfor="composed-tags" aria-describedby="composed-tags-count">
    Tags
  </button>
  <output id="composed-tags-count" class="position-absolute top-0 start-100 translate-middle badge rounded-pill text-bg-danger dropdown-multiselect-count" data-controls="composed-tags" for="composed-tag-html composed-tag-css composed-tag-js">1<span class="visually-hidden"> selected</span></output>
  <ul class="dropdown-menu" popover="auto" id="composed-tags">
    <li>
      <label class="dropdown-item" for="composed-tag-html">
        <input class="form-check-input" type="checkbox" value="html" id="composed-tag-html">
        HTML
      </label>
    </li>
    <li>
      <label class="dropdown-item" for="composed-tag-css">
        <input class="form-check-input" type="checkbox" value="css" id="composed-tag-css" checked>
        CSS
      </label>
    </li>
    <li>
      <label class="dropdown-item" for="composed-tag-js">
        <input class="form-check-input" type="checkbox" value="js" id="composed-tag-js">
        JavaScript
      </label>
    </li>
  </ul>
</div>

Filter drawer

The same <output data-controls> sits beside the toggle. The checkboxes live in an offcanvas, so count is not a dropdown feature.

Offcanvas filters

Open the drawer and toggle filters. The badge updates while the drawer is open or closed.

0 selected
Filters
HTML
<script type="module" src="./node_modules/bootstrap-html/src/behaviors/count.js"></script>

<div class="d-flex align-items-center gap-2">
  <button type="button" class="btn btn-primary" command="show-modal" commandfor="composed-filter" aria-describedby="composed-filter-count">
    Filters
  </button>
  <output id="composed-filter-count" class="badge rounded-pill text-bg-secondary" data-controls="composed-filter-list" for="composed-filter-sale composed-filter-new composed-filter-stock">0<span class="visually-hidden"> selected</span></output>
</div>

<dialog class="offcanvas offcanvas-end" id="composed-filter" closedby="any" aria-labelledby="composed-filter-label">
  <div class="offcanvas-header">
    <h5 class="offcanvas-title" id="composed-filter-label">
      Filters
    </h5>
    <button type="button" class="btn-close" command="close" commandfor="composed-filter" aria-label="Close"></button>
  </div>
  <div class="offcanvas-body">
    <div id="composed-filter-list">
      <div class="form-check">
        <input class="form-check-input" type="checkbox" value="sale" id="composed-filter-sale">
        <label class="form-check-label" for="composed-filter-sale">On sale</label>
      </div>
      <div class="form-check">
        <input class="form-check-input" type="checkbox" value="new" id="composed-filter-new">
        <label class="form-check-label" for="composed-filter-new">New</label>
      </div>
      <div class="form-check">
        <input class="form-check-input" type="checkbox" value="stock" id="composed-filter-stock">
        <label class="form-check-label" for="composed-filter-stock">In stock</label>
      </div>
    </div>
  </div>
</dialog>

A <dialog class="modal"> with an <img>. command="show-modal" opens it. There is no script.

Photo dialog

Open the dialog. Escape or the close button dismisses it.

HTML
<button type="button" class="btn btn-primary" command="show-modal" commandfor="composed-lightbox">
  View photo
</button>

<dialog class="modal" closedby="any" id="composed-lightbox" aria-labelledby="composed-lightbox-label">
  <div class="modal-dialog modal-lg">
    <div class="modal-content">
      <div class="modal-header">
        <h1 class="modal-title fs-5" id="composed-lightbox-label">Lake and mountains</h1>
        <button type="button" class="btn-close" command="close" commandfor="composed-lightbox" aria-label="Close"></button>
      </div>
      <div class="modal-body p-0">
        <img class="img-fluid w-100" width="800" height="450" alt="Lake and mountains" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 800 450'%3E%3Crect fill='%236c757d' width='800' height='450'/%3E%3Ctext fill='white' font-family='system-ui,sans-serif' font-size='28' x='50%25' y='50%25' text-anchor='middle' dy='.3em'%3ELake and mountains%3C/text%3E%3C/svg%3E">
      </div>
    </div>
  </div>
</dialog>

Confirm then toast

<form method="dialog"> sets returnValue. A small page script shows the toast only after Delete — the same pattern as the meter playground. Cancel does nothing. This is not a new behavior.

Delete file

Confirm to show the toast. Cancel closes the dialog only.

Bootstrap HTML
File deleted.
HTML
<script type="module" src="./node_modules/bootstrap-html/src/behaviors/toast.js"></script>

<button type="button" class="btn btn-danger" command="show-modal" commandfor="composed-delete">
  Delete file
</button>

<dialog class="modal" closedby="any" id="composed-delete" aria-labelledby="composed-delete-label">
  <form method="dialog">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-header">
          <h1 class="modal-title fs-5" id="composed-delete-label">Delete file?</h1>
          <button type="submit" class="btn-close" value="cancel" aria-label="Close"></button>
        </div>
        <div class="modal-body">
          This cannot be undone. Cancel leaves the file. Delete shows a toast.
        </div>
        <div class="modal-footer">
          <button type="submit" class="btn btn-secondary" value="cancel">Cancel</button>
          <button type="submit" class="btn btn-danger" value="delete">Delete</button>
        </div>
      </div>
    </div>
  </form>
</dialog>

<div class="toast-container position-fixed bottom-0 end-0 p-3">
  <pe-toast class="toast" id="composed-deleted" popover="manual" data-delay="4000" role="status" aria-live="polite" aria-atomic="true">
    <div class="toast-header">
      <strong class="me-auto">Bootstrap HTML</strong>
      <button type="button" class="btn-close" command="hide-popover" commandfor="composed-deleted" aria-label="Close"></button>
    </div>
    <div class="toast-body">File deleted.</div>
  </pe-toast>
</div>

Command palette

A <dialog>, a <search> landmark, and a static list of links. <datalist> is the typeahead. Do not filter the list with script.

Jump to

Open the dialog. Type to see suggestions. The links stay visible.

HTML
<button type="button" class="btn btn-primary" command="show-modal" commandfor="composed-palette">
  Jump to
</button>

<dialog class="modal" closedby="any" id="composed-palette" aria-labelledby="composed-palette-label">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h1 class="modal-title fs-5" id="composed-palette-label">Jump to</h1>
        <button type="button" class="btn-close" command="close" commandfor="composed-palette" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        <search class="mb-3">
          <label class="form-label" for="composed-palette-q">Search</label>
          <input class="form-control" type="search" id="composed-palette-q" name="q" list="composed-palette-list" placeholder="Accordion, modal, …">
        </search>
        <datalist id="composed-palette-list">
          <option value="Accordion"></option>
          <option value="Buttons"></option>
          <option value="Modal"></option>
          <option value="Output"></option>
        </datalist>
        <ul class="list-unstyled mb-0">
          <li><a href="/bootstrap-html/components/accordion">Accordion</a></li>
          <li><a href="/bootstrap-html/components/buttons">Buttons</a></li>
          <li><a href="/bootstrap-html/components/modal">Modal</a></li>
          <li><a href="/bootstrap-html/forms/output">Output</a></li>
        </ul>
      </div>
    </div>
  </div>
</dialog>

Price toggle

Radio .btn-check from Buttons. :has(:checked) switches the visible price. No script.

Monthly or yearly

Choose yearly. The price switches in CSS. Without :has() the monthly price stays.

Billing

$9 / month $90 / year

HTML
<div class="price-toggle">
  <fieldset class="mb-3">
    <legend class="visually-hidden">Billing</legend>
    <input type="radio" class="btn-check" name="composed-plan" id="composed-plan-monthly" value="monthly" autocomplete="off" checked>
    <label class="btn btn-outline-primary" for="composed-plan-monthly">Monthly</label>
    <input type="radio" class="btn-check" name="composed-plan" id="composed-plan-yearly" value="yearly" autocomplete="off">
    <label class="btn btn-outline-primary" for="composed-plan-yearly">Yearly</label>
  </fieldset>
  <p class="fs-3 mb-0">
    <span class="price-monthly">$9<span class="fs-6 text-body-secondary"> / month</span></span>
    <span class="price-yearly">$90<span class="fs-6 text-body-secondary"> / year</span></span>
  </p>
</div>

Interest preview

Experimental. interestfor plus popover="hint" with more than a one-line tooltip. The polyfill covers hover and focus, not popover="hint". aria-description stays the no-script fallback. Do not announce the preview from a live region.

Link card

Hover or focus Progress. The hint is a short card. The description remains if hint is missing.

Hover or focus Progress for a card preview. The description stays if hint is missing.

Progress

Native progress and meter. Omit value for the indeterminate stripe.

Open page
HTML
<p class="mb-0">
  Hover or focus
  <a href="/bootstrap-html/components/progress" class="link-offset-2" interestfor="composed-preview" aria-description="Progress — native progress and meter.">Progress</a>
  for a card preview. The description stays if hint is missing.
</p>
<div class="tooltip bs-tooltip-top" popover="hint" id="composed-preview">
  <div class="tooltip-arrow"></div>
  <div class="tooltip-inner text-start">
    <strong>Progress</strong>
    <p class="small mb-1">Native progress and meter. Omit value for the indeterminate stripe.</p>
    <a class="link-light" href="/bootstrap-html/components/progress">Open page</a>
  </div>
</div>

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.

Browser support

Three columns, not one baseline. The same row is on the overview.
No script Polyfill No fallback
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. The same command polyfill as dropdown and offcanvas. 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.