# Tabs Segmented Icons — React Tabs Component

> An icon segmented control whose active segment unfolds its label, driving a task view where the same cards re-flow between list, board, grid and timeline.

- Category: [Tabs](https://pixelpapa.com/components/tabs.md)
- Tags: tabs, segmented, icons, layout, views
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/tabs/tabs-segmented-icons

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/tabs-segmented-icons.json"
```

The file is written to `components/pixelpapa/tabs-segmented-icons.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { ChartNoAxesGantt, Columns3, LayoutGrid, LayoutList, type LucideIcon } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useId, useState, type CSSProperties, type KeyboardEvent } from "react";
import { cn } from "@/lib/utils";

export type Segment<T extends string> = { value: T; label: string; icon: LucideIcon };

type SegmentedIconTabsProps<T extends string> = {
  segments: Segment<T>[];
  value: T;
  onValueChange: (value: T) => void;
  label: string;
  className?: string;
};

export function SegmentedIconTabs<T extends string>({ segments, value, onValueChange, label, className }: SegmentedIconTabsProps<T>) {
  const id = useId();
  const index = segments.findIndex((s) => s.value === value);

  function onKeyDown(e: KeyboardEvent<HTMLDivElement>) {
    let next = index;
    if (e.key === "ArrowRight") next = (index + 1) % segments.length;
    else if (e.key === "ArrowLeft") next = (index - 1 + segments.length) % segments.length;
    else return;
    e.preventDefault();
    onValueChange(segments[next].value);
    e.currentTarget.querySelectorAll<HTMLElement>('[role="tab"]')[next]?.focus();
  }

  return (
    <div
      role="tablist"
      aria-label={label}
      onKeyDown={onKeyDown}
      className={cn("inline-flex rounded-xl border border-border bg-muted p-1", className)}
    >
      {segments.map((s) => {
        const selected = s.value === value;
        return (
          <motion.button
            layout
            key={s.value}
            role="tab"
            aria-selected={selected}
            aria-label={s.label}
            tabIndex={selected ? 0 : -1}
            onClick={() => onValueChange(s.value)}
            transition={{ type: "spring", stiffness: 420, damping: 34 }}
            className={cn(
              "relative flex h-8 items-center rounded-lg px-2.5 text-sm font-medium transition-colors duration-200",
              selected ? "text-foreground" : "text-muted-foreground hover:text-foreground",
            )}
          >
            {selected && (
              <motion.span
                layoutId={`tabs-segmented-icons-${id}`}
                className="absolute inset-0 rounded-lg bg-background shadow-sm ring-1 ring-border/70"
                transition={{ type: "spring", stiffness: 420, damping: 34 }}
              />
            )}
            <motion.span layout="position" className="relative flex items-center">
              <s.icon className={cn("size-4", selected && "text-primary")} />
            </motion.span>
            <AnimatePresence initial={false}>
              {selected && (
                <motion.span
                  initial={{ width: 0, opacity: 0 }}
                  animate={{ width: "auto", opacity: 1 }}
                  exit={{ width: 0, opacity: 0 }}
                  transition={{ type: "spring", stiffness: 420, damping: 34 }}
                  className="relative overflow-hidden whitespace-nowrap"
                >
                  <span className="pl-2">{s.label}</span>
                </motion.span>
              )}
            </AnimatePresence>
          </motion.button>
        );
      })}
    </div>
  );
}

type View = "list" | "board" | "grid" | "timeline";

const views: Segment<View>[] = [
  { value: "list", label: "List", icon: LayoutList },
  { value: "board", label: "Board", icon: Columns3 },
  { value: "grid", label: "Grid", icon: LayoutGrid },
  { value: "timeline", label: "Timeline", icon: ChartNoAxesGantt },
];

// status: 0 todo, 1 in progress, 2 done · start/len place the task on a 6-day timeline
const tasks = [
  { title: "Audit onboarding emails", owner: "MT", status: 0, start: 1, len: 2 },
  { title: "Ship usage-based billing", owner: "IO", status: 1, start: 1, len: 4 },
  { title: "Rewrite the import wizard", owner: "TL", status: 1, start: 2, len: 3 },
  { title: "Dark mode for invoices", owner: "RB", status: 2, start: 3, len: 2 },
  { title: "Rate-limit public API", owner: "IO", status: 0, start: 4, len: 3 },
  { title: "Localise the pricing page", owner: "MT", status: 2, start: 5, len: 2 },
];

const tones = ["bg-muted-foreground/50", "bg-amber-500", "bg-emerald-500"];
const columns: Record<View, string> = {
  list: "minmax(0, 1fr)",
  board: "repeat(3, minmax(0, 1fr))",
  grid: "repeat(3, minmax(0, 1fr))",
  timeline: "repeat(6, minmax(0, 1fr))",
};
const rows: Record<View, string> = { list: "34px", board: "54px", grid: "104px", timeline: "30px" };

function place(view: View, i: number): CSSProperties {
  const t = tasks[i];
  if (view === "board") {
    const row = tasks.slice(0, i).filter((o) => o.status === t.status).length + 1;
    return { gridColumn: `${t.status + 1}`, gridRow: `${row}` };
  }
  if (view === "grid") return { gridColumn: `${(i % 3) + 1}`, gridRow: `${Math.floor(i / 3) + 1}` };
  if (view === "timeline") return { gridColumn: `${t.start} / span ${t.len}`, gridRow: `${i + 1}` };
  return { gridColumn: "1", gridRow: `${i + 1}` };
}

export default function TabsSegmentedIconsDemo() {
  const [view, setView] = useState<View>("board");

  return (
    <div className="w-full max-w-xl rounded-2xl border border-border bg-card p-4 shadow-sm">
      <div className="flex items-center justify-between gap-4">
        <div>
          <p className="text-sm font-medium text-card-foreground">Sprint 42</p>
          <p className="text-xs text-muted-foreground">6 tasks · ends Friday</p>
        </div>
        <SegmentedIconTabs segments={views} value={view} onValueChange={setView} label="Task view" />
      </div>

      {/* One grid, four placements: each card keeps its identity and animates to its new cell. */}
      <div
        role="tabpanel"
        aria-label={`${view} view`}
        className="mt-4 grid h-[250px] content-start gap-2"
        style={{ gridTemplateColumns: columns[view], gridAutoRows: rows[view] }}
      >
        {tasks.map((t, i) => (
          <motion.div
            layout
            key={t.title}
            style={{ ...place(view, i), borderRadius: 10 }}
            transition={{ type: "spring", stiffness: 340, damping: 32, delay: i * 0.012 }}
            className={cn(
              "flex min-w-0 overflow-hidden border border-border bg-background px-2.5 text-[13px] text-foreground",
              view === "grid" ? "flex-col justify-between py-2.5" : "items-center gap-2",
              view === "timeline" && "border-primary/25 bg-primary/10",
            )}
          >
            <motion.span layout="position" className="flex min-w-0 items-center gap-2">
              <span className={cn("size-2 shrink-0 rounded-full", tones[t.status])} />
              <span className={cn("min-w-0", view === "grid" ? "line-clamp-2 leading-snug" : "truncate")}>{t.title}</span>
            </motion.span>
            {(view === "list" || view === "grid") && (
              <motion.span
                layout="position"
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                transition={{ delay: 0.15 }}
                className={cn(
                  "flex size-5 shrink-0 items-center justify-center rounded-full bg-muted text-[9px] font-semibold text-muted-foreground",
                  view === "list" && "ml-auto",
                )}
              >
                {t.owner}
              </motion.span>
            )}
          </motion.div>
        ))}
      </div>
    </div>
  );
}
```

## AI prompt

```text
Add the "Tabs Segmented Icons" component from PixelPapa UI to this project.

What it is: An icon segmented control whose active segment unfolds its label, driving a task view where the same cards re-flow between list, board, grid and timeline.
Category: Tabs. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/tabs-segmented-icons.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/tabs-segmented-icons.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/tabs-segmented-icons" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More tabs

- [Tabs Animated Height Content](https://pixelpapa.com/components/tabs/tabs-animated-height-content.md): Tabs inside a card that springs to the height of each panel, so short and long content swap without a layout jump.
- [Tabs Folder](https://pixelpapa.com/components/tabs/tabs-folder.md): Browser-style tabs with scooped corners: the active shape slides between tabs, and tabs can be closed or added with layout springs.
- [Tabs Scrollable Overflow](https://pixelpapa.com/components/tabs/tabs-scrollable-overflow.md) (Pro): A tab strip that scrolls when it runs out of room: edge fades and arrow buttons appear only on the side that has more, and the active tab centres itself.
- [Tabs Sliding Pill](https://pixelpapa.com/components/tabs/tabs-sliding-pill.md) (Pro): A pill glides between tabs on a shared-layout spring while the panel slides in from the side you moved toward.
- [Tabs Underline Spring](https://pixelpapa.com/components/tabs/tabs-underline-spring.md) (Pro): An underline that stretches toward the next tab before its tail catches up, with a hover highlight that follows the pointer.
- [Tabs Vertical With Preview](https://pixelpapa.com/components/tabs/tabs-vertical-with-preview.md) (Pro): Vertical feature tabs that auto-advance with a filling rail and swap a drawn product preview, sliding up or down by direction.
