Components

Modal

Modals are used to overlay content above an interface. They are intended to capture the user's attention in order to inform or shift focus to a pertinent task.

New Modal: We've released Modal V2 with improved accessibility, better TypeScript support, and modern React patterns. The legacy Modal (V1) is deprecated and will be removed in a future version. See the Usage tab for migration guidance.

Modal V2 provides comprehensive accessibility features, keyboard navigation, and focus management out of the box. It supports both controlled and uncontrolled state modes, and includes responsive bottom sheet layout on mobile devices.

import { Modal, ModalHeader, ModalBody, ModalFooter } from '@sproutsocial/racine/modal/v2'
() => {
return (
<Box>
<Modal
modalTrigger={<Button appearance="primary">Open modal</Button>}
aria-label="Assign Chatbot"
>
<ModalHeader
title="Assign Chatbot"
subtitle="The chatbot will respond to customers from this profile."
/>
<ModalBody>
<Text fontSize={300} color="text.body">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem
ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum
dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua.
</Text>
</ModalBody>
<ModalFooter
cancelButton={<Button appearance="unstyled">Go back</Button>}
primaryButton={<Button appearance="primary" px={500}>Submit</Button>}
/>
</Modal>
</Box>
)
}

Prefer the uncontrolled pattern whenever the trigger can live near the modal in the component tree. Pass your trigger element to the modalTrigger prop and Modal handles everything else — ARIA attributes, focus management, and open/close state — automatically. No extra hooks or refs required.

import { Modal, ModalHeader, ModalBody, ModalFooter } from '@sproutsocial/racine/modal/v2'
() => {
return (
<Box>
<Modal
modalTrigger={<Button appearance="primary">Open modal</Button>}
aria-label="Example Modal"
>
<ModalHeader
title="Modal Title"
subtitle="Trigger and accessibility are handled automatically."
/>
<ModalBody>
<Text fontSize={300} color="text.body">
The trigger button's ARIA attributes (aria-haspopup, aria-expanded,
aria-controls) and focus restoration are managed by the Modal
component with no additional setup needed.
</Text>
</ModalBody>
<ModalFooter
cancelButton={<Button appearance="unstyled">Cancel</Button>}
primaryButton={<Button appearance="primary">Confirm</Button>}
/>
</Modal>
</Box>
)
}

Controlled Modal

When the trigger cannot live near the modal — for example, a button in a table row, list item, or page header — use the controlled pattern with open and onOpenChange. You are responsible for applying ARIA attributes to the trigger and restoring focus when the modal closes. Use one of the utilities below to handle this correctly.

The useModalExternalTrigger hook is the recommended approach for controlled modals. It provides:

  • triggerRef — attach to the trigger element so focus returns to it when the modal closes
  • triggerProps(isOpen) — returns aria-haspopup, aria-expanded, and aria-controls to spread onto the trigger
  • onCloseAutoFocus — pass to Modal to automatically restore focus to the trigger on close
import { Modal, ModalHeader, ModalBody, ModalFooter, useModalExternalTrigger } from '@sproutsocial/racine/modal/v2'
() => {
const [isOpen, setIsOpen] = useState(false);
const { triggerRef, triggerProps, onCloseAutoFocus } = useModalExternalTrigger();
return (
<Box>
<Button
ref={triggerRef}
{...triggerProps(isOpen)}
appearance="primary"
onClick={() => setIsOpen(true)}
>
Open modal
</Button>
<Modal
open={isOpen}
onOpenChange={setIsOpen}
onCloseAutoFocus={onCloseAutoFocus}
aria-label="Controlled Modal"
>
<ModalHeader
title="Controlled Modal"
subtitle="Trigger and modal are in separate parts of the component tree."
/>
<ModalBody>
<Text fontSize={300} color="text.body">
The trigger button above is outside the Modal component tree but has
the correct ARIA attributes applied. When this modal closes, focus
automatically returns to the trigger.
</Text>
</ModalBody>
<ModalFooter
cancelButton={<Button appearance="unstyled">Cancel</Button>}
primaryButton={<Button appearance="primary">Confirm</Button>}
/>
</Modal>
</Box>
)
}
ModalExternalTrigger component

When the trigger is specifically a Seeds Button, ModalExternalTrigger provides the same ARIA attributes without needing to spread hook props. Focus restoration still requires passing onCloseAutoFocus manually.

import { Modal, ModalHeader, ModalBody, ModalFooter, ModalExternalTrigger } from '@sproutsocial/racine/modal/v2'
() => {
const [isOpen, setIsOpen] = useState(false);
const triggerRef = useRef(null);
return (
<Box>
<ModalExternalTrigger
ref={triggerRef}
isOpen={isOpen}
onTrigger={() => setIsOpen(true)}
appearance="primary"
>
Open modal
</ModalExternalTrigger>
<Modal
open={isOpen}
onOpenChange={setIsOpen}
onCloseAutoFocus={(e) => {
e.preventDefault();
triggerRef.current?.focus();
}}
aria-label="Controlled Modal"
>
<ModalHeader
title="Controlled Modal"
subtitle="Using ModalExternalTrigger component."
/>
<ModalBody>
<Text fontSize={300} color="text.body">
ModalExternalTrigger is a Seeds Button pre-configured with
aria-haspopup, aria-expanded, and aria-controls attributes.
</Text>
</ModalBody>
<ModalFooter
cancelButton={<Button appearance="unstyled">Cancel</Button>}
primaryButton={<Button appearance="primary">Confirm</Button>}
/>
</Modal>
</Box>
)
}
useModalTriggerProps (low-level)

If the trigger is not a Seeds Button (e.g., a custom component or anchor element), use useModalTriggerProps to get just the ARIA attributes. You are responsible for attaching a ref and implementing onCloseAutoFocus manually.

import { Modal, ModalHeader, ModalBody, ModalFooter, useModalTriggerProps } from '@sproutsocial/racine/modal/v2'
() => {
const [isOpen, setIsOpen] = useState(false);
const triggerRef = useRef(null);
const triggerProps = useModalTriggerProps(isOpen);
return (
<Box>
<Button
ref={triggerRef}
{...triggerProps}
appearance="primary"
onClick={() => setIsOpen(true)}
>
Open modal
</Button>
<Modal
open={isOpen}
onOpenChange={setIsOpen}
onCloseAutoFocus={(e) => {
e.preventDefault();
triggerRef.current?.focus();
}}
aria-label="Controlled Modal"
>
<ModalHeader title="Controlled Modal" />
<ModalBody>
<Text fontSize={300} color="text.body">
useModalTriggerProps returns only the ARIA attributes. Use this
when you need to apply them to a non-Button trigger element.
</Text>
</ModalBody>
<ModalFooter
primaryButton={<Button appearance="primary">Close</Button>}
/>
</Modal>
</Box>
)
}

Modal V2 consists of several components that work together:

The main Modal component that wraps all modal content. It handles accessibility, focus management, and state.

Note: This component extends Radix UI Dialog primitives to provide accessible dialog functionality.

NameTypeDefaultDescriptionRequired?
children
ReactNode
Modal content. Typically includes ModalHeader, ModalBody, and ModalFooter components.
modalTrigger
ReactElement
React element (typically a Button) that triggers the modal when clicked. Used for uncontrolled modals.
open
boolean
Controls whether the modal is open (controlled mode). Use with onOpenChange.
defaultOpen
boolean
Default open state for uncontrolled mode. Use with modalTrigger.
onOpenChange
(open: boolean) => void
Callback fired when the open state changes. Receives the new open state.
aria-label
string
Accessible label for the modal dialog. Required for screen readers.
title
string
Modal title. Automatically creates a ModalHeader if provided.
subtitle
string
Modal subtitle. Automatically creates a ModalHeader if provided.
description
string
Modal description. Automatically wrapped in ModalDescription for accessibility.
draggable
boolean
Enable draggable functionality. When true, the modal header becomes a drag handle.
showOverlay
boolean
Whether to show the background overlay. Defaults to true.
actions
Array<ModalActionConfig>
Array of action configurations for the floating action rail.
closeButtonAriaLabel
string
Accessible label for the close button. Defaults to 'Close'.
data
Record<string, any>
Additional data attributes to pass to the modal content.
onEscapeKeyDown
(e: KeyboardEvent) => void
Callback fired when the Escape key is pressed while the modal is open. Call `e.preventDefault()` to prevent the modal from closing.
onInteractOutside
(e: PointerEvent) => void
Callback fired when the user interacts outside the modal (overlay click). Call `e.preventDefault()` to prevent the modal from closing. Not available when `draggable` is true.
onPointerDownOutside
(e: PointerEvent) => void
Callback fired on pointer down events outside the modal.
onFocusOutside
(e: FocusEvent) => void
Callback fired when focus moves outside the modal.
onOpenAutoFocus
(e: Event) => void
Callback fired when the modal opens and focus is about to move into it. Call `e.preventDefault()` to prevent auto-focus.
onCloseAutoFocus
(e: Event) => void
Callback fired when the modal closes and focus is about to restore. Call `e.preventDefault()` to handle focus restoration manually.
disableEscapeKeyClose
boolean
Prevents the modal from closing when pressing the Escape key. The `onEscapeKeyDown` handler will still fire if provided.
disableOutsideClickClose
boolean
Prevents the modal from closing when clicking outside. The `onInteractOutside` handler will still fire if provided. Not available when `draggable` is true.
closeButtonProps
object
Props to customize the close button including `onClick`, `aria-label`, `id`, `disabled`, and other button HTML attributes.
zIndex
number
Controls the z-index CSS property. Defaults to 6 to match Modal V1.

ModalHeader

Renders the modal's header with title and optional subtitle. Automatically created when title or subtitle props are provided to the root Modal component.

Note: This component extends Radix UI Dialog.Title and Dialog.Description primitives for accessible header content. For custom header layouts with complex content or actions, use ModalCustomHeader instead.

import { Modal, ModalHeader, ModalBody, ModalFooter } from '@sproutsocial/racine/modal/v2'
() => {
return (
<Box>
<Modal
modalTrigger={<Button appearance="primary">Open modal with header</Button>}
aria-label="Example Modal"
>
<ModalHeader
title="Modal Title"
subtitle="This is a subtitle that provides additional context"
/>
<ModalBody>
<Text fontSize={300} color="text.body">
Modal content goes here. The header above was created using the ModalHeader component.
</Text>
</ModalBody>
<ModalFooter
primaryButton={<Button appearance="primary">Close</Button>}
/>
</Modal>
</Box>
)
}
NameTypeDefaultDescriptionRequired?
title
string
Modal title text displayed as a headline
subtitle
string
Modal subtitle text. Automatically wrapped in Dialog.Description for accessibility.
titleProps
Omit< React.ComponentPropsWithoutRef<typeof Dialog.Title>, "asChild" | "children" >
Additional props for Dialog.Title when title is provided
subtitleProps
Omit< React.ComponentPropsWithoutRef<typeof Dialog.Description>, "asChild" | "children" >
Additional props for Dialog.Description when subtitle is provided

ModalBody

Renders the scrollable main content area of the modal between the header and footer.

import { Modal, ModalHeader, ModalBody, ModalFooter } from '@sproutsocial/racine/modal/v2'
() => {
return (
<Box>
<Modal
modalTrigger={<Button appearance="primary">Open modal with body content</Button>}
aria-label="Example Modal"
>
<ModalHeader title="Example Modal" />
<ModalBody>
<Text fontSize={300} color="text.body" mb={400}>
This is the modal body content. It can contain any content you need.
</Text>
<Box bg="neutral.200" p={400} borderRadius={400}>
<Text fontSize={200} color="text.body">
ModalBody accepts all Box props for styling, so you can customize
the layout, spacing, and appearance as needed.
</Text>
</Box>
</ModalBody>
<ModalFooter
primaryButton={<Button appearance="primary">Close</Button>}
/>
</Modal>
</Box>
)
}
NameTypeDefaultDescriptionRequired?
children
React.ReactNode
The main content of the modal body

ModalFooter

Provides automatic button wrapping and layout management. Buttons are automatically wrapped in ModalCloseWrapper to close the modal when clicked.

Note: For custom footer layouts with informational text or complex layouts, use ModalCustomFooter instead.

import { Modal, ModalHeader, ModalBody, ModalFooter } from '@sproutsocial/racine/modal/v2'
() => {
return (
<Box>
<Modal
modalTrigger={<Button appearance="primary">Open modal with footer</Button>}
aria-label="Example Modal"
>
<ModalHeader title="Example Modal" />
<ModalBody>
<Text fontSize={300} color="text.body">
Modal content goes here.
</Text>
</ModalBody>
<ModalFooter
cancelButton={<Button appearance="unstyled">Cancel</Button>}
primaryButton={<Button appearance="primary">Save</Button>}
/>
</Modal>
</Box>
)
}

Footer with left action:

import { Modal, ModalHeader, ModalBody, ModalFooter } from '@sproutsocial/racine/modal/v2'
() => {
return (
<Box>
<Modal
modalTrigger={<Button appearance="primary">Open modal with left action</Button>}
aria-label="Delete Item"
>
<ModalHeader title="Delete Item" />
<ModalBody>
<Text fontSize={300} color="text.body">
Are you sure you want to delete this item?
</Text>
</ModalBody>
<ModalFooter
leftAction={<Button appearance="destructive">Delete</Button>}
cancelButton={<Button appearance="unstyled">Cancel</Button>}
primaryButton={<Button appearance="primary">Save</Button>}
/>
</Modal>
</Box>
)
}

Async primary action:

By default, both cancelButton and primaryButton automatically close the modal when clicked. For async operations like saving data, set closeOnPrimaryAction={false} to keep the modal open until the operation completes. The consumer is then responsible for closing the modal programmatically via controlled state.

import { Modal, ModalHeader, ModalBody, ModalFooter } from '@sproutsocial/racine/modal/v2'
() => {
const [isOpen, setIsOpen] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const handleSave = async () => {
setIsSaving(true);
// Simulate async operation
await new Promise((resolve) => setTimeout(resolve, 1500));
setIsSaving(false);
setIsOpen(false);
};
return (
<Box>
<Button appearance="primary" onClick={() => setIsOpen(true)}>
Open modal
</Button>
<Modal
open={isOpen}
onOpenChange={setIsOpen}
aria-label="Async save example"
>
<ModalHeader
title="Save changes"
subtitle="The modal stays open until the save completes."
/>
<ModalBody>
<Text fontSize={300} color="text.body">
Click Save to trigger an async operation. The modal will remain
open until the operation completes.
</Text>
</ModalBody>
<ModalFooter
closeOnPrimaryAction={false}
cancelButton={<Button appearance="unstyled">Cancel</Button>}
primaryButton={
<Button
appearance="primary"
onClick={handleSave}
disabled={isSaving}
>
{isSaving ? "Saving..." : "Save"}
</Button>
}
/>
</Modal>
</Box>
)
}
NameTypeDefaultDescriptionRequired?
closeOnPrimaryAction
boolean
Whether clicking the primary button automatically closes the modal. Set to `false` when the primary action is async (e.g., saving data) and the modal should stay open until the operation completes. @default true

ModalCloseWrapper

Wraps elements (typically buttons) to automatically close the modal when clicked. This component is primarily used for custom implementations, such as when creating a custom ModalFooter or custom header/footer layouts. ModalFooter automatically wraps its buttons in ModalCloseWrapper, so you typically only need to use this component directly when building custom footer or header implementations.

Note: This component extends Radix UI Dialog.Close primitive to provide accessible close functionality.

import { Modal, ModalHeader, ModalBody, ModalCloseWrapper, ModalFooter } from '@sproutsocial/racine/modal/v2'
() => {
return (
<Box>
<Modal
modalTrigger={<Button appearance="primary">Open modal with custom close wrapper</Button>}
aria-label="Example Modal"
>
<ModalHeader title="Example Modal" />
<ModalBody>
<Text fontSize={300} color="text.body" mb={400}>
This modal uses ModalCloseWrapper to wrap custom buttons that close the modal.
</Text>
<Box display="flex" gap={300}>
<ModalCloseWrapper>
<Button appearance="unstyled">Custom Close Button</Button>
</ModalCloseWrapper>
<ModalCloseWrapper>
<Button appearance="secondary">Another Close Button</Button>
</ModalCloseWrapper>
</Box>
</ModalBody>
<ModalFooter
primaryButton={<Button appearance="primary">Save</Button>}
/>
</Modal>
</Box>
)
}
NameTypeDefaultDescriptionRequired?
children
React.ReactNode
The element to wrap with close functionality
onClick
(e: React.MouseEvent) => void
Optional click handler called before closing the modal
asChild
boolean
Whether to merge props into the child element (default: true)

ModalCloseWrapper does not accept system props. It only accepts children, onClick, and asChild props.

Actions Prop

The actions prop on the Modal component allows you to add quick action buttons to a floating rail alongside the modal. These actions appear in a vertical rail next to the modal (horizontal on mobile) and provide quick access to common actions like close, expand, etc.

import { Modal, ModalHeader, ModalBody, ModalFooter } from '@sproutsocial/racine/modal/v2'
() => {
return (
<Box>
<Modal
modalTrigger={<Button appearance="primary">Open modal with custom actions</Button>}
aria-label="Example Modal"
actions={[
{
actionType: "button",
iconName: "arrows-pointing-out",
"aria-label": "Expand modal",
onClick: () => console.log("Expand clicked"),
},
]}
>
<ModalHeader title="Example Modal" />
<ModalBody>
<Text fontSize={300} color="text.body">
This modal includes custom actions in the floating rail. The close button
is automatically included, and we've added an expand action using the `actions` prop.
</Text>
</ModalBody>
<ModalFooter
primaryButton={<Button appearance="primary">Close</Button>}
/>
</Modal>
</Box>
)
}

Action Configuration:

Each action in the actions array accepts:

  • actionType: "close" (automatically closes modal) or "button" (default, requires onClick)
  • iconName: Icon name from the Seeds icon set
  • aria-label: Required accessible label for the action
  • onClick: Optional click handler (ignored for actionType: "close")

Customizing the Close Button

The default floating close button can be customized in two ways:

Simple customization - Use closeButtonAriaLabel to customize just the accessible label:

import { Modal, ModalHeader, ModalBody, ModalFooter } from '@sproutsocial/racine/modal/v2'
() => {
return (
<Box>
<Modal
modalTrigger={<Button appearance="primary">Open modal</Button>}
closeButtonAriaLabel="Close this dialog"
aria-label="Example Modal"
>
<ModalHeader title="Example Modal" />
<ModalBody>
<Text fontSize={300} color="text.body">
The close button has a custom aria-label.
</Text>
</ModalBody>
<ModalFooter
primaryButton={<Button appearance="primary">Close</Button>}
/>
</Modal>
</Box>
)
}

Advanced customization - Use closeButtonProps to customize the close button with additional props like onClick, id, disabled, className, etc:

import { Modal, ModalHeader, ModalBody, ModalFooter } from '@sproutsocial/racine/modal/v2'
() => {
const [clickCount, setClickCount] = useState(0);
return (
<Box>
<Text fontSize={200} color="text.body" mb={400}>
Close button clicked {clickCount} times
</Text>
<Modal
modalTrigger={<Button appearance="primary">Open modal</Button>}
aria-label="Example Modal"
closeButtonProps={{
"aria-label": "Close this dialog",
onClick: () => {
console.log("Close button clicked!");
setClickCount(prev => prev + 1);
},
id: "custom-close-btn",
}}
>
<ModalHeader title="Example Modal" />
<ModalBody>
<Text fontSize={300} color="text.body">
The close button has custom onClick, id, and aria-label props.
Click the close button to see the counter increment.
</Text>
</ModalBody>
<ModalFooter
primaryButton={<Button appearance="primary">Close</Button>}
/>
</Modal>
</Box>
)
}

Note: You must provide an accessible label through either closeButtonAriaLabel or closeButtonProps["aria-label"]. TypeScript will enforce this requirement.

ModalCustomHeader

For custom headers with complex layouts or actions, use ModalCustomHeader instead of the simple title/subtitle props.

import { Modal, ModalCustomHeader, ModalBody, ModalCloseWrapper } from '@sproutsocial/racine/modal/v2'
() => {
return (
<Box>
<Modal
modalTrigger={<Button appearance="primary">Open modal with custom header</Button>}
aria-label="Example Modal"
>
<ModalCustomHeader>
<Box display="flex" alignItems="center" justifyContent="space-between" width={1}>
<Box>
<Text fontSize={400} fontWeight="bold" color="text.headline">
Custom Header
</Text>
<Text fontSize={200} color="text.subtext">
With custom layout and actions
</Text>
</Box>
<Box display="flex" gap={300}>
<ModalCloseWrapper>
<Button appearance="unstyled">Cancel</Button>
</ModalCloseWrapper>
<ModalCloseWrapper>
<Button appearance="primary">Save</Button>
</ModalCloseWrapper>
</Box>
</Box>
</ModalCustomHeader>
<ModalBody>
<Text fontSize={300} color="text.body">
Modal content goes here.
</Text>
</ModalBody>
</Modal>
</Box>
)
}
NameTypeDefaultDescriptionRequired?
children
ReactNode
Content to render inside the custom header
draggable
boolean
Internal prop for draggable functionality (automatically set when Modal is draggable)
isDragging
boolean
Internal prop indicating if modal is currently being dragged

ModalCustomFooter

For custom footers with informational text or complex layouts, use ModalCustomFooter instead of the standard ModalFooter component.

import { Modal, ModalHeader, ModalBody, ModalCustomFooter, ModalCloseWrapper } from '@sproutsocial/racine/modal/v2'
() => {
return (
<Box>
<Modal
modalTrigger={<Button appearance="primary">Open modal with custom footer</Button>}
aria-label="Example Modal"
>
<ModalHeader title="Example Modal" />
<ModalBody>
<Text fontSize={300} color="text.body">
Modal content goes here.
</Text>
</ModalBody>
<ModalCustomFooter
display="flex"
alignItems="center"
justifyContent="space-between"
>
<Text fontSize={200} color="text.subtext">
This is some information you may need to know!
</Text>
<Box display="flex" gap={300}>
<ModalCloseWrapper>
<Button appearance="unstyled">Cancel</Button>
</ModalCloseWrapper>
<ModalCloseWrapper>
<Button appearance="primary">Save</Button>
</ModalCloseWrapper>
</Box>
</ModalCustomFooter>
</Modal>
</Box>
)
}
NameTypeDefaultDescriptionRequired?
children
ReactNode
Content to render inside the custom footer

Handling close events

Modal V2 provides granular event callbacks that let you distinguish why a modal was closed. This is useful for analytics tracking, conditional close prevention, or triggering different behaviors based on the user's action.

Close reasonCallbackCan prevent close?
Escape keyonEscapeKeyDownYes (e.preventDefault())
Overlay / outside clickonInteractOutsideYes (e.preventDefault())
Close button (X)closeButtonProps.onClickNo
Any state changeonOpenChangeNo

The specific event callbacks (onEscapeKeyDown, onInteractOutside, closeButtonProps.onClick) fire before onOpenChange, so you can identify the close reason before the state updates.

import { Modal, ModalHeader, ModalBody, ModalFooter } from '@sproutsocial/racine/modal/v2'
() => {
const [isOpen, setIsOpen] = useState(false);
const [lastCloseReason, setLastCloseReason] = useState("none");
return (
<Box>
<Box display="flex" flexDirection="column" alignItems="flex-start" gap={300}>
<Text fontSize={200} color="text.body">
Last close reason: <strong>{lastCloseReason}</strong>
</Text>
<Button appearance="primary" onClick={() => setIsOpen(true)}>
Open modal
</Button>
</Box>
<Modal
open={isOpen}
onOpenChange={setIsOpen}
aria-label="Close event tracking example"
onEscapeKeyDown={() => {
setLastCloseReason("escape");
}}
onInteractOutside={() => {
setLastCloseReason("overlay_click");
}}
closeButtonProps={{
"aria-label": "Close modal",
onClick: () => {
setLastCloseReason("close_button");
},
}}
>
<ModalHeader
title="Close event tracking"
subtitle="Try closing this modal different ways and check the label above."
/>
<ModalBody>
<Text fontSize={300} color="text.body">
Close this modal by pressing Escape, clicking the overlay, or
clicking the close button to see different close reasons tracked.
</Text>
</ModalBody>
<ModalFooter
cancelButton={<Button appearance="unstyled">Cancel</Button>}
primaryButton={<Button appearance="primary">Done</Button>}
/>
</Modal>
</Box>
)
}

Preventing close

You can prevent the modal from closing on specific interactions by calling e.preventDefault() in the event handler, or by using the convenience boolean props.

import { Modal, ModalHeader, ModalBody, ModalFooter } from '@sproutsocial/racine/modal/v2'
() => {
return (
<Box>
<Modal
modalTrigger={<Button appearance="primary">Open modal</Button>}
aria-label="Prevent close example"
disableEscapeKeyClose
disableOutsideClickClose
>
<ModalHeader
title="Protected modal"
subtitle="This modal can only be closed via the close button or footer buttons."
/>
<ModalBody>
<Text fontSize={300} color="text.body">
Pressing Escape or clicking the overlay will not close this modal.
Use the close button or footer buttons instead.
</Text>
</ModalBody>
<ModalFooter
cancelButton={<Button appearance="unstyled">Cancel</Button>}
primaryButton={<Button appearance="primary">Confirm</Button>}
/>
</Modal>
</Box>
)
}

The same behavior can be achieved with event handlers for more control:

<Modal
onEscapeKeyDown={(e) => {
e.preventDefault(); // prevent close
// custom logic here
}}
onInteractOutside={(e) => {
e.preventDefault(); // prevent close
// custom logic here
}}
>

Recipes

Modal headers can be omitted when the first item in the modal body is an image or illustration. If the modal body begins with text, a header with a title or subtitle should be used.

The following example shows a modal without a header, which displays only a close button in the floating action rail.

import { Modal, ModalBody, ModalFooter } from '@sproutsocial/racine/modal/v2'
() => {
return (
<Box>
<Modal
modalTrigger={<Button appearance="primary">Open modal</Button>}
aria-label="Example Modal"
>
<ModalBody>
<Box
mb={500}
display="flex"
alignItems="center"
flexDirection="column"
>
<Box width="400px" height="400px" bg="neutral.200" />
<Text
fontSize={400}
fontWeight="bold"
color="text.headline"
pt={600}
>
Some headline
</Text>
<Text
fontSize={300}
textAlign="center"
color="text.body"
pt={450}
px={500}
>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem
ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua.
</Text>
</Box>
</ModalBody>
<ModalFooter
primaryButton={<Button appearance="primary">Close modal</Button>}
/>
</Modal>
</Box>
)
}

Expressive modal

import { Modal, ModalBody, ModalFooter } from '@sproutsocial/racine/modal/v2'
() => {
return (
<Box>
<Modal
modalTrigger={<Button appearance="primary">Open modal</Button>}
aria-label="Example Modal"
bg="blue.400"
>
<ModalBody>
<Image
alt="expressive illustration"
mb={600}
src={withPrefix('/illus.svg')}
/>
<Box
width="100%"
bg="neutral.0"
p={600}
display="flex"
flexDirection="column"
>
<Text
fontSize={400}
color="text.headline"
fontWeight={700}
textAlign="center"
pb={400}
>
Engagement makes the world go 'round.
</Text>
<Text fontSize={200} textAlign="center" color="text.body">
We care about how you engage with your customers and we're here to
make that experience as smooth and seamless as possible.
</Text>
</Box>
</ModalBody>
<ModalFooter
primaryButton={<Button appearance="primary" minWidth={120}>Thanks</Button>}
/>
</Modal>
</Box>
)
}

Destructive confirmation

import { Modal, ModalHeader, ModalBody, ModalFooter } from '@sproutsocial/racine/modal/v2'
() => {
return (
<Box>
<Modal
modalTrigger={<Button appearance="primary">Open modal</Button>}
aria-label="Delete profiles confirmation"
>
<ModalHeader
title="Delete profiles?"
subtitle="This action can't be undone."
/>
<ModalBody>
<Text as='div' py={300} fontSize={300} color="text.body">
Deleting this profile will remove it from all groups as well as
remove any messages associated with this profile. Are you sure you
want to proceed?
</Text>
</ModalBody>
<ModalFooter
cancelButton={<Button appearance="unstyled" mr={400}>Cancel</Button>}
primaryButton={<Button appearance="destructive" minWidth="120px">Confirm</Button>}
/>
</Modal>
</Box>
)
}

Free form modal

This example showcases the freedom and flexibility we have within ModalBody. For custom headers and footers, use ModalCustomHeader and ModalCustomFooter components.

import { Modal, ModalCustomHeader, ModalBody, ModalCloseWrapper } from '@sproutsocial/racine/modal/v2'
() => {
return (
<Box>
<Modal
modalTrigger={<Button appearance="primary">Open modal</Button>}
aria-label="Free form experience"
>
<ModalCustomHeader>
<Box
display="flex"
alignItems="center"
justifyContent="space-between"
width={1}
>
<Box display="flex" flexDirection="column">
<Text fontSize={400} fontWeight="bold" color="text.headline">
Free form experience
</Text>
<Text fontSize={200} color="text.subtext">
Browse and select things from the list as you please, nothing is
required.
</Text>
</Box>
<Box display="flex">
<ModalCloseWrapper>
<Button mr={350} appearance="unstyled">
Cancel
</Button>
</ModalCloseWrapper>
<ModalCloseWrapper>
<Button minWidth="120px" appearance="primary">
Complete
</Button>
</ModalCloseWrapper>
</Box>
</Box>
</ModalCustomHeader>
<ModalBody display="flex">
<Box width={1 / 2} display="flex" flexDirection="column" pr={450}>
<Text
fontWeight={700}
fontSize={300}
color="text.headline"
mb={400}
>
An informational headline
</Text>
<Text fontSize={200} color="text.body" mb={350}>
Authoritatively integrate installed base deliverables without
worldwide intellectual capital. Progressively promote functional
markets before mission-critical potentialities.
</Text>
<Text fontSize={200} color="text.body" mb={350}>
Assertively incentivize 2.0 communities with quality technologies.
Globally benchmark accurate sources for go forward systems.
Energistically develop out-of-the-box ideas and quality services.
</Text>
</Box>
<Box width={1 / 2}>
<ModalListExample />
</Box>
</ModalBody>
</Modal>
</Box>
)
}