Skip to main contentSkip to docs navigation

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/dom and vanilla-calendar-pro are 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:

JavaScript
// 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:

Shell
pnpm add @floating-ui/dom vanilla-calendar-pro

Browser

For browser usage without a bundler, use chassis.bundle.min.js. Both peer dependencies are bundled in, so no additional script is needed:

HTML
<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:

HTML
<!-- 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:

JavaScript
document.querySelector('my\\:id') // \\: escapes the colon in a JS string literal

Programmatic API

Every plugin is a class. Pass a DOM element or a valid CSS selector string, with an optional options object:

JavaScript
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:

MethodDescription
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.
JavaScript
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:

PropertyDescription
NAMEThe plugin's registered name — e.g. Dialog.NAME === 'dialog'.
VERSIONThe plugin's version string.
DefaultThe plugin's default options object. Mutate it to change defaults for all subsequently created instances.
JavaScript
// Disable keyboard dismissal for all subsequently created Dialog instances
Dialog.Default.keyboard = false

Events

Every plugin emits namespaced custom events for lifecycle actions, in two forms:

  • Infinitive (show, hide) — fires before the action; call event.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:

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
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 globally

To replace the sanitizer entirely, pass a sanitizeFn option:

JavaScript
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.