JavaScript
How Chassis JS plugins work — data attributes, the programmatic API, events, and the full list of plugins shipped in the package.
Chassis CSS ships a small set of JavaScript plugins that bring interactive behaviour to a handful of components: dropdowns, modals, accordions, and so on. The plugins are vanilla JavaScript, framework-agnostic, and configured primarily through HTML data attributes — JavaScript is only required when you need programmatic control.
What ships
Chassis JS is published with the @chassis-ui/css package and built in three flavours:
dist/js/chassis.js— UMD bundle, no Popper.dist/js/chassis.bundle.js— UMD bundle with Popper included. Use this if you want one script tag and you use dropdowns, popovers, or tooltips.dist/js/chassis.esm.js— ES module. Use this with bundlers (Webpack, Parcel, Vite) or with<script type="module">.
Each is also published in minified form (*.min.js).
Plugins
The shipped plugins, exported from @chassis-ui/css:
Accordion · Button · Carousel · Chip · Collapse · Dropdown · Modal · Notification · Offcanvas · Popover · ScrollSpy · Tab · Toast · Tooltip
Dropdown, Popover, and Tooltip depend on Popper for positioning. Either include @popperjs/core in your bundle or use the chassis.bundle.* build that includes Popper for you.
Including the JavaScript
Use a bundler when you can; it lets you tree-shake to only the plugins you use. The script-tag approach is fine for prototypes and the Quick Start flow.
With a bundler (recommended):
// All plugins
import * as chassis from '@chassis-ui/css'
// Or just the plugins you use
import { Modal, Tooltip } from '@chassis-ui/css'
// Or load a single plugin from its source path
import Modal from '@chassis-ui/css/js/src/modal.js'
With a script tag:
<!-- Includes Popper -->
<script src="node_modules/@chassis-ui/css/dist/js/chassis.bundle.min.js"></script>
<!-- Or load Popper separately if you don't use dropdowns/popovers/tooltips -->
<script src="node_modules/@popperjs/core/dist/umd/popper.min.js"></script>
<script src="node_modules/@chassis-ui/css/dist/js/chassis.min.js"></script>
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 a small set of data-cx-* attributes that act as configuration, with data-cx-toggle="<plugin>" as the trigger.
<!-- Toggle a modal -->
<button class="button primary" data-cx-toggle="modal" data-cx-target="#myModal">
Open modal
</button>
<!-- Toggle a tooltip -->
<button class="button" data-cx-toggle="tooltip" data-cx-placement="top" title="Hello!">
Hover me
</button>
Use one set of data attributes per element. A single button can't trigger both a modal and a tooltip — the attributes would collide.
Chassis components offer flexible configuration through both data attributes and JavaScript. To use data attributes, prefix any option with data-cx- followed by the option name in kebab-case format (using hyphens instead of camelCase). For example, write data-cx-custom-class="my-class" rather than data-cx-customClass="my-class".
For more complex configurations, Chassis provides the data-cx-config attribute which accepts a JSON string of multiple settings. This approach simplifies markup when configuring several options at once. If the same option appears in both data-cx-config and as a separate data attribute (like data-cx-title), the individual attribute takes precedence. You can also use JSON values in individual attributes, such as data-cx-delay='{"show":100,"hide":200}' for more granular control.
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.
Programmatic API
Every plugin is a class. Instantiate it with a DOM element (or a CSS selector that resolves to one) and an optional options object:
const modalEl = document.querySelector('#myModal')
const modal = new chassis.Modal(modalEl)
// With options
const noKeyboard = new chassis.Modal(modalEl, { keyboard: false })
// Selector strings work too
const dropdown = new chassis.Dropdown('[data-cx-toggle="dropdown"]')
To retrieve an existing instance for an element, use getInstance:
chassis.Modal.getInstance(modalEl) // → instance, or null if uninitialized
chassis.Modal.getOrCreateInstance(modalEl) // → instance, creates one if needed
chassis.Modal.getOrCreateInstance(modalEl, { keyboard: false })
Static properties
Every plugin exposes a few static properties:
| Property | Description |
|---|---|
NAME | The plugin's registered name — e.g. chassis.Tooltip.NAME === 'tooltip'. |
VERSION | The plugin's version. |
Default | The plugin's default options object. Mutate it to change defaults globally. |
// Change the default for all subsequently created Modal instances
chassis.Modal.Default.keyboard = false
Events
Every plugin emits custom events for its lifecycle actions, in two forms:
- Infinitive (
show,hide) — fires before the action;preventDefault()cancels it. - Past participle (
shown,hidden) — fires after the action completes.
Event names are namespaced by plugin — show.cx.modal, hidden.cx.modal, shown.cx.toast, and so on.
const modalEl = document.querySelector('#myModal')
// Listen for show, but cancel it under some condition
modalEl.addEventListener('show.cx.modal', event => {
if (someCondition) {
event.preventDefault()
}
})
// Listen for the action's completion
modalEl.addEventListener('shown.cx.modal', () => {
console.log('Modal is fully visible')
})
Asynchronous transitions
All plugin methods are asynchronous. They return as soon as the transition starts — before it completes. To run code after the 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 transitioning component is ignored. To chain transitions, listen for the previous one's completion event:
const carouselEl = document.querySelector('#myCarousel')
const carousel = chassis.Carousel.getInstance(carouselEl)
carouselEl.addEventListener('slid.cx.carousel', () => {
carousel.to('2') // Slides to slide 2 once slide 1 finishes
})
carousel.to('1')
dispose() after transitions
Calling dispose() immediately after hide() (or any other transition) leads to incorrect state because the transition is still in flight. Wait for the completion event first:
modalEl.addEventListener('hidden.cx.modal', () => {
modal.dispose()
})
modal.hide()
Sanitizer
Tooltip and Popover accept HTML content. To prevent XSS, Chassis sanitizes that content with 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 it for your own use:
const allowList = chassis.Tooltip.Default.allowList
// Allow <table>
allowList.table = []
// Allow <td> with a custom data attribute
allowList.td = ['data-cx-option']
// Allow custom data-* attributes globally
allowList['*'].push(/^data-my-app-[\w-]+/)
To replace the sanitizer with a different library (for example DOMPurify):
new chassis.Tooltip(el, {
sanitizeFn: content => DOMPurify.sanitize(content)
})
Use with JavaScript frameworks
Chassis JS mutates the DOM directly. Frameworks like React, Vue, and Angular also assume control of the DOM, which means using Chassis JS and the framework on the same components causes conflicts — components getting stuck in inconsistent states, event listeners not firing, virtual-DOM diffing breaking visible behaviour.
The recommended approach in those frameworks is to use the Chassis CSS but reimplement the interactive behaviour in framework-idiomatic components. The CSS contracts (class names, ARIA attributes, CSS variables) are framework-agnostic and stable; the JS behaviour is straightforward to reproduce. Some teams maintain framework-specific reimplementations as private packages — there is no official Chassis equivalent of those.
For static parts of an application — pages where you have full control of the DOM and the framework isn't mounted — Chassis JS works as documented.
No JavaScript? No problem (mostly)
Components that depend on JS for behaviour (modals, dropdowns, accordions, etc.) won't function without it. Components that are purely presentational (buttons, badges, cards, layout) work the same with or without JS. If you ship a page that requires JS, use <noscript> to explain the situation and provide a fallback when possible.
Selectors
Chassis JS uses native querySelector / querySelectorAll for DOM lookups, so the strings you pass to constructors must be valid CSS selectors. Special characters in IDs need escaping — for example, an ID containing a colon (my:id) must be queried as my\\:id.