# Navigation links (themes.dev UI block)

A row of text links with the current page marked (aria-current, underlined); in a narrow block it scrolls sideways instead of wrapping. Black and white, dark mode, shadcn-compatible; React, Astro and plain HTML.

38% of the 2,073 sites in The UI Index have a navigation (790 sites).

## Install

- React: `npx shadcn@latest add https://www.themes.dev/ui/r/nav-links.json`
- Astro: `npx shadcn@latest add https://www.themes.dev/ui/r/astro/nav-links.json`
- HTML: https://www.themes.dev/ui/r/html/nav-links.html (with https://www.themes.dev/ui/r/html/caseroom.css)

Page: https://www.themes.dev/ui/blocks/nav-links/ · Rules for agents: https://www.themes.dev/ui/AGENT.md

## Code

### nav-links.astro

```astro
---
import { cn } from '@/lib/utils';
import { navLinks as c, type NavLinksContent } from './nav-links.classes';

type Props = NavLinksContent & { class?: string };
const { links, currentPath, label = 'Primary', class: className } = Astro.props;
---

<div class={cn(c.root, className)}>
  <div class={c.inner}>
    <nav aria-label={label}>
      <ul role="list" class={c.list}>
        {links.map((l) => (
          <li>
            <a href={l.href} aria-current={l.href === currentPath ? 'page' : undefined} class={c.link}>{l.label}</a>
          </li>
        ))}
      </ul>
    </nav>
  </div>
</div>
```

### nav-links.classes.ts

```tsx
/** Navigation links: a row of text links with the current page marked (aria-current, underlined and heavier).
 *  Wide enough, the row sits on one line; in a narrow block it scrolls sideways with the next link's edge showing
 *  instead of wrapping. The block's own width decides (container query), not the window's. */
export const navLinks = {
  root: '@container/nav bg-background text-foreground',
  inner: 'mx-auto max-w-6xl px-6',
  // py-2 / -my-2 keep the focus ring of a link from being clipped by the scroll row.
  list: '-mx-3 -my-2 flex list-none scroll-px-8 overflow-x-auto py-2 ps-3 pe-8 [scrollbar-width:none] [mask-image:linear-gradient(to_right,#000_calc(100%-2rem),transparent)] [&::-webkit-scrollbar]:hidden @3xl/nav:scroll-px-0 @3xl/nav:overflow-visible @3xl/nav:[mask-image:none]',
  link: "relative inline-flex min-h-11 items-center rounded-md px-3 text-base whitespace-nowrap text-muted-foreground transition-colors after:absolute after:inset-x-3 after:bottom-1.5 after:h-0.5 after:rounded-full after:bg-foreground after:opacity-0 hover:bg-accent hover:text-foreground active:bg-pressed aria-[current=page]:text-foreground aria-[current=page]:after:opacity-100",
};

export type NavLinksLink = { label: string; href: string };
export type NavLinksContent = {
  links: NavLinksLink[];
  /** The link whose href equals this is the current page. */
  currentPath?: string;
  /** The navigation's name for screen readers; a page with two navs needs two names. Default "Primary". */
  label?: string;
};
```

### nav-links.tsx

```tsx
import { cn } from '@/lib/utils';
import { navLinks as c, type NavLinksContent } from './nav-links.classes';

export type NavLinksProps = NavLinksContent & { className?: string };

/** A row of text links with the current page marked; a scroll row in a narrow block. */
export function NavLinks({ links, currentPath, label = 'Primary', className }: NavLinksProps) {
  return (
    <div className={cn(c.root, className)}>
      <div className={c.inner}>
        <nav aria-label={label}>
          <ul role="list" className={c.list}>
            {links.map((l) => (
              <li key={l.href}>
                <a href={l.href} aria-current={l.href === currentPath ? 'page' : undefined} className={c.link}>{l.label}</a>
              </li>
            ))}
          </ul>
        </nav>
      </div>
    </div>
  );
}
```
