Deck
Browse the catalog

Build / Organisms

SettingsNav

  • organism
  • play-fn

Switching between the sections of a settings page.

Two controls for one job, chosen by width. From md up it is a list, which shows every section at once and keeps the current one visible while you read the panel beside it. Below that it is a <select> — eight destinations stacked above the content would push the content itself off the screen, and a native select opens as the platform's own picker, which is the one control on a phone nobody has to learn.

The swap is CSS, so there is no viewport JS to disagree with the server, and whichever control is hidden is display: none and therefore out of the accessibility tree rather than merely invisible.

Groups survive the swap. The headings become <optgroup>s, because the split between what is yours and what is your company's is the same information at either width, and dropping it on the smaller screen is the kind of quiet loss that makes a settings page hard to navigate.

import { SettingsNav } from '@meetcard/deck'

Example

<SettingsNav currentId="/settings/profile" groups={[{ label: 'User', items: [...] }]} onSelect={go} />

Stories

Every preview below is the real component, rendered in this page from the same story Storybook runs. The width control resizes each preview's container; CSS media queries still follow the browser window, so breakpoint behaviour is worth checking in Storybook.

Desktop. The list shows every section at once, so the current one stays visible while you read the panel beside it.

Default
Source
const Default = () => <Harness
    groups={[
      {
        label: 'User',
        items: [
          { id: '/settings/profile', href: '/settings/profile', label: 'Profile', icon: <Icon d="M12 13a4 4 0 100-8 4 4 0 000 8zM5 21a7 7 0 0114 0" /> },
          { id: '/settings/account', href: '/settings/account', label: 'Account', icon: <Icon d="M3 6h18v12H3zM3 10h18M7 14h4" /> },
          { id: '/settings/notifications', href: '/settings/notifications', label: 'Notifications', icon: <Icon d="M18 8a6 6 0 10-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9M10.3 21a2 2 0 003.4 0" /> },
        ],
      },
      {
        label: 'Admin',
        items: [
          { id: '/settings/billing', href: '/settings/billing', label: 'Billing', icon: <Icon d="M2 7h20v11H2zM2 11h20" /> },
          { id: '/settings/company', href: '/settings/company', label: 'Company', icon: <Icon d="M4 21V5a1 1 0 011-1h9a1 1 0 011 1v16M15 9h4a1 1 0 011 1v11M8 8h3M8 12h3M8 16h3" /> },
          { id: '/settings/customization', href: '/settings/customization', label: 'Customization', icon: <Icon d="M12 3a9 9 0 100 18h1.5a2 2 0 001.6-3.2 2 2 0 011.6-3.2H19a2 2 0 002-2A9 9 0 0012 3zM7.5 10.5v.01M12 7.5v.01M16.5 10.5v.01" /> },
          { id: '/settings/integrations', href: '/settings/integrations', label: 'Integrations', icon: <Icon d="M9 3v6M15 3v6M6 9h12v4a6 6 0 01-12 0zM12 19v2" /> },
          { id: '/settings/team', href: '/settings/team', label: 'Team', icon: <Icon d="M4 19v-1a4 4 0 014-4h2a4 4 0 014 4v1M9 10a3 3 0 100-6 3 3 0 000 6zM17 11a3 3 0 100-6" /> },
        ],
      },
    ]}
    currentId="/settings/profile" />;

Choosing a section moves the fill and the announcement together.

Switching
Source
const Switching = () => <Harness
    groups={[
      {
        label: 'User',
        items: [
          { id: '/settings/profile', href: '/settings/profile', label: 'Profile', icon: <Icon d="M12 13a4 4 0 100-8 4 4 0 000 8zM5 21a7 7 0 0114 0" /> },
          { id: '/settings/account', href: '/settings/account', label: 'Account', icon: <Icon d="M3 6h18v12H3zM3 10h18M7 14h4" /> },
          { id: '/settings/notifications', href: '/settings/notifications', label: 'Notifications', icon: <Icon d="M18 8a6 6 0 10-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9M10.3 21a2 2 0 003.4 0" /> },
        ],
      },
      {
        label: 'Admin',
        items: [
          { id: '/settings/billing', href: '/settings/billing', label: 'Billing', icon: <Icon d="M2 7h20v11H2zM2 11h20" /> },
          { id: '/settings/company', href: '/settings/company', label: 'Company', icon: <Icon d="M4 21V5a1 1 0 011-1h9a1 1 0 011 1v16M15 9h4a1 1 0 011 1v11M8 8h3M8 12h3M8 16h3" /> },
          { id: '/settings/customization', href: '/settings/customization', label: 'Customization', icon: <Icon d="M12 3a9 9 0 100 18h1.5a2 2 0 001.6-3.2 2 2 0 011.6-3.2H19a2 2 0 002-2A9 9 0 0012 3zM7.5 10.5v.01M12 7.5v.01M16.5 10.5v.01" /> },
          { id: '/settings/integrations', href: '/settings/integrations', label: 'Integrations', icon: <Icon d="M9 3v6M15 3v6M6 9h12v4a6 6 0 01-12 0zM12 19v2" /> },
          { id: '/settings/team', href: '/settings/team', label: 'Team', icon: <Icon d="M4 19v-1a4 4 0 014-4h2a4 4 0 014 4v1M9 10a3 3 0 100-6 3 3 0 000 6zM17 11a3 3 0 100-6" /> },
        ],
      },
    ]}
    currentId="/settings/profile" />;

Phone. Eight destinations stacked above the content would push the content off the screen, so the list becomes the platform's own picker — with the groups intact as <optgroup>s.

Mobile
Source
const Mobile = () => <Harness
    groups={[
      {
        label: 'User',
        items: [
          { id: '/settings/profile', href: '/settings/profile', label: 'Profile', icon: <Icon d="M12 13a4 4 0 100-8 4 4 0 000 8zM5 21a7 7 0 0114 0" /> },
          { id: '/settings/account', href: '/settings/account', label: 'Account', icon: <Icon d="M3 6h18v12H3zM3 10h18M7 14h4" /> },
          { id: '/settings/notifications', href: '/settings/notifications', label: 'Notifications', icon: <Icon d="M18 8a6 6 0 10-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9M10.3 21a2 2 0 003.4 0" /> },
        ],
      },
      {
        label: 'Admin',
        items: [
          { id: '/settings/billing', href: '/settings/billing', label: 'Billing', icon: <Icon d="M2 7h20v11H2zM2 11h20" /> },
          { id: '/settings/company', href: '/settings/company', label: 'Company', icon: <Icon d="M4 21V5a1 1 0 011-1h9a1 1 0 011 1v16M15 9h4a1 1 0 011 1v11M8 8h3M8 12h3M8 16h3" /> },
          { id: '/settings/customization', href: '/settings/customization', label: 'Customization', icon: <Icon d="M12 3a9 9 0 100 18h1.5a2 2 0 001.6-3.2 2 2 0 011.6-3.2H19a2 2 0 002-2A9 9 0 0012 3zM7.5 10.5v.01M12 7.5v.01M16.5 10.5v.01" /> },
          { id: '/settings/integrations', href: '/settings/integrations', label: 'Integrations', icon: <Icon d="M9 3v6M15 3v6M6 9h12v4a6 6 0 01-12 0zM12 19v2" /> },
          { id: '/settings/team', href: '/settings/team', label: 'Team', icon: <Icon d="M4 19v-1a4 4 0 014-4h2a4 4 0 014 4v1M9 10a3 3 0 100-6 3 3 0 000 6zM17 11a3 3 0 100-6" /> },
        ],
      },
    ]}
    currentId="/settings/profile" />;

The breakpoint's own value — 768px is the first width that gets the list.

Tablet
Source
const Tablet = () => <Harness
    groups={[
      {
        label: 'User',
        items: [
          { id: '/settings/profile', href: '/settings/profile', label: 'Profile', icon: <Icon d="M12 13a4 4 0 100-8 4 4 0 000 8zM5 21a7 7 0 0114 0" /> },
          { id: '/settings/account', href: '/settings/account', label: 'Account', icon: <Icon d="M3 6h18v12H3zM3 10h18M7 14h4" /> },
          { id: '/settings/notifications', href: '/settings/notifications', label: 'Notifications', icon: <Icon d="M18 8a6 6 0 10-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9M10.3 21a2 2 0 003.4 0" /> },
        ],
      },
      {
        label: 'Admin',
        items: [
          { id: '/settings/billing', href: '/settings/billing', label: 'Billing', icon: <Icon d="M2 7h20v11H2zM2 11h20" /> },
          { id: '/settings/company', href: '/settings/company', label: 'Company', icon: <Icon d="M4 21V5a1 1 0 011-1h9a1 1 0 011 1v16M15 9h4a1 1 0 011 1v11M8 8h3M8 12h3M8 16h3" /> },
          { id: '/settings/customization', href: '/settings/customization', label: 'Customization', icon: <Icon d="M12 3a9 9 0 100 18h1.5a2 2 0 001.6-3.2 2 2 0 011.6-3.2H19a2 2 0 002-2A9 9 0 0012 3zM7.5 10.5v.01M12 7.5v.01M16.5 10.5v.01" /> },
          { id: '/settings/integrations', href: '/settings/integrations', label: 'Integrations', icon: <Icon d="M9 3v6M15 3v6M6 9h12v4a6 6 0 01-12 0zM12 19v2" /> },
          { id: '/settings/team', href: '/settings/team', label: 'Team', icon: <Icon d="M4 19v-1a4 4 0 014-4h2a4 4 0 014 4v1M9 10a3 3 0 100-6 3 3 0 000 6zM17 11a3 3 0 100-6" /> },
        ],
      },
    ]}
    currentId="/settings/profile" />;

Props

PropTypeDefaultDescription
groupsrequiredSettingsNavGroup[]——
currentIdstring—id of the section being shown.
labelstring'Settings'Accessible name of the landmark.
onSelect(id: string) => void——
selectLabelstring'Select settings option'Accessible name of the narrow-screen control.

Also accepts everything from Omit, so id, data-* and event handlers pass straight through.

Open in Storybookfor controls, the a11y panel and the interaction runner.