Skip to content

Plugins

Bootstrap 5’s interactive components, driven by the same data-bs-* markup, without Bootstrap’s 80 kB JavaScript bundle. 2.1 kB brotli.

import { start } from 'cyclewire';
import { bootstrap } from 'cyclewire/bootstrap';
start({ actions, plugins: [bootstrap({ global: true })] });
Component Markup Notes
Collapse data-bs-toggle="collapse" + data-bs-target or href aria-expanded and .collapsed on triggers; accordions via data-bs-parent
Dropdown data-bs-toggle="dropdown" Closes on outside click, on item click and on Escape (focus returns to the toggle); arrow keys move between items
Modal data-bs-toggle="modal", data-bs-dismiss="modal" Backdrop, body.modal-open, focus moves in, Tab is trapped, Escape closes, focus returns to the trigger; data-bs-backdrop="static" / "false", data-bs-keyboard="false"
Offcanvas data-bs-toggle="offcanvas", data-bs-dismiss="offcanvas" Backdrop click closes; data-bs-scroll="true" keeps the page scrollable
Tabs data-bs-toggle="tab", "pill" or "list" Switches .active, aria-selected and .tab-pane.show.active
Alert data-bs-dismiss="alert" Removes the closest .alert

It fires Bootstrap’s events: show.bs.modal, shown.bs.modal, hide.bs.modal, hidden.bs.modal (the same for offcanvas), show/shown/hide/hidden for collapse and dropdown, show.bs.tab, shown.bs.tab and close.bs.alert. The show and hide events are cancelable.

{ global: true } defines window.bootstrap.Modal and window.bootstrap.Offcanvas when Bootstrap’s JavaScript is absent. Code written against Bootstrap’s API then keeps working:

bootstrap.Modal.getOrCreateInstance(document.getElementById('report')).show();

The module also exports Modal, Offcanvas, openModal(el, trigger?), closeModal(el), start(options?) and stop().

Do not combine it with Bootstrap’s own JavaScript; both would handle the same clicks.

Modern HTML covers most of these with zero JavaScript:

<button commandfor="confirm" command="show-modal">Delete…</button>
<dialog id="confirm">
<form method="dialog"><button>Cancel</button> <button value="yes">Delete</button></form>
</dialog>
<button popovertarget="menu">Menu</button>
<div id="menu" popover>…</div>
<details name="faq"><summary>Shipping</summary>…</details>
<details name="faq"><summary>Returns</summary>…</details>

These use <dialog> with Invoker Commands, the Popover API and exclusive <details> accordions. CycleWire can still react to them: cw-on-command receives custom commands (command="--refresh"), and cw-action on a <details> runs on toggle.

signals() installs two-way bindings and adds ctx.state and ctx.store to actions. See signals.

styles() preloads the stylesheets listed in { module, css } registry entries with their modules, and applies them before the handler runs. See CSS strategy.

A plugin is a plain object with any of these hooks:

export function analytics({ endpoint }) {
return {
// Once, when the plugin is added.
setup({ prefix, wire }) {
document.addEventListener('cw:done', ({ detail }) => {
navigator.sendBeacon(endpoint, JSON.stringify({ action: detail.action }));
});
},
// Extend every action context.
context(ctx) {
ctx.track = (name) => navigator.sendBeacon(endpoint, JSON.stringify({ name }));
},
// Called with every subtree CycleWire scans: at start, for observe(), and for added content.
scan(root) {},
// An action is being preloaded (intent, visible, idle, load): fetch what else it
// needs, once per module. `entry` is its registry entry, so object entries can carry options.
preload(entry, name) {},
// The user is heading for an element that binds actions: the pointer is over it,
// focus moved to it or a finger landed on it. Called every time, for that element.
intent(element) {},
// An action is about to run. The handler waits for the promise returned here,
// which loads in parallel with the module; a rejection fails the run.
load(entry, element, name) {},
// Development build only: what the core schedules, fetches, skips and runs.
trace(event) {},
// On stop().
stop() {},
};
}
start({ actions, plugins: [analytics({ endpoint: '/beacon' })] });

trace receives one object per step, with a type: schedule (a trigger or preload was set up), wait (a trigger fired before its action was registered), preload (with its reason: intent, a cw-preload value, or none for preload()), import and imported, skip (with its reason: unregistered, cancelled, once or busy), debounce, queue, start and end. The start and end of a run share its run object. Only the development build (the development export condition, or dist/esm-dev/) calls it; the production build contains none of these calls. The TraceEvent type lists every field, and cyclewire/devtools shows it all in a panel.

Keep plugins independent of the core’s internals. Import nothing from cyclewire beyond its public API, so a CDN copy and a bundled copy can never disagree.