nucleusUI
All components

NucleusButton

Buttons

A confident button with primary/secondary/outline/ghost variants, a built-in loading state and icon slots on either side.

Preview

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

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 { 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

PropTypeDescription
variant"primary" | "secondary" | "outline" | "ghost" | "destructive" | "link"Visual style of the button.
size"sm" | "md" | "lg" | "icon"Controls height, padding and font size.
loadingbooleanShows a spinner, disables the button and swaps the icon slot.
iconReactNodeOptional icon rendered before the label.
iconEndReactNodeOptional icon rendered after the label.
...restButtonHTMLAttributes<HTMLButtonElement>All native button attributes are forwarded.