Modal
Overview
The Modal component provides an empty, accessible overlay for arbitrary content — for a ready-made confirm or alert, reach for Dialog instead.
It supports both Bulma's modal-card and modal-content variants, with options for title, footer, custom content, color helpers, and close callbacks. Easily control visibility via the active or isActive prop and handle closing with onClose.
The Modal component supports two APIs:
- Legacy Props API: Use
modalCardTitleandmodalCardFootprops for simple modals - Compound Components API: Use
Modal.Background,Modal.Card,Modal.Content, andModal.Closefor full control and better readability
Import
import { Modal } from '@allxsmith/bestax-bulma';
Usage
Modal Card (with title and footer)
To display a modal dialog with a header and footer, use the Modal component with the active prop to control visibility, and provide modalCardTitle and modalCardFoot for the card layout. The onClose prop handles closing the modal, and you can use Bulma color helpers for further customization. This pattern is ideal for forms or any content that requires user attention in a focused overlay.
function example() { const [open, setOpen] = useState(false); return ( <> <Button color="success" onClick={() => setOpen(true)}> Show Modal </Button> <Modal active={open} onClose={() => setOpen(false)} modalCardTitle="Modal Card Title" modalCardFoot={ <Buttons> <Button color="success">Save</Button> <Button>Cancel</Button> </Buttons> } > Modal card body content goes here... </Modal> </> ); }
Modal Card (title only)
Provide only the modalCardTitle prop to display a modal card with a header and body, but no footer. This is useful for simple dialogs or information popups that do not require actions in the footer area.
function example() { const [open, setOpen] = useState(false); return ( <> <Button color="success" onClick={() => setOpen(true)}> Show Modal </Button> <Modal active={open} onClose={() => setOpen(false)} modalCardTitle="Modal Card Title Only" > Modal body content... </Modal> </> ); }
Modal Card (footer only)
Provide only the modalCardFoot prop to display a modal card with a footer and body, but no header. This is useful for footer-driven actions that do not require a title.
function example() { const [open, setOpen] = useState(false); return ( <> <Button color="success" onClick={() => setOpen(true)}> Show Modal </Button> <Modal active={open} onClose={() => setOpen(false)} modalCardFoot={<Button color="success">Save</Button>} > Modal body content... </Modal> </> ); }
Modal Content (no card title or footer)
Omit both modalCardTitle and modalCardFoot to render a modal with only custom content. This is ideal for custom layouts, or when you want full control over the modal's appearance.
function example() { const [open, setOpen] = useState(false); return ( <> <Button color="success" onClick={() => setOpen(true)}> Show Modal </Button> <Modal active={open} onClose={() => setOpen(false)}> <Box> <Title size="4">Custom Content</Title> <Paragraph>Put any content here!</Paragraph> </Box> </Modal> </> ); }
Explicit Modal Types
Force content style
Set the type prop to content to force the modal to use the content style, regardless of whether a title or footer is provided. This is useful for custom layouts or when you want to avoid the card structure.
function example() { const [open, setOpen] = useState(false); return ( <> <Button color="success" onClick={() => setOpen(true)}> Show Modal </Button> <Modal active={open} onClose={() => setOpen(false)} type="content"> <div>Custom content modal (forced type="content")</div> </Modal> </> ); }
Force card style
Set the type prop to card to force the modal to use the card style, even if no title or footer is provided. This is helpful for consistent styling across your app.
function example() { const [open, setOpen] = useState(false); return ( <> <Button color="success" onClick={() => setOpen(true)}> Show Modal </Button> <Modal active={open} onClose={() => setOpen(false)} type="card"> Modal card body (forced type="card") </Modal> </> ); }
Portal
Set portal to render the modal through a portal (into document.body by default, or a custom target) instead of inline, so it isn't clipped by an ancestor with overflow: hidden, filter or transform. It's opt-in: omit it to keep the current inline behavior.
function example() { const [open, setOpen] = useState(false); return ( <> <Button color="success" onClick={() => setOpen(true)}> Show Portaled Modal </Button> <Modal active={open} onClose={() => setOpen(false)} modalCardTitle="Portaled Modal" portal > This modal is portaled, so it escapes any ancestor with an `overflow: hidden` style. </Modal> </> ); }
portal also accepts a document.querySelector selector string or an HTMLElement to target a specific container. With portal set to true, a ConfigProvider portalContainer takes the place of document.body.
It's SSR-safe: a portal has no server-rendered counterpart, so the modal renders inline on the server and during hydration, then moves into the portal once the client takes over. Hydration matches instead of trading an SSR crash for a hydration mismatch.
Forwarded ref
Modal forwards a ref to the root .modal element, so you can measure it or move focus without wrapping it.
function example() { const [open, setOpen] = React.useState(false); const [width, setWidth] = React.useState<number | null>(null); const modalRef = React.useRef<HTMLDivElement>(null); return ( <> <Button color="info" onClick={() => setOpen(true)}> Open and measure </Button> <Modal active={open} onClose={() => setOpen(false)} ref={modalRef} modalCardTitle="Measured through the ref" > <Button onClick={() => setWidth(modalRef.current?.getBoundingClientRect().width ?? null) } > Measure </Button> <p>Width: {width === null ? '—' : `${Math.round(width)}px`}</p> </Modal> </> ); }
The ref keeps working with portal. Moving into the portal remounts the subtree, so the ref detaches from the inline node and re-points at the portaled one rather than leaving you holding a detached element.
Callback refs behave the same on React 18 and 19: if your callback returns a cleanup function, it runs on detach instead of the ref being called with null.
Compound (dot-notation) usage
Modal.Card with compound components
The compound components API provides more flexibility and better readability for complex modals. Use Modal.Background, Modal.Card, and related sub-components for full control over the modal structure.
Pass onClose to the Modal here too. Escape calls it, and so does Modal.Close, unless you give the button an onClick of your own: yours then replaces the close, so call onClose from it if the button should still close the modal. Modal.Background doesn't call onClose on its own, so a click on the background closes the modal only once you pass it onClick={onClose}.
function example() { const [open, setOpen] = useState(false); const closeModal = () => setOpen(false); return ( <> <Button color="success" onClick={() => setOpen(true)}> Show Modal </Button> <Modal isActive={open} onClose={closeModal} aria-labelledby="compound-modal-title" > <Modal.Background onClick={closeModal} /> <Modal.Card> <Modal.Card.Head> <Modal.Card.Title id="compound-modal-title"> Compound Component Modal </Modal.Card.Title> <Modal.Close /> </Modal.Card.Head> <Modal.Card.Body> This modal uses the compound components API for better control and readability. </Modal.Card.Body> <Modal.Card.Foot> <Buttons> <Button color="success" onClick={closeModal}> Save </Button> <Button onClick={closeModal}>Cancel</Button> </Buttons> </Modal.Card.Foot> </Modal.Card> </Modal> </> ); }
Modal.Content with compound components
For custom content modals, use Modal.Content with Modal.Close (use variant="floating" for the overlay close button):
function example() { const [open, setOpen] = useState(false); const closeModal = () => setOpen(false); return ( <> <Button color="success" onClick={() => setOpen(true)}> Show Modal </Button> <Modal isActive={open} onClose={closeModal} aria-labelledby="custom-content-title" > <Modal.Background onClick={closeModal} /> <Modal.Content> <Box> <Title size="4" id="custom-content-title"> Custom Content </Title> <Paragraph>Using compound components for full control!</Paragraph> </Box> </Modal.Content> <Modal.Close variant="floating" /> </Modal> </> ); }
Accessibility
- The modal root gets
role="dialog"andaria-modal="true"while active — pass an explicitroleto override (e.g.Dialogpassesrole="presentation"on theModalit wraps so its ownrole="alertdialog"is the only one announced). - Give the dialog an accessible name. With the legacy API,
modalCardTitleis wired up asaria-labelledbyfor you. With the compound API, pass your ownaria-labeloraria-labelledbyon<Modal>— arole="dialog"with no name is announced as just "dialog". An explicitaria-labelledbyalways wins over the generated one. - Escape closes the modal (calls
onClose) by default; opt out withcloseOnEscape={false}. One Escape closes the topmost overlay only, whichever open Modal, Dialog, Sidebar or full-page Loading opened last, and the next Escape closes the one under it. A modal on top withcloseOnEscape={false}still keeps Escape from the layers under it. A Toast waits until no overlay is open. - The modal calls
preventDefault()on the Escape it closes on, so a listener of your own that runs after it can tell the key was used. - A popup inside the modal takes its own Escape first. With an Autocomplete list, a Dropdown menu, a Popover or a date picker's calendar open, the first Escape closes that and the next one closes the modal.
- A popup of your own inside a modal should call
preventDefault()on the Escape it closes on. The modal leaves an Escape that was prevented alone, whether your popup hears it in a ReactonKeyDownor in a listener ondocument. Call it only when the popup is open and the key was pressed in it, so an Escape with nothing to close still closes the modal, and so does one pressed in a modal opened over your popup. - Body scroll is locked while the modal is active; opt out with
lockScroll={false}. The lock is ref-counted and shared withDialog,SidebarandLoading, so whichever overlay closes first doesn't unlock the page underneath one that is still open. - Focus moves to the first tab stop (or the modal itself) on open, and is restored to the previously focused element on close — unless another modal is still open on top, in which case that one keeps focus.
- Tab and Shift+Tab cycle within the modal while it is the topmost modal overlay, so the keyboard order agrees with what
aria-modal="true"tells assistive technology. They wrap at the same tab stopsuseFocusTrapdoes, so disabled, hidden andtabIndex={-1}elements are skipped, and so is inert content, such as a closed Sidebar inside the modal, and the content of a closed<details>. A radio group counts once. A Tab from outside the modal brings focus back in, and so does a Tab the browser takes out anyway, as Safari does past links when its settings say to. With another Modal or Dialog, a Sidebar with an overlay or a full-page Loading opened over it, the modal leaves Tab alone until that one closes. A Sidebar without an overlay isn't modal: opened over the modal, it takes Escape first, but the modal keeps holding Tab and keeps focus, so the sidebar doesn't take focus when it opens, and a Tab from its controls comes back into the modal. A focus trap of your own inside it, such as a portaled panel's, keeps a Tab it callspreventDefault()on, and the modal leaves focus where your trap put it. Note thataria-modaldoes not by itself make background content inert, and noinertattribute is applied, so background content remains reachable by pointer and by browser UI such as find-in-page. - In the props form, the background closes the modal on click, and a close button is provided for both modal-card and modal-content variants (both call
onClose, so pass it). - In the compound form,
Modal.Closecalls the modal'sonCloseunless you give it anonClickof your own, andModal.Backgroundcloses the modal only when you pass itonClick={onClose}.
Always provide an onClose handler for accessibility and to allow users to dismiss the modal.
Related Components
Dialog: Confirm/alert — use this unless you need arbitrary modal content.Popover: A non-modal panel anchored to its trigger, for content that shouldn't block the page.Button: Use for actions in modal footers.Field,Input: For forms inside modals.- Helper Props: All Bulma utility helpers can be used.
Additional Resources
You can use all Bulma helper props with <Modal /> for powerful utility-based styling.
Props
Main Modal Props
| Prop | Type | Default | Description |
|---|---|---|---|
active | boolean | false | Whether the modal is open/visible. |
isActive | boolean | false | Alias for active. Whether the modal is open/visible. |
onClose | () => void | — | Called to ask for the modal to close. The props form calls it from the background and the close button it renders. In the compound form, Modal.Close calls it unless you give it an onClick of your own, and Modal.Background doesn't call it on its own: pass it onClick={onClose} for a click on the background to close the modal. Escape calls it in both forms unless closeOnEscape is false. |
className | string | — | Additional CSS classes for the modal. |
textColor | Bulma color | 'inherit' | 'current' | — | Text color for modal content. |
bgColor | Bulma color | 'inherit' | 'current' | — | Background color for modal content. |
modalCardTitle | React.ReactNode | — | Title/header for modal-card variant. (Legacy API only) |
modalCardFoot | React.ReactNode | — | Footer for modal-card variant. (Legacy API only) |
type | 'card' | 'content' | auto | Modal style: 'card' for modal-card, 'content' for modal-content. (Legacy API only) |
children | React.ReactNode | — | Modal body/content or compound components. |
closeOnEscape | boolean | true | Close the modal when the Escape key is pressed (calls onClose). Only the topmost open overlay responds, whichever Modal, Dialog, Sidebar or full-page Loading opened last, so Escape closes one layer at a time. The modal marks the Escape it closes on as used with preventDefault(), and a Toast waits until no overlay is open. Turned off, a modal on top still keeps Escape from the layers under it. A popup inside it, such as a Dropdown's menu or a date picker's calendar, takes its own Escape first, and so does content that calls preventDefault() on it. |
lockScroll | boolean | true | Lock body scroll while the modal is active. Ref-counted and shared with Dialog, Sidebar and Loading, so whichever overlay closes first does not unlock the page underneath one that is still open. |
portal | boolean | string | HTMLElement | false | Renders the modal into a portal target instead of inline, so it isn't clipped by an ancestor with overflow: hidden, filter or transform. true portals to the portalContainer a ConfigProvider sets, document.body by default; a string is used as a document.querySelector selector; an element is used directly. Renders inline on the server and while hydrating, moving into the portal once the client takes over, so hydration matches. |
ref | React.Ref<HTMLDivElement> | — | Ref forwarded to the root .modal element. |
... | All standard <div> attributes and Bulma helper props | — | See Helper Props |
Subcomponents:
Modal.Background: Modal.Background - Renders the modal background overlay. Pass itonClick={onClose}for a click on it to close the modal.Modal.Content: Modal.Content - Renders modal content wrapper for custom content.Modal.Card: Modal.Card - Renders modal card wrapper with compound components. Use with Modal.Card.Head, Modal.Card.Title, Modal.Card.Body, and Modal.Card.Foot.Modal.Card.Head: Modal.Card.Head - Renders modal card header section.Modal.Card.Title: Modal.Card.Title - Renders modal card title.Modal.Card.Body: Modal.Card.Body - Renders modal card body section.Modal.Card.Foot: Modal.Card.Foot - Renders modal card footer section.Modal.Close: Modal.Close - Renders modal close button with two variant styles. It calls the modal'sonCloseunless given anonClickof its own.
Modal.Background
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes. |
onClick | React.MouseEventHandler<HTMLDivElement> | — | Called when the background is clicked. The background doesn't call the modal's onClose on its own, so pass onClick={onClose} for a click on it to close the modal. |
... | All standard <div> attributes | — |
Modal.Content
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes. |
... | All standard <div> attributes | — |
Modal.Card
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes. |
... | All standard <div> attributes | — |
Modal.Card.Head
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes. |
... | All standard HTML attributes | — |
Modal.Card.Title
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes. |
... | All standard <p> attributes | — |
Modal.Card.Body
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes. |
... | All standard HTML attributes | — |
Modal.Card.Foot
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes. |
... | All standard HTML attributes | — |
Modal.Close
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes. |
size | 'small' | 'medium' | 'large' | 'large' | Size of the close button (only applies to 'floating' variant). |
variant | 'delete' | 'floating' | 'delete' | Button variant. 'delete' (default) for modal card headers, 'floating' for overlay close button. |
type | 'button' | 'submit' | 'reset' | 'button' | Button type. Defaults to 'button', so a close button inside a form does not submit it. Pass 'submit' or 'reset' and yours is used; any other value, or a spread carrying type: undefined, renders 'button'. |
aria-label | string | 'close' | Accessible name. Pass your own to replace it. An empty one, or a spread carrying 'aria-label': undefined, keeps the default rather than leaving the button unnamed. |
onClick | React.MouseEventHandler<HTMLButtonElement> | — | Called when the button is clicked. Without one, the button calls the onClose of the Modal around it. Yours replaces that rather than running before it, so call onClose from it if the button should still close the modal. |
... | All standard <button> attributes | — |
Compound component props
| Component | Description |
|---|---|
Modal.Background | Modal background overlay. Pass onClick={onClose} for a click on it to close the modal |
Modal.Content | Modal content wrapper |
Modal.Card | Modal card wrapper |
Modal.Card.Head | Modal card header |
Modal.Card.Title | Modal card title |
Modal.Card.Body | Modal card body |
Modal.Card.Foot | Modal card footer |
Modal.Close | Modal close button. Calls the modal's onClose unless given an onClick of its own. Props: variant ('delete' [default] for card headers, 'floating' for overlay), size ('small', 'medium', 'large') for floating variant |
CSS & Sass Variables
Modal registers these variables on its own .modal element. Override them there (or via className) — a value set on an ancestor is only inherited, and loses to the component-level declaration. See Theme.
| CSS Variable | Sass Variable | Default |
|---|---|---|
--bulma-modal-z | $modal-z | 40 |
--bulma-modal-background-background-color | $modal-background-background-color | hsla(var(--bulma-scheme-h), var(--bulma-scheme-s), var(--bulma-scheme-invert-l), 0.86) |
--bulma-modal-content-width | $modal-content-width | 40rem |
--bulma-modal-content-margin-mobile | $modal-content-margin-mobile | 1.25rem |
--bulma-modal-content-spacing-mobile | $modal-content-spacing-mobile | 10rem |
--bulma-modal-content-spacing-tablet | $modal-content-spacing-tablet | 2.5rem |
--bulma-modal-close-dimensions | $modal-close-dimensions | 2.5rem |
--bulma-modal-close-right | $modal-close-right | 1.25rem |
--bulma-modal-close-top | $modal-close-top | 1.25rem |
--bulma-modal-card-spacing | $modal-card-spacing | 2.5rem |
--bulma-modal-card-head-background-color | $modal-card-head-background-color | var(--bulma-scheme-main) |
--bulma-modal-card-head-padding | $modal-card-head-padding | 2rem |
--bulma-modal-card-head-radius | $modal-card-head-radius | var(--bulma-radius-large) |
--bulma-modal-card-title-color | $modal-card-title-color | var(--bulma-text-strong) |
--bulma-modal-card-title-line-height | $modal-card-title-line-height | 1 |
--bulma-modal-card-title-size | $modal-card-title-size | var(--bulma-size-4) |
--bulma-modal-card-foot-background-color | $modal-card-foot-background-color | var(--bulma-scheme-main-bis) |
--bulma-modal-card-foot-radius | $modal-card-foot-radius | var(--bulma-radius-large) |
--bulma-modal-card-body-background-color | $modal-card-body-background-color | var(--bulma-scheme-main) |
--bulma-modal-card-body-padding | $modal-card-body-padding | 2rem |