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 into document.body (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="Rendered in document.body" 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.
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.
function example() { const [open, setOpen] = useState(false); const closeModal = () => setOpen(false); return ( <> <Button color="success" onClick={() => setOpen(true)}> Show Modal </Button> <Modal isActive={open}> <Modal.Background onClick={closeModal} /> <Modal.Card> <Modal.Card.Head> <Modal.Card.Title>Compound Component Modal</Modal.Card.Title> <Modal.Close onClick={closeModal} /> </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}> <Modal.Background onClick={closeModal} /> <Modal.Content> <Box> <Title size="4">Custom Content</Title> <Paragraph>Using compound components for full control!</Paragraph> </Box> </Modal.Content> <Modal.Close variant="floating" onClick={closeModal} /> </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}. Only the topmost open modal responds, so Escape closes one layer at a time rather than dismissing a whole stack. - 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 enabled focusable element (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 topmost modal while it is open, so the keyboard order agrees with what
aria-modal="true"tells assistive technology. Note thataria-modaldoes not by itself make background content inert, and noinertattribute is applied — background content remains reachable by pointer and by browser UI such as find-in-page. - The modal background closes the modal on click (
onCloserequired). - Close buttons are provided for both modal-card and modal-content variants.
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 | — | Callback invoked to request modal close (background or close button). |
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 modal responds, so Escape closes one layer at a time. |
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 document.body; 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.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.
Modal.Background
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes. |
... | 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. |
... | All standard <button> attributes | — |
Compound component props
| Component | Description |
|---|---|
Modal.Background | Modal background overlay (supports onClick) |
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. 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 |