@four-leaf-studios/moda11y
v0.0.2
Published
Tiny, framework-agnostic, fully accessible modal dialogs (TS, a11y-first)
Readme
moda11y
Tiny, framework‑agnostic, fully accessible modal dialogs. Focused, modular, and TypeScript‑first.
- Package:
@four-leaf-studios/moda11y - Repo: https://github.com/Four-Leaf-Studios/moda11y
- License: MIT
Install
- npm:
npm i @four-leaf-studios/moda11y - pnpm:
pnpm add @four-leaf-studios/moda11y - yarn:
yarn add @four-leaf-studios/moda11y
Optional styles:
import '@four-leaf-studios/moda11y/styles.css';The CSS provides sensible, accessible defaults and animations. You can also bring your own styles.
Core Usage (vanilla JS/TS)
<!-- Your dialog container -->
<div id="my-dialog">
<div class="m11y__surface">
<h2 id="title">Hello</h2>
<p id="desc">This modal is fully accessible.</p>
<button id="close">Close</button>
</div>
<!-- Clicking outside .m11y__surface will close when BackdropClickModule is used -->
<!-- Container gets [data-mod11y="dialog"] and [hidden] managed by the library -->
<!-- Include styles.css for default backdrop and surface styling (optional) -->
</div>import '@four-leaf-studios/moda11y/styles.css'; // optional defaults
import {
createModal,
AriaModule,
FocusTrapModule,
CloseOnEscModule,
BackdropClickModule,
ScrollLockModule,
} from '@four-leaf-studios/moda11y';
const modal = createModal('#my-dialog', {
modules: [
AriaModule({ role: 'dialog', labelledBy: 'title', describedBy: 'desc' }),
FocusTrapModule(),
CloseOnEscModule(),
BackdropClickModule(),
ScrollLockModule(),
],
});
document.getElementById('open')?.addEventListener('click', () => modal.open());
document.getElementById('close')?.addEventListener('click', () => modal.close());
// Optional events
modal.on('open', () => console.log('opened'));
modal.on('close', () => console.log('closed'));Core API
createModal(elOrSelector: string | HTMLElement, options?: {
modules?: ModalModule[];
state?: Record<string, unknown>;
}): ModalInstanceModalInstance:
el: host elementisOpen: booleanopen() | close() | toggle() | destroy()on(event, handler) | off(event, handler) | emit(event, detail?)state: shared bag for modules (e.g.,returnFocusflag)
Events: mount | open | close | destroy.
React Usage
React bindings are exported from the react subpath.
import '@four-leaf-studios/moda11y/styles.css';
import { Modal, useMod11y } from '@four-leaf-studios/moda11y/react';
import { AriaModule, FocusTrapModule, CloseOnEscModule, BackdropClickModule, ScrollLockModule } from '@four-leaf-studios/moda11y';
export function MyModal({ open, onOpenChange }: { open: boolean; onOpenChange: (v: boolean) => void }) {
return (
<Modal
open={open}
onOpenChange={onOpenChange}
modules={[
AriaModule({ labelledBy: 'title', describedBy: 'desc' }),
FocusTrapModule(),
CloseOnEscModule(),
BackdropClickModule(),
ScrollLockModule(),
]}
role="dialog"
aria-modal="true"
>
<h2 id="title">Hello</h2>
<p id="desc">This modal is fully accessible.</p>
<button onClick={() => onOpenChange(false)}>Close</button>
</Modal>
);
}Also available: useMod11y(modules) returns a ref‑callback and an instance ref for advanced control.
Built‑in Modules
Import from the package root: @four-leaf-studios/moda11y or from @four-leaf-studios/moda11y/modules.
AriaModule({ role, labelledBy, describedBy, ariaModal }): setsrole,aria-modal, andaria-*associations.FocusTrapModule({ initialFocus, returnFocus }): keeps focus within the dialog and optionally restores focus on close.CloseOnEscModule({ stopPropagation }): closes on Escape; stops propagation by default.BackdropClickModule({ surfaceSelector = '.m11y__surface' }): closes when clicking outside the surface element.ScrollLockModule({ compensateScrollbar }): disables page scroll while open, optionally compensates for the scrollbar.
Styling
- Default styles:
import '@four-leaf-studios/moda11y/styles.css'. - Structure: the root receives
data-mod11y="dialog"; put content in a.m11y__surfacechild for sensible defaults and backdrop‑click behavior. - Bring your own: override or omit the stylesheet to fully customize appearance.
Accessibility
moda11y aims for practical, robust a11y by default:
- Applies ARIA semantics (
role,aria-modal, labels/description) viaAriaModule. - Traps focus and restores focus to the previously active element.
- Closes on Escape; optional backdrop close.
- Maintains visibility with
[hidden]and DOM‑only control (no portals required).
Scripts (development)
dev: builds in watch modebuild: production buildpreview: serves the built outputplay: run the local playgroundplay:build/play:preview: build/serve playgroundtest,test:watch,test:ci: run tests (Vitest)
Contributing
Issues and PRs welcome! Please open an issue first for significant changes: https://github.com/Four-Leaf-Studios/moda11y/issues
License
MIT © Four Leaf Studios
