Components

Tabs

Tabs allow users to navigate to specific content sections. Before hydration, each tab functions as a link, and every click triggers a server-side render of the current page. Once hydration is complete, switching between tabs becomes a client-side action, similar to what you typically see.

  import {
    TabsContentPrimitive,
    TabsListPrimitive,
    TabsPrimitive,
    TabsTriggerPrimitive,
    useTabs,
  } from "@cychien/cotton-ui";

  function TabsComponent() {
    const { pathname, search } = useLocation();
    const tabsProps = useTabs({
      id: "tabs-demo",
      url: pathname + search,
      defaultValue: "cotton",
    });

    return (
      <Tabs {...tabsProps}>
        <TabsList>
          <TabsTrigger value="cotton">Cotton</TabsTrigger>
          <TabsTrigger value="linen">Linen</TabsTrigger>
          <TabsTrigger value="jute">Jute</TabsTrigger>
          <TabsTrigger value="sisal">Sisal</TabsTrigger>
          <TabsTrigger value="coir">Coir</TabsTrigger>
        </TabsList>
        <TabsContent value="cotton">Cotton content</TabsContent>
        <TabsContent value="linen">Linen content</TabsContent>
        <TabsContent value="jute">Jute content</TabsContent>
        <TabsContent value="sisal">Sisal content</TabsContent>
        <TabsContent value="coir">Coir content</TabsContent>
      </Tabs>
    );
  }

  interface TabsProps
    extends React.ComponentPropsWithoutRef<typeof TabsPrimitive> {}
  type TabsRef = React.ElementRef<typeof TabsPrimitive>;

  const Tabs = React.forwardRef<TabsRef, TabsProps>(
    ({ className, ...props }, ref) => {
      return <TabsPrimitive ref={ref} className={className} {...props} />;
    }
  );
  Tabs.displayName = "Tabs";

  interface TabsListProps
    extends React.ComponentPropsWithoutRef<typeof TabsListPrimitive> {}
  type TabsListRef = React.ElementRef<typeof TabsListPrimitive>;

  const TabsList = React.forwardRef<TabsListRef, TabsListProps>(
    ({ className, ...props }, ref) => {
      return (
        <TabsListPrimitive
          ref={ref}
          className={cn(
            "relative flex items-center space-x-2 after:absolute after:bottom-0 after:left-0 after:block after:h-[3px] after:w-full after:bg-slate-100 after:content-['']",
            className
          )}
          {...props}
        />
      );
    }
  );
  TabsList.displayName = "TabsList";

  interface TabsTriggerProps
    extends React.ComponentPropsWithoutRef<typeof TabsTriggerPrimitive> {}
  type TabsTriggerRef = React.ElementRef<typeof TabsTriggerPrimitive>;

  const TabsTrigger = React.forwardRef<TabsTriggerRef, TabsTriggerProps>(
    ({ className, ...props }, ref) => {
      return (
        <TabsTriggerPrimitive
          ref={ref}
          className={cn(
            "relative z-10 inline-flex items-center justify-center whitespace-nowrap rounded p-3 text-sm font-medium text-slate-500 transition-all hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-200 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-slate-900 data-[state=active]:after:absolute data-[state=active]:after:bottom-0 data-[state=active]:after:left-0 data-[state=active]:after:block data-[state=active]:after:h-[3px] data-[state=active]:after:w-full data-[state=active]:after:bg-slate-900 data-[state=active]:after:content-['']",
            className
          )}
          {...props}
        />
      );
    }
  );
  TabsTrigger.displayName = "TabsTrigger";

  interface TabsContentProps
    extends React.ComponentPropsWithoutRef<typeof TabsContentPrimitive> {}
  type TabsContentRef = React.ElementRef<typeof TabsContentPrimitive>;

  const TabsContent = React.forwardRef<TabsContentRef, TabsContentProps>(
    ({ className, ...props }, ref) => {
      return (
        <TabsContentPrimitive
          ref={ref}
          className={cn(
            "mt-6 text-sm font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-200 focus-visible:ring-offset-2",
            className
          )}
          {...props}
        />
      );
    }
  );
  TabsContent.displayName = "TabsContent";
Props
idstringRequired
A unique id for the tabs. Used as the key of the query parameter of the redirect url before hydration.
urlstringRequired
The url of the current page. Used to redirect users before hydration. It should also be available on the server side.
valuestring
The selected tab. Used when you want controlled tabs
defaultValuestring
The default selected tab.
Plus all props from Radix UI Tabs

Usage

  function Tabs() {
    const { pathname, search } = useLocation(); // Get current url. It should also be available on the server side
    const tabsProps = useTabs({
      id: "tabs-id",
      url: pathname + search,
      defaultValue: "a",
    });

    return (
      <Tabs {...tabsProps}>
        <TabsList>
          <TabsTrigger value="a">A</TabsTrigger>
          <TabsTrigger value="b">B</TabsTrigger>
          <TabsTrigger value="c">C</TabsTrigger>
        </TabsList>
        <TabsContent value="a">
          A
        </TabsContent>
        <TabsContent value="b">
          B
        </TabsContent>
        <TabsContent value="c">
          C
        </TabsContent>
      </Tabs>
    )
  }

Behaviors

Before hydration
  • All tabs are implemented as <a> tags, so when a tab is clicked, the user will be redirected to the same page with different content
After hydration
  • Supports keyboard navigation
  • Can access the active value from tabs when the active tab changes
  • Preserves all internal states, including user actions on tabs before hydration. For example, the checkbox state will be brought from the unhydrated world to the hydrated world

Recipes

Examples you can implement based on Cotton UI Tabs. (currently only in Chinese)