Skip to content

Nav Pill

A row of links with one pill that slides to the link you point at and drops out of sight when you leave.

$npx shadcn@latest add https://framebrick.co/r/nav-pill.json

From the Metuo template, see it in context.

Examples

The page you are on

The pill rests under the current page's link, and returns there when you leave.

<NavPill label="Main" items={[{ href: "/", label: "Home" }, { href: "/work", label: "Work", current: true }]} />

Installation

$ npx shadcn@latest add https://framebrick.co/r/nav-pill.json

Usage

import { NavPill } from "@/components/ui/nav-pill";

<NavPill
  label="Main"
  items={[
    { href: "/", label: "Home" },
    { href: "/about", label: "About" },
    { href: "/services", label: "Services" },
  ]}
/>

// the page you are on: the pill rests under it
<NavPill label="Main" items={links.map((link) => ({ ...link, current: link.href === pathname }))} />

Props

PropTypeDefaultDescription
labelstring—Accessible name of the navigation.
items{ href, label, current? }[]—The links, in order. A link marked current is where the pill rests, and carries aria-current="page". http(s) links open in a new tab.
classNamestring—Classes for the row: its colour, radius, padding (bg-muted, rounded-lg and p-1 by default).
linkClassNamestring—Classes for every link and for its copy in the pill: the padding and the text size.
  • It is Metuo's header pill: a dark pill rests hidden below the row, under the first link; pointing at a link brings it up under that link (a diagonal slide the first time), moving to another slides it sideways, and leaving the row sends it back down. Framer's spring, bounce 0, 0.4 s (measured: 112 → 0 px in about 0.5 s). Keyboard focus moves it too (focus-visible only).
  • The pill carries its own copy of every label in the opposite colour, so the text changes colour exactly where the pill is. A label that turned light by itself would sit light on a light row until the pill arrived; Metuo's does, and this does not. The pill ignores the pointer, so the links underneath stay the links, and the copies are aria-hidden: a screen reader and the keyboard use the real links.
  • The row is measured before the first paint, and again when anything resizes it, and the pill is only drawn after that, so the server sends plain links and nothing can mismatch. A resize jumps the pill to the new box; a move to another link animates.
  • A link marked current is where the pill rests, the way a nav shows the page you are on; Metuo marks none (its Home is not highlighted on Home), so its pill rests out of sight. The label roll that Metuo's links also have is Flip Button's, and is not part of this.
  • Reduced motion jumps: the move is imperative animate(), which <MotionConfig> would not stop, so the component reads the preference itself, in an effect. Touch has no hover, so on a touch screen the row is plain links.
  • The row does not wrap or scroll, and it clips what does not fit (the clip is what hides the resting pill). On a narrow screen show the menu button instead, as Metuo does with its hamburger.
  • The pill is bg-primary with text-primary-foreground and the row is bg-muted with text-foreground, so a themed project gets its own colours; both pairs are the theme's own.

AI prompt

Paste into v0, Lovable, Bolt or Claude to rebuild this component in your own project.

Create a React + Tailwind CSS + Motion (motion/react) component called NavPill — a row of links with one pill that slides to the link you point at.
- Props: label (aria-label of the nav), items ({ href, label, current? }[]; a Next.js Link each, external http(s) links open in a new tab with rel noopener noreferrer, aria-current="page" on current), className, linkClassName.
- A nav (relative inline-flex overflow-hidden rounded-lg bg-muted p-1 text-foreground) holding a ul of links (inline-flex items-center rounded-md px-3 py-1.5 text-sm font-medium, focus ring ring-[3px] ring-ring/50) and, after it, the pill.
- Measure the links (offsetLeft, offsetTop, offsetWidth, offsetHeight) and the nav (clientWidth, clientHeight) in a layout effect and again from a ResizeObserver on the nav and every link. Draw the pill only once measured.
- The pill is an absolutely positioned, pointer-events-none, overflow-hidden rounded-md bg-primary text-primary-foreground box whose x, y, width and height are motion values. Inside it, a layer the size of the nav, moved by the negative of the pill's x and y (useTransform), holds an absolutely positioned copy of every label at its link's measured box (same classes, aria-hidden), so each label turns to text-primary-foreground exactly where the pill is.
- Target: the link that is hovered (a pointer that is not touch, set on pointer enter, cleared when the pointer leaves the nav), else the focused one (set in onFocus when event.currentTarget.matches(":focus-visible")), else the current one, else rest: the first link's box with y set to the nav's height, below the clip.
- On a change of target animate x, y, width and height with animate() and a spring (bounce 0, duration 0.4); the first placement, a change of measurements without a change of target, and prefers-reduced-motion: reduce (read in the effect) jump with .jump() instead.

Be the first to know

Get new templates and components first

One short email when we release a template or a free component. No spam — unsubscribe anytime.

We use your email only for Framebrick updates. Privacy policy