Skip to main content

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 modalCardTitle and modalCardFoot props for simple modals
  • Compound Components API: Use Modal.Background, Modal.Card, Modal.Content, and Modal.Close for full control and better readability
info

Use Modal for forms or custom content — an empty overlay you build the contents of. For a ready-made confirm or alert, use Dialog instead. For a panel anchored to the button that opens it, use Popover. Modal supports card-style layouts (header/body/footer) or arbitrary content modals.


Import​

import { Modal } from '@allxsmith/bestax-bulma';

Usage​

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>
    </>
  );
}


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>
    </>
  );
}


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>
    </>
  );
}


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" and aria-modal="true" while active — pass an explicit role to override (e.g. Dialog passes role="presentation" on the Modal it wraps so its own role="alertdialog" is the only one announced).
  • Give the dialog an accessible name. With the legacy API, modalCardTitle is wired up as aria-labelledby for you. With the compound API, pass your own aria-label or aria-labelledby on <Modal> — a role="dialog" with no name is announced as just "dialog". An explicit aria-labelledby always wins over the generated one.
  • Escape closes the modal (calls onClose) by default; opt out with closeOnEscape={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 with Dialog, Sidebar and Loading, 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 that aria-modal does not by itself make background content inert, and no inert attribute 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 (onClose required).
  • Close buttons are provided for both modal-card and modal-content variants.
note

Always provide an onClose handler for accessibility and to allow users to dismiss the modal.


  • 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​

Pro Tip

You can use all Bulma helper props with <Modal /> for powerful utility-based styling.


Props​

Main Modal Props​

PropTypeDefaultDescription
activebooleanfalseWhether the modal is open/visible.
isActivebooleanfalseAlias for active. Whether the modal is open/visible.
onClose() => void—Callback invoked to request modal close (background or close button).
classNamestring—Additional CSS classes for the modal.
textColorBulma color | 'inherit' | 'current'—Text color for modal content.
bgColorBulma color | 'inherit' | 'current'—Background color for modal content.
modalCardTitleReact.ReactNode—Title/header for modal-card variant. (Legacy API only)
modalCardFootReact.ReactNode—Footer for modal-card variant. (Legacy API only)
type'card' | 'content'autoModal style: 'card' for modal-card, 'content' for modal-content. (Legacy API only)
childrenReact.ReactNode—Modal body/content or compound components.
closeOnEscapebooleantrueClose the modal when the Escape key is pressed (calls onClose). Only the topmost open modal responds, so Escape closes one layer at a time.
lockScrollbooleantrueLock 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.
portalboolean | string | HTMLElementfalseRenders 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.
refReact.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​

PropTypeDefaultDescription
classNamestring—Additional CSS classes.
...All standard <div> attributes—

Modal.Content​

PropTypeDefaultDescription
classNamestring—Additional CSS classes.
...All standard <div> attributes—

Modal.Card​

PropTypeDefaultDescription
classNamestring—Additional CSS classes.
...All standard <div> attributes—

Modal.Card.Head​

PropTypeDefaultDescription
classNamestring—Additional CSS classes.
...All standard HTML attributes—

Modal.Card.Title​

PropTypeDefaultDescription
classNamestring—Additional CSS classes.
...All standard <p> attributes—

Modal.Card.Body​

PropTypeDefaultDescription
classNamestring—Additional CSS classes.
...All standard HTML attributes—

Modal.Card.Foot​

PropTypeDefaultDescription
classNamestring—Additional CSS classes.
...All standard HTML attributes—

Modal.Close​

PropTypeDefaultDescription
classNamestring—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-labelstring'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​

ComponentDescription
Modal.BackgroundModal background overlay (supports onClick)
Modal.ContentModal content wrapper
Modal.CardModal card wrapper
Modal.Card.HeadModal card header
Modal.Card.TitleModal card title
Modal.Card.BodyModal card body
Modal.Card.FootModal card footer
Modal.CloseModal 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 VariableSass VariableDefault
--bulma-modal-z$modal-z40
--bulma-modal-background-background-color$modal-background-background-colorhsla(var(--bulma-scheme-h), var(--bulma-scheme-s), var(--bulma-scheme-invert-l), 0.86)
--bulma-modal-content-width$modal-content-width40rem
--bulma-modal-content-margin-mobile$modal-content-margin-mobile1.25rem
--bulma-modal-content-spacing-mobile$modal-content-spacing-mobile10rem
--bulma-modal-content-spacing-tablet$modal-content-spacing-tablet2.5rem
--bulma-modal-close-dimensions$modal-close-dimensions2.5rem
--bulma-modal-close-right$modal-close-right1.25rem
--bulma-modal-close-top$modal-close-top1.25rem
--bulma-modal-card-spacing$modal-card-spacing2.5rem
--bulma-modal-card-head-background-color$modal-card-head-background-colorvar(--bulma-scheme-main)
--bulma-modal-card-head-padding$modal-card-head-padding2rem
--bulma-modal-card-head-radius$modal-card-head-radiusvar(--bulma-radius-large)
--bulma-modal-card-title-color$modal-card-title-colorvar(--bulma-text-strong)
--bulma-modal-card-title-line-height$modal-card-title-line-height1
--bulma-modal-card-title-size$modal-card-title-sizevar(--bulma-size-4)
--bulma-modal-card-foot-background-color$modal-card-foot-background-colorvar(--bulma-scheme-main-bis)
--bulma-modal-card-foot-radius$modal-card-foot-radiusvar(--bulma-radius-large)
--bulma-modal-card-body-background-color$modal-card-body-background-colorvar(--bulma-scheme-main)
--bulma-modal-card-body-padding$modal-card-body-padding2rem