JavaScript
How Chassis CSS plugins work — ESM-only, data attributes, the programmatic API, events, and the full list of plugins shipped in the package.
Chassis CSS ships a set of JavaScript plugins that bring interactive behaviour to components: dialogs, menus, accordions, carousels, and more. The plugins are vanilla JavaScript, ESM-only, and framework-agnostic. Most components can be wired up entirely through HTML data attributes — programmatic instantiation is only needed for method calls, event listening, or dynamic configuration.
What ships
Chassis CSS ships two JavaScript builds with the @chassis-ui/css package:
dist/js/chassis.js— standalone ESM file.@floating-ui/domandvanilla-calendar-proare external peer dependencies; include them separately when using menus, popovers, tooltips, or the datepicker.dist/js/chassis.bundle.js— ESM bundle with both peer dependencies included. Use this when you want a single file with no external dependencies.
Both also ship in minified form (*.min.js).
Plugins
The full list of plugins exported from @chassis-ui/css:
Accordion · Button · Carousel · Chip · ChipInput · Collapse · Combobox · Datepicker · Dialog · Drawer · Menu · NavOverflow · Notification · OtpInput · Popover · ScrollSpy · Strength · Tab · Toast · Toggler · Tooltip
Menu, Popover, and Tooltip depend on Floating UI for positioning. Datepicker depends on Vanilla Calendar Pro. Both are included in chassis.bundle.js.
Loading
Two loading paths are available depending on whether a bundler is part of your build.
Bundler
With a bundler (Webpack, Parcel, Vite), import from the package entry point. The bundler tree-shakes to only the plugins actually used:
// Named imports — recommended
import { Dialog, Menu, Tooltip } from '@chassis-ui/css'
// All plugins
import * as chassis from '@chassis-ui/css'When using Menu, Popover, Tooltip, or Datepicker, install the respective peer dependencies and let the bundler resolve them:
pnpm add @floating-ui/dom vanilla-calendar-proBrowser
For browser usage without a bundler, use chassis.bundle.min.js. Both peer dependencies are bundled in, so no additional script is needed:
<script type="module">
import { Dialog, Menu } from './node_modules/@chassis-ui/css/dist/js/chassis.bundle.min.js'
new Dialog(document.querySelector('#myDialog'))
</script>To use chassis.min.js in a browser without a bundler, provide an import map that resolves @floating-ui/dom and vanilla-calendar-pro to their ESM browser builds. Refer to each library's documentation for the correct browser entry point path.
Data attributes
The simplest way to wire up a Chassis plugin is through HTML data attributes — no JavaScript required on your part. Every plugin observes data-cx-* attributes for configuration, with data-cx-toggle="<plugin>" as the trigger:
<!-- Toggle a dialog -->
<button class="button primary" data-cx-toggle="dialog" data-cx-target="#myDialog">
Open dialog
</button>
<!-- Toggle a tooltip -->
<button class="button" data-cx-toggle="tooltip" data-cx-placement="top" title="Hello!">
Hover me
</button>Only one set of data attributes per element — a single button cannot trigger both a dialog and a tooltip simultaneously.
Options are set via data-cx-* attributes or passed to the constructor as an object. Attribute names use the kebab-case form of the option name — data-cx-custom-class, not data-cx-customClass. Attribute values are parsed to their native types:"true" → true, "0" → 0, and valid JSON strings to objects.
Use data-cx-config to pass multiple options as a JSON string:data-cx-config='{"delay":200}'. Individual data-cx-* attributes take precedence over data-cx-config. You can also use JSON values in individual attributes, such as data-cx-delay='{"show":100,"hide":200}'.
When initializing components, Chassis merges configurations from multiple sources in this priority order: default settings, data-cx-config values, individual data-cx-*attributes, and finally any JavaScript object options. Values defined later in this sequence override earlier ones.
Selectors
DOM lookups use native querySelector and querySelectorAll, so strings passed to data-cx-target and plugin constructors must be valid CSS selectors. Special characters in IDs need escaping — for example, an ID containing a colon requires \\ in a JavaScript string to produce the single backslash the CSS selector needs:
document.querySelector('my\\:id') // \\: escapes the colon in a JS string literalProgrammatic API
Every plugin is a class. Pass a DOM element or a valid CSS selector string, with an optional options object:
const dialogEl = document.querySelector('#myDialog')
const dialog = new Dialog(dialogEl)
const dialog = new Dialog(dialogEl, { keyboard: false })
const menu = new Menu('[data-cx-toggle="menu"]')Methods
Every plugin class exposes three universal instance and static methods:
| Method | Description |
|---|---|
dispose() | Destroys the instance and removes stored data from the element. |
getInstance(element) | Static. Returns the instance associated with the element, or null if not yet initialized. |
getOrCreateInstance(element, config) | Static. Returns the existing instance, or creates and returns a new one. |
Dialog.getInstance(dialogEl) // → instance, or null if not yet initialized
Dialog.getOrCreateInstance(dialogEl) // → instance, creates one if needed
Dialog.getOrCreateInstance(dialogEl, { keyboard: false })Static properties
Every plugin class also exposes three static properties for inspection and global configuration:
| Property | Description |
|---|---|
NAME | The plugin's registered name — e.g. Dialog.NAME === 'dialog'. |
VERSION | The plugin's version string. |
Default | The plugin's default options object. Mutate it to change defaults for all subsequently created instances. |
// Disable keyboard dismissal for all subsequently created Dialog instances
Dialog.Default.keyboard = falseEvents
Every plugin emits namespaced custom events for lifecycle actions, in two forms:
- Infinitive (
show,hide) — fires before the action; callevent.preventDefault()to cancel it. - Past participle (
shown,hidden) — fires after the action completes.
Event names follow the pattern <action>.cx.<plugin> — for example show.cx.dialog, hidden.cx.dialog, shown.cx.toast:
const dialogEl = document.querySelector('#myDialog')
dialogEl.addEventListener('show.cx.dialog', event => {
if (someCondition) {
event.preventDefault() // cancels the show
}
})
dialogEl.addEventListener('shown.cx.dialog', () => {
// runs after the dialog is fully visible
})Async transitions
All plugin methods are asynchronous — they return as soon as the transition starts, before it completes. To run code after a transition, listen for the past-participle event:
const collapseEl = document.querySelector('#myCollapse')
collapseEl.addEventListener('shown.cx.collapse', () => {
// runs only after the expand animation finishes
})A method call on a component that is still transitioning is ignored. To chain transitions, wait for the completion event:
const carouselEl = document.querySelector('#myCarousel')
const carousel = Carousel.getInstance(carouselEl)
carouselEl.addEventListener('slid.cx.carousel', () => {
carousel.to('2') // begins only after slide 1 finishes
})
carousel.to('1')Dispose and transitions
Calling dispose() immediately after hide() — or any other transitioning method — produces incorrect state because the transition is still in flight. Wait for the completion event first:
dialogEl.addEventListener('hidden.cx.dialog', () => {
dialog.dispose()
})
dialog.hide()Sanitizer
Tooltip and Popover accept HTML content. To prevent XSS, Chassis sanitizes that content against a built-in allow-list before injecting it into the DOM. The default allow-list:
const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i
export const DefaultAllowlist = {
// Global attributes allowed on any supplied element below.
'*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],
a: ['target', 'href', 'title', 'rel'],
area: [],
b: [],
br: [],
col: [],
code: [],
dd: [],
div: [],
dl: [],
dt: [],
em: [],
hr: [],
h1: [],
h2: [],
h3: [],
h4: [],
h5: [],
h6: [],
i: [],
img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],
li: [],
ol: [],
p: [],
pre: [],
s: [],
small: [],
span: [],
sub: [],
sup: [],
strong: [],
u: [],
ul: []
}To extend the allow-list for custom content needs:
const allowList = Tooltip.Default.allowList
allowList.table = [] // allow <table>
allowList.td = ['data-cx-option'] // allow <td> with a custom attribute
allowList['*'].push(/^data-my-app-[\w-]+/) // allow custom data-* attributes globallyTo replace the sanitizer entirely, pass a sanitizeFn option:
import DOMPurify from 'dompurify'
new Tooltip(el, {
sanitizeFn: content => DOMPurify.sanitize(content)
})Framework compatibility
The Chassis CSS plugins mutate the DOM directly. Frameworks like React, Vue, and Angular also control the DOM, which causes conflicts when both operate on the same elements — components stuck in inconsistent states, event listeners not firing, virtual-DOM diffing breaking visible behaviour.
The recommended approach in component-based frameworks is to use the Chassis CSS (class names, ARIA attributes, CSS custom properties) and reimplement the interactive behaviour in framework-idiomatic components. The CSS contracts are stable and framework-agnostic.
For static parts of an application where the framework is not mounted — server-rendered pages, static HTML sections — the plugins work as documented.
Disabled JavaScript
Components that depend on JS for behaviour (dialogs, menus, accordions) will not function without it. Purely presentational components (buttons, badges, cards, layout) work the same regardless. Use <noscript> to communicate the requirement to users and provide a fallback when possible.