Modal
Accessible dialog overlay with focus trap, escape handling, and scroll lock.
Rendering Modal…
Installation
Copy the source into your project with the CLI. It lands under @/components/ui and is yours to edit.
npx mlola-ui add modalUsage
"use client";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Modal, ModalBody, ModalFooter, ModalHeader } from "@/components/ui/modal";
export default function Example() {
const [open, setOpen] = React.useState(false);
const close = () => setOpen(false);
return (
<>
<Button variant="danger" onClick={() => setOpen(true)}>Delete project</Button>
<Modal open={open} onClose={close} size="sm" role="alertdialog">
<ModalHeader title="Delete project" onClose={close} />
<ModalBody>This removes the project and every deployment attached to it.</ModalBody>
<ModalFooter>
<Button variant="secondary" onClick={close}>Keep it</Button>
<Button variant="danger" onClick={close}>Delete</Button>
</ModalFooter>
</Modal>
</>
);
}Styling comes from the engine stylesheet and the nearest theme. Set it once at the root:
<html data-theme="graphite" data-mode="light">Examples
A form in a dialog
Focus moves to the first field, stays inside, and returns to the button that opened it.
Rendering A form in a dialog…
HTML
Without React, write this markup and load @mlola-ui/behavior once. It runs the keyboard, focus and state for every element marked data-ml="modal", including markup added later, and needs no build step.
<button class="ml-button" type="button" data-variant="danger" data-ml-opens="delete-project">
<span class="ml-button-label">Delete project</span>
</button>
<div class="ml-modal-overlay" data-ml-for="delete-project" hidden></div>
<div class="ml-modal" id="delete-project" data-ml="modal" role="alertdialog" aria-modal="true" aria-labelledby="delete-project-title" data-size="sm" tabindex="-1" hidden>
<div class="ml-modal-header">
<h2 class="ml-modal-title" id="delete-project-title">Delete project</h2>
<button class="ml-modal-close" type="button" aria-label="Close dialog"><svg class="mlola-glyph" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
</div>
<div class="ml-modal-body">This removes the project and every deployment attached to it.</div>
<div class="ml-modal-footer">
<button class="ml-button" type="button" data-variant="secondary" data-ml-close><span class="ml-button-label">Keep it</span></button>
<button class="ml-button" type="button" data-variant="danger"><span class="ml-button-label">Delete</span></button>
</div>
</div><script type="module">
import { observe } from "@mlola-ui/behavior";
observe();
</script>Options
| Prop | Values | Default |
|---|---|---|
size | smmdlgxlfull | md |
Every prop with a fixed set of values, read from the exported TypeScript types with its doc comment. Open the Code tab above for the rest of the API.
Classes and attributes
A class names each element; data-* and aria-* attributes carry its variant and state. The values listed are the ones the stylesheet draws; the default needs no attribute.
| Class | Attributes |
|---|---|
ml-modal-overlay | — |
ml-modal | data-size="full | lg | sm | xl" |
ml-modal-header | — |
ml-modal-title | — |
ml-modal-close | — |
ml-modal-body | — |
ml-modal-footer | — |
Accessibility and theming
- Semantic elements, accessible names and state relationships are part of the contract.
- Keyboard, pointer and touch paths are operable; focus stays visible in every theme.
- Light and dark modes pass WCAG AA contrast in all 5 canonical themes.
- Reduced motion and forced colors keep meaning and operation.
Dependencies
Installed alongside this component. No third-party UI runtime is ever added.
@mlola-ui/behavior@mlola-ui/icons