Modal
Accessible focus-trapped dialog for critical actions and deep workflows.
Overview
The Modal component is a core building block of the Byteion design system, providing a robust, accessibility-compliant interface for its specific UI capabilities.
Why this exists
Modals interrupt the user to demand attention for critical actions or focused workflows. A well-built modal prevents background interaction, traps focus, and provides clear exit paths.
Mental model
A modal is a temporary overlay that blocks interaction with the page behind it. Use it when the task requires full attention — confirmations, forms, or detail views that should not navigate away.
When to use
When not to use
Basic example
The following example demonstrates standard usage, fully compliant with Byteion's token-first approach.
import { useState } from 'react';
import { Button, Modal } from '@byteion/react/client';
type Size = 'sm' | 'md' | 'lg' | 'confirm' | 'form';
export default function ModalDemo() {
const [open, setOpen] = useState<Size | null>(null);
const close = () => setOpen(null);
return (
<div className="flex flex-wrap gap-3 p-6">
<Button variant="outline" onClick={() => setOpen('sm')}>Small</Button>
<Button variant="outline" onClick={() => setOpen('md')}>Medium</Button>
<Button variant="outline" onClick={() => setOpen('lg')}>Large</Button>
<Button tone="danger" onClick={() => setOpen('confirm')}>Delete confirmation</Button>
<Button onClick={() => setOpen('form')}>Form</Button>
<Modal open={open === 'sm'} onClose={close} size="sm" title="Small modal"
description="A focused 24rem-wide dialog. Useful for quick prompts." />
<Modal open={open === 'md'} onClose={close} size="md" title="Medium modal"
description="Default size for most use cases"
footer={<><Button variant="ghost" onClick={close}>Cancel</Button>
<Button onClick={close}>Confirm</Button></>}>
Body content for the medium modal. Best for short forms, confirmations
with extra detail, or summary previews.
</Modal>
<Modal open={open === 'lg'} onClose={close} size="lg" title="Large modal">
Up to 48rem wide — ideal for forms, tables, or extended content. Body scrolls if needed.
</Modal>
<Modal open={open === 'confirm'} onClose={close} size="sm" tone="danger"
title="Delete this item?"
description="This action cannot be undone. The item will be removed permanently."
footer={<><Button variant="ghost" onClick={close}>Cancel</Button>
<Button tone="danger" onClick={close}>Yes, delete</Button></>} />
<Modal open={open === 'form'} onClose={close} size="md" title="Invite teammate"
description="Send an invitation to your workspace"
footer={<><Button variant="ghost" onClick={close}>Cancel</Button>
<Button onClick={close}>Send invite</Button></>}>
<form className="space-y-4">
<label className="block space-y-1.5">
<span className="text-sm font-medium text-foreground">Email</span>
<input type="email" required placeholder="teammate@example.com"
className="h-10 w-full rounded-lg border border-border bg-background px-3.5 text-sm" />
</label>
<label className="block space-y-1.5">
<span className="text-sm font-medium text-foreground">Role</span>
<select className="h-10 w-full rounded-lg border border-border bg-background px-3.5 text-sm">
<option>Editor</option><option>Viewer</option><option>Admin</option>
</select>
</label>
</form>
</Modal>
</div>
);
}
<script setup lang="ts">
import { ref } from 'vue';
import { Button, Modal } from '@byteion/vue';
type Size = 'sm' | 'md' | 'lg' | 'confirm' | 'form';
const open = ref<Size | null>(null);
const close = () => (open.value = null);
</script>
<template>
<div class="flex flex-wrap gap-3 p-6">
<Button variant="outline" @click="open = 'sm'">Small</Button>
<Button variant="outline" @click="open = 'md'">Medium</Button>
<Button variant="outline" @click="open = 'lg'">Large</Button>
<Button tone="danger" @click="open = 'confirm'">Delete confirmation</Button>
<Button @click="open = 'form'">Form</Button>
<Modal :open="open === 'sm'" @close="close" size="sm" title="Small modal"
description="A focused 24rem-wide dialog. Useful for quick prompts." />
<Modal :open="open === 'md'" @close="close" size="md" title="Medium modal"
description="Default size for most use cases">
Body content for the medium modal. Best for short forms, confirmations, or summaries.
<template #footer>
<Button variant="ghost" @click="close">Cancel</Button>
<Button @click="close">Confirm</Button>
</template>
</Modal>
<Modal :open="open === 'lg'" @close="close" size="lg" title="Large modal">
Up to 48rem wide — ideal for forms, tables, or extended content.
</Modal>
<Modal :open="open === 'confirm'" @close="close" size="sm" tone="danger"
title="Delete this item?"
description="This action cannot be undone. The item will be removed permanently.">
<template #footer>
<Button variant="ghost" @click="close">Cancel</Button>
<Button tone="danger" @click="close">Yes, delete</Button>
</template>
</Modal>
<Modal :open="open === 'form'" @close="close" size="md" title="Invite teammate"
description="Send an invitation to your workspace">
<form class="space-y-4">
<label class="block space-y-1.5">
<span class="text-sm font-medium text-foreground">Email</span>
<input type="email" required placeholder="teammate@example.com"
class="h-10 w-full rounded-lg border border-border bg-background px-3.5 text-sm" />
</label>
<label class="block space-y-1.5">
<span class="text-sm font-medium text-foreground">Role</span>
<select class="h-10 w-full rounded-lg border border-border bg-background px-3.5 text-sm">
<option>Editor</option><option>Viewer</option><option>Admin</option>
</select>
</label>
</form>
<template #footer>
<Button variant="ghost" @click="close">Cancel</Button>
<Button @click="close">Send invite</Button>
</template>
</Modal>
</div>
</template>
Variants
Each variant communicates a different meaning. Choose based on priority, not aesthetics.
Single default variant.
Sizes
Size should match the importance and context of the component.
- sm — sm size.
- md — md size.
- lg — lg size.
- xl — xl size.
States
This component reacts seamlessly to the following interaction and data states:
- open — Component is expanded or visible.
- closed — Component is collapsed or hidden.
Anatomy
Usage guidelines
- Always provide a clear exit path — close button, Escape key, and backdrop click.
- Trap focus inside the modal while open.
- Scroll lock the body to prevent background scrolling.
- Only one modal should be open at a time.
- The Cancel button should be ghost/outline, Confirm should be primary or destructive.
Do and Don't
Do
- Trap focus while open.
- Provide close, Escape, and backdrop-click exits.
- Scroll lock the body.
- One modal at a time.
Don't
- Do not nest modals.
- Do not use for navigation.
- Do not remove the Escape exit.
- Do not open modals on page load.
Accessibility
Modal has role="dialog" and aria-modal="true". Title is linked via aria-labelledby. Focus is trapped and restored on close. Escape key dismisses.
Role:dialog
ARIA Attributes
aria-modalaria-labelledbyaria-describedbyaria-hidden
Keyboard Navigation
- Escape: Closes the modal (unless data-bi-modal-keyboard='false')
- Tab / Shift+Tab: Cycles focus within the modal
- Enter: Activates the focused control
Customization
Overlay uses --z-modal and backdrop blur. Dialog max-width and border-radius are customizable.
Compose with React cva()
Use the exportedcva() +cn() utilities to compose your own variant on top of the Byteion tokens.
import { cva, cn, type VariantProps } from '@byteion/react';
const myModal = cva('base classes…', {
variants: {
accent: { soft:'border-primary-200', loud:'border-primary-500' },
},
defaultVariants: { accent:'soft' },
});
type Props = React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof myModal>;
Compose with Vue cva()
Use the exportedcva() +cn() utilities inside standard Vue setup blocks.
<script setup lang="ts">
import { computed, useAttrs } from 'vue';
import { cva, type VariantProps } from '@byteion/vue';
interface Props { accent?:'soft' |'loud' }
const props = withDefaults(defineProps<Props>(), { accent:'soft' });
const attrs = useAttrs();
const myModal = cva('base classes…', {
variants: { accent: { soft:'border-primary-200', loud:'border-primary-500' } },
});
const cls = computed(() => myModal({ accent: props.accent, class: attrs.class as string | undefined }));
</script>
Runtime events
Byteion keeps framework APIs idiomatic: HTML uses DOM custom events, React uses callback props, and Vue uses emitted events / v-model updates.
HTML / JS events
bi:modal:hidebi:modal:show
React callbacks
onClose() => void
Vue emits
update:modelValue[v: boolean]update:open[v: boolean]close[]
Remote / AJAX
This component does not implement Byteion JS remote loading. Use normal HTML form submission, framework state, or your application data layer around it.
API Reference
Requires data-bi-modal="id" on the container. Toggle with data-bi-modal-toggle="id". Hide with data-bi-modal-hide.
HTML Data Attributes
data-bi-modaldata-bi-modal-contentdata-bi-modal-toggledata-bi-modal-hidedata-bi-backdrop
React Props · ModalProps
Extracted directly fromreact/src/components/Modal.tsx.
export interface ModalProps {
/** Controlled open state. */
open: boolean;
/** Fired when the user dismisses via Escape, backdrop click, or close button. */
onClose: () => void;
/** Block backdrop click + Escape from closing. */
staticBackdrop?: boolean;
/** Disable Escape (keeps backdrop click). */
keyboard?: boolean;
/** Backdrop visual treatment. */
backdrop?: 'clear' | 'soft';
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | 'full';
title?: ReactNode;
description?: ReactNode;
children?: ReactNode;
footer?: ReactNode;
className?: string;
role?: string;
componentName?: string;
contentRef?: React.Ref<HTMLDivElement>;
}
Vue Props · ModalProps
Extracted directly fromvue/src/components/Modal.vue.
export interface ModalProps {
/** v-model:open style. */
modelValue?: boolean;
/** Alias of modelValue — pass `open` if you don't use v-model. */
open?: boolean;
title?: string;
description?: string;
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | 'full';
staticBackdrop?: boolean;
keyboard?: boolean;
backdrop?: 'clear' | 'soft';
role?: string;
componentName?: string;
}
Real-world examples
See how this component composes in common application contexts.
In a form
Developer tips
Common mistakes
Production usage tips
1. Static HTML modals require @byteion/js for data-bi-modal behavior. React and Vue modals are self-contained — no JS runtime needed.
2. React and Vue modals should be controlled through state: open/onClose in React; v-model:open or :open + @close in Vue.
3. Test keyboard navigation with Tab, Shift+Tab, Escape, Enter, and Space. Verify focus does not escape to elements behind the backdrop.
4. Test focus restoration after close — focus should return to the trigger button, not to the top of the page.
5. Test screen reader announcements: modal should announce title, description, and role="dialog" on open.
6. Test body scroll lock on all viewport sizes. On iOS Safari, overflow: hidden may need position: fixed or touch-action: none as a companion.
7. Test dark mode and branded themes — the dialog uses bg-card and shadow-modal tokens that respond to theme changes.
Premium examples
Advanced patterns available in Byteion Pro:
Related components
Unlock Premium Patterns
Get access to copy-paste ready Pro blocks, SaaS templates, and advanced data-tables built with this component.
Explore Pro Components