suiss/ui
  • Docs
  • Components
  • Patterns
  • Interactions
  • Charts
  • Blocks
GitHubGet started
DocsComponentsPatternsInteractionsChartsBlocks
Patterns19
  • Overview
  • Action Bar
  • Action Menu
  • App Shell
  • Command Palette
  • Confirm
  • Context Actions
  • Date Picker
  • Flyout
  • Modal
  • Navbar
  • Paged List
  • Panel
  • Picker
  • Preview
  • Search Picker
  • Segment Picker
  • Split View
  • Table List
  • Trail

suiss/ui · Pattern

Trail

A navigation trail that shows breadcrumbs on desktop and a back link with history on mobile.

Trail is an open-source pattern from suiss/ui for React. The @esuiss/ui CLI copies its source into your project, so you own the code.

Trail shows where the current page sits in a hierarchy. On desktop it renders a suiss/ui Breadcrumb that links every ancestor and marks the last item as the current page; with maxItems, it keeps the root and the nearest ancestors and folds the rest into an ellipsis.

On mobile (below 48rem, via useIsMobile) it renders a single back link to the parent page, like an iOS navigation bar. When there is more than one ancestor, a long press, right-click, the Context Menu key or Shift+F10 opens a menu of all ancestors, nearest first. Use it on documentation, support and settings pages with nested structure.

Installation

CLI

$ pnpm dlx shadcn@latest add @esuiss/trail

Works in any project with a components.json. See Installation for the one-line registry setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add patterns trail

Manual

  1. Install the dependencies and add the cn helper:
$ pnpm add @base-ui/react @phosphor-icons/react
  1. Add the pieces it builds on: Breadcrumb, use-mobile.

  2. Copy the source into components/patterns/trail.tsx:

  1. Update the import paths to match your project.

Usage

import { Trail } 
<TrailExample
  • Pass items in order from the root to the current page. Each TrailItem has a label and an optional href; the last item is the current page.
  • On mobile, nothing renders when there is only one item.
  • Use render to output links through your router, for example a framework Link element. It applies to breadcrumb links, the mobile back link and the history menu items.

Examples

Collapsed

Folds the middle of a deep trail into an ellipsis with maxItems.

Back label

Labels the mobile back link Back instead of the parent's name.

Two levels

Shows a trail with one parent and the current page.

API Reference

01

Trail

The navigation element that renders the breadcrumbs or the mobile back link.

PropTypeDefaultDescription
itemsTrailItem[]RequiredThe pages from the root to the current page.
maxItemsnumber-The maximum number of items shown on desktop before the middle collapses into an ellipsis.
backLabelstring-Text for the mobile back link; defaults to the parent's label.
ellipsisLabelstring"More"The accessible label of the collapsed ellipsis.
renderReact.ReactElement<TrailAnchor />The element used to render links.

Also accepts the props of React.ComponentProps<"nav">.

Accessibility

  • On desktop the trail is a navigation landmark named breadcrumb, and the current page has aria-current="page".
  • On mobile it is a navigation landmark named Back. When a history menu is available, the back link has aria-haspopup="menu" and aria-expanded.
  • The history menu opens from the keyboard with the Context Menu key or Shift+F10.

Notes

Styling hooks: data-slot="trail", trail-back, trail-history and trail-history-item. A long press opens the history only for touch and pen input, and cancels if the pointer moves more than 10 pixels; the press does not also follow the back link.

NextInteractionsPreviousTable List

On this page

  1. 01Installation
    • CLI
    • Manual
  2. 02Usage
  3. 03Examples
    • Collapsed
    • Back label
    • Two levels
  4. 04API Reference
    • Trail
  5. 05Accessibility
  6. 06Notes
from
"@/components/patterns/trail"
items
=
{article} />
import * as React from "react"
import { Trail, type TrailItem } from "@/components/patterns/trail"

const article: TrailItem[] = [
  { label: "Support", href: "#support" },
  { label: "Account", href: "#account" },
  { label: "Security", href: "#security" },
  { label: "Turn on two-factor authentication" },
]

type Page = { label: string; parent?: string; children: string[] }

const pages: Record<string, Page> = {
  support: { label: "Support", children: ["account", "billing"] },
  account: {
    label: "Account",
    parent: "support",
    children: ["security", "profile"],
  },
  billing: { label: "Billing", parent: "support", children: [] },
  security: {
    label: "Security",
    parent: "account",
    children: ["two-factor", "passwords"],
  },
  profile: { label: "Profile", parent: "account", children: [] },
  "two-factor": {
    label: "Turn on two-factor authentication",
    parent: "security",
    children: [],
  },
  passwords: {
    label: "Change your password",
    parent: "security",
    children: [],
  },
}

function pageOf(id: string) {
  const page = pages[id]
  if (!page) throw new Error(`Unknown page ${id}`)
  return page
}

function trailTo(id: string): TrailItem[] {
  const items: TrailItem[] = []
  for (let key: string | undefined = id; key; key = pageOf(key).parent) {
    items.unshift({ label: pageOf(key).label, href: `#${key}` })
  }
  return items.map((item, index) =>
    index === items.length - 1 ? { label: item.label } : item
  )
}

function TrailExample(props: Partial<React.ComponentProps<typeof Trail>>) {
  const [page, setPage] = React.useState("two-factor")
  const current = pageOf(page)

  const navigation = {
    onClick: (event: React.MouseEvent<HTMLAnchorElement>) => {
      const id = event.currentTarget.getAttribute("href")?.slice(1)
      if (!(id && pages[id])) return
      event.preventDefault()
      setPage(id)
    },
  }

  return (
    <div className="flex flex-col gap-6">
      <Trail {...props} items={trailTo(page)} render={<a {...navigation} />} />
      <section className="flex flex-col gap-3">
        <h2 className="text-2xl font-semibold tracking-tight">
          {current.label}
        </h2>
        {current.children.length > 0 ? (
          <ul className="flex flex-col gap-1">
            {current.children.map((child) => (
              <li key={child}>
                <a
                  href={`#${child}`}
                  {...navigation}
                  className="text-primary outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring/50"
                >
                  {pageOf(child).label}
                </a>
              </li>
            ))}
          </ul>
        ) : (
          <p className="text-muted-foreground">
            Pick an earlier page in the trail to go back up.
          </p>
        )}
      </section>
    </div>
  )
}

export default function TrailDemo() {
  return <TrailExample items={article} />
}
components/patterns/trail.tsx
"use client"

import { Menu as MenuPrimitive } from "@base-ui/react/menu"
import { useRender } from "@base-ui/react/use-render"
import { ArrowBendUpLeftIcon, CaretLeftIcon } from "@phosphor-icons/react"
import * as React from "react"
import {
  Breadcrumb,
  BreadcrumbEllipsis,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import { useIsMobile } from "@/hooks/use-mobile"
import { Trail, type TrailItem } from "@/components/patterns/trail"

const article: TrailItem[] = [
  { label: "Support", href: "#support" },
  { label: "Account", href: "#account" },
  { label: "Security", href: "#security" },
  { label: "Turn on two-factor authentication" },
]

export default function TrailBackLabel() {
  return <Trail items={article} backLabel="Back" />
}
import { Trail, type TrailItem } from "@/components/patterns/trail"

const article: TrailItem[] = [
  { label: "Support", href: "#support" },
  { label: "Account", href: "#account" },
  { label: "Security", href: "#security" },
  { label: "Turn on two-factor authentication" },
]

export default function TrailTwoLevels() {
  return <Trail items={article.slice(2)} />
}
import { Trail, type TrailItem } from "@/components/patterns/trail"

const deep: TrailItem[] = [
  { label: "Support", href: "#support" },
  { label: "Products", href: "#products" },
  { label: "Workspace", href: "#workspace" },
  { label: "Billing", href: "#billing" },
  { label: "Invoices", href: "#invoices" },
  { label: "Download an invoice as PDF" },
]

export default function TrailCollapsed() {
  return <Trail items={deep} maxItems={4} />
}