All components
NucleusButton
ButtonsA confident button with primary/secondary/outline/ghost variants, a built-in loading state and icon slots on either side.
Preview
Installation
- 1shadcn/ui initialized (`npx shadcn@latest init`)
- 2Tailwind CSS v4 configured with the nucleusUI theme tokens
npm install lucide-react clsx tailwind-mergeAdd 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 { NucleusButton } from "@/components/nucleus/nucleus-button";
import { Rocket } from "lucide-react";
export function Example() {
return (
<div className="flex gap-3">
<NucleusButton>Launch</NucleusButton>
<NucleusButton variant="secondary">Plan</NucleusButton>
<NucleusButton variant="outline" iconEnd={<Rocket />}>
Outline
</NucleusButton>
<NucleusButton variant="ghost">Ghost</NucleusButton>
<NucleusButton loading>Saving…</NucleusButton>
</div>
);
}Source
Copy this file into components/nucleus/nucleus-button.tsx in your project — it's yours to own and modify.
components/nucleus/nucleus-button.tsx
"use client";
import * as React from "react";
import { Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
type Variant = "primary" | "secondary" | "outline" | "ghost" | "destructive" | "link";
type Size = "sm" | "md" | "lg" | "icon";
export interface NucleusButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/** Visual style of the button. */
variant?: Variant;
/** Control the height / padding. */
size?: Size;
/** Shows a spinner and disables interaction when true. */
loading?: boolean;
/** Optional icon rendered before the label. */
icon?: React.ReactNode;
/** Optional icon rendered after the label. */
iconEnd?: React.ReactNode;
}
const variantClasses: Record<Variant, string> = {
primary:
"bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 active:bg-primary/95",
secondary:
"bg-secondary text-secondary-foreground border border-border/60 hover:bg-secondary/70",
outline:
"border border-border bg-transparent hover:bg-muted hover:border-primary/40",
ghost: "hover:bg-muted",
destructive: "bg-destructive text-white shadow-sm hover:bg-destructive/90",
link: "text-primary underline-offset-4 hover:underline",
};
const sizeClasses: Record<Size, string> = {
sm: "h-8 gap-1.5 rounded-lg px-3 text-xs",
md: "h-10 gap-2 rounded-xl px-4 text-sm",
lg: "h-12 gap-2 rounded-xl px-6 text-base",
icon: "size-10 rounded-xl",
};
export const NucleusButton = React.forwardRef<HTMLButtonElement, NucleusButtonProps>(
function NucleusButton(
{
className,
variant = "primary",
size = "md",
loading = false,
icon,
iconEnd,
children,
disabled,
type = "button",
...props
},
ref
) {
return (
<button
ref={ref}
type={type}
disabled={disabled || loading}
data-slot="nucleus-button"
data-variant={variant}
className={cn(
"inline-flex shrink-0 cursor-pointer items-center justify-center font-medium whitespace-nowrap transition-all duration-150 select-none",
"focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-none",
"disabled:pointer-events-none disabled:opacity-55 active:translate-y-px",
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
variantClasses[variant],
sizeClasses[size],
className
)}
{...props}
>
{loading ? (
<Loader2 className="animate-spin" aria-hidden />
) : (
icon
)}
{children}
{!loading && iconEnd}
</button>
);
}
);
Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "primary" | "secondary" | "outline" | "ghost" | "destructive" | "link" | "primary" | Visual style of the button. |
size | "sm" | "md" | "lg" | "icon" | "md" | Controls height, padding and font size. |
loading | boolean | false | Shows a spinner, disables the button and swaps the icon slot. |
icon | ReactNode | — | Optional icon rendered before the label. |
iconEnd | ReactNode | — | Optional icon rendered after the label. |
...rest | ButtonHTMLAttributes<HTMLButtonElement> | — | All native button attributes are forwarded. |