Skip to main content
Sidebar
@coinbase/cds-web@8.30.1
A composable and customizable vertical navigation component with support for multiple variants, collapsible states, and custom content areas.
Import
import { Sidebar } from '@coinbase/cds-web/navigation/Sidebar'
SourceView source codeStorybookView StorybookFigmaView Figma (internal only)
Related components
View as Markdown

Sidebar is a vertical navigation component for accessing different sections of an application. It supports multiple variants, collapsible states, and custom content areas.

Basics

A Sidebar is composed of the following parts:

  • Sidebar - The main container with logo and navigation items
  • SidebarItem - Individual navigation items with icon and title
  • SidebarMoreMenu - Overflow menu for additional navigation options
Loading...
Live Code
function BasicSidebar() {
  const [activeIndex, setActiveIndex] = useState(0);
  const items = [
    { title: 'Home', icon: 'home' },
    { title: 'Assets', icon: 'chartPie' },
    { title: 'Trade', icon: 'trading' },
    { title: 'Settings', icon: 'cog' },
  ];

  return (
    <HStack alignItems="flex-start" justifyContent="center" overflow="hidden">
      <Sidebar logo={<LogoMark />}>
        {items.map((item, index) => (
          <SidebarItem
            key={item.title}
            active={index === activeIndex}
            icon={item.icon}
            onClick={() => setActiveIndex(index)}
            title={item.title}
            tooltipContent={item.title}
          />
        ))}
      </Sidebar>
    </HStack>
  );
}

Variants

Default

Use the Default variant on standard consumer-facing surfaces like Retail where maximum navigation and content space is desired. This variant shows full labels alongside icons.

Loading...
Live Code
function DefaultVariant() {
  const items = [
    { title: 'Home', icon: 'home' },
    { title: 'Assets', icon: 'chartPie' },
    { title: 'Trade', icon: 'trading' },
    { title: 'Pay', icon: 'pay' },
    { title: 'For you', icon: 'newsFeed' },
    { title: 'Earn', icon: 'giftBox' },
    { title: 'Borrow', icon: 'cash' },
    { title: 'DeFi', icon: 'defi' },
  ];
  const [activeIndex, setActiveIndex] = useState(0);
  const [moreMenuValue, setMoreMenuValue] = useState();
  const navItems = items.slice(0, 8);
  const moreMenuOptions = items.slice(4);
  const handleMoreMenuChange = (newValue) => {
    const moreIndex =
      moreMenuOptions.findIndex((option) => option.title === newValue) + navItems.length;
    setActiveIndex(moreIndex);
    setMoreMenuValue(newValue);
  };
  const handleItemPress = (index) => {
    setActiveIndex(index);
    setMoreMenuValue(undefined);
  };
  const renderEnd = () => {
    return (
      <Pressable
        as="button"
        background="bgPrimaryWash"
        borderRadius={1000}
        transparentWhileInactive
        width="100%"
        onClick={() => console.log}
      >
        <HStack alignItems="center" gap={2} paddingX={2} paddingY={2}>
          <Icon name="documentation" />
          <Text as="span" font="headline" color="foreground">
            End item
          </Text>
        </HStack>
      </Pressable>
    );
  };
  return (
    <HStack alignItems="flex-start" justifyContent="center" overflow="hidden">
      <Sidebar
        logo={
          <Box height={32}>
            <SubBrandLogoMark type="commerce" />
          </Box>
        }
        renderEnd={renderEnd}
        styles={{
          end: {
            width: '100%',
          },
        }}
      >
        {navItems.map((item, index) => (
          <SidebarItem
            key={`sidebar-item--${item.title}`}
            active={index === activeIndex}
            onClick={() => handleItemPress(index)}
            tooltipContent={item.title}
            {...item}
          />
        ))}
        <SidebarMoreMenu
          active={activeIndex >= navItems.length}
          onChange={handleMoreMenuChange}
          tooltipContent="More"
          value={moreMenuValue}
        >
          {moreMenuOptions.map((item) => (
            <SelectOption
              key={`sidebar-more-menu-item--${item.title}`}
              description={item.title}
              media={<Icon name={item.icon} />}
              value={item.title}
            />
          ))}
        </SidebarMoreMenu>
      </Sidebar>
    </HStack>
  );
}

Condensed

Use in specialized workflows with complex data displays, such as Exchange and Advanced Trade, where navigation space is minimized to focus on core tasks. This variant displays icons with small labels below.

Loading...
Live Code
function CondensedVariant() {
  const items = [
    { title: 'Spot', icon: 'chartCandles' },
    { title: 'Futures', icon: 'chartBar' },
    { title: 'Portfolio', icon: 'chartPie' },
    { title: 'Orders', icon: 'documentation' },
    { title: 'For you', icon: 'newsFeed' },
    { title: 'Earn', icon: 'giftBox' },
    { title: 'Borrow', icon: 'cash' },
    { title: 'DeFi', icon: 'defi' },
  ];
  const [activeIndex, setActiveIndex] = useState(0);
  const [moreMenuValue, setMoreMenuValue] = useState();
  const navItems = items.slice(0, 4);
  const moreMenuOptions = items.slice(4);
  const handleMoreMenuChange = (newValue) => {
    const moreIndex =
      moreMenuOptions.findIndex((option) => option.title === newValue) + navItems.length;
    setActiveIndex(moreIndex);
    setMoreMenuValue(newValue);
  };
  const handleItemClick = (index) => {
    setActiveIndex(index);
    setMoreMenuValue(undefined);
  };
  return (
    <HStack alignItems="flex-start" justifyContent="center" overflow="hidden">
      <Sidebar logo={<LogoMark foreground />} variant="condensed">
        {navItems.map((item, index) => (
          <SidebarItem
            key={`sidebar-item--${item.title}`}
            active={index === activeIndex}
            onClick={() => handleItemClick(index)}
            tooltipContent={item.title}
            {...item}
          />
        ))}
        <SidebarMoreMenu
          active={activeIndex >= navItems.length}
          onChange={handleMoreMenuChange}
          tooltipContent="More"
          value={moreMenuValue}
        >
          {moreMenuOptions.map((item) => (
            <SelectOption
              key={`sidebar-more-menu-item--${item.title}`}
              description={item.title}
              media={<Icon name={item.icon} />}
              value={item.title}
            />
          ))}
        </SidebarMoreMenu>
      </Sidebar>
    </HStack>
  );
}

Collapsed State

Controlled Collapse

Use the collapsed prop to control the sidebar's collapsed state. When collapsed, only icons are shown with tooltips for labels.

Loading...
Live Code
function ControlledCollapse() {
  const items = [
    { title: 'Home', icon: 'home' },
    { title: 'Assets', icon: 'chartPie' },
    { title: 'Trade', icon: 'trading' },
    { title: 'Pay', icon: 'pay' },
    { title: 'For you', icon: 'newsFeed' },
    { title: 'Earn', icon: 'giftBox' },
    { title: 'Borrow', icon: 'cash' },
    { title: 'DeFi', icon: 'defi' },
  ];
  const [activeIndex, setActiveIndex] = useState(0);
  const [moreMenuValue, setMoreMenuValue] = useState();
  const [collapsed, setCollapsed] = useState(true);
  const moreMenuOptions = items.slice(4);
  const handleMoreMenuChange = (newValue) => {
    const moreIndex =
      moreMenuOptions.findIndex((option) => option.title === newValue) + items.length;
    setActiveIndex(moreIndex);
    setMoreMenuValue(newValue);
  };
  const handleItemPress = (index) => {
    setActiveIndex(index);
    setMoreMenuValue(undefined);
  };
  const renderEnd = () => (
    <IconButton
      name={collapsed ? 'caretRight' : 'caretLeft'}
      onClick={() => setCollapsed(!collapsed)}
      width="48px"
      height="48px"
    />
  );
  return (
    <HStack alignItems="flex-start" justifyContent="center" overflow="hidden">
      <Sidebar collapsed={collapsed} logo={<LogoMark />} renderEnd={renderEnd}>
        {items.map((item, index) => (
          <SidebarItem
            key={`sidebar-item--${item.title}`}
            active={index === activeIndex}
            onClick={() => handleItemPress(index)}
            tooltipContent={item.title}
            {...item}
          />
        ))}
        <SidebarMoreMenu
          active={activeIndex >= items.length}
          onChange={handleMoreMenuChange}
          tooltipContent="More"
          value={moreMenuValue}
        >
          {moreMenuOptions.map((item) => (
            <SelectOption
              key={`sidebar-more-menu-item--${item.title}`}
              description={item.title}
              media={<Icon name={item.icon} />}
              value={item.title}
            />
          ))}
        </SidebarMoreMenu>
      </Sidebar>
    </HStack>
  );
}

Auto Collapse

Use the autoCollapse prop to automatically collapse the sidebar at or below the tablet breakpoint (768px). This is useful for responsive layouts where the sidebar should adapt to screen size.

Loading...
Live Code
function AutoCollapse() {
  const items = [
    { title: 'Home', icon: 'home' },
    { title: 'Assets', icon: 'chartPie' },
    { title: 'Trade', icon: 'trading' },
    { title: 'Settings', icon: 'cog' },
  ];
  const [activeIndex, setActiveIndex] = useState(0);

  return (
    <HStack alignItems="flex-start" justifyContent="center" overflow="hidden">
      <Sidebar autoCollapse logo={<LogoMark />}>
        {items.map((item, index) => (
          <SidebarItem
            key={item.title}
            active={index === activeIndex}
            icon={item.icon}
            onClick={() => setActiveIndex(index)}
            title={item.title}
            tooltipContent={item.title}
          />
        ))}
      </Sidebar>
      <VStack flexGrow={1} padding={3}>
        <Text color="fgMuted" font="label1">
          Resize the browser window to see the sidebar auto-collapse at the tablet breakpoint.
        </Text>
      </VStack>
    </HStack>
  );
}

Custom Content

The logo prop accepts either a React element or a render function that receives the collapsed state. Use the render function when you need different logos for collapsed and expanded states.

Loading...
Live Code
function CustomLogo() {
  const items = [
    { title: 'Home', icon: 'home' },
    { title: 'Assets', icon: 'chartPie' },
    { title: 'Trade', icon: 'trading' },
  ];
  const [activeIndex, setActiveIndex] = useState(0);
  const [collapsed, setCollapsed] = useState(false);

  const renderLogo = (isCollapsed) =>
    isCollapsed ? (
      <LogoMark />
    ) : (
      <Box height={32}>
        <SubBrandLogoMark type="commerce" />
      </Box>
    );

  return (
    <HStack alignItems="flex-start" justifyContent="center" overflow="hidden">
      <Sidebar
        collapsed={collapsed}
        logo={renderLogo}
        renderEnd={() => (
          <IconButton
            name={collapsed ? 'caretRight' : 'caretLeft'}
            onClick={() => setCollapsed(!collapsed)}
          />
        )}
      >
        {items.map((item, index) => (
          <SidebarItem
            key={item.title}
            active={index === activeIndex}
            icon={item.icon}
            onClick={() => setActiveIndex(index)}
            title={item.title}
            tooltipContent={item.title}
          />
        ))}
      </Sidebar>
    </HStack>
  );
}

Render End

The renderEnd prop places content at the bottom of the sidebar. It receives the collapsed state as a parameter, allowing you to adapt the content based on the sidebar's state.

Loading...
Live Code
function RenderEndExample() {
  const items = [
    { title: 'Home', icon: 'home' },
    { title: 'Assets', icon: 'chartPie' },
    { title: 'Trade', icon: 'trading' },
  ];
  const [activeIndex, setActiveIndex] = useState(0);

  const renderEnd = (isCollapsed) => (
    <Pressable
      as="button"
      background="bgPrimaryWash"
      borderRadius={1000}
      transparentWhileInactive
      width="100%"
      onClick={() => alert('Help clicked!')}
    >
      <HStack alignItems="center" gap={2} paddingX={2} paddingY={2}>
        <Icon name="questionCircle" />
        {!isCollapsed && (
          <TextHeadline as="span" color="foreground">
            Help & Support
          </TextHeadline>
        )}
      </HStack>
    </Pressable>
  );

  return (
    <HStack alignItems="flex-start" justifyContent="center" overflow="hidden">
      <Sidebar
        logo={<LogoMark />}
        renderEnd={renderEnd}
        styles={{
          end: {
            width: '100%',
          },
        }}
      >
        {items.map((item, index) => (
          <SidebarItem
            key={item.title}
            active={index === activeIndex}
            icon={item.icon}
            onClick={() => setActiveIndex(index)}
            title={item.title}
            tooltipContent={item.title}
          />
        ))}
      </Sidebar>
    </HStack>
  );
}

Styling

Use the styles prop to customize specific parts of the sidebar.

Loading...
Live Code
function CustomStyles() {
  const items = [
    { title: 'Home', icon: 'home' },
    { title: 'Assets', icon: 'chartPie' },
    { title: 'Trade', icon: 'trading' },
    { title: 'Settings', icon: 'cog' },
  ];
  const [activeIndex, setActiveIndex] = useState(0);

  return (
    <HStack alignItems="flex-start" justifyContent="center" overflow="hidden">
      <Sidebar
        logo={<LogoMark />}
        renderEnd={() => (
          <Pressable
            as="button"
            background="bgPrimaryWash"
            borderRadius={1000}
            transparentWhileInactive
            width="100%"
          >
            <HStack alignItems="center" gap={2} paddingX={2} paddingY={2}>
              <Icon name="questionCircle" />
              <TextHeadline as="span">Help</TextHeadline>
            </HStack>
          </Pressable>
        )}
        styles={{
          root: {
            background:
              'linear-gradient(180deg, var(--color-bg) 0%, var(--color-bgAlternate) 100%)',
          },
          logo: { paddingBottom: 32 },
          end: { width: '100%' },
        }}
      >
        {items.map((item, index) => (
          <SidebarItem
            key={item.title}
            active={index === activeIndex}
            icon={item.icon}
            onClick={() => setActiveIndex(index)}
            title={item.title}
            tooltipContent={item.title}
          />
        ))}
      </Sidebar>
    </HStack>
  );
}

You can also use custom class names on the various subcomponents in Sidebar using the classNames prop.

const customLogoStyles = css`
padding-bottom: var(--spacing-6);
`;

function CustomClassNamesExample() {
const [activeIndex, setActiveIndex] = useState(0);
const items = [
{ title: 'Home', icon: 'home' },
{ title: 'Assets', icon: 'chartPie' },
{ title: 'Trade', icon: 'trading' },
{ title: 'Settings', icon: 'cog' },
];

return (
<Sidebar
logo={<LogoMark />}
classNames={{
logo: customLogoStyles,
}}
>
{items.map((item, index) => (
<SidebarItem
key={item.title}
active={index === activeIndex}
icon={item.icon}
onClick={() => setActiveIndex(index)}
title={item.title}
tooltipContent={item.title}
/>
))}
</Sidebar>
);
}

Composed Examples

Application Shell

A complete application layout with sidebar navigation, main content area, and responsive behavior.

Loading...
Live Code
function ApplicationShell() {
  const items = [
    { title: 'Dashboard', icon: 'home' },
    { title: 'Analytics', icon: 'chartPie' },
    { title: 'Transactions', icon: 'trading' },
    { title: 'Payments', icon: 'pay' },
    { title: 'News Feed', icon: 'newsFeed' },
    { title: 'Rewards', icon: 'giftBox' },
    { title: 'Lending', icon: 'cash' },
    { title: 'DeFi', icon: 'defi' },
  ];
  const [activeIndex, setActiveIndex] = useState(0);
  const [moreMenuValue, setMoreMenuValue] = useState();
  const navItems = items.slice(0, 5);
  const moreMenuOptions = items.slice(5);

  const handleMoreMenuChange = (newValue) => {
    const moreIndex =
      moreMenuOptions.findIndex((option) => option.title === newValue) + navItems.length;
    setActiveIndex(moreIndex);
    setMoreMenuValue(newValue);
  };

  const handleItemPress = (index) => {
    setActiveIndex(index);
    setMoreMenuValue(undefined);
  };

  const currentPage = items[activeIndex]?.title || 'Dashboard';

  return (
    <HStack alignItems="stretch" height={400} overflow="hidden">
      <Sidebar
        autoCollapse
        logo={<LogoMark />}
        renderEnd={(isCollapsed) => (
          <VStack gap={1}>
            <Pressable
              as="button"
              background="bgPrimaryWash"
              borderRadius={1000}
              transparentWhileInactive
              width="100%"
            >
              <HStack alignItems="center" gap={2} paddingX={2} paddingY={2}>
                <Icon name="cog" />
                {!isCollapsed && <TextHeadline as="span">Settings</TextHeadline>}
              </HStack>
            </Pressable>
            <Pressable
              as="button"
              background="bgPrimaryWash"
              borderRadius={1000}
              transparentWhileInactive
              width="100%"
            >
              <HStack alignItems="center" gap={2} paddingX={2} paddingY={2}>
                <Avatar size="s" />
                {!isCollapsed && <TextHeadline as="span">Profile</TextHeadline>}
              </HStack>
            </Pressable>
          </VStack>
        )}
      >
        {navItems.map((item, index) => (
          <SidebarItem
            key={item.title}
            active={index === activeIndex}
            icon={item.icon}
            onClick={() => handleItemPress(index)}
            title={item.title}
            tooltipContent={item.title}
          />
        ))}
        <SidebarMoreMenu
          active={activeIndex >= navItems.length}
          onChange={handleMoreMenuChange}
          tooltipContent="More"
          value={moreMenuValue}
        >
          {moreMenuOptions.map((item) => (
            <SelectOption
              key={item.title}
              description={item.title}
              media={<Icon name={item.icon} />}
              value={item.title}
            />
          ))}
        </SidebarMoreMenu>
      </Sidebar>
      <VStack background="bgAlternate" flexGrow={1} padding={3}>
        <Text as="h1" font="title2">
          {currentPage}
        </Text>
        <Text color="fgMuted" font="body">
          Welcome to the {currentPage.toLowerCase()} page. This is a sample application shell
          demonstrating the Sidebar component with responsive behavior.
        </Text>
      </VStack>
    </HStack>
  );
}

Condensed Trading Interface

A condensed sidebar optimized for professional trading interfaces with minimal visual footprint.

Loading...
Live Code
function TradingInterface() {
  const items = [
    { title: 'Spot', icon: 'chartCandles' },
    { title: 'Futures', icon: 'chartBar' },
    { title: 'Portfolio', icon: 'chartPie' },
    { title: 'Orders', icon: 'documentation' },
  ];
  const [activeIndex, setActiveIndex] = useState(0);

  return (
    <HStack>
      <Sidebar logo={<LogoMark foreground />} variant="condensed">
        {items.map((item, index) => (
          <SidebarItem
            key={item.title}
            active={index === activeIndex}
            icon={item.icon}
            onClick={() => setActiveIndex(index)}
            title={item.title}
          />
        ))}
      </Sidebar>
      <VStack background="bgAlternate" flexGrow={1} gap={2} padding={3}>
        <HStack justifyContent="space-between">
          <Text font="title3">BTC/USD</Text>
          <Text color="fgPositive" font="title3">
            $67,432.50
          </Text>
        </HStack>
        <Box
          background="bg"
          borderRadius={200}
          flexGrow={1}
          style={{
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <Text color="fgMuted" font="label1">
            {items[activeIndex].title} Chart Area
          </Text>
        </Box>
      </VStack>
    </HStack>
  );
}

Is this page useful?

Coinbase Design is an open-source, adaptable system of guidelines, components, and tools that aid the best practices of user interface design for crypto products.