Blocks are opt-in source recipes copied into the application. They compose native HTML and primitives without registering custom elements. Most are CSS-only; a block may include a small Mado helper when a repeated lifecycle cannot be expressed declaratively without duplicating application glue.
Most blocks depend only on theme. notification-region composes alert;
command-palette composes the native dialog and editable combobox
contracts; navigation-popover composes popover and navigation-list.
The examples below also use optional primitive styles such as
.mado-ui-button and .mado-ui-link. Install those items separately when
their classes are wanted:
mado-ui add button form-controlsUnless an item appears in a block's dependency graph, native or application-owned control and action styles work equally well.
.mado-ui-field arranges one control with its explicit native label,
supporting description and current validation message. The field block owns
structure and message presentation; it does not create a control, validate
input, move focus or announce updates.
The canonical root is a neutral container with a separate <label for>.
That association continues to work when description and error nodes sit
beside the label instead of inside its pointer target:
<div class="mado-ui-field" data-invalid>
<label class="mado-ui-field-label" for="project-name">
Project name (required)
</label>
<input
id="project-name"
class="mado-ui-control"
name="project"
required
aria-invalid="true"
aria-describedby="project-description project-error"
/>
<p id="project-description" class="mado-ui-field-description">
Use 3–24 lowercase letters, digits and hyphens.
</p>
<p id="project-error" class="mado-ui-field-error">
Error: spaces are not allowed.
</p>
</div>The example composes the optional form-controls primitive. Install both
items to reproduce it:
mado-ui add field form-controlsThe attributes have deliberately separate jobs:
required,disabledandreadonlyremain native control attributes;aria-invalid="true"exposes the control's current application-owned invalid state;aria-describedbylists every currently relevant description and error ID in DOM order;data-invalidchanges only field presentation and never replacesaria-invalid.
Do not set invalid state merely because an untouched required control matches
:invalid. Choose a lifecycle such as submit, blur or attempted step
navigation. When the error becomes visible, update the text,
aria-describedby, aria-invalid and data-invalid together. Remove that
state when the value is accepted.
Validation announcements also belong to the application. If submission
moves focus to the first invalid native control after its error is connected,
the label and description provide the focus context without a competing live
region. If focus deliberately remains elsewhere, a persistent
role="status" can announce a non-urgent update. Reserve role="alert" for
new urgent information; do not put it on every field error or combine a role
with redundant aria-live.
Related choices use a native fieldset and legend:
<fieldset
class="mado-ui-fieldset"
aria-describedby="notification-description"
>
<legend class="mado-ui-field-legend">Notifications</legend>
<p
id="notification-description"
class="mado-ui-field-description"
>
Choose how release updates reach you.
</p>
<label class="mado-ui-check">
<input
class="mado-ui-checkbox"
name="notification"
type="radio"
value="email"
/>
Email
</label>
</fieldset>The fieldset and legend come from field; the optional native choice styles
come from form-controls. Do not replace this group with a generic field and
ARIA role.
Public properties:
| Purpose | Property |
|---|---|
| Layout | --mado-ui-field-gap, --mado-ui-fieldset-gap |
| Label | --mado-ui-field-label-color, --mado-ui-field-invalid-label-color |
| Description | --mado-ui-field-description-color |
| Error | --mado-ui-field-error-color, --mado-ui-field-error-accent |
.mado-ui-form-section divides a native form into a thematic section with
supporting copy, fields and actions. It provides a transparent wrapping
layout; it does not create a form, submit data, validate controls or establish
a new form owner.
The canonical root is a native section with a heading whose level follows
the surrounding document:
<form action="/settings/workspace" method="post">
<section class="mado-ui-form-section">
<div class="mado-ui-form-section-heading">
<h2 class="mado-ui-form-section-title">Workspace profile</h2>
<p class="mado-ui-form-section-description">
Choose the public name used across projects.
</p>
</div>
<div class="mado-ui-form-section-content">
<div class="mado-ui-field">
<label class="mado-ui-field-label" for="workspace-name">
Workspace name
</label>
<input
id="workspace-name"
class="mado-ui-control"
name="workspace-name"
required
/>
</div>
<div class="mado-ui-form-section-actions">
<a class="mado-ui-link" href="/settings">Cancel</a>
<button class="mado-ui-button" type="submit">
Save changes
</button>
</div>
</div>
</section>
</form>The HTML Standard defines section as a thematic grouping and recommends
identifying it with a heading. Use a div instead when the wrapper is only
visual and does not represent a distinct topic. Do not add role="region" to
every form section: create a named region only when it is important enough to
appear among the page's landmarks. See the
native section contract.
One native form may contain several form sections. Never nest forms to give
each visual section independent actions. Submit and reset buttons belong to
their actual form owner and need explicit type values; a cancel destination
remains a native anchor when it navigates elsewhere. Server requests, pending
state, validation, focus after failure and success announcements all remain
application behavior.
Use a native fieldset and legend instead of this generic heading pattern
when a set of related controls needs a programmatic group label. The
WAI form-group guidance
describes that distinction.
Heading content, form content and actions remain in source order. The heading
and content columns wrap according to their available inline size, so narrow
containers and high zoom do not require a component breakpoint. The visual
border does not represent a thematic break and should not be replaced with an
hr.
The example composes the optional field, form-controls and button items.
Install them separately when their classes are wanted:
mado-ui add button field form-controls form-sectionPublic properties:
| Purpose | Property |
|---|---|
| Layout | --mado-ui-form-section-align, --mado-ui-form-section-gap, --mado-ui-form-section-padding-block |
| Frame | --mado-ui-form-section-border, --mado-ui-form-section-color |
| Heading | --mado-ui-form-section-heading-basis, --mado-ui-form-section-heading-gap, --mado-ui-form-section-title-size |
| Description | --mado-ui-form-section-description-width, --mado-ui-form-section-description-color |
| Content | --mado-ui-form-section-content-basis, --mado-ui-form-section-content-gap |
| Actions | --mado-ui-form-section-actions-gap, --mado-ui-form-section-actions-justify |
.mado-ui-settings-row arranges one explicitly labelled native control beside
its supporting description. The row is a layout container, not a control:
click, keyboard, validation and form behavior continue to belong to the
native input, select or textarea.
<div class="mado-ui-settings-row">
<div class="mado-ui-settings-row-content">
<label
class="mado-ui-settings-row-label"
for="release-summary"
>
Weekly release summary
</label>
<p
id="release-summary-description"
class="mado-ui-settings-row-description"
>
Receive one email after the final deployment each week.
</p>
</div>
<div class="mado-ui-settings-row-control">
<input
id="release-summary"
class="mado-ui-checkbox"
name="release-summary"
type="checkbox"
aria-describedby="release-summary-description"
/>
</div>
</div>The for value must match the unique control ID. This native association
provides the accessible name and lets activation of the visible label reach
the control without a row-level click handler. aria-describedby connects
the optional supporting copy; omit both the attribute and description when
that copy is absent. See the
HTML label element
and the
WAI form-label guidance.
Keep exactly one setting control in the control container. Related checkboxes
or radio buttons require a native fieldset and legend; a larger group of
fields belongs in a form section. An action that is not a setting control
remains a native button or link in a more appropriate action layout.
Put required, disabled, readonly and current validation attributes on
the native control. Do not mirror them onto the row, lower the opacity of the
whole row or use aria-disabled to imitate native disabled behavior. Never
make the entire row a clickable div, and do not replace the checkbox with a
visual switch unless its implementation has complete native-equivalent
semantics and keyboard behavior.
Content precedes the control in DOM order. The wrapping flex layout keeps that
order when the row becomes narrow, while the control remains in the form's
normal Tab sequence. The optional form-controls item styles the checkbox in
the example but is not a registry dependency:
mado-ui add form-controls settings-rowPublic properties:
| Purpose | Property |
|---|---|
| Layout | --mado-ui-settings-row-align, --mado-ui-settings-row-justify, --mado-ui-settings-row-gap, --mado-ui-settings-row-padding-block |
| Frame | --mado-ui-settings-row-border, --mado-ui-settings-row-color |
| Content | --mado-ui-settings-row-content-basis, --mado-ui-settings-row-content-gap |
| Label | --mado-ui-settings-row-label-color, --mado-ui-settings-row-label-size |
| Description | --mado-ui-settings-row-description-color |
.mado-ui-filter-bar keeps a filtering workflow and related collection
actions on one responsive line when space permits. Its native structure
separates the search landmark, GET form and page-level actions; the recipe
only supplies a transparent wrapping layout.
<div class="mado-ui-filter-bar">
<search
class="mado-ui-filter-bar-search"
aria-label="Filter projects"
>
<form
class="mado-ui-filter-bar-form"
action="/projects"
method="get"
>
<div class="mado-ui-filter-bar-fields">
<div class="mado-ui-field">
<label
class="mado-ui-field-label"
for="project-query"
>
Search
</label>
<input
id="project-query"
class="mado-ui-control"
name="query"
type="search"
/>
</div>
<div class="mado-ui-field">
<label
class="mado-ui-field-label"
for="project-status"
>
Status
</label>
<select
id="project-status"
class="mado-ui-control"
name="status"
>
<option value="">All statuses</option>
<option value="active">Active</option>
<option value="paused">Paused</option>
</select>
</div>
</div>
<div class="mado-ui-filter-bar-submit">
<button class="mado-ui-button" type="submit">
Apply filters
</button>
<a class="mado-ui-link" href="/projects">
Clear filters
</a>
</div>
</form>
</search>
<div
class="mado-ui-filter-bar-actions"
role="group"
aria-label="Project actions"
>
<a class="mado-ui-link" href="/projects/new">
Create project
</a>
<button class="mado-ui-button" type="button">
Export results
</button>
</div>
</div>The native
search element
represents controls or content used for searching or filtering. Give
multiple search landmarks on one page distinct accessible names. The nested
form does not need its own landmark name: adding one would expose a redundant
form landmark inside the named search landmark.
The default progressive-enhancement contract is a real method="get" form.
Every successful control has a stable name, the text query uses
type="search", and visible
label elements use for
values matching unique control IDs. Enter in a text field and the explicit
submit button therefore retain native submission behavior, while the result
URL remains bookmarkable and shareable.
The browser's clear affordance inside an input[type="search"] only edits
that control; it does not submit the form. Likewise, type="reset" restores
initial control values but neither removes current query parameters nor
applies the new values. A link to the collection's base URL is the honest
progressive action for “Clear filters”. A client-side application may instead
implement clear-and-submit behavior, but it owns synchronization between
controls, URL state, results and history.
Collection actions stay outside the filter form. Navigation remains a native
link, while an in-place command uses button type="button" so it cannot
submit filters accidentally. The optional role="group" is useful only when
the collection actions benefit from a group name; omit both the role and name
otherwise.
Neither the root nor its action group is an ARIA toolbar. All links, buttons
and controls remain in ordinary Tab order, and native inputs and selects keep
their own arrow-key behavior. Use the separate toolbar block only when the
content is actually a general action layout; implementing role="toolbar"
also requires application-owned roving focus and arrow-key navigation.
Applications may apply filters immediately from input and change events,
as the Mado site does with URL query signals. That behavior is not part of
this CSS item. Avoid unexpected changes of context, keep focus stable, and
make any dynamic result count or loading announcement part of the results'
lifecycle. See
WCAG guidance for changes on input.
The DOM order is fields, submit/clear actions, then collection actions. Every
group wraps without order, reverse directions or a horizontal overflow
shell, preserving that reading and focus order at narrow widths and enlarged
text. This supports the
WCAG reflow objective.
The example composes the optional field, form-controls and button
items. The filter bar itself depends only on theme:
mado-ui add button field filter-bar form-controlsPublic properties:
| Purpose | Property |
|---|---|
| Root | --mado-ui-filter-bar-align, --mado-ui-filter-bar-color, --mado-ui-filter-bar-gap |
| Form | --mado-ui-filter-bar-form-gap |
| Fields | --mado-ui-filter-bar-fields-gap, --mado-ui-filter-bar-field-basis |
| Submit actions | --mado-ui-filter-bar-submit-gap |
| Collection actions | --mado-ui-filter-bar-actions-gap |
.mado-ui-command-palette presents a searchable command accelerator inside a
native modal dialog. The block adds only layout and appearance: native
dialog owns modality, focus containment, Escape and return values, while the
editable combobox primitive owns suggestion filtering and selection.
Commands, shortcuts, navigation, local actions and announcements remain
application behavior.
Install the block and its complete dependency graph with one command:
mado-ui add command-paletteThat copies theme, form-controls, combobox, dialog and
command-palette in dependency order. Import the copied styles and initialize
the combobox helper from application code; there is deliberately no separate
command-palette helper.
The trigger may use the native declarative command contract. Keep a
feature-detected showModal() fallback while browser support is uneven:
<button
type="button"
commandfor="project-command-palette"
command="show-modal"
aria-keyshortcuts="Control+K Meta+K"
>
Open command palette
<kbd class="mado-ui-command-palette-shortcut">Ctrl/⌘K</kbd>
</button>
<dialog
id="project-command-palette"
class="mado-ui-command-palette"
aria-labelledby="project-command-palette-title"
aria-describedby="project-command-palette-description"
>
<header class="mado-ui-command-palette-header">
<div>
<h2
id="project-command-palette-title"
class="mado-ui-command-palette-title"
>
Run a command
</h2>
<p id="project-command-palette-description">
Search the commands available in this project.
</p>
</div>
<form method="dialog">
<button
class="mado-ui-command-palette-close"
type="submit"
value="close"
aria-label="Close command palette"
>
<span aria-hidden="true">×</span>
</button>
</form>
</header>
<form
class="mado-ui-command-palette-content"
action="/commands"
method="get"
>
<div class="mado-ui-command-palette-search">
<label
class="mado-ui-command-palette-label"
for="project-command-query"
>
Command
</label>
<div class="mado-ui-combobox">
<input
id="project-command-query"
class="mado-ui-control mado-ui-combobox-input"
name="command"
type="search"
list="project-command-options"
aria-describedby="project-command-hint"
autocomplete="off"
autofocus
/>
<button
class="mado-ui-combobox-toggle"
type="button"
tabindex="-1"
popovertarget="project-command-listbox"
aria-label="Show available commands"
hidden
>
<span aria-hidden="true">⌄</span>
</button>
<datalist id="project-command-options">
<option
value="Create project"
label="Create a local project"
data-command-id="create-project"
></option>
<option
value="View tabs"
label="Open the tabs page"
data-command-id="view-tabs"
></option>
<option
value="Delete project"
label="Unavailable"
data-command-id="delete-project"
disabled
></option>
</datalist>
<ul
id="project-command-listbox"
class="mado-ui-combobox-listbox"
popover="auto"
aria-label="Available commands"
hidden
></ul>
</div>
<span
id="project-command-hint"
class="mado-ui-command-palette-hint"
>
Type an exact command or use the arrow keys to choose one.
</span>
</div>
<p
class="mado-ui-command-palette-status"
role="status"
aria-atomic="true"
>
2 commands available.
</p>
<footer class="mado-ui-command-palette-footer">
<p class="mado-ui-command-palette-hint">
Escape closes suggestions first, then the dialog.
</p>
<button
class="mado-ui-command-palette-submit"
type="submit"
>
Run command
</button>
</footer>
</form>
</dialog>The close form and search form are siblings; never nest them. method="dialog"
lets the native close button set the dialog return value without submitting a
request. The search form may keep a real GET action as a useful unenhanced
fallback. Client-side submission should run a command only when the normalized
input value exactly matches one enabled command. Suggestions are not an
authorization boundary: re-check the command ID and disabled state immediately
before execution.
Keep one application-owned command collection as the source for datalist
options, disabled state and execution. Store a stable command ID in
data-command-id; do not infer behavior from translated labels or listbox
text. Pass onSelect and onResultsChange to madoUiCombobox() to execute the
original option and update the in-dialog status. Close the dialog before
running a local action or Mado navigation so native focus restoration and
announcement order stay predictable.
The global Ctrl/⌘K listener is also application-owned. Ignore already handled,
repeated, composing and modified keystrokes, and never steal the shortcut from
an input, textarea, select or editable region. Call preventDefault() only
when a closed dialog can actually open, and remove the document listener when
its owning Mado view disconnects. Advertise aria-keyshortcuts only when that
listener is active.
Open with showModal(), not by setting open; do not add redundant
role="dialog", aria-modal or tabindex to the native dialog. autofocus
places initial focus on the query. While the listbox is open, the combobox
consumes the first Escape and keeps focus in its input; a subsequent Escape
closes the modal. Native Tab containment and focus restoration must not be
reimplemented. Clear the form, active suggestion and result message before
each opening and after close so stale commands cannot run.
The palette must remain an accelerator, not the only way to reach critical actions. Preserve ordinary links, buttons and forms elsewhere in the application. The dialog contract and declarative command contract define the native behavior composed here.
Public properties:
| Purpose | Property |
|---|---|
| Dialog frame | --mado-ui-command-palette-inline-size, --mado-ui-command-palette-max-block-size, --mado-ui-command-palette-background, --mado-ui-command-palette-color, --mado-ui-command-palette-border, --mado-ui-command-palette-radius, --mado-ui-command-palette-shadow, --mado-ui-command-palette-backdrop |
| Header | --mado-ui-command-palette-header-gap, --mado-ui-command-palette-header-padding, --mado-ui-command-palette-title-size |
| Content and search | --mado-ui-command-palette-content-gap, --mado-ui-command-palette-content-padding, --mado-ui-command-palette-search-gap, --mado-ui-command-palette-label-color |
| Results | --mado-ui-command-palette-results-inline-size, --mado-ui-command-palette-results-max-block-size, --mado-ui-command-palette-status-color |
| Footer | --mado-ui-command-palette-footer-gap, --mado-ui-command-palette-footer-padding-block-start, --mado-ui-command-palette-hint-color |
| Controls | --mado-ui-command-palette-control-height, --mado-ui-command-palette-control-padding, --mado-ui-command-palette-control-border, --mado-ui-command-palette-control-radius, --mado-ui-command-palette-focus, --mado-ui-command-palette-focus-ring, --mado-ui-command-palette-disabled-opacity |
| Close action | --mado-ui-command-palette-close-background, --mado-ui-command-palette-close-color, --mado-ui-command-palette-close-hover-background, --mado-ui-command-palette-close-hover-color |
| Submit action | --mado-ui-command-palette-submit-background, --mado-ui-command-palette-submit-color, --mado-ui-command-palette-submit-hover-background, --mado-ui-command-palette-submit-hover-color |
| Shortcut | --mado-ui-command-palette-shortcut-background, --mado-ui-command-palette-shortcut-border, --mado-ui-command-palette-shortcut-color, --mado-ui-command-palette-shortcut-shadow |
| Compact layout | --mado-ui-command-palette-compact-padding |
.mado-ui-breadcrumbs presents the path to the current page as a labelled
native navigation landmark and ordered list. The application owns every URL,
the localized landmark name and the current-page state:
<nav class="mado-ui-breadcrumbs" aria-label="Breadcrumb">
<ol class="mado-ui-breadcrumbs-list">
<li class="mado-ui-breadcrumbs-item">
<a class="mado-ui-breadcrumbs-link" href="/">Home</a>
</li>
<li class="mado-ui-breadcrumbs-item">
<a class="mado-ui-breadcrumbs-link" href="/projects">Projects</a>
</li>
<li class="mado-ui-breadcrumbs-item">
<span aria-current="page">Mado site</span>
</li>
</ol>
</nav>Use either a localized aria-label or aria-labelledby; a page with several
navigation landmarks needs a unique name for each one. Keep the hierarchical
source order in an <ol>. Previous levels are ordinary anchors with real
href values. Exactly one item identifies the current page with
aria-current="page", normally the final item. A non-link current label keeps
it out of the Tab order; use an anchor instead only when navigating to the
same page is genuinely useful.
The separator is a presentation-only logical border on each item after the
first. Do not add / or > text to the DOM, and do not add menu,
menuitem, roving tabindex or arrow-key behavior. Breadcrumb links retain
native Tab and Enter behavior as described by the
WAI-ARIA breadcrumb pattern.
Long labels and the list wrap in source order instead of clipping or requiring
horizontal scrolling.
Public properties:
| Purpose | Property |
|---|---|
| Text | --mado-ui-breadcrumbs-color, --mado-ui-breadcrumbs-font-size |
| Spacing | --mado-ui-breadcrumbs-gap, --mado-ui-breadcrumbs-row-gap |
| Links | --mado-ui-breadcrumbs-link-color, --mado-ui-breadcrumbs-link-hover-color |
| Current page | --mado-ui-breadcrumbs-current-color |
| Separator | --mado-ui-breadcrumbs-separator-color |
.mado-ui-navigation-list styles a meaningful group of page or section links.
It remains a native navigation landmark containing a native list:
<nav
class="mado-ui-navigation-list"
aria-labelledby="project-navigation-title"
>
<h2
id="project-navigation-title"
class="mado-ui-navigation-list-label"
>
Project
</h2>
<ul class="mado-ui-navigation-list-items">
<li class="mado-ui-navigation-list-item">
<a
class="mado-ui-navigation-list-link"
href="/overview"
aria-current="page"
>
Overview
</a>
</li>
<li class="mado-ui-navigation-list-item">
<a class="mado-ui-navigation-list-link" href="/activity">
Activity
</a>
</li>
</ul>
</nav>Use <nav> only when the links form a significant navigation region. Its
visible label can be any heading level required by the surrounding document;
connect that heading with aria-labelledby. An aria-label is suitable when
no visible heading belongs in the design. Use <ul> when order is irrelevant
and <ol> when the sequence itself carries meaning.
Set aria-current="page" for the current page or
aria-current="location" for the current location within a page. Keep one
current item in each related link set. The state is visibly reinforced with
background, border and font weight rather than color alone.
The default layout is vertical. Set data-layout="horizontal" on the root for
a wrapping row:
<nav
class="mado-ui-navigation-list"
data-layout="horizontal"
aria-label="Documentation"
>
<ul class="mado-ui-navigation-list-items">
<!-- Native list items and anchors. -->
</ul>
</nav>Horizontal changes presentation only. Links stay in source and Tab order,
Enter follows the focused link, Space retains browser scrolling behavior and
arrow keys do not move focus. Do not replace this contract with menu,
menubar, menuitem, tree, aria-selected or roving tabindex; ordinary
site navigation uses the native structure in the
WAI navigation menu guidance.
Both layouts wrap long content without visually reordering it.
Public properties:
| Purpose | Property |
|---|---|
| Layout | --mado-ui-navigation-list-gap, --mado-ui-navigation-list-items-gap |
| Label | --mado-ui-navigation-list-label-color |
| Link size | --mado-ui-navigation-list-link-min-height, --mado-ui-navigation-list-link-gap, --mado-ui-navigation-list-link-padding, --mado-ui-navigation-list-link-radius |
| Link color | --mado-ui-navigation-list-link-color, --mado-ui-navigation-list-link-hover-background, --mado-ui-navigation-list-link-hover-color |
| Current location | --mado-ui-navigation-list-current-background, --mado-ui-navigation-list-current-border, --mado-ui-navigation-list-current-color |
| Focus | --mado-ui-navigation-list-focus-ring |
navigation-popover composes the native Popover API with one or more ordinary
navigation lists. It is intended for responsive application navigation that
opens from a compact trigger but may contain many or dynamically rendered
links. Its header and footer stay pinned while only the navigation body
scrolls, so account and session actions do not disappear at the end of a long
mobile menu.
Install it once; the dependency graph also installs popover and
navigation-list:
mado-ui add navigation-popoverCreate one helper instance for one trigger/panel relationship. closeAt is an
optional media query, normally the breakpoint where the compact trigger is
replaced by visible desktop navigation:
import { html } from "@madojs/mado";
import { madoUiNavigationPopover } from "./shared/ui/mado-ui-navigation-popover.js";
const navigation = madoUiNavigationPopover({
closeAt: "(min-width: 56rem)",
focusAfterCloseAt: () =>
document.querySelector<HTMLElement>("#desktop-navigation-current"),
});
html`
<button
id="application-navigation-trigger"
class="mado-ui-popover-invoker mado-ui-navigation-popover-trigger"
type="button"
popovertarget="application-navigation"
ref=${navigation.trigger}
>
Menu
</button>
<aside
id="application-navigation"
class="mado-ui-popover mado-ui-navigation-popover"
popover="auto"
aria-labelledby="application-navigation-title"
ref=${navigation.panel}
>
<header
class="mado-ui-popover-header mado-ui-navigation-popover-header"
>
<h2
id="application-navigation-title"
class="mado-ui-popover-title mado-ui-navigation-popover-title"
>
Navigation
</h2>
</header>
<div
class="mado-ui-popover-content mado-ui-navigation-popover-content"
>
<nav class="mado-ui-navigation-list" aria-label="Application">
<ul class="mado-ui-navigation-list-items">
<li class="mado-ui-navigation-list-item">
<a
class="mado-ui-navigation-list-link"
href="/journeys"
aria-current="page"
>
Journeys
</a>
</li>
<li class="mado-ui-navigation-list-item">
<a
class="mado-ui-navigation-list-link"
href="/publish"
>
Publish
</a>
</li>
</ul>
</nav>
</div>
<footer
class="mado-ui-popover-actions mado-ui-navigation-popover-footer"
>
<span>Signed in as Ada</span>
<button type="button">Sign out</button>
<button
class="mado-ui-popover-dismiss"
type="button"
popovertarget="application-navigation"
popovertargetaction="hide"
>
Close
</button>
</footer>
</aside>
`;Keep the trigger a native button type="button" and connect it to the unique
panel ID with popovertarget. The panel uses popover="auto" and needs its
own accessible name through aria-labelledby or aria-label. Each nested
nav also needs a useful name when the panel contains more than one
navigation landmark. The paired elements must share one document or shadow
tree; the panel ID and any referenced label IDs must resolve uniquely in that
tree. This block is not a menu or dialog: do not add
menuitem, roving tabindex, arrow-key navigation, modality or a focus trap.
Links retain their native Tab and Enter behavior.
The helper validates and reconciles the paired Mado refs without assigning
ARIA. One delegated listener on the panel closes it after an unmodified,
primary-button activation of any same-context a[href] in the panel. It
respects events already prevented before they reach the panel listener and
does not close for downloads, disabled links, new browsing contexts,
modifier-key activation or unrelated buttons and form controls. Because
activation is delegated, keyed each() updates and other application-owned
additions need no new refs, listeners or mutation observer.
Native auto-popover behavior owns opening, light dismissal, Escape and focus
return to the invoker. Link activation deliberately does not force focus back
to the trigger; the destination route owns its page-focus policy. Call the
returned idempotent close() only when another application lifecycle needs
it. For example, close after an asynchronous sign-out succeeds, but keep the
panel open when it fails so the error and retry action remain available.
When closeAt changes from non-matching to matching, the helper closes the
panel. If that desktop breakpoint also hides the focused compact invoker,
provide focusAfterCloseAt to resolve a visible native link or control after
the close. Omit the callback when the invoker remains visible. The helper
owns one matchMedia listener and removes it with its refs; it does not infer
application breakpoints or inspect rendered items.
While both refs are connected, an open panel also closes on popstate and
hashchange. This covers browser back/forward, query or hash changes and
routers that emit those platform events. The helper removes both global
listeners when either ref disconnects.
The root's maximum size accounts for the viewport and safe-area insets.
Header and footer remain fixed within that surface, while
.mado-ui-navigation-popover-content owns local block-axis scrolling and
overscroll containment. Navigation links span the available inline size and
align their labels to the logical start, including current-page state. Keep
account actions in the footer rather than appending them to the scrollable
link list. A nested popover or menu starts a new surface boundary, so it keeps
its own size and overflow instead of inheriting the navigation frame.
At 30rem and below, the composed popover uses its viewport-centred compact
placement instead of an anchor side that may be too short for a growing menu.
Its block limit uses the stable small viewport, preventing mobile browser
chrome from stretching or collapsing an already open navigation surface while
the page scrolls.
In browsers without native popover support, the dependency stylesheet hides the inert invoker and leaves the labelled panel in normal document flow. The native links and controls therefore remain available without enhancement.
Public properties:
| Purpose | Property |
|---|---|
| Surface size | --mado-ui-navigation-popover-inline-size, --mado-ui-navigation-popover-max-block-size |
| Surface spacing | --mado-ui-navigation-popover-gap, --mado-ui-navigation-popover-padding, --mado-ui-navigation-popover-offset |
| Anchor placement | --mado-ui-navigation-popover-position-area |
| Link row | --mado-ui-navigation-popover-link-min-height |
| Current rail | --mado-ui-navigation-popover-current-rail-width, --mado-ui-navigation-popover-current-rail |
| Footer | --mado-ui-navigation-popover-footer-padding-block-start, --mado-ui-navigation-popover-footer-border, --mado-ui-navigation-popover-footer-justify |
The composed popover and navigation-list properties remain available for
their own surface, typography, state and focus styling.
.mado-ui-description-list presents related names and values with the native
dl, dt and dd relationship intact. Each direct div wraps one complete
name-value group for layout and styling without changing that relationship:
<dl class="mado-ui-description-list">
<div class="mado-ui-description-list-group">
<dt class="mado-ui-description-list-term">Repository</dt>
<dd class="mado-ui-description-list-value">madojs/ui</dd>
</div>
<div class="mado-ui-description-list-group">
<dt class="mado-ui-description-list-term">License</dt>
<dd class="mado-ui-description-list-value">MIT</dd>
</div>
</dl>The HTML Standard defines a description list as an association list. A group
contains one or more dt names followed by one or more dd values, and
explicitly permits a div around the whole group. A dt is not necessarily a
dictionary term; use dfn inside it only when the content really is being
defined. See the
description-list model in the HTML Standard
and the
WAI description-list guidance.
The one-name, one-value pair is the common compact layout, but the recipe does not narrow native HTML to that case. Several names or values remain inside one group when they share one association:
<dl class="mado-ui-description-list">
<div class="mado-ui-description-list-group">
<dt class="mado-ui-description-list-term">Owner</dt>
<dt class="mado-ui-description-list-term">Maintainer</dt>
<dd class="mado-ui-description-list-value">Mado core team</dd>
<dd class="mado-ui-description-list-value">UI working group</dd>
</div>
</dl>Multiple dd elements represent separate values or descriptions in the same
group. Put several paragraphs that form one value inside a single dd
instead. Complex groups stack every term and value in source order so their
presentation does not imply false one-to-one pairs.
By default a simple pair shares a row when its container has enough space and
wraps term before value when it does not. Set data-layout="stacked" on the
dl to keep every simple pair vertical at all widths:
<dl class="mado-ui-description-list" data-layout="stacked">
<div class="mado-ui-description-list-group">
<dt class="mado-ui-description-list-term">Support window</dt>
<dd class="mado-ui-description-list-value">
The current minor Mado release.
</dd>
</div>
</dl>The attribute changes presentation only. Do not add list, listitem,
term or definition roles, focus the list, or add arrow-key behavior;
native elements already expose the relationship. A heading outside the list
can introduce the surrounding section when one is needed. Dynamic value
formatting and announcement policy remain application behavior.
Use this block for one-dimensional metadata, glossary entries, questions and
answers, and other name-value associations. Use a native table when readers
must compare values across rows and columns. Use metric-card when a compact
surface and prominent numerical value are part of the design, and use
settings-row for an editable control. Row dividers are presentational
borders, not thematic hr elements.
Values may contain ordinary flow content such as paragraphs, links, lists,
time and data. Long content wraps instead of creating an overflow shell,
and visual order always follows source order.
Public properties:
| Purpose | Property |
|---|---|
| Layout | --mado-ui-description-list-gap, --mado-ui-description-list-group-padding, --mado-ui-description-list-term-width, --mado-ui-description-list-value-min |
| Text | --mado-ui-description-list-color, --mado-ui-description-list-font-size, --mado-ui-description-list-term-color |
| Divider | --mado-ui-description-list-divider |
.mado-ui-metric-grid lays out compact .mado-ui-metric-card surfaces
without depending on the general layout or panel items. A native
description list is the canonical structure for related metric labels and
values:
<dl class="mado-ui-metric-grid">
<div class="mado-ui-metric-card">
<dt class="mado-ui-metric-card-label">
Production deployments
</dt>
<dd class="mado-ui-metric-card-value">
<data value="128">128</data>
<span class="mado-ui-metric-card-detail">
12 more than during the previous 30 days.
</span>
</dd>
</div>
<div class="mado-ui-metric-card">
<dt class="mado-ui-metric-card-label">
Median build duration
</dt>
<dd class="mado-ui-metric-card-value">
<time datetime="PT42S">42 s</time>
<span class="mado-ui-metric-card-detail">
6 seconds faster than during the previous 30 days.
</span>
</dd>
</div>
</dl>The HTML Standard defines <dl> as name-value groups and explicitly permits
each dt/dd group to be wrapped in a div for styling. Keep the visible
value and its supporting detail inside one dd; multiple dd elements in a
group represent alternative values, not separate visual rows. See the
description-list contract in the HTML Standard.
data[value] is optional when a machine-readable equivalent is useful.
Use the more specific time[datetime] for dates, times and durations. Plain
text remains correct when neither element adds meaning. The recipe styles
the supplied markup but never calculates, formats or announces a value.
Comparisons must be complete visible text such as “12 more than the previous
period”. Do not communicate direction with an arrow or color alone. The
shared block deliberately has no positive, negative, increase or
decrease state because the product meaning of a rising value depends on the
metric. Applications may compose a separate badge when a semantic tone is
truly useful.
CSS grid is presentation only. Do not add role="grid", roving tabindex or
arrow-key handling to a collection of read-only values. A card is not a
clickable div; add a native anchor inside its detail when navigation is
needed. For live metrics, the application chooses an update and announcement
lifecycle instead of placing aria-live on the entire grid.
The grid uses auto-fit and collapses to one column when its configured
minimum cannot fit. Cards remain in source order and wrap long labels, values
and details instead of clipping or introducing document-level scrolling.
Public properties:
| Purpose | Property |
|---|---|
| Grid | --mado-ui-metric-grid-min, --mado-ui-metric-grid-gap, --mado-ui-metric-grid-align |
| Surface | --mado-ui-metric-card-background, --mado-ui-metric-card-color, --mado-ui-metric-card-border, --mado-ui-metric-card-radius, --mado-ui-metric-card-shadow |
| Spacing | --mado-ui-metric-card-padding, --mado-ui-metric-card-gap |
| Type | --mado-ui-metric-card-label-color, --mado-ui-metric-card-value-size, --mado-ui-metric-card-detail-color |
.mado-ui-table-shell keeps a native data table intact while containing the
horizontal overflow that can be necessary at narrow widths and high zoom.
The shell is an explicitly focusable, named scroll region; its accessible
name reuses the table's short native caption:
<div
class="mado-ui-table-shell"
role="region"
tabindex="0"
aria-labelledby="projects-table-caption"
>
<table class="mado-ui-table">
<caption
id="projects-table-caption"
class="mado-ui-table-caption"
>
Projects
</caption>
<thead class="mado-ui-table-header">
<tr class="mado-ui-table-row">
<th class="mado-ui-table-head" scope="col">Project</th>
<th
class="mado-ui-table-head"
scope="col"
data-align="center"
>
Status
</th>
<th class="mado-ui-table-head" scope="col" data-align="end">
Updated
</th>
</tr>
</thead>
<tbody class="mado-ui-table-body">
<tr class="mado-ui-table-row">
<th class="mado-ui-table-head" scope="row">Mado UI</th>
<td class="mado-ui-table-cell" data-align="center">Ready</td>
<td class="mado-ui-table-cell" data-align="end">
<time datetime="2026-07-28">28 July 2026</time>
</td>
</tr>
<tr class="mado-ui-table-row">
<th class="mado-ui-table-head" scope="row">Mado site</th>
<td class="mado-ui-table-cell" data-align="center">Building</td>
<td class="mado-ui-table-cell" data-align="end">
<time datetime="2026-07-27">27 July 2026</time>
</td>
</tr>
</tbody>
<tfoot class="mado-ui-table-footer">
<tr class="mado-ui-table-row">
<th class="mado-ui-table-head" scope="row">Visible projects</th>
<td class="mado-ui-table-cell" colspan="2" data-align="end">
2
</td>
</tr>
</tfoot>
</table>
</div>The HTML Standard defines table for multidimensional data, and its
caption as the table title. Header and data relationships stay native:
use th[scope="col"] for column headers, th[scope="row"] for row headers
and td for data. See the
HTML table model and the
WAI tables tutorial.
data-align="center" and data-align="end" change only visual alignment.
The header, body and footer classes style their corresponding native
thead, tbody and optional tfoot elements; they do not replace them.
For grouped headers, use the native colgroup and rowgroup scope values
only when they match the table model. Irregular or multi-level tables can
associate cells explicitly with unique header IDs and headers. Keep the
caption short. When a complex table needs orientation instructions, place
the visible summary nearby and connect it to the table with
aria-describedby.
The shell owns overflow, not table semantics. tabindex="0" lets keyboard
users reach and scroll it, while role="region" and aria-labelledby
provide context when it receives focus. Each shell needs a unique caption ID
and accessible name. CSS cannot add these attributes; keep them in copied
markup. The
MDN overflow accessibility guidance
documents this focus contract.
At 320 CSS pixels and under zoom, only the table may scroll horizontally.
The surrounding page must continue to reflow, as described by
WCAG's data-table Reflow exception.
Cells wrap long content instead of clipping it. Do not hide “optional”
columns at a breakpoint, turn rows into cards with CSS, change native table
elements to block layout or visually reorder cells. Those approaches can
remove data or break the relationship between a cell and its headers.
Likewise, do not add role="grid", cell tabindex, roving focus or
arrow-key behavior to a read-only data table. Sticky rows and columns are
outside this base contract because they can obscure content and focus at
high zoom.
Sorting is application behavior. When sorting is implemented, put a native
button inside the sortable header and apply aria-sort to the currently
sorted th:
<th
class="mado-ui-table-head"
scope="col"
aria-sort="ascending"
>
<button type="button">
Updated
<span aria-hidden="true">↑</span>
</button>
</th>The application must make the button work, reorder the rows and synchronize
the visible indicator and aria-sort. Remove aria-sort from the previous
header when the sort key changes, and expose it on at most one header.
Unsorted headers do not need aria-sort="none". Do not use
aria-pressed, a click handler on th or a non-functional sort button. The
WAI sortable-table example
uses the same native button and single-header state boundary.
The public class surface is exactly .mado-ui-table-shell,
.mado-ui-table, .mado-ui-table-caption, .mado-ui-table-header,
.mado-ui-table-body, .mado-ui-table-footer, .mado-ui-table-row,
.mado-ui-table-head and .mado-ui-table-cell.
Public properties:
| Purpose | Property |
|---|---|
| Layout | --mado-ui-table-min-inline-size, --mado-ui-table-font-size |
| Surface | --mado-ui-table-background, --mado-ui-table-color, --mado-ui-table-border, --mado-ui-table-radius, --mado-ui-table-shadow |
| Scroll focus | --mado-ui-table-focus-ring |
| Caption | --mado-ui-table-caption-color, --mado-ui-table-caption-padding |
| Header | --mado-ui-table-header-background, --mado-ui-table-header-color |
| Cells and rows | --mado-ui-table-cell-padding, --mado-ui-table-row-border, --mado-ui-table-row-hover-background |
| Footer | --mado-ui-table-footer-background |
.mado-ui-pagination presents a bounded set of page links in a labelled
native navigation landmark. The application owns every URL, localized label,
current page and page-window calculation:
<nav class="mado-ui-pagination" aria-label="Project pages">
<ul class="mado-ui-pagination-list">
<li class="mado-ui-pagination-item">
<a
class="mado-ui-pagination-link"
href="/projects?page=1"
rel="prev"
>
Previous
</a>
</li>
<li class="mado-ui-pagination-item">
<a
class="mado-ui-pagination-link"
href="/projects?page=1"
aria-label="Page 1"
>
1
</a>
</li>
<li class="mado-ui-pagination-item">
<a
class="mado-ui-pagination-link"
href="/projects?page=2"
aria-label="Page 2"
aria-current="page"
>
2
</a>
</li>
<li class="mado-ui-pagination-item">
<span class="mado-ui-pagination-ellipsis" aria-hidden="true">
…
</span>
</li>
<li class="mado-ui-pagination-item">
<a
class="mado-ui-pagination-link"
href="/projects?page=20"
aria-label="Page 20"
>
20
</a>
</li>
<li class="mado-ui-pagination-item">
<a
class="mado-ui-pagination-link"
href="/projects?page=3"
rel="next"
>
Next
</a>
</li>
</ul>
</nav>Use a localized aria-label or aria-labelledby that distinguishes this
navigation landmark from the site's other navigation. Real page destinations
remain anchors with real href values, so Tab, Enter, browser history,
copy-link and open-in-new-tab behavior all remain native. rel="prev" and
rel="next" describe adjacent documents in a sequence in the
HTML link-type standard.
Numeric aria-label values such as “Page 2” are application-owned and must
be localized.
Exactly one link in the set uses aria-current="page", including when that
current page remains a useful self-link. This follows the
W3C pagination pattern
and the
WAI aria-current technique.
The ellipsis only indicates an omitted range. It is never a link or button
and is hidden from the accessibility tree in the canonical markup.
At the first or last page, omit an unavailable boundary link when its
presence adds no value. If keeping the placeholder helps users understand
the sequence, remove href and use this exact disabled contract:
<a
class="mado-ui-pagination-link"
role="link"
aria-disabled="true"
>
Previous
</a>HTML has no disabled attribute for anchors, and aria-disabled does not
cancel a working hyperlink. Never combine href with
aria-disabled="true", and do not add the placeholder to the Tab order.
The
ARIA in HTML guidance for disabled links
specifies removing href before applying the explicit disabled link role.
The list wraps in source order and remains outside the data-table Reflow
exception. At narrow widths and high zoom, the application supplies a
bounded window such as first, nearby and last pages while preserving useful
Previous and Next links. CSS must not hide an arbitrary subset of a large
page list. Every link retains a comfortable pointer target and visible focus
state. Do not use menu, menubar, aria-selected, roving tabindex,
arrow-key handling or a live region for ordinary page navigation. Client-side
data replacement, focus policy and any update announcement are separate
application behavior.
The public class surface is exactly .mado-ui-pagination,
.mado-ui-pagination-list, .mado-ui-pagination-item,
.mado-ui-pagination-link and .mado-ui-pagination-ellipsis.
Public properties:
| Purpose | Property |
|---|---|
| Layout | --mado-ui-pagination-gap, --mado-ui-pagination-justify |
| Link size | --mado-ui-pagination-link-min-size, --mado-ui-pagination-link-padding, --mado-ui-pagination-link-radius |
| Link surface | --mado-ui-pagination-link-border, --mado-ui-pagination-link-background, --mado-ui-pagination-link-color |
| Link hover | --mado-ui-pagination-link-hover-background, --mado-ui-pagination-link-hover-color |
| Current page | --mado-ui-pagination-current-background, --mado-ui-pagination-current-border, --mado-ui-pagination-current-color |
| Disabled link | --mado-ui-pagination-disabled-background, --mado-ui-pagination-disabled-border, --mado-ui-pagination-disabled-color |
| Focus | --mado-ui-pagination-focus-ring |
| Ellipsis | --mado-ui-pagination-ellipsis-color |
.mado-ui-panel provides one visual surface for two related patterns:
- a panel groups persistent controls or content within a page;
- a card represents one self-contained item, often in a list or grid.
Choose the root element from the content, not its appearance. A labelled
section suits a distinct page region, article suits independently useful
content, and li suits an item in a semantic list. Use div when none of
those meanings apply. The recipe does not assign a role.
<section class="mado-ui-panel" aria-labelledby="deployments-title">
<header class="mado-ui-panel-header">
<div class="mado-ui-panel-heading">
<h2 id="deployments-title" class="mado-ui-panel-title">
Deployments
</h2>
<p class="mado-ui-panel-description">
Production activity from the last seven days.
</p>
</div>
<div class="mado-ui-panel-actions">
<a class="mado-ui-button" data-variant="secondary" href="/deployments">
View all
</a>
</div>
</header>
<div class="mado-ui-panel-content">
<!-- Native lists, forms, tables or application content. -->
</div>
<footer class="mado-ui-panel-footer">
Updated five minutes ago
</footer>
</section>The same structure can be an independently meaningful card:
<article class="mado-ui-panel" aria-labelledby="project-title">
<header class="mado-ui-panel-header">
<div class="mado-ui-panel-heading">
<h3 id="project-title" class="mado-ui-panel-title">Docs site</h3>
<p class="mado-ui-panel-description">Ready to deploy.</p>
</div>
</header>
<div class="mado-ui-panel-content">
Last build passed in 42 seconds.
</div>
<footer class="mado-ui-panel-footer">
<a class="mado-ui-link" href="/projects/docs-site">Open project</a>
</footer>
</article>Header, heading, description, actions, content and footer are optional. Keep heading levels consistent with the surrounding page. Do not add a click handler or link role to the whole container; use a native anchor or button for each action. If a compact card truly has one navigation target, its root may be a native anchor only when it contains no nested interactive content.
Public properties:
| Purpose | Property |
|---|---|
| Surface | --mado-ui-panel-background, --mado-ui-panel-color |
| Frame | --mado-ui-panel-border, --mado-ui-panel-radius, --mado-ui-panel-shadow |
| Spacing | --mado-ui-panel-padding, --mado-ui-panel-gap, --mado-ui-panel-header-gap |
| Actions | --mado-ui-panel-actions-gap |
| Footer | --mado-ui-panel-footer-gap, --mado-ui-panel-footer-justify |
prose provides a readable width and vertical rhythm for guides, policies,
help pages and other long-form content. It is an opt-in document block, not a
global typography reset. The application still chooses whether the root is
article, main, section or a neutral div, and it owns the document
outline and landmark names.
<article class="mado-ui-prose" aria-labelledby="handbook-title">
<header class="mado-ui-prose-section">
<h1 id="handbook-title">Community project handbook</h1>
<p>
This guide explains how maintainers review and publish changes.
</p>
</header>
<section
class="mado-ui-prose-section"
aria-labelledby="handbook-principles"
>
<h2 id="handbook-principles">Principles</h2>
<p>Prefer durable browser semantics and explicit ownership.</p>
<ul>
<li>Keep the source close to its maintainers.</li>
<li>Document recovery and review expectations.</li>
</ul>
<blockquote>
Documentation is part of the interface people depend on.
</blockquote>
</section>
<footer class="mado-ui-prose-note">
The application owns dates, authorship and revision metadata.
</footer>
</article>Only direct headings, paragraphs, lists and blockquotes inside an explicit
.mado-ui-prose-section receive document typography. This boundary lets an
alert, panel, navigation list or another Mado block remain a direct child of
.mado-ui-prose without inheriting accidental element styles. Wrap ordinary
authored flow in a prose section; keep independently styled blocks outside
that wrapper. Native anchors retain their own behavior and can compose the
independent button or link recipe when a different visual treatment is
wanted.
Use native ul, ol and blockquote elements for their real meanings.
.mado-ui-prose-note is only a separated visual note: it does not assign a
contentinfo role, live-region behavior or metadata. A footer inside an
article may be appropriate for document metadata, while an ordinary p
suits supporting copy that is not a footer. Without the stylesheet, every
element remains readable in source order.
Public properties:
| Purpose | Property |
|---|---|
| Width and rhythm | --mado-ui-prose-max-inline-size, --mado-ui-prose-gap, --mado-ui-prose-line-height, --mado-ui-prose-section-gap |
| Text | --mado-ui-prose-color, --mado-ui-prose-heading-color, --mado-ui-prose-muted-color |
| Lists | --mado-ui-prose-list-gap |
| Quotation | --mado-ui-prose-quote-border, --mado-ui-prose-quote-padding-inline |
| Note | --mado-ui-prose-note-border, --mado-ui-prose-note-font-size |
An alert is contextual feedback, not an accessibility role by default.
data-tone accepts neutral, info, success, warning and danger;
omitting it uses neutral.
<div class="mado-ui-alert" data-tone="warning">
<div class="mado-ui-alert-content">
<p class="mado-ui-alert-title">Deployment paused</p>
<p class="mado-ui-alert-description">
Add a production domain before deploying.
</p>
</div>
<div class="mado-ui-alert-actions">
<a class="mado-ui-button" data-variant="secondary" href="/domains">
Configure domain
</a>
</div>
</div>The visible title and description must communicate the state by themselves.
Color, an accent border or an icon can reinforce meaning, but cannot be its
only signal. data-tone changes presentation only: it never assigns an ARIA
role, live-region behavior or urgency.
Choose announcement behavior from when and why the message appears:
- For feedback already present when the page loads, use ordinary semantic HTML without a live-region role.
- For a non-urgent update, keep a
role="status"container in the DOM before the update and replace its text when the state changes. This announces politely without moving focus. - Use
role="alert"only for a new, urgent message that warrants an assertive interruption. Do not use it for routine success, neutral guidance or every validation message.
Do not add redundant aria-live to status or alert, and do not choose
role="alert" merely because data-tone="danger" is used. A dismiss action
is application behavior: render a labelled native button, remove the message
only after activation, and place focus somewhere logical if the focused
control is removed with it.
Public properties:
| Purpose | Property |
|---|---|
| Surface | --mado-ui-alert-background, --mado-ui-alert-color |
| Frame | --mado-ui-alert-border, --mado-ui-alert-accent, --mado-ui-alert-radius |
| Spacing | --mado-ui-alert-padding, --mado-ui-alert-gap, --mado-ui-alert-actions-gap |
notification-region is an ordinary in-flow stack for application-owned
updates. It composes the existing alert block instead of defining a second
message surface, and it deliberately separates announcement text from the
visual message list:
<div class="mado-ui-notification-region">
<!-- Mount this empty node before the first dynamic update. -->
<p
class="mado-ui-notification-announcer"
role="status"
aria-atomic="true"
></p>
<ol class="mado-ui-notification-list" aria-label="Notifications">
<li class="mado-ui-notification-item">
<div class="mado-ui-alert" data-tone="success">
<div class="mado-ui-alert-content">
<p class="mado-ui-alert-title">Project saved</p>
<p class="mado-ui-alert-description">
The latest settings are available to the team.
</p>
</div>
<div class="mado-ui-alert-actions">
<button
type="button"
aria-label="Dismiss project saved notification"
>
Dismiss
</button>
</div>
</div>
</li>
</ol>
</div>When the application adds that list item, it separately replaces the stable announcer's text with one concise update:
<p
class="mado-ui-notification-announcer"
role="status"
aria-atomic="true"
>
Project saved. The latest settings are available to the team.
</p>The visual ol and its li children remain ordinary list semantics. Do not
put role="status", role="alert" or aria-live on the list, an item or its
.mado-ui-alert: the dedicated announcer owns the one polite announcement.
Its status role already implies aria-live="polite"; do not add the
redundant attribute. aria-atomic="true" makes each replacement one coherent
message. Give the ordered list a concise accessible name when its surrounding
content does not already label it.
Install the block with:
mado-ui add notification-regionThe dependency graph installs theme, alert and notification-region.
The optional dismiss button may use the independent button recipe, but it
is not required by the region.
The public class surface is exactly .mado-ui-notification-region,
.mado-ui-notification-announcer, .mado-ui-notification-list and
.mado-ui-notification-item; each visible item composes one
.mado-ui-alert. Put the native hidden attribute on the ordered list while
the application has no visual messages, and remove the attribute when the
first item is rendered. The stable announcer remains mounted either way.
The region and list classes own only normal-flow sizing, spacing and local overflow. They do not create a queue, insert or remove alerts, assign ARIA, announce list mutations, move focus or intercept keyboard input. The announcer class supplies presentation for the authored live node, while its role, text and update lifecycle remain application-owned.
Notifications are persistent by default. Application state decides when a
message is complete and removes its li; this first contract has no automatic
expiry or timer policy. Adding a notification never moves focus. Native links
and buttons inside an alert remain in normal Tab order. If a focused dismiss
button removes its own item, move focus to the action that opened it or another
stable, logical target.
The announcer communicates a concise change, while the visual alert may keep
longer explanation and actions available. Do not duplicate the same update
through another live region. An urgent interruption belongs in a separate,
pre-existing role="alert" node or a modal response; data-tone="danger"
changes appearance only.
Without the stylesheet, the announcer and ordered list retain their authored semantics and the alerts remain in normal document flow. Keep one stable region at the relevant application-shell or page location instead of mounting an independent region for every feature.
This lifecycle follows the W3C
role="status" technique:
the live node exists before its text changes and receives no focus. Persistent
messages also avoid silently imposing the reading time discussed by
WCAG Timing Adjustable.
Public properties:
| Purpose | Property |
|---|---|
| Region width | --mado-ui-notification-inline-size |
| List size and spacing | --mado-ui-notification-max-block-size, --mado-ui-notification-gap |
content-state is one visual block for the current state of a content area.
Empty, loading, completed, constrained and error states are not separate
registry items: they share layout but have different application-owned data,
actions and announcement lifecycles. The block adds no JavaScript, request
state machine, focus behavior or live-region role.
The public class surface is exactly .mado-ui-content-state,
.mado-ui-content-state-media, .mado-ui-content-state-content,
.mado-ui-content-state-title, .mado-ui-content-state-description and
.mado-ui-content-state-actions. All child elements are optional.
data-tone accepts neutral, info, success, warning and danger;
omitting it uses neutral. Tone changes presentation only. In particular,
data-tone="success" does not imply role="status", and
data-tone="warning" or data-tone="danger" does not imply role="alert".
Media that only reinforces the title is decorative and uses
aria-hidden="true". If an image adds information that the text does not,
keep it exposed and supply an appropriate native text alternative under
WCAG Non-text Content.
The root is normally a generic div inside the stable content region that
owns the resource heading and aria-busy state. Use a native section only
when the state itself is a thematic document section that belongs in the
outline; section is not a generic styling container in the
HTML Standard.
Heading level and landmark choice come from the surrounding page.
An initial empty collection is ordinary document content. It needs a clear heading, explanation and useful native action, but no live-region role:
<section aria-labelledby="projects-title">
<h2 id="projects-title">Projects</h2>
<div class="mado-ui-content-state">
<span class="mado-ui-content-state-media" aria-hidden="true">
∅
</span>
<div class="mado-ui-content-state-content">
<h3 class="mado-ui-content-state-title">No projects yet</h3>
<p class="mado-ui-content-state-description">
Create a project to publish your first Mado application.
</p>
</div>
<div class="mado-ui-content-state-actions">
<a class="mado-ui-button" href="/projects/new">Create project</a>
</div>
</div>
</section>The optional button item styles the anchor but is not a registry dependency.
Use an anchor with a real href when the action navigates and a native
button type="button" when it changes the current interface.
“No data yet” and “no results match these filters” are different product
states even though they use the same block. The application owns the query,
count, copy and recovery action: a first-use state might create or import
data, while an empty search should normally clear or adjust filters. When
zero results appear dynamically without navigation or focus movement, update
a persistent role="status" with a localized message such as “No projects
match the current filters”. WCAG explicitly identifies dynamic “No results”
text as a
status message.
Keep focus on the search field, filter or action that initiated the update;
do not make the empty block focusable.
Put aria-busy="true" on the stable region being updated, not on the spinner.
The spinner remains decorative because the visible loading text supplies the
meaning:
<section
id="project-results"
aria-labelledby="project-results-title"
aria-busy="true"
>
<h2 id="project-results-title">Projects</h2>
<div class="mado-ui-content-state" data-tone="info">
<span class="mado-ui-content-state-media" aria-hidden="true">
<span class="mado-ui-spinner" aria-hidden="true"></span>
</span>
<div class="mado-ui-content-state-content">
<h3 class="mado-ui-content-state-title">Loading projects…</h3>
<p class="mado-ui-content-state-description">
Recent deployments will appear when the request completes.
</p>
</div>
</div>
</section>
<!-- Present before any client-side request and outside the busy region. -->
<p id="project-results-status" role="status" aria-atomic="true"></p>The optional spinner item supplies only the ring. Its wrapper is
aria-hidden="true" so the visual is not exposed as an unlabeled control.
Static loading content delivered with the initial document is not a content
change and does not need a live announcement. The empty status node is useful
for later client-side loads and refreshes; it must already exist before its
text is changed. The official
role="status" technique
uses the same persistent-container lifecycle. status is polite and atomic
by default and
must not receive focus because its text changed;
do not add redundant aria-live="polite".
Compose the independent skeleton primitive when
preserving the expected content geometry is more useful than a centered
loading block. Its placeholder group stays decorative and aria-hidden; the
same stable region, visible loading copy, external status and aria-busy
lifecycle still apply. Keep usable stale content in place during a background
refresh instead of replacing it with either loading presentation.
For a request that begins after the page is interactive, the application performs these updates as one lifecycle:
- Set
aria-busy="true"on the target region and place concise waiting text in its already-present status node. - Keep the live node outside that region. Assistive technologies may defer
descendant changes while
aria-busyis true. - Apply the final data, empty or error DOM, then set
aria-busy="false"after the last related mutation. - Replace the waiting text with a useful completion, result-count or failure message. Do not merely remove “Loading…”: disappearance alone does not communicate the end of a waiting state to non-visual users.
When progress is measurable, compose the native progress primitive instead
of turning the spinner into a progressbar:
<label for="project-load-progress">Loading projects</label>
<progress
id="project-load-progress"
class="mado-ui-progress"
value="6"
max="10"
>
6 of 10
</progress>Omit value completely when progress is indeterminate. The
native progress element
already exposes progressbar semantics, but it is not a live region. If
intermediate progress must be announced, update the separate status at useful
intervals rather than speaking every value change, following the
W3C progress-status technique.
Use success when the visible state represents a completed operation or
available result, and warning when content is constrained by an important
precondition such as authentication, permissions or incomplete setup:
<div class="mado-ui-content-state" data-tone="success">
<span class="mado-ui-content-state-media" aria-hidden="true">✓</span>
<div class="mado-ui-content-state-content">
<h3 class="mado-ui-content-state-title">Projects are ready</h3>
<p class="mado-ui-content-state-description">
The latest project data is available.
</p>
</div>
</div>
<div class="mado-ui-content-state" data-tone="warning">
<span class="mado-ui-content-state-media" aria-hidden="true">!</span>
<div class="mado-ui-content-state-content">
<h3 class="mado-ui-content-state-title">Sign in to continue</h3>
<p class="mado-ui-content-state-description">
This area is private. Continue with your account.
</p>
</div>
</div>These tones do not determine whether the state appeared dynamically. When a
completion or access change is a useful status message, update one stable,
application-owned role="status" outside the block. Persistent success and
warning content normally remains ordinary document content. Authentication,
authorization and recovery actions remain application policy rather than a
CSS state machine.
A recoverable request failure keeps the explanation and Retry action visible. The error block itself remains ordinary content; a separate, pre-existing status node announces a non-urgent dynamic failure:
<section
id="project-results"
aria-labelledby="project-results-title"
>
<h2 id="project-results-title">Projects</h2>
<div class="mado-ui-content-state" data-tone="danger">
<span class="mado-ui-content-state-media" aria-hidden="true">!</span>
<div class="mado-ui-content-state-content">
<h3 class="mado-ui-content-state-title">
Could not load projects
</h3>
<p class="mado-ui-content-state-description">
Check the connection and try the request again.
</p>
</div>
<div class="mado-ui-content-state-actions">
<button
class="mado-ui-button"
type="button"
aria-controls="project-results"
>
Try again
</button>
</div>
</div>
</section>
<!-- This node existed empty before the request began. -->
<p role="status" aria-atomic="true">
Could not load projects. Try the request again.
</p>Retry behavior belongs to the application. It guards repeated requests,
sets the controlled region busy, keeps the button's accessible label and
chooses whether native disabled is appropriate. Do not remove the focused
Retry button merely to show another loading block. After another failure,
leave focus on Retry and update the visible explanation and status text. On
success, preserve an enduring focus target; if the focused Retry control must
be removed, move focus to a stable logical result heading or region according
to the application's navigation policy. The content-state root does not gain
tabindex by default and an error does not receive automatic focus.
Reserve a separate role="alert" node for a rare, newly inserted message
that is important and time-sensitive enough to interrupt current speech. It
must also exist before its text update, stay outside the busy region and
contain only the concise announcement, not the content-state actions:
<p id="project-urgent-error" role="alert" aria-atomic="true"></p>alert is assertive and atomic. It does not move focus, and a message that
requires a modal response belongs to an alert-dialog implementation instead,
as specified by the
WAI-ARIA alert role and
WAI-ARIA alert pattern.
Do not add redundant aria-live="assertive", put role="alert" on every
danger tone or duplicate the same message through both status and alert.
The W3C
error live-region technique
documents why the announcement container is established before the error.
A background refresh is not a content-replacement state while usable data is
still present. Keep the data and its focused descendants in place, set their
stable region to aria-busy="true" and use the external status for a concise
“Refreshing…” message. After a successful coherent update, clear busy and
announce the useful result or count. After failure, clear busy, preserve the
stale data and focus, and show or announce that refresh failed while previous
data remains available. An inline alert block and native Retry button can
be composed for that case; replacing usable content with a full error state
would discard context.
A fatal route failure is a page, not an assertive notification. Use the
normal main landmark, a visible h1, explanatory text and native Retry,
Back or Home actions. On full navigation, no live-region role is needed. In a
client-side router, the application updates the document title and applies
its normal new-view focus policy. It must not also announce the same content
as an alert. Authentication, offline detection, error details, request
cancellation, retry backoff and fatal-versus-recoverable classification all
remain outside this CSS block.
Public properties:
| Purpose | Property |
|---|---|
| Layout | --mado-ui-content-state-align, --mado-ui-content-state-min-block-size, --mado-ui-content-state-gap, --mado-ui-content-state-padding |
| Surface | --mado-ui-content-state-background, --mado-ui-content-state-color, --mado-ui-content-state-border, --mado-ui-content-state-radius |
| Content | --mado-ui-content-state-content-max-inline-size, --mado-ui-content-state-title-size, --mado-ui-content-state-description-color |
| Media | --mado-ui-content-state-media-size, --mado-ui-content-state-media-background, --mado-ui-content-state-media-border, --mado-ui-content-state-media-color, --mado-ui-content-state-media-radius |
| Actions | --mado-ui-content-state-actions-gap |
.mado-ui-page-header arranges a page title, supporting copy and actions
without deciding their document semantics. Its class name describes the
visual recipe: it does not create a heading, a banner landmark or a
particular outline level.
<header class="mado-ui-page-header">
<div class="mado-ui-page-header-heading">
<p class="mado-ui-page-header-eyebrow">Workspace</p>
<h1 class="mado-ui-page-header-title">Deployments</h1>
<p class="mado-ui-page-header-description">
Review production activity and create a new deployment.
</p>
</div>
<div class="mado-ui-page-header-actions">
<a class="mado-ui-link" href="/docs/deployments">Documentation</a>
<button class="mado-ui-button" type="button">New deployment</button>
</div>
</header>Use the native heading level required by the surrounding document. h1
normally suits the main title of a page, while an embedded region may require
h2 through h6. Do not replace a heading with a styled div.
The root can be a native header when it introduces the page's main content.
It can instead be a div when the same layout appears in a context where
header would convey the wrong structure. The recipe never assigns
role="banner": applications own landmark choice, and a page should not gain
duplicate banner landmarks merely for visual consistency.
Heading content and actions are optional. The flex layout wraps in source
order on narrow viewports, so put the title before its actions in the DOM.
The button item remains optional; install it separately when using the
example action classes.
Public properties:
| Purpose | Property |
|---|---|
| Layout | --mado-ui-page-header-gap, --mado-ui-page-header-align |
| Color | --mado-ui-page-header-color, --mado-ui-page-header-eyebrow-color |
| Heading | --mado-ui-page-header-heading-gap, --mado-ui-page-header-title-size |
| Description | --mado-ui-page-header-description-width, --mado-ui-page-header-description-color |
| Actions | --mado-ui-page-header-actions-gap, --mado-ui-page-header-actions-align |
.mado-ui-toolbar is a responsive visual layout for related native controls.
The copied item contains no focus-management JavaScript and therefore does
not implement the ARIA toolbar composite-widget pattern.
For an ordinary set of related actions, keep every native control in the
normal Tab sequence. Add a labelled group only when announcing the set as a
group is useful:
<div
class="mado-ui-toolbar"
role="group"
aria-labelledby="release-actions-title"
>
<p id="release-actions-title" class="mado-ui-toolbar-label">
Release actions
</p>
<div class="mado-ui-toolbar-group">
<button class="mado-ui-button" type="button">Build</button>
<button class="mado-ui-button" type="button">Preview</button>
</div>
<div class="mado-ui-toolbar-group" data-align="end">
<button class="mado-ui-button" type="button" data-variant="secondary">
Settings
</button>
</div>
</div>The visible label supplies the group's accessible name. When no grouping
semantics are useful, omit both role="group" and its accessible-name
attribute. Nested .mado-ui-toolbar-group elements are layout wrappers, not
ARIA groups by default. Separators and spacers are decorative in this
contract. All actions remain native buttons, links or form controls and keep
their standard Enter, Space and Tab behavior.
role="toolbar" is a different, application-owned contract. Use it only when
application code also provides the complete composite behavior:
- an accessible name for the toolbar;
- one item in the page Tab sequence and the other items at
tabindex="-1"; - roving focus with Left/Right Arrow for a horizontal toolbar or Up/Down Arrow for a vertical toolbar;
- an
aria-orientationvalue that matches a non-horizontal presentation; - synchronized focus state after both keyboard and pointer interaction.
Tab and Shift+Tab then enter and leave the toolbar as one stop; they do not
replace arrow-key navigation between its controls. Home/End and focus wrapping
are optional policy decisions, but must be consistent. Do not add
role="toolbar" merely because the controls appear on one row. This CSS-only
registry item deliberately promises layout, not that keyboard pattern.
The toolbar and each group wrap in source order at narrow widths.
data-align="end" gives a group an automatic inline-start margin when room
exists; it changes alignment, never DOM or focus order. The button item is
optional and is not a registry dependency.
Public properties:
| Purpose | Property |
|---|---|
| Surface | --mado-ui-toolbar-background, --mado-ui-toolbar-color |
| Frame | --mado-ui-toolbar-border, --mado-ui-toolbar-radius |
| Spacing | --mado-ui-toolbar-padding, --mado-ui-toolbar-gap, --mado-ui-toolbar-group-gap |
| Alignment | --mado-ui-toolbar-align, --mado-ui-toolbar-justify, --mado-ui-toolbar-group-align |
| Label | --mado-ui-toolbar-label-color |
All block recipes use :where() inside @layer mado-ui, so normal unlayered
application CSS overrides them without specificity escalation. Public custom
properties start with --mado-ui-; edit the copied source when a product
needs a different structural contract.
See the primitive contracts for buttons, links and layout recipes commonly composed inside blocks.