Skip to main content

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

npm install monochrome
<script type="module">
  import "monochrome"
</script>

How It Works

Monochrome uses event delegation and ID conventions to identify components:
  1. Include the script once
  2. Render HTML with correct structure + ARIA attributes
  3. Use ID prefixes (mct:, mcc:, mcr:)
  4. Core handles all interactions automatically

ID Convention

PrefixRoleExample
mct:aAccordion triggermct:accordion:faq-1
mct:cCollapsible triggermct:collapsible:details
mct:mMenu triggermct:menu:file
mct:tTabs triggermct:tabs:home
mcc:Content panelmcc:accordion:faq-1
mcr:Root containermcr: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

ElementAttributeValueRequired
Rootidmcr:accordion:*Yes
Rootdata-mode"single" or "multiple"Yes
Triggeridmct:accordion:*Yes
Triggertype"button"Yes
Triggeraria-expanded"true" or "false"Yes
Triggeraria-controlsPanel idYes
Triggeraria-disabled"true" (for disabled items)No
Panelidmcc:* (matches aria-controls)Yes
Panelrole"region"Yes
Panelaria-labelledbyTrigger idYes
Panelaria-hidden"true" or "false"Yes
Panelhidden"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

ElementAttributeValueRequired
Triggeridmct:collapsible:*Yes
Triggertype"button"Yes
Triggeraria-expanded"true" or "false"Yes
Triggeraria-controlsPanel idYes
Panelidmcc:* (matches aria-controls)Yes
Panelaria-hidden"true" or "false"Yes
Panelhidden"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

ElementAttributeValueRequired
Rootdata-orientation"horizontal" or "vertical"Yes
Listrole"tablist"Yes
Listaria-orientation"horizontal" or "vertical"Yes
Tabrole"tab"Yes
Tabidmct:tabs:*Yes
Tabtype"button"Yes
Tabaria-selected"true" or "false"Yes
Tabaria-controlsPanel idYes
Tabtabindex0 (selected) or -1 (others)Yes
Tabaria-disabled"true" (for disabled tabs)No
Panelrole"tabpanel"Yes
Panelidmcc:* (matches aria-controls)Yes
Panelaria-labelledbyTab idYes
Panelaria-hidden"true" or "false"Yes
Panelhidden"until-found" (when inactive)Yes
Paneltabindex0 (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

ElementAttributeValueRequired
Triggeridmct:menu:*Yes
Triggertype"button"Yes
Triggeraria-haspopup"menu"Yes
Triggeraria-expanded"true" or "false"Yes
Triggeraria-controlsPopover idYes
Popoverrole"menu" or "menubar"Yes
Popoveridmcc:menu:* (matches aria-controls)Yes
Popoverpopover"manual"Yes
Popoveraria-hidden"true" or "false"Yes
Itemrole"menuitem", "menuitemcheckbox", "menuitemradio"Yes
Itemtype"button" (for <button>)If <button>
Itemaria-disabled"true" (for disabled items, must be <span>)No
Itemaria-checked"true" or "false" (for checkbox/radio)If checkbox/radio
Labelrole"presentation"Yes
Separatorrole"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:
AttributeComponentPurpose
data-modeAccordion Root"single" or "multiple"
data-orientationTabs Root"horizontal" or "vertical"
data-safeMenu GroupSafety triangle active (set by core)

CSS Custom Properties

The core sets CSS custom properties on menu popovers for positioning:
PropertySet OnValue
--topPopover, Group"123px" (trigger top)
--rightPopover, Group"456px" (trigger right)
--bottomPopover, Group"789px" (trigger bottom)
--leftPopover, Group"101px" (trigger left or cursor X)
--centerGroup"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

  1. Follow the ID conventionmct: for triggers, mcc: for content, mcr: for roots
  2. Match aria-controls to panel id — Core relies on this link
  3. Use hidden="until-found" — Never display: none, or find-in-page breaks
  4. Disabled = aria-disabled="true" — Never the HTML disabled attribute
  5. Disabled menu items = <span> — Not <button>, to prevent click bubbling
  6. Respect required structure — Accordion nesting, Tab button siblings, Menu <li> wrappers
  7. 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

Build docs developers (and LLMs) love