View Switcher

URL param-based toggle between named views with pill/tab styling.

Report a bug

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/view-switcher.json

Storybook

Explore all variants, controls, and accessibility checks in the interactive Storybook playground.

View in Storybook

Code

"use client"; import { memo, Suspense } from "react"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { cn } from "../../lib/utils"; type ViewOption = { key: string; label: string; }; type ViewSwitcherProps = { className?: string; defaultKey?: string; options: ViewOption[]; paramName?: string; }; function ViewSwitcherInner({ className, defaultKey, options, paramName: parameterName = "view", }: ViewSwitcherProps) { const router = useRouter(); const pathname = usePathname(); const searchParameters = useSearchParams(); const resolvedDefault = defaultKey ?? options[0]?.key ?? ""; const currentKey = searchParameters.get(parameterName) ?? resolvedDefault; function handleSelect(key: string): void { const parameters = new URLSearchParams(searchParameters.toString()); if (key === resolvedDefault) { parameters.delete(parameterName); } else { parameters.set(parameterName, key); } const query = parameters.toString(); router.push(query ? `${pathname}?${query}` : pathname, { scroll: false }); } return ( <div className={cn( "inline-flex items-center rounded-lg border bg-muted p-1", className, )} role="tablist" > {options.map((option) => ( <button aria-selected={currentKey === option.key} className={cn( "rounded-md px-3 py-1.5 text-sm font-medium transition-colors", currentKey === option.key ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground", )} key={option.key} onClick={() => { handleSelect(option.key); }} role="tab" type="button" > {option.label} </button> ))} </div> ); } function ViewSwitcherFallback({ className, defaultKey, options, }: ViewSwitcherProps) { const resolvedDefault = defaultKey ?? options[0]?.key ?? ""; return ( <div className={cn( "inline-flex items-center rounded-lg border bg-muted p-1", className, )} role="tablist" > {options.map((option) => ( <button aria-selected={resolvedDefault === option.key} className={cn( "rounded-md px-3 py-1.5 text-sm font-medium transition-colors", resolvedDefault === option.key ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground", )} key={option.key} role="tab" type="button" > {option.label} </button> ))} </div> ); } const ViewSwitcher = memo(function ViewSwitcher(props: ViewSwitcherProps) { return ( <Suspense fallback={<ViewSwitcherFallback {...props} />}> <ViewSwitcherInner {...props} /> </Suspense> ); }); ViewSwitcher.displayName = "ViewSwitcher"; export { ViewSwitcher }; export type { ViewOption, ViewSwitcherProps };

Dependencies

  • @vllnt/ui@^0.2.1