All components
NucleusTabs
NavigationNewA 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
- 1shadcn/ui initialized (`npx shadcn@latest init`)
- 2Tailwind CSS v4 configured with the nucleusUI theme tokens
npm install lucide-react clsx tailwind-merge framer-motionAdd 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
| Prop | Type | Default | Description |
|---|---|---|---|
items | { value: string; label: string; content: ReactNode }[] | — | Tab definition: id, strip label and panel content. |
defaultValue | string | — | Initially active tab id. Defaults to the first item. |
className | string | — | Extra classes for the root element. |