NavPill
Floating pill-shaped navigation, the only global navigation element
Preview, desktop
The real component in an iframe (it is position:fixed; mounted loose it would cover this page). Top left, sliding indicator on hover/focus, smart-hide when you scroll inside the frame.
Preview, mobile
Below 768px: bottom, centered, always visible, intrinsic width.
Props
| Prop | Type | Note |
|---|---|---|
logo | string | Required. Path to the symbol; <img> in a fixed 30×30 box (avoids reflow). The asset arrives ready: it must be legible on --nav-bg as-is; the component never recolors it. |
items | { key, href, label, icon?, isActive }[] | Min 2; max: as many as fit at 375px with labels visible. icon is an optional inline SVG. |
locale | 'es' | 'en' | Default 'es'. Accessibility labels. |
homeUrl | string | Default '/'. Destination of the symbol. |
Tokens
| Property | Value | Token |
|---|---|---|
| Background | #000000, both color modes (deliberate exception: maximum contrast, theme-invariant) | --nav-bg |
| Text | #FFFFFF | --nav-text |
| Border-radius | 16px, derived from the scale: var(--radius-lg) | --nav-radius |
| Height | 52px | --nav-height |
| Shadow | 0 8px 24px -4px rgba(0,0,0,0.15) | --nav-shadow |
| Typography | Georama 14px / 600 | --text-nav |
| Item padding | 8px 12px (desktop) / 6px 8px (mobile) | --nav-item-padding (desktop) |
| Gap between items | space-sm (desktop) / space-xs (mobile) | n/a |
| Item radius | 8px, derived from the scale: var(--radius-md) | --nav-item-radius |
| Active background (indicator at rest) | rgba(255,255,255,0.12) | --nav-active-bg |
| Hover background (indicator) | rgba(255,255,255,0.2) | --nav-hover-bg |
States, sliding indicator
A single element (.nav-pill__indicator) acts as the highlight: items have no background of their own. It follows the hovered or focused item and rests on the active one (aria-current="page"). With no active item, it hides. All items always at full opacity.
| State | Treatment |
|---|---|
| Inactive | White text, no background. Full opacity. |
| Hover / focus | The indicator slides under the pointed item. |
| Active (at rest) | The indicator rests under the item with aria-current. |
| No active item | Indicator hidden (opacity 0). |
The indicator is positioned with getBoundingClientRect; it jumps without transition on first paint and under prefers-reduced-motion, and is recalculated on document.fonts.ready and on resize. When pointer and focus leave the nav, it returns to the active item.
Position
| Viewport | Position | Behavior |
|---|---|---|
| Desktop (≥768px) | Top, left | Smart scroll: hides when scrolling down (>80px), shows when scrolling up. translateY, 0.3s ease-out. |
| Mobile (<768px) | Bottom, centered | Fixed, intrinsic width. Slides out while the <footer> is on screen (IntersectionObserver) so it doesn't cover its links; with no footer on the page, it never hides. |
Constraints
- Minimum 2 items; maximum, as many as fit at 375px with every label visible (in practice 3–4 short ones). The invariant is the fit, not the count.
- Labels always visible. Never icons only. No truncation, no scroll, no overflow menu.
- No dropdowns.
- No language selector (the Footer handles it).
- No theme toggle. No badges or counters.
- The logo arrives ready for the pill: the component never recolors it.
Decisions
| Decision | Why |
|---|---|
| Left-aligned on desktop | With 2-3 items, centering leaves dead space |
| Hides on scroll down (smart scroll) | Narrative site: a fixed nav steals reading space |
| Bottom center on mobile | Within thumb reach. Auto width avoids stretching |
| 16px radius, not a full pill | Softer, less app-like |
| No opacity dimming | Items at 0.5 opacity looked broken. A background fill is cleaner. |
| Hover bg > active bg | Three clear states without touching text opacity |
| Hide over the footer (mobile) | The pill fixed at the bottom covered the footer links right when the reader reached them (promoted from elastic) |
| Maximum by fit, not by count | "Max 3" was the initial estimate; elastic and teresa proved that 4 short labels fit. The real invariant is fitting at 375px. |
| No logo recoloring (v3) | The brightness/invert filter assumed dark monochrome icons and destroyed photographic logos (teresa needed an !important fork). The site provides the asset ready. |