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.
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><ModalmodalTrigger={<Button appearance="primary">Open modal</Button>}aria-label="Assign Chatbot"><ModalHeadertitle="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 doeiusmod tempor incididunt ut labore et dolore magna aliqua. Loremipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmodtempor incididunt ut labore et dolore magna aliqua. Lorem ipsumdolor sit amet, consectetur adipiscing elit, sed do eiusmod temporincididunt ut labore et dolore magna aliqua.</Text></ModalBody><ModalFootercancelButton={<Button appearance="unstyled">Go back</Button>}primaryButton={<Button appearance="primary" px={500}>Submit</Button>}/></Modal></Box>)}
Controlled Modal
For controlled modals, use the open and onOpenChange props instead of modalTrigger:
import { Modal, ModalHeader, ModalBody, ModalFooter } from '@sproutsocial/racine/modal/v2'() => {const [isOpen, setIsOpen] = useState(false);return (<Box><Button appearance="primary" onClick={() => setIsOpen(true)}>Open controlled modal</Button><Modalopen={isOpen}onOpenChange={setIsOpen}aria-label="Controlled Modal Example"><ModalHeadertitle="Controlled Modal"subtitle="This modal's state is managed by the parent component."/><ModalBody><Text fontSize={300} color="text.body">The modal is controlled via the `open` and `onOpenChange` props.Click the button outside the modal or use the close button to close it.</Text></ModalBody><ModalFooterprimaryButton={<Button appearance="primary">Close</Button>}/></Modal></Box>)}
Modal Components
Modal V2 consists of several components that work together:
Modal (Root Component)
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.
| Name | Type | Default | Description | Required? |
|---|---|---|---|---|
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. |
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><ModalmodalTrigger={<Button appearance="primary">Open modal with header</Button>}aria-label="Example Modal"><ModalHeadertitle="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><ModalFooterprimaryButton={<Button appearance="primary">Close</Button>}/></Modal></Box>)}
| Name | Type | Default | Description | Required? |
|---|---|---|---|---|
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><ModalmodalTrigger={<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 customizethe layout, spacing, and appearance as needed.</Text></Box></ModalBody><ModalFooterprimaryButton={<Button appearance="primary">Close</Button>}/></Modal></Box>)}
| Name | Type | Default | Description | Required? |
|---|---|---|---|---|
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><ModalmodalTrigger={<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><ModalFootercancelButton={<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><ModalmodalTrigger={<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><ModalFooterleftAction={<Button appearance="destructive">Delete</Button>}cancelButton={<Button appearance="unstyled">Cancel</Button>}primaryButton={<Button appearance="primary">Save</Button>}/></Modal></Box>)}
| Name | Type | Default | Description | Required? |
|---|---|---|---|---|
primaryButton | ReactNode | Primary action button - automatically wrapped in ModalCloseWrapper to close the modal. At least one action (primaryButton, cancelButton, or leftAction) must be provided. | ||
cancelButton | ReactNode | Cancel/secondary button - automatically wrapped in ModalCloseWrapper to close the modal. | ||
leftAction | ReactNode | Optional action on the far left (e.g., Delete button) - NOT automatically wrapped in ModalCloseWrapper. Use for destructive actions that don't close the modal. |
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><ModalmodalTrigger={<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><ModalFooterprimaryButton={<Button appearance="primary">Save</Button>}/></Modal></Box>)}
| Name | Type | Default | Description | Required? |
|---|---|---|---|---|
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><ModalmodalTrigger={<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 buttonis automatically included, and we've added an expand action using the `actions` prop.</Text></ModalBody><ModalFooterprimaryButton={<Button appearance="primary">Close</Button>}/></Modal></Box>)}
Action Configuration:
Each action in the actions array accepts:
actionType:"close"(automatically closes modal) or"button"(default, requiresonClick)iconName: Icon name from the Seeds icon setaria-label: Required accessible label for the actiononClick: Optional click handler (ignored foractionType: "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><ModalmodalTrigger={<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><ModalFooterprimaryButton={<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><ModalmodalTrigger={<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><ModalFooterprimaryButton={<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><ModalmodalTrigger={<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>)}
| Name | Type | Default | Description | Required? |
|---|---|---|---|---|
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><ModalmodalTrigger={<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><ModalCustomFooterdisplay="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>)}
| Name | Type | Default | Description | Required? |
|---|---|---|---|---|
children | ReactNode | Content to render inside the custom footer |
Recipes
Modal with no header
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><ModalmodalTrigger={<Button appearance="primary">Open modal</Button>}aria-label="Example Modal"><ModalBody><Boxmb={500}display="flex"alignItems="center"flexDirection="column"><Box width="400px" height="400px" bg="neutral.200" /><TextfontSize={400}fontWeight="bold"color="text.headline"pt={600}>Some headline</Text><TextfontSize={300}textAlign="center"color="text.body"pt={450}px={500}>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed doeiusmod tempor incididunt ut labore et dolore magna aliqua. Loremipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmodtempor incididunt ut labore et dolore magna aliqua.</Text></Box></ModalBody><ModalFooterprimaryButton={<Button appearance="primary">Close modal</Button>}/></Modal></Box>)}
Expressive modal
import { Modal, ModalBody, ModalFooter } from '@sproutsocial/racine/modal/v2'() => {return (<Box><ModalmodalTrigger={<Button appearance="primary">Open modal</Button>}aria-label="Example Modal"bg="blue.400"><ModalBody><Imagealt="expressive illustration"mb={600}src={withPrefix('/illus.svg')}/><Boxwidth="100%"bg="neutral.0"p={600}display="flex"flexDirection="column"><TextfontSize={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 tomake that experience as smooth and seamless as possible.</Text></Box></ModalBody><ModalFooterprimaryButton={<Button appearance="primary" minWidth={120}>Thanks</Button>}/></Modal></Box>)}
Destructive confirmation
import { Modal, ModalHeader, ModalBody, ModalFooter } from '@sproutsocial/racine/modal/v2'() => {return (<Box><ModalmodalTrigger={<Button appearance="primary">Open modal</Button>}aria-label="Delete profiles confirmation"><ModalHeadertitle="Delete profiles?"subtitle="This action can not be undone."/><ModalBody><Text as='div' py={300} fontSize={300} color="text.body">Deleting this profile will remove it from all groups as well asremove any messages associated with this profile. Are you sure youwant to proceed?</Text></ModalBody><ModalFootercancelButton={<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><ModalmodalTrigger={<Button appearance="primary">Open modal</Button>}aria-label="Free form experience"><ModalCustomHeader><Boxdisplay="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 isrequired.</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}><TextfontWeight={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 withoutworldwide intellectual capital. Progressively promote functionalmarkets 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>)}