Workspace Switcher
Segmented workspace picker for switching between canvas views and operational contexts.
Preview
Switch between light and dark to inspect the embedded Storybook preview.
Installation
pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/workspace-switcher.jsonbash
Storybook
Explore all variants, controls, and accessibility checks in the interactive Storybook playground.
View in StorybookCode
"use client";
import { forwardRef, useMemo, useState } from "react";
import { cn } from "../../lib/utils";
export type WorkspaceOption = {
description?: string;
id: string;
label: string;
};
export type WorkspaceSwitcherProps = Omit<
React.ComponentPropsWithoutRef<"div">,
"defaultValue" | "onChange"
> & {
defaultValue?: string;
onValueChange?: (value: string) => void;
value?: string;
workspaces: WorkspaceOption[];
};
const WorkspaceSwitcher = forwardRef<HTMLDivElement, WorkspaceSwitcherProps>(
(
{ className, defaultValue, onValueChange, value, workspaces, ...props },
ref,
) => {
const fallbackValue = defaultValue ?? workspaces[0]?.id ?? "";
const [internalValue, setInternalValue] = useState(fallbackValue);
const currentValue = value ?? internalValue;
const currentWorkspace = useMemo(
() => workspaces.find((workspace) => workspace.id === currentValue),
[currentValue, workspaces],
);
function handleSelect(nextValue: string) {
if (value === undefined) {
setInternalValue(nextValue);
}
onValueChange?.(nextValue);
}
return (
<div
className={cn(
"inline-flex min-w-0 items-center gap-1 rounded-full border border-border/70 bg-muted/50 p-1",
className,
)}
ref={ref}
role="radiogroup"
{...props}
>
{workspaces.map((workspace) => {
const isActive = workspace.id === currentValue;
return (
<button
aria-checked={isActive}
className={cn(
"rounded-full px-3 py-1.5 text-sm font-medium transition-colors",
isActive
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground",
)}
key={workspace.id}
onClick={() => {
handleSelect(workspace.id);
}}
role="radio"
title={workspace.description}
type="button"
>
{workspace.label}
</button>
);
})}
{currentWorkspace?.description ? (
<span className="hidden pl-2 pr-1 text-xs text-muted-foreground md:inline">
{currentWorkspace.description}
</span>
) : null}
</div>
);
},
);
WorkspaceSwitcher.displayName = "WorkspaceSwitcher";
export { WorkspaceSwitcher };
typescript