nucleusUI
All components

NucleusTabs

NavigationNew

A tabbed interface with a sliding violet underline indicator, spring animations and full keyboard + screen-reader accessibility.

Preview

nucleusUI is an open-source component collection built on top of shadcn/ui. Copy the source, own the code, ship faster.

Installation

  1. 1shadcn/ui initialized (`npx shadcn@latest init`)
  2. 2Tailwind CSS v4 configured with the nucleusUI theme tokens
npm install lucide-react clsx tailwind-merge framer-motion

Add to your globals.css

The nucleusUI accent lives in these CSS variables. Without them the component falls back to the default shadcn zinc theme.

app/globals.css
:root {
  --primary: oklch(0.54 0.24 291);
  --primary-foreground: oklch(0.985 0 0);
  --ring: oklch(0.54 0.24 291);
  --accent: oklch(0.96 0.03 291);
  --accent-foreground: oklch(0.4 0.19 291);
}

.dark {
  --primary: oklch(0.68 0.21 291);
  --primary-foreground: oklch(0.145 0 0);
  --ring: oklch(0.68 0.21 291);
  --accent: oklch(0.26 0.06 291);
  --accent-foreground: oklch(0.87 0.09 291);
}

Usage

tsx
import { NucleusTabs } from "@/components/nucleus/nucleus-tabs";

const items = [
  { value: "overview", label: "Overview", content: <p>First panel.</p> },
  { value: "install", label: "Install", content: <p>Second panel.</p> },
  { value: "own", label: "Own it", content: <p>Third panel.</p> },
];

export function Example() {
  return <NucleusTabs items={items} defaultValue="overview" />;
}

Source

Copy this file into components/nucleus/nucleus-tabs.tsx in your project, it's yours to own and modify.

components/nucleus/nucleus-tabs.tsx
"use client";

import * as React from "react";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";

export interface NucleusTabItem {
  /** Unique tab id. */
  value: string;
  /** Label shown in the tab strip. */
  label: string;
  /** Panel content. */
  content: React.ReactNode;
}

export interface NucleusTabsProps {
  items: NucleusTabItem[];
  /** Initially active tab id. Default: first item. */
  defaultValue?: string;
  className?: string;
}

/**
 * Tabs with a sliding underline indicator driven by Motion layout
 * animations. Keyboard accessible: native buttons + role attributes.
 */
export function NucleusTabs({ items, defaultValue, className }: NucleusTabsProps) {
  const active = defaultValue ?? items[0]?.value;
  const [selected, setSelected] = React.useState(active);
  const tabsId = React.useId();

  const current = items.find((t) => t.value === selected) ?? items[0];

  return (
    <div className={cn("flex w-full flex-col", className)} data-slot="nucleus-tabs">
      <div
        role="tablist"
        aria-label="Tabs"
        className="flex items-center gap-1 border-b border-border/70"
      >
        {items.map((item) => {
          const isActive = item.value === selected;
          return (
            <button
              key={item.value}
              role="tab"
              id={`${tabsId}-tab-${item.value}`}
              aria-selected={isActive}
              aria-controls={`${tabsId}-panel-${item.value}`}
              tabIndex={isActive ? 0 : -1}
              onClick={() => setSelected(item.value)}
              className={cn(
                "relative cursor-pointer px-3.5 py-2.5 text-sm font-medium whitespace-nowrap transition-colors",
                isActive
                  ? "text-foreground"
                  : "text-muted-foreground hover:text-foreground"
              )}
            >
              {item.label}
              {isActive && (
                <motion.span
                  layoutId={`${tabsId}-underline`}
                  className="absolute inset-x-2 -bottom-px h-0.5 rounded-full bg-primary"
                  transition={{ type: "spring", stiffness: 380, damping: 32 }}
                />
              )}
            </button>
          );
        })}
      </div>
      <div
        role="tabpanel"
        id={`${tabsId}-panel-${current?.value}`}
        aria-labelledby={`${tabsId}-tab-${current?.value}`}
        className="pt-4 focus-visible:outline-none"
        tabIndex={0}
      >
        {current?.content}
      </div>
    </div>
  );
}

Props

PropTypeDescription
items{ value: string; label: string; content: ReactNode }[]Tab definition: id, strip label and panel content.
defaultValuestringInitially active tab id. Defaults to the first item.
classNamestringExtra classes for the root element.