import { Tray } from '@coinbase/cds-web/overlays/tray/Tray'
- framer-motion: ^10.18.0,
- react-dom: ^18.3.1
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> ); }