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 / hidden for disclosures
  • aria-selected / tabindex for tabs
  • aria-checked for menu checkbox and radio items
  • aria-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 → content id
  • Content aria-labelledby → trigger id (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:

javascript
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).

See also