HTML Usage
Monochrome’s core is framework-agnostic. You can use it with plain HTML, server-rendered templates, or any framework that outputs HTML.
Installation
Via CDN
<script src="https://unpkg.com/monochrome"></script>
The script auto-activates on load. No initialization needed.
Via npm
<script type="module">
import "monochrome"
</script>
How It Works
Monochrome uses event delegation and ID conventions to identify components:
- Include the script once
- Render HTML with correct structure + ARIA attributes
- Use ID prefixes (
mct:, mcc:, mcr:)
- Core handles all interactions automatically
ID Convention
| Prefix | Role | Example |
|---|
mct:a | Accordion trigger | mct:accordion:faq-1 |
mct:c | Collapsible trigger | mct:collapsible:details |
mct:m | Menu trigger | mct:menu:file |
mct:t | Tabs trigger | mct:tabs:home |
mcc: | Content panel | mcc:accordion:faq-1 |
mcr: | Root container | mcr:accordion:faq |
The core uses id.startsWith() to identify components. The full ID after the prefix is flexible — use descriptive names.
Accordion
HTML Structure
<div data-mode="single" id="mcr:accordion:faq">
<div>
<h3>
<button
type="button"
id="mct:accordion:faq-1"
aria-expanded="false"
aria-controls="mcc:accordion:faq-1"
>
What is Monochrome?
</button>
</h3>
<div
id="mcc:accordion:faq-1"
role="region"
aria-labelledby="mct:accordion:faq-1"
aria-hidden="true"
hidden="until-found"
>
<p>A tiny, accessible UI component library.</p>
</div>
</div>
<div>
<h3>
<button
type="button"
id="mct:accordion:faq-2"
aria-expanded="false"
aria-controls="mcc:accordion:faq-2"
aria-disabled="true"
>
Disabled Section
</button>
</h3>
<div
id="mcc:accordion:faq-2"
role="region"
aria-labelledby="mct:accordion:faq-2"
aria-hidden="true"
hidden="until-found"
>
<p>This content is disabled.</p>
</div>
</div>
</div>
Required Structure
The nesting must be exact:
Root [data-mode] [id=mcr:accordion:*]
└── Item (div)
├── Header (h1-h6)
│ └── Trigger (button) [id=mct:accordion:*]
└── Panel (div) [role=region] [id=mcc:*]
The core traverses item.firstElementChild.firstElementChild to find triggers. If you break this nesting, the accordion will not work.
Attributes
| Element | Attribute | Value | Required |
|---|
| Root | id | mcr:accordion:* | Yes |
| Root | data-mode | "single" or "multiple" | Yes |
| Trigger | id | mct:accordion:* | Yes |
| Trigger | type | "button" | Yes |
| Trigger | aria-expanded | "true" or "false" | Yes |
| Trigger | aria-controls | Panel id | Yes |
| Trigger | aria-disabled | "true" (for disabled items) | No |
| Panel | id | mcc:* (matches aria-controls) | Yes |
| Panel | role | "region" | Yes |
| Panel | aria-labelledby | Trigger id | Yes |
| Panel | aria-hidden | "true" or "false" | Yes |
| Panel | hidden | "until-found" (when closed) | Yes |
Collapsible
HTML Structure
<div>
<button
type="button"
id="mct:collapsible:details"
aria-expanded="false"
aria-controls="mcc:collapsible:details"
>
Toggle Details
</button>
<div
id="mcc:collapsible:details"
aria-hidden="true"
hidden="until-found"
>
<p>Hidden content here.</p>
</div>
</div>
Required Structure
Container (any element)
├── Trigger (button) [id=mct:collapsible:*]
└── Panel (any element) [id=mcc:*]
Attributes
| Element | Attribute | Value | Required |
|---|
| Trigger | id | mct:collapsible:* | Yes |
| Trigger | type | "button" | Yes |
| Trigger | aria-expanded | "true" or "false" | Yes |
| Trigger | aria-controls | Panel id | Yes |
| Panel | id | mcc:* (matches aria-controls) | Yes |
| Panel | aria-hidden | "true" or "false" | Yes |
| Panel | hidden | "until-found" (when closed) | Yes |
Tabs
HTML Structure
<div data-orientation="horizontal">
<div role="tablist" aria-orientation="horizontal">
<button
role="tab"
type="button"
id="mct:tabs:home"
aria-selected="true"
aria-controls="mcc:tabs:home"
tabindex="0"
>
Home
</button>
<button
role="tab"
type="button"
id="mct:tabs:about"
aria-selected="false"
aria-controls="mcc:tabs:about"
aria-disabled="true"
tabindex="-1"
>
About
</button>
<button
role="tab"
type="button"
id="mct:tabs:contact"
aria-selected="false"
aria-controls="mcc:tabs:contact"
tabindex="-1"
>
Contact
</button>
</div>
<div
role="tabpanel"
id="mcc:tabs:home"
aria-labelledby="mct:tabs:home"
aria-hidden="false"
tabindex="0"
>
<p>Home content</p>
</div>
<div
role="tabpanel"
id="mcc:tabs:about"
aria-labelledby="mct:tabs:about"
aria-hidden="true"
hidden="until-found"
tabindex="-1"
>
<p>About content</p>
</div>
<div
role="tabpanel"
id="mcc:tabs:contact"
aria-labelledby="mct:tabs:contact"
aria-hidden="true"
hidden="until-found"
tabindex="0"
>
<p>Contact content</p>
</div>
</div>
Required Structure
Root [data-orientation]
├── List [role=tablist] [aria-orientation]
│ ├── Tab (button) [role=tab] [id=mct:tabs:*]
│ ├── Tab (button) [role=tab] [id=mct:tabs:*]
│ └── Tab (button) [role=tab] [id=mct:tabs:*]
├── Panel [role=tabpanel] [id=mcc:*]
├── Panel [role=tabpanel] [id=mcc:*]
└── Panel [role=tabpanel] [id=mcc:*]
Critical: All Tab buttons must be direct children of the List. The core iterates via parentElement.firstElementChild.
Attributes
| Element | Attribute | Value | Required |
|---|
| Root | data-orientation | "horizontal" or "vertical" | Yes |
| List | role | "tablist" | Yes |
| List | aria-orientation | "horizontal" or "vertical" | Yes |
| Tab | role | "tab" | Yes |
| Tab | id | mct:tabs:* | Yes |
| Tab | type | "button" | Yes |
| Tab | aria-selected | "true" or "false" | Yes |
| Tab | aria-controls | Panel id | Yes |
| Tab | tabindex | 0 (selected) or -1 (others) | Yes |
| Tab | aria-disabled | "true" (for disabled tabs) | No |
| Panel | role | "tabpanel" | Yes |
| Panel | id | mcc:* (matches aria-controls) | Yes |
| Panel | aria-labelledby | Tab id | Yes |
| Panel | aria-hidden | "true" or "false" | Yes |
| Panel | hidden | "until-found" (when inactive) | Yes |
| Panel | tabindex | 0 (focusable) or -1 (not focusable) | Optional |
Focusable panels: Set tabindex="0" if the panel itself should be focusable. Set tabindex="-1" if it contains interactive children (buttons, links, inputs). Inactive panels always get tabindex="-1".
HTML Structure
<div>
<button
type="button"
id="mct:menu:file"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="mcc:menu:file"
>
File
</button>
<ul
role="menu"
id="mcc:menu:file"
popover="manual"
aria-hidden="true"
>
<li role="none">
<button role="menuitem" type="button">New</button>
</li>
<li role="none">
<button role="menuitem" type="button">Open</button>
</li>
<li role="none">
<span role="menuitem" aria-disabled="true">Disabled</span>
</li>
<li role="none">
<a role="menuitem" href="/link">Link</a>
</li>
<li role="separator"></li>
<li role="none">
<button role="menuitemcheckbox" type="button" aria-checked="false">Bold</button>
</li>
<li role="none">
<button role="menuitemradio" type="button" aria-checked="true">Small</button>
</li>
<li role="none">
<button role="menuitemradio" type="button" aria-checked="false">Large</button>
</li>
<li role="separator"></li>
<li role="none">
<span role="presentation">Section Label</span>
</li>
<li role="none">
<!-- Submenu Group -->
<button
role="menuitem"
type="button"
id="mct:menu:file-submenu"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="mcc:menu:file-submenu"
>
Submenu
</button>
<ul
role="menu"
id="mcc:menu:file-submenu"
popover="manual"
aria-hidden="true"
>
<li role="none">
<button role="menuitem" type="button">Sub Action</button>
</li>
</ul>
</li>
</ul>
</div>
Required Structure
Root
├── Trigger (button) [id=mct:menu:*] [aria-haspopup=menu]
└── Popover (ul) [role=menu] [popover=manual] [id=mcc:menu:*]
├── li [role=none]
│ └── Item (button|a|span) [role=menuitem*]
├── li [role=separator]
└── li [role=none] (Group)
├── Trigger (button) [id=mct:menu:*] [aria-haspopup=menu]
└── Popover (ul) [role=menu] [popover=manual] [id=mcc:menu:*]
Every menu item must be wrapped in <li role="none">. This satisfies HTML semantics while allowing custom ARIA roles.
Attributes
| Element | Attribute | Value | Required |
|---|
| Trigger | id | mct:menu:* | Yes |
| Trigger | type | "button" | Yes |
| Trigger | aria-haspopup | "menu" | Yes |
| Trigger | aria-expanded | "true" or "false" | Yes |
| Trigger | aria-controls | Popover id | Yes |
| Popover | role | "menu" or "menubar" | Yes |
| Popover | id | mcc:menu:* (matches aria-controls) | Yes |
| Popover | popover | "manual" | Yes |
| Popover | aria-hidden | "true" or "false" | Yes |
| Item | role | "menuitem", "menuitemcheckbox", "menuitemradio" | Yes |
| Item | type | "button" (for <button>) | If <button> |
| Item | aria-disabled | "true" (for disabled items, must be <span>) | No |
| Item | aria-checked | "true" or "false" (for checkbox/radio) | If checkbox/radio |
| Label | role | "presentation" | Yes |
| Separator | role | "separator" | Yes |
Disabled items must be <span>, not <button>. This prevents click events from bubbling and interfering with the core’s event delegation.
For a menubar, set role="menubar" on the top-level popover:
<div>
<ul role="menubar" aria-hidden="false">
<li role="none">
<button
role="menuitem"
type="button"
id="mct:menu:file"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="mcc:menu:file"
>
File
</button>
<ul role="menu" id="mcc:menu:file" popover="manual" aria-hidden="true">
<li role="none">
<button role="menuitem" type="button">New</button>
</li>
</ul>
</li>
<li role="none">
<button
role="menuitem"
type="button"
id="mct:menu:edit"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="mcc:menu:edit"
>
Edit
</button>
<ul role="menu" id="mcc:menu:edit" popover="manual" aria-hidden="true">
<li role="none">
<button role="menuitem" type="button">Undo</button>
</li>
</ul>
</li>
</ul>
</div>
Data Attributes
Monochrome uses ID prefixes, not data attributes, to identify components. However, some data attributes are used for configuration:
| Attribute | Component | Purpose |
|---|
data-mode | Accordion Root | "single" or "multiple" |
data-orientation | Tabs Root | "horizontal" or "vertical" |
data-safe | Menu Group | Safety triangle active (set by core) |
CSS Custom Properties
The core sets CSS custom properties on menu popovers for positioning:
| Property | Set On | Value |
|---|
--top | Popover, Group | "123px" (trigger top) |
--right | Popover, Group | "456px" (trigger right) |
--bottom | Popover, Group | "789px" (trigger bottom) |
--left | Popover, Group | "101px" (trigger left or cursor X) |
--center | Group | "112px" (cursor Y for safety triangle) |
Use these in your CSS:
[role="menu"] {
position: fixed;
top: var(--bottom);
left: var(--left);
}
Server-Side Rendering
Monochrome works with any server-side rendering setup:
- PHP —
<?php echo $content ?>
- Ruby/ERB —
<%= content %>
- Python/Jinja —
{{ content }}
- Node.js — Template strings, Handlebars, EJS, etc.
Just render the HTML with correct IDs and ARIA attributes. The core activates on page load.
Browser Requirements
Monochrome requires Baseline 2024 features:
- Popover API —
<element popover>, .showPopover(), .hidePopover()
hidden="until-found" — Hidden content discoverable via find-in-page
beforematch event — Fires when find-in-page reveals hidden content
Supported browsers:
- Chrome 114+
- Edge 114+
- Safari 17+
- Firefox 125+
Best Practices
- Follow the ID convention —
mct: for triggers, mcc: for content, mcr: for roots
- Match
aria-controls to panel id — Core relies on this link
- Use
hidden="until-found" — Never display: none, or find-in-page breaks
- Disabled =
aria-disabled="true" — Never the HTML disabled attribute
- Disabled menu items =
<span> — Not <button>, to prevent click bubbling
- Respect required structure — Accordion nesting, Tab button siblings, Menu
<li> wrappers
- Test keyboard navigation — Arrow keys, Home, End, Enter, Space, Escape
Example: Complete Page
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Monochrome HTML Example</title>
<script src="https://unpkg.com/monochrome"></script>
<style>
[popover]:popover-open { display: flex; }
[role="menu"] {
position: fixed;
top: var(--bottom);
left: var(--left);
background: white;
border: 1px solid #ccc;
padding: 8px;
}
button:focus-visible { outline: 2px solid blue; }
</style>
</head>
<body>
<h1>Monochrome with Plain HTML</h1>
<h2>Accordion</h2>
<div data-mode="single" id="mcr:accordion:faq">
<div>
<h3>
<button type="button" id="mct:accordion:faq-1"
aria-expanded="false" aria-controls="mcc:accordion:faq-1">
Question 1?
</button>
</h3>
<div id="mcc:accordion:faq-1" role="region"
aria-labelledby="mct:accordion:faq-1" aria-hidden="true" hidden="until-found">
Answer 1.
</div>
</div>
</div>
<h2>Tabs</h2>
<div data-orientation="horizontal">
<div role="tablist" aria-orientation="horizontal">
<button role="tab" type="button" id="mct:tabs:home"
aria-selected="true" aria-controls="mcc:tabs:home" tabindex="0">
Home
</button>
<button role="tab" type="button" id="mct:tabs:about"
aria-selected="false" aria-controls="mcc:tabs:about" tabindex="-1">
About
</button>
</div>
<div role="tabpanel" id="mcc:tabs:home"
aria-labelledby="mct:tabs:home" aria-hidden="false" tabindex="0">
Home content
</div>
<div role="tabpanel" id="mcc:tabs:about"
aria-labelledby="mct:tabs:about" aria-hidden="true" hidden="until-found" tabindex="0">
About content
</div>
</div>
<h2>Menu</h2>
<div>
<button type="button" id="mct:menu:file"
aria-haspopup="menu" aria-expanded="false" aria-controls="mcc:menu:file">
File
</button>
<ul role="menu" id="mcc:menu:file" popover="manual" aria-hidden="true">
<li role="none">
<button role="menuitem" type="button">New</button>
</li>
<li role="none">
<button role="menuitem" type="button">Open</button>
</li>
</ul>
</div>
</body>
</html>
Troubleshooting
Nothing happens when I click
- Check that IDs start with the correct prefix (
mct:, mcc:, mcr:)
- Verify
aria-controls matches the panel id
- Ensure the script is loaded before the HTML renders
Accordion doesn’t work
- Check nesting: Item > Header > Trigger (exactly 3 levels)
- Ensure Root has
data-mode attribute
- Verify all triggers have
type="button"
Tabs don’t navigate
- Ensure all Tab buttons are direct children of the List
- Check that List has
role="tablist"
- Verify
data-orientation matches aria-orientation
- Add required CSS (see Styling Guide)
- Check that Popover has
popover="manual"
- Ensure
[role="menu"] has position: fixed
Additional Resources