Workspace Switcher

Segmented workspace picker for switching between canvas views and operational contexts.

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/workspace-switcher.json
bash

Storybook

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

View in Storybook

Code

"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

Dependencies

  • @vllnt/ui@^0.2.1