Skip to content

Add active sidebar item indicator with aria-current support - #126

Merged
jalexw merged 4 commits into
mainfrom
claude/dashboard-active-page-indicator-lbudot
Sep 27, 2026
Merged

jalexw merged 4 commits into
mainfrom
claude/dashboard-active-page-indicator-lbudot

Conversation

@jalexw

@jalexw jalexw commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds visual and semantic indicators for the currently active sidebar item in the DashboardLayout. The active item is determined by matching the current pathname against sidebar item URLs, with the longest matching path winning. The active state is shown with a two-colour gradient treatment (a gradient wash across the row, a glowing gradient bar down its left edge, and a bold gradient label) and exposed through aria-current="page" on the link.

Key Changes

  • New active item detection system (dashboard-sidebar-active-item.ts):

    • normalizeDashboardSidebarPath() - Normalizes hrefs by dropping query strings, fragments, and trailing slashes; only matches root-relative paths
    • isDashboardSidebarPathWithin() - Checks if a current path equals or is nested beneath an item path
    • resolveActiveDashboardSidebarItemPath() - Resolves the best matching sidebar item for a given pathname using longest-match logic
  • Context providers (dashboard-sidebar-active-item-context.tsx):

    • DashboardSidebarActiveItemProvider - Accepts explicit currentPathname prop
    • DashboardSidebarActiveItemFromPathnameHookProvider - Reads current pathname from consumer's usePathname hook
    • useIsDashboardSidebarItemActive() - Hook for sidebar items to check if they are active
  • Gradient styling for the active item (dashboard-sidebar-active-item-indicator.tsx, dashboard-sidebar-item-renderer.tsx):

    • The two gradient colours are the --sidebar-active-start / --sidebar-active-end tokens from @schemavaults/theme 0.30.0, which default to the SchemaVaults brand blue and red. Deployments re-theme them with --sv-theme-{light,dark}-sidebar-active-{start,end} or the THEME_{LIGHT,DARK}_SIDEBAR_ACTIVE_{START,END} environment variables; setting the tokens on an ancestor re-colours a single layout. Brand-colour fallbacks keep the gradient intact for apps still loading an older theme's globals.css
    • The row gets a gradient wash that fades out before its right edge, and a gradient bar with a soft glow that grows in when the item becomes active (instant under reduced motion)
    • The icon and the gradient label use each colour mixed 60/40 with the page foreground, which keeps them readable in both modes (≥ 5.6:1 on the tinted row for the default pair)
    • Admin-only items keep their red icon and label (bold when active) on the same gradient backdrop
    • Added data-active="true" attribute to active item <li> elements
    • Added aria-current="page" to active item links
  • DashboardLayout integration (dashboard-layout.tsx):

    • New activeHref prop to explicitly set the current page pathname
    • Wraps sidebar with appropriate active item provider based on whether usePathname hook is supplied
    • Providers are only applied to the sidebar, not the page content
  • Dependencies: @schemavaults/theme 0.29.0 → 0.30.0 (adds the two sidebar-active-* tokens and their Tailwind colours; no other changes)

  • Type updates:

    • Extended LinkComponentProps to include optional aria-current attribute
    • Updated DashboardLayoutProps documentation for usePathname and new activeHref prop
  • Storybook stories (DashboardLayout.stories.tsx):

    • Added activeHref control to component args
    • Four new stories demonstrating active item behavior:
      • ActiveItem - Shows active item with explicit activeHref; its play test checks the gradient bar resolves to the theme tokens' brand defaults and follows --sv-theme-*-sidebar-active-* overrides on <html>
      • ActiveAdminItem - Shows an active admin item (red label on the gradient backdrop)
      • ActiveItemCustomGradient - Re-colours one layout by setting --sidebar-active-start / --sidebar-active-end on a wrapper
      • ActiveItemFollowsNavigation - Demonstrates active item following client-side navigation via usePathname hook
  • Unit tests (dashboard-sidebar-active-item.test.ts):

    • Comprehensive tests for path normalization and active item resolution
    • Tests cover exact matches, nested paths, longest-match logic, trailing slashes, and edge cases

Implementation Details

  • The active item matching uses longest-path-wins logic: /settings/billing is more specific than /settings, so only "Billing" lights up on that path
  • Root path / only matches itself to prevent a "Home" item from claiming every page
  • Query strings and fragments are ignored during matching
  • Only root-relative paths (starting with /) participate in matching; external links and fragments are never marked active
  • The active item context is only applied to the sidebar subtree, avoiding unnecessary re-renders of page content
  • Storybook stories include interaction tests (play functions) that verify the correct link has aria-current="page" and the item has data-active="true"

https://claude.ai/code/session_01LEDndwAbCg9BHyK3WRGCkX

…five activeItemStyle variants

The sidebar now highlights the item for the current page. The current page
comes from the new `activeHref` prop, or from `usePathname` when supplied;
the longest item url that the path equals or is nested beneath wins. The
active link gets aria-current="page" and its <li> data-active="true".

`activeItemStyle` picks the treatment: highlight (default), right-border,
color-shift, tinted, solid, or none. Stories for each, plus a story where
the marker follows client-side navigation, and unit tests for the matcher.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LEDndwAbCg9BHyK3WRGCkX
Drops the `activeItemStyle` prop and the highlight, right-border,
color-shift, solid and none styles. The active sidebar item always gets a
blue tint, a left-edge bar and a bold blue label (red in an adminOnly
group). The per-style stories collapse into a single ActiveItem story.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LEDndwAbCg9BHyK3WRGCkX
@vercel

vercel Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
ui Ready Ready Preview Sep 27, 2026 3:26am UTC

Request Review

@jalexw jalexw self-assigned this Sep 26, 2026
The active row now gets a gradient wash, a glowing gradient bar down its
left edge that grows in, and a bold gradient label. The two colours come
from --sidebar-active-start / --sidebar-active-end, falling back to the
SchemaVaults brand blue and brand red, so any ancestor (or a future theme
token) can re-colour it. Icon and label use each colour mixed 60/40 with
the foreground to stay readable in both modes. Admin rows keep their red
label and icon. Adds an ActiveItemCustomGradient story.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LEDndwAbCg9BHyK3WRGCkX
…e gradient tokens

Theme 0.30.0 defines --sidebar-active-start / --sidebar-active-end (brand
blue / brand red by default, overridable via --sv-theme-*-sidebar-active-*
and THEME_*_SIDEBAR_ACTIVE_* env vars), which the active sidebar item
already reads. Docs now point at the theme tokens, the brand-colour
fallbacks stay for apps on an older theme's globals.css, and the ActiveItem
story asserts the bar follows the theme defaults and a deployment override.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LEDndwAbCg9BHyK3WRGCkX
@jalexw
jalexw merged commit f5c68b6 into main Sep 27, 2026
12 checks passed
@jalexw
jalexw deleted the claude/dashboard-active-page-indicator-lbudot branch September 27, 2026 03:34

This branch was successfully deployed

1 active deployment
Preview — 93669632 Deployed Sep 27, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants