Conventions
Shared patterns for every monochrome component. Read this once; component pages assume it.
ID prefixes
Core routes events by id prefix. No classes, data attributes, or mount registries.
| Prefix | Role | Examples |
|---|---|---|
mct: |
Trigger (almost always a <button>) |
mct:accordion:q1, mct:menu:file, mct:dialog-open:notice |
mcc: |
Content (panel, popover, dialog, tooltip) | mcc:accordion:q1, mcc:menu:file, mcc:dialog:notice |
mcr: |
Root / boundary | mcr:accordion:faq, mcr:tabs:demo |
Form: mct:<component>:<id> (and matching mcc: / mcr:). The <id> segment must be unique for that trigger/content pair on the page.
Internally the core matches the shortest unique prefix (mct:a accordion, mct:c collapsible, mct:dialog-o / mct:dialog-c dialog open/close, mct:m menu, mct:p popover, mct:ta tabs, mct:to tooltip). Authors still write the full readable ids above.
ARIA is state
There is no JS state mirror. Open, selected, checked, and disabled are the ARIA attributes on the elements:
aria-expanded/aria-hidden/hiddenfor disclosuresaria-selected/tabindexfor tabsaria-checkedfor menu checkbox and radio itemsaria-disabled="true"to skip activation
After user interaction, the DOM is the source of truth. Wrapper props such as open or defaultValue only seed the initial markup.
Linking
Triggers and content pair through ids, not DOM adjacency:
- Trigger
aria-controls→ contentid - Content
aria-labelledby→ triggerid(or a title id for titled dialogs)
Collapsible Trigger and Panel need not be siblings. Accordion Trigger and Panel are linked the same way even though Panel is a sibling of Header, not of Trigger.
Structure walks
Core walks parentElement, firstElementChild, and nextElementSibling. Extra wrappers break contracts that assume direct children:
| Contract | Requirement |
|---|---|
| Accordion items | Direct children of Root |
| Accordion Header → Trigger | Trigger is Header's first element child (button) |
| Tabs | Tab buttons are direct children of the tablist |
| Menu items | role="menuitem*" under li role="none" (or separators / labels) inside ul role="menu" |
| Menubar | Triggers are menubar items; exactly one has tabindex="0" (HTML: set by hand; React/Vue: Menubar.Menu first), others -1 |
HTML vs wrappers
| HTML | React / Vue | |
|---|---|---|
| Markup | Author writes ids and ARIA | Wrappers mint ids (useId / equivalent) and emit ARIA |
| Root | Often optional when ids link Trigger + Content | Usually required for context |
| Behavior | Always from import "monochrome" |
Same core; wrappers are markup only |
Import the core once:
import "monochrome"CSS and browsers
Monochrome ships no CSS. Position floating UI with the CSS custom properties the core sets on open (--top, --right, --bottom, --left, --pw, --ph).
Requires Baseline 2024 browsers: Popover API (popover="manual", showPopover / hidePopover) and native <dialog> (showModal). No polyfills.
Activation
Enter and Space are not handled in keydown. The browser synthesizes click on buttons. Core uses event.detail === 0 for keyboard-synthesized clicks vs mouse clicks (menus focus the first item on keyboard open, stay on the trigger for mouse open).