import { Tray } from '@coinbase/cds-web/overlays/tray/Tray'
Basics
The recommended way to use a Tray is to add to dom when visible, and use onCloseComplete to remove it.
It is also recommended to pin it to the right side of the screen on tablet and desktop, and pin to bottom with handlebar on mobile.
function BasicTray() { const [visible, setVisible] = useState(false); const { isPhone } = useBreakpoints(); const handleOpen = () => setVisible(true); const handleClose = () => setVisible(false); return ( <VStack gap={2}> <Button onClick={handleOpen}>Open Tray</Button> {visible && ( <Tray pin={isPhone ? 'bottom' : 'right'} showHandleBar={isPhone} onCloseComplete={handleClose} title="Example title" footer={({ handleClose }) => ( <PageFooter borderedTop justifyContent={isPhone ? 'center' : 'flex-end'} action={ <Button block={isPhone} onClick={handleClose}> Close </Button> } /> )} > <Text color="fgMuted" paddingBottom={2}> Curabitur commodo nulla vel dolor vulputate vestibulum. Nulla et nisl molestie, interdum lorem id, viverra. </Text> </Tray> )} </VStack> ); }
Pinning
While you can pin the tray to any side of the screen, it is recommended to only use pin to bottom or right. Bottom is recommended for mobile, and right is recommended for tablet and desktop.
Handlebar is only shown on bottom pinned trays, and adjusts the padding to match other pins. It is deprecated to use bottom pin without handlebar.
function PinnedTray() { const [pinDirection, setPinDirection] = useState(null); const { isPhone } = useBreakpoints(); const handleClose = () => setPinDirection(null); return ( <VStack gap={2}> <HStack gap={2} flexWrap="wrap"> <Button onClick={() => setPinDirection('right')}>Open Right Tray</Button> <Button onClick={() => setPinDirection('bottom')}>Open Bottom Tray</Button> <Button onClick={() => setPinDirection('left')}>Open Left Tray</Button> <Button onClick={() => setPinDirection('top')}>Open Top Tray</Button> </HStack> {pinDirection !== null && ( <Tray pin={pinDirection} showHandleBar onCloseComplete={handleClose} title="Example title" footer={({ handleClose }) => ( <PageFooter borderedTop justifyContent={isPhone ? 'center' : 'flex-end'} action={ <Button block={isPhone} onClick={handleClose}> Close </Button> } /> )} > <Text color="fgMuted" paddingBottom={2}> Curabitur commodo nulla vel dolor vulputate vestibulum. Nulla et nisl molestie, interdum lorem id, viverra. </Text> </Tray> )} </VStack> ); }
Content
Web Tray will automatically be scrollable when the content is too large to fit. You can adjust verticalDrawerPercentageOfView to control the maximum height of the tray when pinned to the bottom or top.
When scrolling, a border is added to the header.
function ResponsiveTray() { function ResponsiveTray({ styles, ...props }) { const [visible, setVisible] = useState(false); const { isPhone } = useBreakpoints(); const handleOpen = () => setVisible(true); const handleClose = () => setVisible(false); return ( <Tray {...props} pin={isPhone ? 'bottom' : 'right'} showHandleBar={isPhone} styles={{ ...styles, content: { paddingBottom: 'var(--space-3)', ...styles?.content, }, }} verticalDrawerPercentageOfView="90%" /> ); } function Example() { const [visible, setVisible] = useState(false); const handleOpen = () => setVisible(true); const handleClose = () => setVisible(false); return ( <VStack gap={2}> <Button onClick={handleOpen}>Open Scrolling Tray</Button> {visible && ( <ResponsiveTray onCloseComplete={handleClose} title="Header"> {Array.from({ length: 20 }, (_, i) => ( <ListCell key={i} spacingVariant="condensed" title="Title" description="Description" accessory="arrow" onClick={() => alert('Cell clicked!')} innerSpacing={{ marginX: -4, paddingX: 4, paddingY: 1, }} /> ))} </ResponsiveTray> )} </VStack> ); } return <Example />; }
With Illustration in Header
You can pass in a custom node to title to render a custom header.
function IllustrationSectionHeaderTray() { const [visible, setVisible] = useState(false); const { isPhone } = useBreakpoints(); const handleOpen = () => setVisible(true); const handleClose = () => setVisible(false); const titleId = useId(); return ( <VStack gap={2}> <Button onClick={handleOpen}>Open Illustration Tray</Button> {visible && ( <Tray pin={isPhone ? 'bottom' : 'right'} showHandleBar={isPhone} onCloseComplete={handleClose} title={ <VStack gap={{ phone: 1.5, tablet: 2, desktop: 2 }}> <Pictogram name="addWallet" /> <Text id={titleId} font="title3"> Section header </Text> </VStack> } accessibilityLabelledBy={titleId} > <Text color="fgMuted" font="body" paddingBottom={2}> Curabitur commodo nulla vel dolor vulputate vestibulum. Nulla et nisl molestie, interdum lorem id, viverra. </Text> </Tray> )} </VStack> ); }
With Full Bleed Header
You can use a full bleed header with a background image. Use header and title to add a section header that stays fixed below the image while content scrolls. When scrolling, a border appears below the header area.
function FullBleedHeaderTray() { const [visible, setVisible] = useState(false); const { isPhone } = useBreakpoints(); const handleOpen = () => setVisible(true); const handleClose = () => setVisible(false); const titleId = useId(); return ( <VStack gap={2}> <Button onClick={handleOpen}>Open Full Bleed Header Tray</Button> {visible && ( <Tray pin={isPhone ? 'bottom' : 'right'} showHandleBar={isPhone} onCloseComplete={handleClose} title={ <Box flexGrow={1} marginX={{ base: -4, phone: -3 }}> <img alt="Full Bleed" height={180} src="/img/tray_header.png" style={{ objectFit: 'cover', pointerEvents: 'none' }} width="100%" /> </Box> } header={ <Text id={titleId} font="title3" paddingTop={2} paddingX={{ base: 4, phone: 3 }}> Section header </Text> } styles={{ handleBar: { position: 'absolute', top: 0, left: 0, right: 0, zIndex: 1, }, closeButton: { position: 'absolute', top: 'var(--space-4)', right: 'var(--space-4)', zIndex: 1, }, header: { paddingTop: 0, }, content: { paddingBottom: 'var(--space-3)' }, }} accessibilityLabelledBy={titleId} > <Text color="fgMuted" font="body" paddingBottom={2}> Curabitur commodo nulla vel dolor vulputate vestibulum. Nulla et nisl molestie, interdum lorem id, viverra. </Text> </Tray> )} </VStack> ); }
With Scrollable List Cells
When using a full bleed header with scrollable content, the header prop keeps the section header fixed while list cells scroll beneath it.
function FullBleedHeaderScrollableTray() { const [visible, setVisible] = useState(false); const { isPhone } = useBreakpoints(); const handleOpen = () => setVisible(true); const handleClose = () => setVisible(false); const titleId = useId(); return ( <VStack gap={2}> <Button onClick={handleOpen}>Open Full Bleed Scrollable Tray</Button> {visible && ( <Tray pin={isPhone ? 'bottom' : 'right'} showHandleBar={isPhone} onCloseComplete={handleClose} title={ <Box flexGrow={1} marginX={{ base: -4, phone: -3 }}> <img alt="Full Bleed" height={180} src="/img/tray_header.png" style={{ objectFit: 'cover', pointerEvents: 'none' }} width="100%" /> </Box> } header={ <Text id={titleId} font="title3" paddingTop={2} paddingX={{ base: 4, phone: 3 }}> Section header </Text> } styles={{ handleBar: { position: 'absolute', top: 0, left: 0, right: 0, zIndex: 1, }, closeButton: { position: 'absolute', top: 'var(--space-4)', right: 'var(--space-4)', zIndex: 1, }, header: { paddingTop: 0, }, content: { paddingBottom: 'var(--space-3)' }, }} accessibilityLabelledBy={titleId} verticalDrawerPercentageOfView="90%" > {Array.from({ length: 20 }, (_, i) => ( <ListCell key={i} spacingVariant="condensed" title="Title" description="Description" accessory="arrow" onClick={() => alert('Cell clicked!')} innerSpacing={{ marginX: -4, paddingX: 4, paddingY: 1, }} /> ))} </Tray> )} </VStack> ); }
Controlled
You have various ways to control the state of a tray.
Via Ref
You can use a ref to control the tray, which provides a close() method.
A ref to the trigger that opens the tray, along with an onClosedComplete method to reset focus on the trigger when the tray closes, needs to be wired up for accessibility.
function TrayWithRef() { const [visible, setVisible] = useState(false); const trayRef = useRef(null); const triggerRef = useRef(null); const handleOpen = () => setVisible(true); const handleClose = () => { setVisible(false); triggerRef.current?.focus(); }; return ( <VStack gap={2}> <Button ref={triggerRef} onClick={handleOpen}> Open Tray </Button> {visible && ( <Tray ref={trayRef} title="Ref Controlled Tray" onCloseComplete={handleClose} pin="right"> <VStack gap={2}> <Text>Control this tray using the ref.</Text> <Button onClick={() => trayRef.current?.close()}>Close</Button> </VStack> </Tray> )} </VStack> ); }
Prevent Dismissal
You can prevent the user from dismissing the tray with preventDismiss. This will remove built in dismiss functionality, including swipe to close with handlebar, close button, pressing ESC, and clicking outside.
You must provide an explicit action button to close the tray.
function PreventDismissTray() { const [visible, setVisible] = useState(false); const { isPhone } = useBreakpoints(); const handleOpen = () => setVisible(true); const handleClose = () => setVisible(false); return ( <VStack gap={2}> <Button onClick={handleOpen}>Open Tray</Button> {visible && ( <Tray preventDismiss pin={isPhone ? 'bottom' : 'right'} showHandleBar={isPhone} onCloseComplete={handleClose} title="Example title" footer={({ handleClose }) => ( <PageFooter borderedTop justifyContent={isPhone ? 'center' : 'flex-end'} action={ <Button block={isPhone} onClick={handleClose}> Close </Button> } /> )} > <Text color="fgMuted" paddingBottom={2}> You cannot dismiss this tray by clicking outside or pressing ESC. You must click the close button below to close it. </Text> </Tray> )} </VStack> ); }
Accessibility
Accessibility labels
Trays require an accessibility label. If you pass in a ReactNode to title, make sure to set accessibilityLabel or accessibilityLabelledBy.
Reduce Motion
Use the reduceMotion prop to accommodate users with reduced motion settings.
function ReducedMotionTray() { const [visible, setVisible] = useState(false); const { isPhone } = useBreakpoints(); const handleOpen = () => setVisible(true); const handleClose = () => setVisible(false); return ( <VStack gap={2}> <Button onClick={handleOpen}>Open Tray</Button> {visible && ( <Tray reduceMotion pin={isPhone ? 'bottom' : 'right'} showHandleBar={isPhone} onCloseComplete={handleClose} title="Reduced Motion" footer={({ handleClose }) => ( <PageFooter borderedTop justifyContent={isPhone ? 'center' : 'flex-end'} action={ <Button block={isPhone} onClick={handleClose}> Close </Button> } /> )} > <Text paddingBottom={2}>This tray fades in and out using opacity.</Text> </Tray> )} </VStack> ); }
Styling
The Tray component exposes styles and classNames props for customizing various parts of the component. Available keys include: root, overlay, container, header, title, content, footer, handleBar, handleBarHandle, and closeButton.
Container
You can customize the tray's outer container to adjust the border radius for floating trays or change the max width.
function CustomContainerTray() { const [visible, setVisible] = useState(false); const { isPhone } = useBreakpoints(); const handleOpen = () => setVisible(true); const handleClose = () => setVisible(false); return ( <VStack gap={2}> <Button onClick={handleOpen}>Open Rounded Tray</Button> {visible && ( <Tray pin={isPhone ? 'bottom' : 'right'} showHandleBar={isPhone} onCloseComplete={handleClose} title="Custom container" styles={{ container: { borderRadius: 'var(--borderRadius-600)', top: 'var(--space-2)', bottom: 'var(--space-2)', right: 'var(--space-2)', }, }} > <Text color="fgMuted" paddingBottom={2}> This tray has custom border radius and margin applied to the container. </Text> </Tray> )} </VStack> ); }
Title
For full bleed images, use the title prop with a Box containing an image.
function BackgroundImageHeaderTray() { const [visible, setVisible] = useState(false); const { isPhone } = useBreakpoints(); const handleOpen = () => setVisible(true); const handleClose = () => setVisible(false); const titleId = useId(); return ( <VStack gap={2}> <Button onClick={handleOpen}>Open Image Header Tray</Button> {visible && ( <Tray pin={isPhone ? 'bottom' : 'right'} showHandleBar={isPhone} onCloseComplete={handleClose} title={ <Box flexGrow={1} marginX={{ base: -4, phone: -3 }}> <img alt="Full Bleed" height={180} src="/img/tray_header.png" style={{ objectFit: 'cover', pointerEvents: 'none' }} width="100%" /> </Box> } header={ <Text id={titleId} font="title3" paddingTop={2} paddingX={{ base: 4, phone: 3 }}> Section header </Text> } styles={{ handleBar: { position: 'absolute', top: 0, left: 0, right: 0, zIndex: 1, }, closeButton: { position: 'absolute', top: 'var(--space-4)', right: 'var(--space-4)', zIndex: 1, }, header: { paddingTop: 0, }, content: { paddingBottom: 'var(--space-3)' }, }} accessibilityLabelledBy={titleId} > <Text color="fgMuted" paddingBottom={2}> The header displays a full bleed background image. </Text> </Tray> )} </VStack> ); }
Content
You can customize the content area to adjust padding, background, or other properties.
function CustomContentTray() { const [visible, setVisible] = useState(false); const { isPhone } = useBreakpoints(); const handleOpen = () => setVisible(true); const handleClose = () => setVisible(false); return ( <VStack gap={2}> <Button onClick={handleOpen}>Open Custom Content Tray</Button> {visible && ( <Tray pin={isPhone ? 'bottom' : 'right'} showHandleBar={isPhone} onCloseComplete={handleClose} title="Custom content styling" styles={{ content: { backgroundColor: 'var(--color-bgSecondary)', paddingTop: 'var(--space-3)', paddingBottom: 'var(--space-3)', }, }} > <Text color="fgMuted"> The content area has a secondary background color and custom padding. </Text> </Tray> )} </VStack> ); }
Footer
You can customize the footer section's appearance, such as the background color.
function CustomFooterTray() { const [visible, setVisible] = useState(false); const { isPhone } = useBreakpoints(); const handleOpen = () => setVisible(true); const handleClose = () => setVisible(false); return ( <VStack gap={2}> <Button onClick={handleOpen}>Open Custom Footer Tray</Button> {visible && ( <Tray pin={isPhone ? 'bottom' : 'right'} showHandleBar={isPhone} onCloseComplete={handleClose} title="Custom footer styling" styles={{ footer: { backgroundColor: 'var(--color-bgSecondary)', }, }} footer={({ handleClose }) => ( <PageFooter borderedTop justifyContent={isPhone ? 'center' : 'flex-end'} action={ <Button block={isPhone} onClick={handleClose}> Close </Button> } /> )} > <Text color="fgMuted" paddingBottom={2}> The footer has a secondary background color. </Text> </Tray> )} </VStack> ); }
Handlebar
You can customize the handlebar appearance to change its color and opacity. This is useful when the default handlebar color does not have enough contrast against an image header, such as inverting it to white for dark or colorful backgrounds.
function FullBleedWithInvertedHandlebar() { const [visible, setVisible] = useState(false); const { isPhone } = useBreakpoints(); const handleOpen = () => setVisible(true); const handleClose = () => setVisible(false); const titleId = useId(); return ( <VStack gap={2}> <Button onClick={handleOpen}>Open Full Bleed Tray</Button> {visible && ( <Tray pin={isPhone ? 'bottom' : 'right'} showHandleBar={isPhone} onCloseComplete={handleClose} title={ <Box flexGrow={1} marginX={{ base: -4, phone: -3 }}> <img alt="Full Bleed" height={180} src="/img/tray_header.png" style={{ objectFit: 'cover', pointerEvents: 'none' }} width="100%" /> </Box> } header={ <Text id={titleId} font="title3" paddingTop={2} paddingX={{ base: 4, phone: 3 }}> Section header </Text> } styles={{ handleBar: { position: 'absolute', top: 0, left: 0, right: 0, zIndex: 1, }, handleBarHandle: { backgroundColor: 'white', opacity: 1, }, closeButton: { position: 'absolute', top: 'var(--space-4)', right: 'var(--space-4)', zIndex: 1, }, header: { paddingTop: 0, }, content: { paddingBottom: 'var(--space-3)' }, }} accessibilityLabelledBy={titleId} > <Text color="fgMuted" font="body" paddingBottom={2}> Curabitur commodo nulla vel dolor vulputate vestibulum. Nulla et nisl molestie, interdum lorem id, viverra. </Text> </Tray> )} </VStack> ); }
Close Button
You can customize the close button to adjust the button's appearance, such as to improve visibility against header images or custom backgrounds.
function FullBleedWithStyledCloseButton() { const [visible, setVisible] = useState(false); const { isPhone } = useBreakpoints(); const handleOpen = () => setVisible(true); const handleClose = () => setVisible(false); const titleId = useId(); return ( <VStack gap={2}> <style>{` .tray-close-button-inverted { color: white; } .tray-close-button-inverted:hover, .tray-close-button-inverted:focus-visible { background-color: rgba(255, 255, 255, 0.15); } `}</style> <Button onClick={handleOpen}>Open Full Bleed Tray</Button> {visible && ( <Tray pin={isPhone ? 'bottom' : 'right'} showHandleBar={false} onCloseComplete={handleClose} title={ <Box flexGrow={1} marginX={{ base: -4, phone: -3 }}> <img alt="Full Bleed" height={180} src="/img/tray_header.png" style={{ objectFit: 'cover', pointerEvents: 'none' }} width="100%" /> </Box> } header={ <Text id={titleId} font="title3" paddingTop={2} paddingX={{ base: 4, phone: 3 }}> Section header </Text> } styles={{ closeButton: { position: 'absolute', top: 'var(--space-4)', right: 'var(--space-4)', zIndex: 1, }, header: { paddingTop: 0, }, content: { paddingBottom: 'var(--space-3)' }, }} classNames={{ closeButton: 'tray-close-button-inverted', }} accessibilityLabelledBy={titleId} > <Text color="fgMuted" font="body" paddingBottom={2}> Curabitur commodo nulla vel dolor vulputate vestibulum. Nulla et nisl molestie, interdum lorem id, viverra. </Text> </Tray> )} </VStack> ); }
Composed Examples
Floating
You can create a floating tray by adjusting the inset based on pin direction.
function FloatingTrayExample() { function FloatingTray({ pin = 'right', offset = '2', borderRadius = '600', children, styles, ...props }) { const theme = useTheme(); const offsetPx = theme.space[offset]; const borderRadiusVar = `var(--borderRadius-${borderRadius})`; const floatingInsets = useMemo(() => { switch (pin) { case 'right': return { top: offsetPx, bottom: offsetPx, right: offsetPx }; case 'left': return { top: offsetPx, bottom: offsetPx, left: offsetPx }; case 'top': return { top: offsetPx, left: offsetPx, right: offsetPx }; case 'bottom': return { bottom: offsetPx, left: offsetPx, right: offsetPx }; default: return { top: offsetPx, bottom: offsetPx, right: offsetPx }; } }, [pin, offsetPx]); return ( <Tray {...props} pin={pin} showHandleBar styles={{ ...styles, container: { ...floatingInsets, // All corners rounded since the tray is floating (not flush to edge) borderRadius: borderRadiusVar, ...styles?.container, }, }} > {children} </Tray> ); } function Example() { const [pinDirection, setPinDirection] = useState(null); const handleClose = () => setPinDirection(null); return ( <VStack gap={2}> <HStack gap={2} flexWrap="wrap"> <Button onClick={() => setPinDirection('right')}>Open Right Tray</Button> <Button onClick={() => setPinDirection('bottom')}>Open Bottom Tray</Button> <Button onClick={() => setPinDirection('left')}>Open Left Tray</Button> <Button onClick={() => setPinDirection('top')}>Open Top Tray</Button> </HStack> {pinDirection !== null && ( <FloatingTray pin={pinDirection} onCloseComplete={handleClose} title="Example title"> <VStack paddingBottom={2}> {Array.from({ length: 20 }, (_, i) => ( <ListCell key={i} spacingVariant="condensed" title="Title" description="Description" accessory="arrow" onClick={() => alert('Cell clicked!')} innerSpacing={{ marginX: -4, paddingX: 4, paddingY: 1, }} /> ))} </VStack> </FloatingTray> )} </VStack> ); } return <Example />; }
Multiple Screen Example
You can create a tray with multiple screens that have back navigation.
function MultiScreenTrayExample() { function MultiScreenTray({ screens, initialScreen = 0, onCloseComplete, ...props }) { const [currentScreen, setCurrentScreen] = useState(initialScreen); const screen = screens[currentScreen]; const handleBack = () => setCurrentScreen(0); const handleNavigate = (index) => setCurrentScreen(index); return ( <Tray {...props} onCloseComplete={onCloseComplete} title={ <VStack alignItems="flex-start" gap={{ phone: 1.5, tablet: 2, desktop: 2 }}> {currentScreen > 0 && ( <IconButton transparent name="backArrow" onClick={handleBack} accessibilityLabel="Go back" margin={-1.5} /> )} <Text font="title3">{screen.title}</Text> </VStack> } accessibilityLabel={screen.title} > {screen.render({ onNavigate: handleNavigate })} </Tray> ); } function Example() { const [visible, setVisible] = useState(false); const { isPhone } = useBreakpoints(); const handleOpen = () => setVisible(true); const handleClose = () => setVisible(false); const screens = [ { title: 'Settings', render: ({ onNavigate }) => ( <VStack> <ListCell spacingVariant="condensed" title="Account" description="Manage your account settings" accessory="arrow" onClick={() => onNavigate(1)} innerSpacing={{ marginX: -4, paddingX: 4, paddingY: 1 }} /> <ListCell spacingVariant="condensed" title="Notifications" description="Configure notification preferences" accessory="arrow" onClick={() => onNavigate(2)} innerSpacing={{ marginX: -4, paddingX: 4, paddingY: 1 }} /> <ListCell spacingVariant="condensed" title="Privacy" description="Review privacy settings" accessory="arrow" onClick={() => onNavigate(3)} innerSpacing={{ marginX: -4, paddingX: 4, paddingY: 1 }} /> </VStack> ), }, { title: 'Account', render: () => ( <Text color="fgMuted" paddingBottom={2}> Account settings content goes here. </Text> ), }, { title: 'Notifications', render: () => ( <Text color="fgMuted" paddingBottom={2}> Notification preferences content goes here. </Text> ), }, { title: 'Privacy', render: () => ( <Text color="fgMuted" paddingBottom={2}> Privacy settings content goes here. </Text> ), }, ]; return ( <VStack gap={2}> <Button onClick={handleOpen}>Open Multi-Screen Tray</Button> {visible && ( <MultiScreenTray screens={screens} pin={isPhone ? 'bottom' : 'right'} showHandleBar={isPhone} onCloseComplete={handleClose} /> )} </VStack> ); } return <Example />; }
Header with Illustration
You can create a reusable responsive tray with a pictogram and title in the header.
function IllustrationTrayExample() { function IllustrationTray({ pictogramName, title, children, ...props }) { const titleId = useId(); return ( <Tray {...props} title={ <VStack gap={{ phone: 1.5, tablet: 2, desktop: 2 }}> <Pictogram name={pictogramName} /> <Text id={titleId} font="title3"> {title} </Text> </VStack> } accessibilityLabelledBy={titleId} > {children} </Tray> ); } function Example() { const [visible, setVisible] = useState(false); const { isPhone } = useBreakpoints(); const handleOpen = () => setVisible(true); const handleClose = () => setVisible(false); return ( <VStack gap={2}> <Button onClick={handleOpen}>Open Illustration Tray</Button> {visible && ( <IllustrationTray pictogramName="addWallet" title="Section header" pin={isPhone ? 'bottom' : 'right'} showHandleBar={isPhone} onCloseComplete={handleClose} > <Text color="fgMuted" font="body" paddingBottom={2}> Curabitur commodo nulla vel dolor vulputate vestibulum. Nulla et nisl molestie, interdum lorem id, viverra. </Text> </IllustrationTray> )} </VStack> ); } return <Example />; }
Responsive
You can create a reusable responsive tray that adapts its pin direction and handle bar visibility based on the viewport size.
function ResponsiveTrayExample() { function ResponsiveTray({ pin, showHandleBar, footer, footerLabel, children, ...props }) { const { isPhone } = useBreakpoints(); const resolvedFooter = footer ?? (footerLabel ? ({ handleClose }) => ( <PageFooter borderedTop justifyContent={isPhone ? 'center' : 'flex-end'} action={ <Button block={isPhone} onClick={handleClose}> {footerLabel} </Button> } /> ) : undefined); return ( <Tray {...props} pin={pin ?? (isPhone ? 'bottom' : 'right')} showHandleBar={showHandleBar ?? isPhone} footer={resolvedFooter} > {children} </Tray> ); } function Example() { const [visible, setVisible] = useState(false); const handleOpen = () => setVisible(true); const handleClose = () => setVisible(false); return ( <VStack gap={2}> <Button onClick={handleOpen}>Open Responsive Tray</Button> {visible && ( <ResponsiveTray onCloseComplete={handleClose} title="Example title" footerLabel="Close"> <Text color="fgMuted" paddingBottom={2}> Curabitur commodo nulla vel dolor vulputate vestibulum. Nulla et nisl molestie, interdum lorem id, viverra. </Text> </ResponsiveTray> )} </VStack> ); } return <Example />; }