Framework Usage
Monochrome provides framework wrappers for React and Vue. Both generate the same HTML and ARIA attributes — all behavior comes from the shared core.
Installation
The package includes:
Core runtime (~2.2KB)
React wrappers
Vue wrappers
Import what you need:
import "monochrome" // Core (import once at app entry)
import { Accordion , Menu , Tabs } from "monochrome/react"
Import "monochrome" once at your app’s entry point. The core auto-activates and handles all components on the page via event delegation.
React Usage
Basic Example
import "monochrome"
import { Accordion } from "monochrome/react"
export function FAQ () {
return (
< Accordion.Root type = "single" >
< Accordion.Item open >
< Accordion.Header as = "h3" >
< Accordion.Trigger >
What is Monochrome?
</ Accordion.Trigger >
</ Accordion.Header >
< Accordion.Panel >
< p > A tiny, accessible UI component library. </ p >
</ Accordion.Panel >
</ Accordion.Item >
< Accordion.Item disabled >
< Accordion.Header >
< Accordion.Trigger >
Disabled Section
</ Accordion.Trigger >
</ Accordion.Header >
< Accordion.Panel >
< p > This content is disabled. </ p >
</ Accordion.Panel >
</ Accordion.Item >
</ Accordion.Root >
)
}
Boolean Props
React uses JSX boolean shorthand:
// These are equivalent:
< Accordion.Item open />
< Accordion.Item open = { true } />
< Accordion.Item disabled />
< Menu.CheckboxItem checked />
CSS Classes
Use className (standard React):
< Accordion.Trigger className = "trigger" >
Click me
</ Accordion.Trigger >
All Components
import { Accordion , Collapsible , Menu , Tabs } from "monochrome/react"
// Accordion
< Accordion.Root type = "single" | "multiple" >
< Accordion.Item open disabled >
< Accordion.Header as = "h1" | "h2" | "h3" | "h4" | "h5" | "h6" >
< Accordion.Trigger />
</ Accordion.Header >
< Accordion.Panel />
</ Accordion.Item >
</ Accordion.Root >
// Collapsible
< Collapsible.Root open >
< Collapsible.Trigger />
< Collapsible.Panel />
</ Collapsible.Root >
// Tabs
< Tabs.Root defaultValue = "tab1" orientation = "horizontal" | "vertical" >
< Tabs.List >
< Tabs.Tab value = "tab1" disabled />
</ Tabs.List >
< Tabs.Panel value = "tab1" focusable />
</ Tabs.Root >
// Menu
< Menu.Root menubar >
< Menu.Trigger />
< Menu.Popover >
< Menu.Item disabled href = "/link" />
< Menu.CheckboxItem checked disabled />
< Menu.RadioItem checked disabled />
< Menu.Label />
< Menu.Separator />
< Menu.Group >
< Menu.Trigger /> { /* Submenu */ }
< Menu.Popover />
</ Menu.Group >
</ Menu.Popover >
</ Menu.Root >
Vue Usage
Basic Example
< script setup lang = "ts" >
import "monochrome"
import { Accordion } from "monochrome/vue"
</ script >
< template >
< Accordion.Root type = "single" >
< Accordion.Item : open = " true " >
< Accordion.Header as = "h3" >
< Accordion.Trigger >
What is Monochrome?
</ Accordion.Trigger >
</ Accordion.Header >
< Accordion.Panel >
< p > A tiny, accessible UI component library. </ p >
</ Accordion.Panel >
</ Accordion.Item >
< Accordion.Item : disabled = " true " >
< Accordion.Header >
< Accordion.Trigger >
Disabled Section
</ Accordion.Trigger >
</ Accordion.Header >
< Accordion.Panel >
< p > This content is disabled. </ p >
</ Accordion.Panel >
</ Accordion.Item >
</ Accordion.Root >
</ template >
Boolean Props
Vue requires : binding for boolean props:
<!-- Correct -->
< Accordion . Item : open = " true " />
< Accordion . Item : disabled = " true " />
< Menu . CheckboxItem : checked = " false " />
<!-- Incorrect (will not work) -->
< Accordion . Item open />
< Accordion . Item disabled />
Vue treats boolean attributes without : binding as strings. Always use :open="true", not open.
Kebab-Case Props
Vue auto-converts camelCase to kebab-case in templates:
<!-- These are equivalent -->
< Tabs . Root default-value = "tab1" />
< Tabs . Root defaultValue = "tab1" />
<!-- But kebab-case is conventional in Vue templates -->
< Tabs . Root default-value = "tab1" orientation = "horizontal" />
CSS Classes
Use class (standard Vue):
< Accordion . Trigger class = "trigger" >
Click me
</Accordion.Trigger>
Dot Notation
Vue 3 supports dot notation natively:
< Accordion . Root > <!-- Works in Vue 3 -->
<Accordion.Item>
<Accordion.Trigger />
</Accordion.Item>
</Accordion.Root>
All Components
<!-- Accordion -->
< Accordion . Root type = "single" | "multiple" >
<Accordion.Item :open="true" :disabled="true">
<Accordion.Header as="h1" | "h2" | "h3" | "h4" | "h5" | "h6">
<Accordion.Trigger />
</Accordion.Header>
<Accordion.Panel />
</Accordion.Item>
</Accordion.Root>
<!-- Collapsible -->
<Collapsible.Root :open="true">
<Collapsible.Trigger />
<Collapsible.Panel />
</Collapsible.Root>
<!-- Tabs -->
<Tabs.Root default-value="tab1" orientation="horizontal" | "vertical">
<Tabs.List>
<Tabs.Tab value="tab1" :disabled="true" />
</Tabs.List>
<Tabs.Panel value="tab1" :focusable="true" />
</Tabs.Root>
<!-- Menu -->
<Menu.Root :menubar="true">
<Menu.Trigger />
<Menu.Popover>
<Menu.Item :disabled="true" href="/link" />
<Menu.CheckboxItem :checked="false" :disabled="true" />
<Menu.RadioItem :checked="true" :disabled="true" />
<Menu.Label />
<Menu.Separator />
<Menu.Group>
<Menu.Trigger /> <!-- Submenu -->
<Menu.Popover />
</Menu.Group>
</Menu.Popover>
</Menu.Root>
React vs Vue Differences
Feature React Vue Boolean props open or open={true}:open="true"CSS classes className="..."class="..."camelCase props defaultValue="tab1"default-value="tab1" (kebab-case in templates)Dot notation Native Native (Vue 3) Context createContext / useContextprovide / inject
Wrapper Implementation
Both React and Vue wrappers are thin . They:
Render the correct HTML structure
Set ARIA attributes based on props
Wire up component context (linking triggers to panels)
Let the core handle all behavior
No JSX, No SFCs
React uses createElement() directly (no JSX)
Vue uses defineComponent() with h() render functions (no SFCs)
This design:
Produces smaller bundles
Keeps source framework-agnostic in style
Eliminates compiler overhead
Improves Preact compatibility (React)
Why Render Functions?
React (createElement)
Vue (h function)
import { createElement as h } from "react"
export const Trigger = ( props : TriggerProps ) => {
return h ( "button" , {
... props ,
type: "button" ,
id: buildId ( "trigger" , context . id ),
"aria-expanded" : props . open ? "true" : "false" ,
"aria-controls" : buildId ( "content" , context . id )
})
}
Benefits:
React: No react/jsx-runtime import, smaller bundle, better Preact compat
Vue: No SFC compiler overhead (patch flags, block tracking), 50% smaller bundle
Context Management
React Context
const AccordionContext = createContext < AccordionContextValue | null >( null )
export const Root = ({ type , children } : RootProps ) => {
const id = useId ()
return (
< AccordionContext.Provider value = { { id , type } } >
< div data-mode = { type } id = { buildId ( "root" , id ) } >
{ children }
</ div >
</ AccordionContext.Provider >
)
}
export const Trigger = ( props : TriggerProps ) => {
const context = useContext ( AccordionContext )
if ( ! context ) throw new Error ( "Trigger must be inside Root" )
// ...
}
Vue Context
const AccordionKey = Symbol ( "accordion" )
export const Root = defineComponent ({
setup ( props , { slots }) {
const id = Math . random (). toString ( 36 ). slice ( 2 )
provide ( AccordionKey , { id , type: props . type })
return () => h ( "div" , {
"data-mode" : props . type ,
id: buildId ( "root" , id )
}, slots . default ?.())
}
})
export const Trigger = defineComponent ({
setup ( props , { slots }) {
const context = inject ( AccordionKey )
if ( ! context ) throw new Error ( "Trigger must be inside Root" )
// ...
}
})
Event Handling
By default, clicking a menu item closes the menu. Prevent this by calling e.stopPropagation():
< Menu.Item onClick = { ( e ) => {
console . log ( "Item clicked" )
e . stopPropagation () // Keeps menu open
} } >
Action
</ Menu.Item >
Server-Side Rendering (SSR)
Both wrappers support SSR:
React: Works with renderToString() (tested in Monochrome’s test suite)
Vue: Works with renderToString() from @vue/server-renderer (tested)
hidden="until-found" Issue
React and Vue don’t support hidden="until-found" as a prop value. Both wrappers inject a small inline <script> that upgrades hidden="" to hidden="until-found" during HTML parsing.
<!-- Rendered HTML -->
< script >
document . querySelectorAll ( '[hidden]' ). forEach ( el => {
if ( el . getAttribute ( 'aria-hidden' ) === 'true' ) {
el . setAttribute ( 'hidden' , 'until-found' )
}
})
</ script >
This script runs before the page is interactive, ensuring find-in-page works immediately.
TypeScript Support
Both wrappers are written in TypeScript with full type definitions:
import type { ComponentProps } from "react"
import { Accordion } from "monochrome/react"
type TriggerProps = ComponentProps < typeof Accordion . Trigger >
const MyTrigger = ( props : TriggerProps ) => {
return < Accordion.Trigger { ... props } />
}
Best Practices
Import core once — At your app’s entry point (main.tsx, App.vue, etc.)
Let wrappers handle IDs — Don’t set id props manually
Use boolean bindings in Vue — Always :open="true", never open
Don’t manually set ARIA — Wrappers manage all ARIA attributes
Test with both frameworks — If you maintain a component library, test HTML, React, and Vue
Framework Compatibility
React
✅ React 16.8+ (hooks required)
✅ React 18
✅ Next.js (SSR tested)
✅ Remix (SSR tested)
✅ Preact (with preact/compat)
Vue
✅ Vue 3.0+
✅ Nuxt 3 (SSR tested)
❌ Vue 2 (not supported, dot notation requires Vue 3)
Migration Between Frameworks
The API is nearly identical. Migrating from React to Vue (or vice versa) requires only prop syntax changes:
< Accordion.Item open disabled >
< Accordion.Trigger className = "trigger" />
</ Accordion.Item >
< Tabs.Root defaultValue = "tab1" />
Additional Resources