Propel

Tabs

A set of layered sections that show one panel at a time.

A high-level summary of the project.
Show code
import { Icon } from "@makeplane/propel/components/icon";
import { Tab, Tabs, TabsList, TabsPanel } from "@makeplane/propel/components/tabs";
import { Activity, LayoutGrid, Settings } from "lucide-react";
import * as React from "react";

const TAB_ITEMS = [
  {
    value: "overview",
    label: "Overview",
    icon: <Icon icon={LayoutGrid} />,
    panel: "A high-level summary of the project.",
  },
  {
    value: "activity",
    label: "Activity",
    icon: <Icon icon={Activity} />,
    panel: "The latest activity feed.",
  },
  {
    value: "settings",
    label: "Settings",
    icon: <Icon icon={Settings} />,
    panel: "Configuration and preferences.",
  },
];

export default function BasicDemo() {
  const [value, setValue] = React.useState("overview");

  return (
    <div className="w-96">
      <Tabs variant="contained" value={value} onValueChange={setValue}>
        <TabsList>
          {TAB_ITEMS.map((tab) => (
            <Tab key={tab.value} value={tab.value} icon={tab.icon} label={tab.label} />
          ))}
        </TabsList>
        {TAB_ITEMS.map((tab) => (
          <TabsPanel key={tab.value} value={tab.value}>
            {tab.panel}
          </TabsPanel>
        ))}
      </Tabs>
    </div>
  );
}

Installation

import { Tabs, TabsList, Tab, TabsPanel } from "@makeplane/propel/components/tabs";

Usage

import { Icon } from "@makeplane/propel/components/icon";
import { Tab, Tabs, TabsList, TabsPanel } from "@makeplane/propel/components/tabs";
import { Activity, LayoutGrid, Settings } from "lucide-react";
import * as React from "react";

const TAB_ITEMS = [
  {
    value: "overview",
    label: "Overview",
    icon: <Icon icon={LayoutGrid} />,
    panel: "A high-level summary of the project.",
  },
  {
    value: "activity",
    label: "Activity",
    icon: <Icon icon={Activity} />,
    panel: "The latest activity feed.",
  },
  {
    value: "settings",
    label: "Settings",
    icon: <Icon icon={Settings} />,
    panel: "Configuration and preferences.",
  },
];

export default function BasicDemo() {
  const [value, setValue] = React.useState("overview");

  return (
    <div className="w-96">
      <Tabs variant="contained" value={value} onValueChange={setValue}>
        <TabsList>
          {TAB_ITEMS.map((tab) => (
            <Tab key={tab.value} value={tab.value} icon={tab.icon} label={tab.label} />
          ))}
        </TabsList>
        {TAB_ITEMS.map((tab) => (
          <TabsPanel key={tab.value} value={tab.value}>
            {tab.panel}
          </TabsPanel>
        ))}
      </Tabs>
    </div>
  );
}

Examples

Underline

The underline variant keeps the tabs flat and slides a bar under the active one, in contrast to the raised contained pill.

A high-level summary of the project.
Show code
import { Tab, Tabs, TabsList, TabsPanel } from "@makeplane/propel/components/tabs";

const TAB_ITEMS = [
  { value: "overview", label: "Overview", panel: "A high-level summary of the project." },
  { value: "activity", label: "Activity", panel: "The latest activity feed." },
  { value: "settings", label: "Settings", panel: "Configuration and preferences." },
];

export default function UnderlineDemo() {
  return (
    <div className="w-96">
      <Tabs variant="underline" defaultValue="overview">
        <TabsList>
          {TAB_ITEMS.map((tab) => (
            <Tab key={tab.value} value={tab.value} label={tab.label} />
          ))}
        </TabsList>
        {TAB_ITEMS.map((tab) => (
          <TabsPanel key={tab.value} value={tab.value}>
            {tab.panel}
          </TabsPanel>
        ))}
      </Tabs>
    </div>
  );
}

Disabled

A high-level summary of the project.
Show code
import { Tab, Tabs, TabsList, TabsPanel } from "@makeplane/propel/components/tabs";

const TAB_ITEMS = [
  {
    value: "overview",
    label: "Overview",
    panel: "A high-level summary of the project.",
    disabled: false,
  },
  {
    value: "activity",
    label: "Activity",
    panel: "The latest activity feed.",
    disabled: false,
  },
  {
    value: "billing",
    label: "Billing",
    panel: "Upgrade the workspace to manage billing.",
    disabled: true,
  },
];

export default function DisabledDemo() {
  return (
    <div className="w-96">
      <Tabs variant="contained" defaultValue="overview">
        <TabsList>
          {TAB_ITEMS.map((tab) => (
            <Tab key={tab.value} value={tab.value} label={tab.label} disabled={tab.disabled} />
          ))}
        </TabsList>
        {TAB_ITEMS.map((tab) => (
          <TabsPanel key={tab.value} value={tab.value}>
            {tab.panel}
          </TabsPanel>
        ))}
      </Tabs>
    </div>
  );
}

Overflow

TabsList composes a horizontal ScrollArea: when the tabs are wider than their container the row scrolls instead of overflowing, its overlay scrollbar fading in on hover or scroll. Keyboard navigation still reaches every tab — the focused tab is kept in view.

API Reference

Tabs

The ready-made tab set: grafts the Base UI `Tabs.Root` behavior onto the styled `elements/tabs` root and shares its `variant` with the `TabsList`/`Tab`s inside via context, so you don't repeat it.

PropTypeDefaultDescription
variant(required)"contained" | "underline"Whether the set is a contained pill or a flat underline strip. Shared with its parts.
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, TabsRootState>Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render.

TabsList

The ready-made tab strip: grafts the Base UI `ScrollArea.Root` onto the styled `TabsListScrollArea` frame, then grafts the Base UI `Tabs.List` behavior onto the styled `TabsList` (taking the set's `variant` from context) rendered as the scroll viewport, plus a horizontal scrollbar so a long row of tabs scrolls, and renders the active-tab underline `TabsIndicator` for the `underline` variant.

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, TabsListState>Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render.
activateOnFocusbooleantrueWhether moving focus to a tab with the arrow keys also activates it immediately (updates `aria-selected` and swaps the panel), vs. only moving focus until committed with Enter/Space. Pass `false` when switching tabs triggers an expensive operation (a network request, heavy computation) to avoid firing it on every arrow keystroke.

Tab

The ready-made tab button: grafts the Base UI `Tabs.Tab` behavior onto the styled `elements/tabs` tab (taking the set's `variant` from context, so you don't pass it) and lays out an optional `icon` with the label. The `underline` variant additionally renders the sliding bar track beneath the label.

PropTypeDefaultDescription
label(required)stringVisible tab label.
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, TabsTabState>Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render.
iconReactNodeElement rendered before the label (inline-start), e.g. `<Icon icon={Folder} />`.