Tabs

A set of layered sections of content—known as tab panels—that are displayed one at a time.

All
import Tabs from "@tailus-ui/tabs/Tabs"
import {
  type TabsListProps as ListProps,
  type TabsIndicatorProps as IndicatorProps,
} from "@tailus/themer";
import { useEffect, useRef, useState } from "react";

type TabsAppProps = "all" | "unread" | "archived" 

interface TabsUIProps extends ListProps {
  indicatorVariant? : IndicatorProps["variant"]
}

export const Overview = () => {

    const [state, setState] = useState<TabsAppProps>("all");
    const spanRef = useRef<HTMLSpanElement>(null);

    useEffect(() => {
        const activeTrigger = document.getElementById(state) as HTMLElement;
        if (spanRef.current) {
            spanRef.current.style.left = activeTrigger.offsetLeft + "px";
            spanRef.current.style.width = activeTrigger.offsetWidth + "px";
        }
    }, [state]);

    return (
        <Tabs.Root className="space-y-4" defaultValue={state} onValueChange={(value) => setState(value as TabsAppProps)}>
            <Tabs.List data-shade="925" variant="soft" triggerVariant="plain" size="md">
                <Tabs.Indicator ref={spanRef} variant="elevated" className="bg-white"/>
                <Tabs.Trigger value="all" id="all">All</Tabs.Trigger>
                <Tabs.Trigger value="unread" id="unread">Unread</Tabs.Trigger>
                <Tabs.Trigger value="archived" id="archived">Archived</Tabs.Trigger>
            </Tabs.List>
            <Tabs.Content value="all" className="text-[--caption-text-color]">
                All
            </Tabs.Content>
            <Tabs.Content value="unread" className="text-[--caption-text-color]">
                Unread
            </Tabs.Content>
            <Tabs.Content value="archived" className="text-[--caption-text-color]">
                Archived
            </Tabs.Content>
        </Tabs.Root>
    )
}

Installation

Install the primitive and Copy-Paste the component code in a .tsx file.

npm install @radix-ui/react-tabs
import * as TabsPrimitive from "@radix-ui/react-tabs";
import React from "react";
import {
  tabs,
  type TabsListProps as ListProps,
  type TabsIndicatorProps as IndicatorProps
} from "@tailus/themer";

const {list, trigger, indicator} = tabs();

const TabsContext = React.createContext<Omit<ListProps, "variant">>({
  intent: "primary",
  size: "md",
  triggerVariant: "plain"
});

const TabsRoot = TabsPrimitive.Root;

const TabsList = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.List>,
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & ListProps
  >(({ className, variant="soft", triggerVariant="plain", intent="primary", size="md", ...props }, forwardedRef) => {

  variant = variant || "soft";
    
  return (
    <TabsContext.Provider value={{triggerVariant, intent, size}}>
        <TabsPrimitive.List
          {...props}
          ref={forwardedRef}
          className={list({listVariant:variant, size, triggerVariant, className})}
        />
    </TabsContext.Provider>
  )
});

const TabsTrigger = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.Trigger>,
  React.ComponentProps<typeof TabsPrimitive.Trigger>
>(({className, ...props}, forwardedRef) => {

  const { triggerVariant, size, intent } = React.useContext(TabsContext);
  
  return (
    <TabsPrimitive.Trigger
      {...props}
      ref={forwardedRef}
      className={trigger({triggerVariant, size, intent, className})}
    />
  )
});

const TabsIndicator = React.forwardRef<
  React.ElementRef<"span">,
  React.ComponentProps<"span"> & Pick<IndicatorProps, "variant">
  >(({ className, variant = "bottom", ...props }, forwardedRef) => {
  
  const { intent } = React.useContext(TabsContext);
    
  return (
    <span
      {...props}
      aria-hidden
      ref={forwardedRef}
      className={indicator({indicatorVariant:variant, intent, className})}
    />
  );
});

const TabsContent = TabsPrimitive.Content;

export default {
  Root: TabsRoot,
  List: TabsList,
  Trigger: TabsTrigger,
  Content: TabsContent,
  Indicator: TabsIndicator,
}

export {
  TabsRoot,
  TabsList,
  TabsTrigger,
  TabsContent,
  TabsIndicator,
}

Usage

Import all the parts and build your Tabs.

import Tabs from "@tailus-ui/tabs/Tabs"
import {
  type TabsListProps as ListProps,
  type TabsIndicatorProps as IndicatorProps,
} from "@tailus/themer";
import { useEffect, useRef, useState } from "react";

type TabsAppProps = "all" | "unread" | "archived" 

interface TabsUIProps extends ListProps {
  indicatorVariant? : IndicatorProps["variant"]
}

export const Overview = () => {

    const [state, setState] = useState<TabsAppProps>("all");
    const spanRef = useRef<HTMLSpanElement>(null);

    useEffect(() => {
        const activeTrigger = document.getElementById(state) as HTMLElement;
        if (spanRef.current) {
            spanRef.current.style.left = activeTrigger.offsetLeft + "px";
            spanRef.current.style.width = activeTrigger.offsetWidth + "px";
        }
    }, [state]);

    return (
        <Tabs.Root className="space-y-4" defaultValue={state} onValueChange={(value) => setState(value as TabsAppProps)}>
            <Tabs.List data-shade="925" variant="soft" triggerVariant="plain" size="md">
                <Tabs.Indicator ref={spanRef} variant="elevated" className="bg-white"/>
                <Tabs.Trigger value="all" id="all">All</Tabs.Trigger>
                <Tabs.Trigger value="unread" id="unread">Unread</Tabs.Trigger>
                <Tabs.Trigger value="archived" id="archived">Archived</Tabs.Trigger>
            </Tabs.List>
            <Tabs.Content value="all" className="text-[--caption-text-color]">
                All
            </Tabs.Content>
            <Tabs.Content value="unread" className="text-[--caption-text-color]">
                Unread
            </Tabs.Content>
            <Tabs.Content value="archived" className="text-[--caption-text-color]">
                Archived
            </Tabs.Content>
        </Tabs.Root>
    )
}

Reference

List

The parent component of the tabs triggers

Prop
Type
Default
variant
enum
bottomOutlined
triggerVariant
enum
plain
intent
enum
primary
size
enum
md

Indicator

The element that indicates the current tab

Prop
Type
Default
variant
enum
bottom

Examples

Bottom Indicator

All
import Tabs from "@tailus-ui/tabs/Tabs"
import {
  type TabsListProps as ListProps,
  type TabsIndicatorProps as IndicatorProps,
} from "@tailus/themer";
import { useEffect, useRef, useState } from "react";

type TabsAppProps = "all" | "unread" | "archived" 

interface TabsUIProps extends ListProps {
  indicatorVariant? : IndicatorProps["variant"]
}

export const BottomIndicator = () => {

    const [state, setState] = useState<TabsAppProps>("all");
    const spanRef = useRef<HTMLSpanElement>(null);

    useEffect(() => {
        const activeTrigger = document.getElementById(state) as HTMLElement;
        if (spanRef.current) {
            spanRef.current.style.left = activeTrigger.offsetLeft + "px";
            spanRef.current.style.width = activeTrigger.offsetWidth + "px";
        }
    }, [state]);

    return (
        <Tabs.Root className="space-y-4" defaultValue={state} onValueChange={(value) => setState(value as TabsAppProps)}>
            <Tabs.List data-shade="900" variant="bottomOutlined" triggerVariant="plain" size="md" className="gap-2">
                <Tabs.Indicator ref={spanRef} variant="bottom"/>
                <Tabs.Trigger value="all" id="all">All</Tabs.Trigger>
                <Tabs.Trigger value="unread" id="unread">Unread</Tabs.Trigger>
                <Tabs.Trigger value="archived" id="archived">Archived</Tabs.Trigger>
            </Tabs.List>
            <Tabs.Content value="all" className="text-[--caption-text-color]">
                All
            </Tabs.Content>
            <Tabs.Content value="unread" className="text-[--caption-text-color]">
                Unread
            </Tabs.Content>
            <Tabs.Content value="archived" className="text-[--caption-text-color]">
                Archived
            </Tabs.Content>
        </Tabs.Root>
    )
}

Soft To Soft

All
import Tabs from "@tailus-ui/tabs/Tabs"

export const SoftToSoft = () => {
    return (
        <Tabs.Root className="space-y-4" defaultValue="all">
            <Tabs.List data-shade="925" variant="plain" triggerVariant="softToSoft" intent="primary" size="md">
                <Tabs.Trigger value="all">All</Tabs.Trigger>
                <Tabs.Trigger value="unread">Unread</Tabs.Trigger>
                <Tabs.Trigger value="archive">Archived</Tabs.Trigger>
            </Tabs.List>
            <Tabs.Content value="all" className="text-[--caption-text-color]">
                All
            </Tabs.Content>
            <Tabs.Content value="unread" className="text-[--caption-text-color]">
                Unread
            </Tabs.Content>
            <Tabs.Content value="archived" className="text-[--caption-text-color]">
                Archived
            </Tabs.Content>
        </Tabs.Root>
    )
}

Soft To Solid

All
import Tabs from "@tailus-ui/tabs/Tabs"

export const SoftToSolid = () => {
    return (
        <Tabs.Root className="space-y-4" defaultValue="all">
            <Tabs.List variant="plain" triggerVariant="softToSolid" intent="primary" size="md">
                <Tabs.Trigger value="all">All</Tabs.Trigger>
                <Tabs.Trigger value="unread">Unread</Tabs.Trigger>
                <Tabs.Trigger value="archive">Archived</Tabs.Trigger>
            </Tabs.List>
            <Tabs.Content value="all" className="text-[--caption-text-color]">
                All
            </Tabs.Content>
            <Tabs.Content value="unread" className="text-[--caption-text-color]">
                Unread
            </Tabs.Content>
            <Tabs.Content value="archived" className="text-[--caption-text-color]">
                Archived
            </Tabs.Content>
        </Tabs.Root>
    )
}

Outlined To Solid

All
import Tabs from "@tailus-ui/tabs/Tabs"

export const SoftToSolid = () => {
    return (
        <Tabs.Root className="space-y-4" defaultValue="all">
            <Tabs.List data-shade="900" variant="plain" triggerVariant="outlinedToSolid" intent="primary" size="md">
                <Tabs.Trigger value="all">All</Tabs.Trigger>
                <Tabs.Trigger value="unread">Unread</Tabs.Trigger>
                <Tabs.Trigger value="archive">Archived</Tabs.Trigger>
            </Tabs.List>
            <Tabs.Content value="all" className="text-[--caption-text-color]">
                All
            </Tabs.Content>
            <Tabs.Content value="unread" className="text-[--caption-text-color]">
                Unread
            </Tabs.Content>
            <Tabs.Content value="archived" className="text-[--caption-text-color]">
                Archived
            </Tabs.Content>
        </Tabs.Root>
    )
}

With Icon and Badge

Grape
import Badge from "@tailus-ui/Badge";
import Tabs from "@tailus-ui/tabs/Tabs"
import {
  type TabsListProps as ListProps,
  type TabsIndicatorProps as IndicatorProps,
} from "@tailus/themer";
import { Grape, Hop, IceCreamBowl } from "lucide-react";
import { useEffect, useRef, useState } from "react";

export type TabsAppProps = "grape" | "hop" | "icecream" 

export interface TabsUIProps extends ListProps {
  indicatorVariant? : IndicatorProps["variant"]
}

export const WithIconBadge = () => {

    const [state, setState] = useState<TabsAppProps>("grape");
    const spanRef3 = useRef<HTMLSpanElement>(null);

    useEffect(() => {
        const activeTrigger = document.getElementById(state) as HTMLElement;
        if (spanRef3.current) {
            spanRef3.current.style.left = activeTrigger.offsetLeft + "px";
            spanRef3.current.style.width = activeTrigger.offsetWidth + "px";
        }
    }, [state]);

    return (
        <Tabs.Root className="space-y-4 w-fit" defaultValue={state} onValueChange={(value) => setState(value as TabsAppProps)}>
            <Tabs.List data-shade="900" variant="bottomOutlined" triggerVariant="plain" size="lg" className="pb-2 w-full max-w-full">
                <Tabs.Indicator ref={spanRef3} variant="bottom"/>
                <Tabs.Trigger value="grape" id="grape" className="text-nowrap">
                    <Grape className="size-4 opacity-50 mr-2"/>
                    Grape
                </Tabs.Trigger>
                <Tabs.Trigger value="hop" id="hop" className="text-nowrap">
                    <Hop className="size-4 opacity-50 mr-2"/>
                    Hop
                    <Badge className="ml-2 border" size="xs" variant="soft" intent="gray">3</Badge>
                </Tabs.Trigger>
                <Tabs.Trigger value="icecream" id="icecream" className="text-nowrap">
                    <IceCreamBowl className="size-4 opacity-50 mr-2"/>
                    Ice Cream
                    <Badge className="ml-2 border" size="xs" variant="soft" intent="gray">12</Badge>
                </Tabs.Trigger>
            </Tabs.List>
            <Tabs.Content value="grape" className="text-[--caption-text-color]">
                Grape
            </Tabs.Content>
            <Tabs.Content value="hop" className="text-[--caption-text-color]">
                Hop
            </Tabs.Content>
            <Tabs.Content value="icecream" className="text-[--caption-text-color]">
                Ice cream
            </Tabs.Content>
        </Tabs.Root>
    )
}