# Navigation item

One link in a navigation list, with an optional leading icon, an optional count or dot, and a caret when it opens a group of further links.

Block: `smbk-nav-item`. Status: candidate. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|
| `size` | `lg`, `md`, `sm` | `lg` | lg is 40 tall with 16 px text and a 20 px icon, md 32 tall with 14 px text and a 16 px icon, sm 24 tall with 12 px text and a 12 px icon. The app bar, the header and their stacked menus use the large one; the footer uses the middle one. Figma: lg, md, sm (its default is md). |
| `block` | on / off | off | Fills its row with the caret at the far end, for stacked lists. Figma: the expanded mobile menus. |

Parts: `smbk-nav-item__icon`, `smbk-nav-item__label`, `smbk-nav-item__pause`, `smbk-nav-item__count`, `smbk-nav-item__dot`, `smbk-nav-item__caret`

## Markup

```html
<a class="smbk-nav-item smbk-nav-item--size-lg" href="#">
  <i
    class="ph ph-folder-simple smbk-icon smbk-icon--size-md smbk-nav-item__icon"
    aria-hidden="true"
  ></i>
  <span class="smbk-nav-item__label" data-label="Projects">Projects</span>
  <i
    class="ph ph-caret-down smbk-icon smbk-icon--size-md smbk-nav-item__caret"
    aria-hidden="true"
  ></i>
</a>
```

## React

```tsx
import { NavItem } from './design-system/components/nav-item/nav-item.js';

<NavItem size="lg" href="#" icon="FolderSimple" caret>Projects</NavItem>
```

## Stylesheet

```css
/* smbk-nav-item — Navigation item. Figma "NavItem" 4209:20209 (State × Size: lg, md, sm).
 * One link in a navigation list: header, app bar, footer columns, drawer.
 * An optional icon leads the label and an optional caret trails it. Each size
 * takes the text, icon, side padding and gap of its height. Hover raises the
 * item on the raised surface with a highlight edge and the resting lift. The
 * current page sits pressed into the bar at every size: the canvas surface,
 * the highlight edge and the inset shadow, and its label one weight bolder. */

.smbk-nav-item {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  gap: var(--smbk-space-inline-md);
  height: var(--smbk-size-control-md);
  margin: 0;
  padding: 0 var(--smbk-space-inset-xl);
  border: var(--smbk-border-width-hairline) solid transparent;
  border-radius: var(--smbk-radius-control-sm);
  background: transparent;
  color: var(--smbk-color-text-subtle);
  font-family: var(--smbk-typography-body-md-font-family);
  font-size: var(--smbk-typography-body-md-font-size);
  font-weight: var(--smbk-typography-body-md-font-weight);
  line-height: var(--smbk-typography-body-md-line-height);
  letter-spacing: var(--smbk-typography-body-md-letter-spacing);
  text-align: left;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}

/* The block frames the words it hides for screen readers, so they stay inside it wherever it is placed: without a
   frame they hang from the page itself and stretch it, past the end of an area that scrolls. No weight, so a parent
   that places the block keeps its say. */
:where(.smbk-nav-item) {
  position: relative;
}

@media (hover: hover) {
  .smbk-nav-item:hover {
    border-color: var(--smbk-color-border-rim);
    background: var(--smbk-color-surface-raised);
    color: var(--smbk-color-text-link-hover);
    box-shadow: var(--smbk-elevation-control);
  }
}

.smbk-nav-item[aria-current="page"] {
  border-color: var(--smbk-color-border-rim);
  background: var(--smbk-color-surface-canvas);
  color: var(--smbk-color-text-default);
  font-weight: var(--smbk-typography-body-md-medium-font-weight);
  box-shadow: var(--smbk-elevation-inset-strong);
}

.smbk-nav-item[aria-expanded="true"] {
  color: var(--smbk-color-text-default);
}

/* The navigation menu's item whose panel is open stays raised, as hover raises it (Figma's NavigationMenu, Open). */
.smbk-nav-item.smbk-navigation-menu__trigger[aria-expanded="true"] {
  border-color: var(--smbk-color-border-rim);
  background: var(--smbk-color-surface-raised);
  box-shadow: var(--smbk-elevation-control);
}

.smbk-nav-item:focus-visible {
  outline: var(--smbk-focus-ring-width) solid var(--smbk-color-border-focus);
  outline-offset: var(--smbk-focus-ring-offset);
}

.smbk-nav-item:disabled,
.smbk-nav-item[aria-disabled="true"] {
  color: var(--smbk-color-text-disabled);
  pointer-events: none;
}

.smbk-nav-item__label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The label is always as wide as its bolder self: an invisible copy of its words (`data-label`, which the wrapper
   writes) at the current item's weight lies under it with no height, so an item that becomes the current page
   keeps its width and the items beside it stay where they are. The copy is generated content, hidden, with empty
   words for assistive technology, so the label is read once. */
.smbk-nav-item__label[data-label]::after {
  content: attr(data-label);
  content: attr(data-label) / "";
  display: block;
  height: 0;
  overflow: hidden;
  visibility: hidden;
  font-weight: var(--smbk-typography-body-md-medium-font-weight);
  pointer-events: none;
  user-select: none;
}

.smbk-nav-item__icon {
  flex-shrink: 0;
  font-size: var(--smbk-size-icon-md);
}

/* A count (the badge, small) and a dot (the status dot) after the label, each read after a comma
   only a screen reader hears, so the name is "Issues, 18". */
.smbk-nav-item__count,
.smbk-nav-item__dot {
  flex-shrink: 0;
}

/* .smbk-nav-item__pause: hidden from view, kept for screen readers and the keyboard, by the shared rule in visually-hidden/visually-hidden.css. */

.smbk-nav-item__caret {
  flex-shrink: 0;
  font-size: var(--smbk-size-icon-sm);
  transition: transform var(--smbk-motion-state-duration) var(--smbk-motion-state-easing);
}

.smbk-nav-item[aria-expanded="true"] .smbk-nav-item__caret {
  transform: rotate(180deg);
}

/* ---- sizes: lg (the block rules above) 40 tall with 16 text, a 20 icon, 16 at the sides and 8
   between; md 32 tall with 14 text, a 16 icon, 12 and 6; sm 24 tall with 12 text, a 12 icon, 8 and 4.
   The caret is a step smaller than the icon, and 12 at both smaller sizes. The current label takes
   the medium weight of its text style; Body XS has none, so the small current item keeps its weight
   and is marked by its fill, edge and shadow, as Figma draws it. ---- */

.smbk-nav-item--size-md {
  gap: var(--smbk-space-inline-sm);
  height: var(--smbk-size-control-sm);
  padding: 0 var(--smbk-space-inset-lg);
  font-family: var(--smbk-typography-body-sm-font-family);
  font-size: var(--smbk-typography-body-sm-font-size);
  font-weight: var(--smbk-typography-body-sm-font-weight);
  line-height: var(--smbk-typography-body-sm-line-height);
  letter-spacing: var(--smbk-typography-body-sm-letter-spacing);
}

.smbk-nav-item--size-md[aria-current="page"],
.smbk-nav-item--size-md .smbk-nav-item__label[data-label]::after {
  font-weight: var(--smbk-typography-body-sm-medium-font-weight);
}

.smbk-nav-item--size-sm {
  gap: var(--smbk-space-inline-xs);
  height: var(--smbk-size-control-xs);
  padding: 0 var(--smbk-space-inset-md);
  border-radius: var(--smbk-radius-control-xs);
  font-family: var(--smbk-typography-body-xs-font-family);
  font-size: var(--smbk-typography-body-xs-font-size);
  font-weight: var(--smbk-typography-body-xs-font-weight);
  line-height: var(--smbk-typography-body-xs-line-height);
  letter-spacing: var(--smbk-typography-body-xs-letter-spacing);
}

.smbk-nav-item--size-sm[aria-current="page"],
.smbk-nav-item--size-sm .smbk-nav-item__label[data-label]::after {
  font-weight: var(--smbk-typography-body-xs-font-weight);
}

.smbk-nav-item--size-md .smbk-nav-item__icon {
  font-size: var(--smbk-size-icon-sm);
}

.smbk-nav-item--size-sm .smbk-nav-item__icon {
  font-size: var(--smbk-size-icon-xs);
}

.smbk-nav-item:is(.smbk-nav-item--size-md, .smbk-nav-item--size-sm) .smbk-nav-item__caret {
  font-size: var(--smbk-size-icon-xs);
}

/* ---- block: fills its row, label first and caret at the far end ---- */

.smbk-nav-item--block {
  display: flex;
  width: 100%;
  justify-content: space-between;
}

/* In a stacked list a long label wraps onto more lines instead of being cut short with an
   ellipsis, since a navigation label cut short can become ambiguous. The
   item keeps its size's height for one line and grows for more. A nav item in a bar keeps
   one line: a bar that runs short of room goes to its narrow form instead. */
.smbk-nav-item--block {
  height: auto;
  min-height: var(--smbk-size-control-md);
  white-space: normal;
}

.smbk-nav-item--block.smbk-nav-item--size-md {
  min-height: var(--smbk-size-control-sm);
}

.smbk-nav-item--block.smbk-nav-item--size-sm {
  min-height: var(--smbk-size-control-xs);
}

.smbk-nav-item--block .smbk-nav-item__label {
  text-overflow: clip;
  overflow-wrap: break-word;
}

/* ---- motion: the caret turns with the state purpose as what it opens opens. None under reduced motion. ---- */

@media (prefers-reduced-motion: reduce) {
  .smbk-nav-item__caret {
    transition: none;
  }
}

/* ---- touch: the item takes an invisible press area ----
   On a touch screen an invisible area, centred on it, takes the press out to the comfortable target in each direction
   it falls short of it; its look, its focus ring and its place stay as drawn. */

@media (pointer: coarse) {
  .smbk-nav-item {
    position: relative;
  }

  .smbk-nav-item::after {
    content: '';
    position: absolute;
    inset: min(0%, (100% - var(--smbk-size-target-comfortable)) / 2);
  }
}

/* ---- touch: block items one under another stop halfway to each other ----
   Block items stacked a stack's gap apart reach only halfway into it, so each stays one press. Nothing drawn moves. */

@media (pointer: coarse) {
  .smbk-nav-item--block + .smbk-nav-item--block::after {
    inset-block-start: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-stack-sm) / -2);
  }

  .smbk-nav-item--block:has(+ .smbk-nav-item--block)::after {
    inset-block-end: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-stack-sm) / -2);
  }
}

/* ---- high contrast (forced colors) ---- */
/* In Windows high-contrast mode the browser drops fills and shadows; this puts the meaning back with the
   user's own colors. Nothing here applies outside that mode. */

@media (forced-colors: active) {
  .smbk-nav-item[aria-current="page"] {
    forced-color-adjust: none;
    border-color: Highlight;
    background: Highlight;
    color: HighlightText;
    box-shadow: none;
  }

  .smbk-nav-item[aria-current="page"]:focus-visible {
    outline-color: CanvasText;
  }

  .smbk-nav-item:disabled,
  .smbk-nav-item[aria-disabled="true"] {
    border-color: GrayText;
    color: GrayText;
  }
}
```

## Tokens it reads

```json
{
  "space": {
    "inline": {
      "md": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Gap between things side by side, medium."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{space.6}}",
        "$description": "Gap between things side by side, small."
      },
      "xs": {
        "$type": "dimension",
        "$value": "{{space.4}}",
        "$description": "Gap between things side by side, extra small."
      }
    },
    "inset": {
      "xl": {
        "$type": "dimension",
        "$value": "{{space.16}}",
        "$description": "Padding inside a component, extra large."
      },
      "lg": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Padding inside a component, large."
      },
      "md": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Padding inside a component, medium."
      }
    },
    "stack": {
      "sm": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Gap between things stacked one above another, small."
      }
    }
  },
  "size": {
    "control": {
      "md": {
        "$type": "dimension",
        "$value": "{{size.40}}",
        "$description": "Height of a medium control, where controls insert by default: 40 comfortable, 32 compact. Dashboard recipe: use compact density; 40 for the page’s one main action and for forms; 32 for toolbars and filters, the size used most; 28 for actions inside table rows and cards. On phones switch to comfortable density, or give small controls an invisible 44 px tap area (size/target/comfortable)."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{size.32}}",
        "$description": "Height of a small control: 32 comfortable, 28 compact. Dashboard recipe: use compact density; 40 for the page’s one main action and for forms; 32 for toolbars and filters, the size used most; 28 for actions inside table rows and cards. On phones switch to comfortable density, or give small controls an invisible 44 px tap area (size/target/comfortable)."
      },
      "xs": {
        "$type": "dimension",
        "$value": "{{size.24}}",
        "$description": "Height of the extra-small control: 24 in both densities, the smallest target allowed. Dashboard recipe: use compact density; 40 for the page’s one main action and for forms; 32 for toolbars and filters, the size used most; 28 for actions inside table rows and cards. On phones switch to comfortable density, or give small controls an invisible 44 px tap area (size/target/comfortable)."
      }
    },
    "icon": {
      "md": {
        "$type": "dimension",
        "$value": "{{size.20}}",
        "$description": "Icon box, medium."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{size.16}}",
        "$description": "Icon box, small."
      },
      "xs": {
        "$type": "dimension",
        "$value": "{{size.12}}",
        "$description": "Icon box, extra small."
      }
    },
    "target": {
      "comfortable": {
        "$type": "dimension",
        "$value": "{{size.44}}",
        "$description": "Comfortable touch target (44)."
      }
    }
  },
  "border-width": {
    "hairline": {
      "$type": "dimension",
      "$value": "{{border-width.1}}",
      "$description": "Thin lines: dividers and almost every edge."
    }
  },
  "radius": {
    "control": {
      "sm": {
        "$type": "dimension",
        "$value": "{{radius.4}}",
        "$description": "Corners of controls, small."
      },
      "xs": {
        "$type": "dimension",
        "$value": "{{radius.2}}",
        "$description": "Corners of controls, extra small."
      }
    }
  },
  "color": {
    "text": {
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.700}}",
        "$description": "Secondary text: descriptions, metadata."
      },
      "link-hover": {
        "$type": "color",
        "$value": "{{color.grey.950}}",
        "$description": "Link text on hover."
      },
      "default": {
        "$type": "color",
        "$value": "{{color.grey.900}}",
        "$description": "Main text."
      },
      "disabled": {
        "$type": "color",
        "$value": "{{color.grey.500}}",
        "$description": "Text in unavailable controls."
      }
    },
    "border": {
      "rim": {
        "$type": "color",
        "$value": "{{color.alpha.white.90}}",
        "$description": "The light rim that makes a surface look lit: fields, chosen pills, cards."
      },
      "focus": {
        "$type": "color",
        "$value": "{{color.red.600}}",
        "$description": "Keyboard focus ring on controls."
      }
    },
    "surface": {
      "raised": {
        "$type": "color",
        "$value": "{{color.grey.50}}",
        "$description": "One floor above default: popovers, raised panels."
      },
      "canvas": {
        "$type": "color",
        "$value": "{{color.grey.200}}",
        "$description": "Page background."
      }
    }
  },
  "typography": {
    "body-md": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "16px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    },
    "body-md-medium": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "16px",
        "fontWeight": 500,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      },
      "$description": "Body MD one weight step bolder, for the current item, so the current item is not shown by colour alone."
    },
    "body-sm": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "14px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    },
    "body-sm-medium": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "14px",
        "fontWeight": 500,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      },
      "$description": "Body SM one weight step bolder, for the current item, so the current item is not shown by colour alone."
    },
    "body-xs": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "12px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    }
  },
  "elevation": {
    "control": {
      "$type": "shadow",
      "$value": {
        "light": "0px 0px 1px 0px #373d430d, 0px 1px 2px 0px #373d431a, 0px 2px 4px 0px #373d430d",
        "dark": "0px 0px 1px 0px #0000001a, 0px 1px 2px 0px #00000026, 0px 2px 4px 0px #0000001a"
      }
    },
    "inset-strong": {
      "$type": "shadow",
      "$value": {
        "light": "inset -1px 2px 2px 0px #373d4333",
        "dark": "inset -1px 2px 2px 0px #0000004d"
      }
    }
  },
  "focus": {
    "ring": {
      "width": {
        "$type": "dimension",
        "$value": "{{border-width.2}}",
        "$description": "Thickness of the focus ring."
      },
      "offset": {
        "$type": "dimension",
        "$value": "{{space.2}}",
        "$description": "Gap between a control and its focus ring."
      }
    }
  },
  "motion": {
    "state": {
      "duration": {
        "$type": "duration",
        "$value": "{{duration.200}}"
      },
      "easing": {
        "$type": "cubicBezier",
        "$value": "{{easing.cubic-in-out}}"
      }
    }
  }
}
```

## Purpose

One entry in a list of places to go: a header link, a footer column link, a row in the narrow-screen menu. It is quieter than the link component, which is for links inside running text, and it can carry a caret when it opens a group of further links rather than going somewhere itself.

## Anatomy

- The block: a link when it has an address, a button when it opens a group.
- The icon part, optional, before the label.
- The label part.
- The count part, optional: a small badge with a number after the label, such as open issues.
- The dot part, optional: a status dot after the label saying something here changed.
- The pause part, before a count or a dot: a comma only a screen reader hears.
- The caret part, optional, pointing down and turning up while the group is open.

## When to use

- Inside the header, app bar, footer, sidebar and drawer.
- Any list of destinations laid out as a navigation region.

## When not to use

- A link inside a sentence. Use the [link](link.md) component.
- An action. Use a [button](button.md).

## Behavior

Static as a link; hovering raises it on the raised surface with a highlight edge and a slight lift. The current page sits pressed into the bar at every size, on the canvas surface with an inset shadow and its text in the default color, its label one weight bolder at the large and middle sizes. Every label keeps the room of its bolder self, so an item that becomes the current page keeps its width and the items beside it do not move; markup written by hand gets this by repeating the label's words in the label's `data-label` attribute. Only the words seen keep that room: words inside the label for a screen reader only, such as a link's "(opens in a new tab)", go in an element marked `data-unseen`, which the React wrapper leaves out of the copy, so they never widen the item; markup written by hand leaves them out of `data-label`. As a group opener it is a button whose expanded state the parent component sets; the caret turns with it, with the state motion, and at once under reduced motion. The block flag stretches it across its row with the caret at the far end, which the stacked menus use. Three sizes, each with the text, icon, side padding and gap of its height: the large one, 40 tall with 16 px text and a 20 px icon, is the default and is what the app bar, the header and their stacked menus use; the middle one, 32 tall with 14 px text and a 16 px icon, is what the footer uses; the small one, 24 tall with 12 px text and a 12 px icon, is for dense lists. In a stacked list (the block form) a long label wraps onto more lines rather than being cut short, and the item grows to hold it; in a bar a label keeps one line, and a bar that runs short of room goes to its narrow form.

On a touch screen every item keeps its size and takes an invisible press area out to the comfortable touch target.

## Accessibility

### What it does for you

- It is a real link, or a real button when it opens a group, so Enter follows a link and Enter or Space presses a button.
- A count and a dot are part of the item's name, after a pause: "Issues, 18", "Inbox, changed". The dot never shows the change by color alone: it is read out.
- A link carries the current-page marker when it is the page the reader is on.
- A disabled item keeps its text, loses its address, and is announced as unavailable.
- In a forced-color mode, the current page is drawn in the system's highlight colors.

### What you must do

- Give the dot words of its own with `dotLabel` when "changed" does not say what it means.
- Make a group opener carry the expanded state and control the group it opens; the parent component does this.
- Mark exactly one item current per list, and keep the item inside a navigation region.

### How to test

- Tab to an item: the focus ring shows, in both themes and in a forced-color mode, where the current page is still told from the other items.
- Press Enter on a link: it is followed. Press Enter, then Space, on a group opener: each opens or closes the group.
- With a screen reader, an item is read as a link, or a button, by its label, then its count or dot words. The current page is read as current, and a disabled item as unavailable.
- With text at 200% and the page 320 pixels wide, a label in a stacked menu wraps and the item grows to hold it.
- On a touch screen, a press just outside the item's edge, within the comfortable touch target, presses it.

### Keyboard

| Key | What it does |
|---|---|
| Tab | Moves focus to the item. |
| Enter | Follows the link, or presses the group opener. |
| Space | Presses a group opener. A link does not take it. |

## Usage rules

### Do

- Keep labels to one or two words.
- Give every item in one list an icon, or none of them.
- Mark exactly one item current per list.

### Don't

- Use it outside a navigation region.
- Give a group opener an address as well; it is one or the other.
