Appearance
Modal
A modal with backdrop, container, header, body, and footer, built with the slots shape of defineComponent. The parts are tightly coupled inside a single container, so slots give them a shared namespace instead of forcing consumers to remember five separate prefixed classes (the way Dropdown's siblings do).
Authoring
ts
// recipes/modal.config.ts
import { defineComponent } from 'varia'
export default defineComponent('modal', {
slots: {
root: 'fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4',
container:
'relative w-full rounded-lg bg-white shadow-xl ring-1 ring-gray-200 max-h-[90vh] overflow-hidden flex flex-col',
header: 'flex items-start justify-between gap-4 p-4 border-b border-gray-200',
title: 'text-lg font-semibold text-gray-900',
description: 'mt-1 text-sm text-gray-600',
body: 'p-4 overflow-y-auto flex-1',
footer: 'flex items-center justify-end gap-2 p-4 border-t border-gray-200',
close:
'absolute top-3 right-3 inline-flex items-center justify-center rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500',
},
variants: {
size: {
sm: { container: 'max-w-sm' },
md: { container: 'max-w-md' },
lg: { container: 'max-w-lg' },
xl: { container: 'max-w-2xl' },
},
},
})Two notes on the config:
- The
rootslot maps to the bare component class (modal); every other slot maps tomodal__slotNameusing BEM. The double underscore separates slot classes from variant classes, which use single dashes. - The
sizevariant is slot-keyed: each size value targets thecontainerslot.modal-size-mddoesn't change the backdrop or header; it only sets the inner box's max-width. Slot-keyed variants emit as descendant-selector CSS rules (.modal-size-md .modal__container { max-width: ... }), so they apply through the tree without consumers adding a class to the container element.
Live preview
A modal rendered statically (open, inline, contained inside the docs page rather than full-viewport) so you can see all the slots at once:
Confirm deletion
This action can't be undone.
Deleting this project will permanently remove all of its files, history, and shared links. Type the project name to confirm.
Size variants in action
The interesting property of size being a slot-keyed variant is that swapping it changes only the container's max-width. The backdrop, header padding, and close-button position don't react. Here are the same modal contents at the smallest and largest sizes; md and lg sit between them:
modal-size-smSave changes?
You have unsaved edits.
modal-size-xlSave changes?
You have unsaved edits.
The CSS rule each variant emits, for reference:
css
.modal-size-sm .modal__container { max-width: var(--container-sm); }
.modal-size-md .modal__container { max-width: var(--container-md); }
.modal-size-lg .modal__container { max-width: var(--container-lg); }
.modal-size-xl .modal__container { max-width: var(--container-2xl); }The variant class lives on the root, but the styling lands on the container via the descendant selector. No class change is needed on modal__container itself; varia writes the descendant rule into a preflight at preset construction.
Consumption
html
<div class="modal modal-size-md" role="dialog" aria-modal="true" aria-labelledby="m-title">
<div class="modal__container">
<div class="modal__header">
<div>
<h2 class="modal__title" id="m-title">Confirm deletion</h2>
<p class="modal__description">This action can't be undone.</p>
</div>
<button class="modal__close" type="button" aria-label="Close">×</button>
</div>
<div class="modal__body">
...body content...
</div>
<div class="modal__footer">
<button class="btn btn-c-neutral btn-style-outline btn-s-sm" type="button">Cancel</button>
<button class="btn btn-c-danger btn-style-solid btn-s-sm" type="button">Delete</button>
</div>
</div>
</div>Behavior is the consumer's problem: varia doesn't ship open/close logic, focus trapping, or scroll locking. Pair these classes with your framework's dialog primitive (native <dialog>, Radix Dialog, Headless UI). varia doesn't apply default variants, so the container has no max-width unless a size class is present.
Generated class names
| Class | Slot |
|---|---|
modal | The root — full-viewport backdrop with centering |
modal__container | The dialog box |
modal__header | Top bar (title row) |
modal__title | Heading inside the header |
modal__description | Optional subtitle under the title |
modal__body | Scrollable middle section |
modal__footer | Action row (right-aligned by default) |
modal__close | Floating close button |
modal-size-sm / -md / -lg / -xl | Container max-width |
Nine consumer-facing classes, one component, slot-keyed sizing.
Why a <dialog>-shaped helper isn't part of varia
varia emits classes; it doesn't render DOM. The native <dialog> element handles focus trapping and the top-layer paint for free in modern browsers, but you opt into it from your framework. A typical pairing:
html
<dialog class="modal modal-size-md" role="dialog">
<div class="modal__container">...</div>
</dialog>The .modal's position: fixed and inset: 0 give you the backdrop styling even outside the top layer, so the same classes work whether you reach for <dialog>, a portaled <div>, or your framework's dialog component.