Web InnoventixFreeCode

Solid Set Button

Original · free

A set of solid buttons: primary, secondary, success and danger, in three sizes.

byWeb InnoventixReact + Tailwind
btnsolidsetbuttons
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/btn-solid-set.json
btn-solid-set.tsx
"use client";

import { useState, type ReactNode } from "react";

type Variant = "primary" | "secondary" | "success" | "danger";
type Size = "sm" | "md" | "lg";

const VARIANT_CLASSES: Record<Variant, string> = {
  primary:
    "bg-indigo-600 text-white shadow-sm shadow-indigo-600/25 hover:bg-indigo-500 active:bg-indigo-700 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:active:bg-indigo-600 dark:shadow-indigo-500/20 dark:focus-visible:outline-indigo-400",
  secondary:
    "bg-slate-800 text-white shadow-sm shadow-slate-900/25 hover:bg-slate-700 active:bg-slate-900 focus-visible:outline-slate-700 dark:bg-slate-200 dark:text-slate-900 dark:hover:bg-white dark:active:bg-slate-300 dark:shadow-black/20 dark:focus-visible:outline-slate-300",
  success:
    "bg-emerald-600 text-white shadow-sm shadow-emerald-600/25 hover:bg-emerald-500 active:bg-emerald-700 focus-visible:outline-emerald-600 dark:bg-emerald-500 dark:hover:bg-emerald-400 dark:active:bg-emerald-600 dark:shadow-emerald-500/20 dark:focus-visible:outline-emerald-400",
  danger:
    "bg-rose-600 text-white shadow-sm shadow-rose-600/25 hover:bg-rose-500 active:bg-rose-700 focus-visible:outline-rose-600 dark:bg-rose-500 dark:hover:bg-rose-400 dark:active:bg-rose-600 dark:shadow-rose-500/20 dark:focus-visible:outline-rose-400",
};

const SIZE_CLASSES: Record<Size, string> = {
  sm: "h-8 gap-1.5 rounded-lg px-3 text-xs",
  md: "h-10 gap-2 rounded-lg px-4 text-sm",
  lg: "h-12 gap-2.5 rounded-xl px-6 text-base",
};

const SPINNER_SIZE: Record<Size, string> = {
  sm: "h-3.5 w-3.5",
  md: "h-4 w-4",
  lg: "h-5 w-5",
};

const ICON_SIZE: Record<Size, string> = {
  sm: "h-3.5 w-3.5",
  md: "h-4 w-4",
  lg: "h-5 w-5",
};

interface SolidButtonProps {
  variant: Variant;
  size: Size;
  children: ReactNode;
  loading?: boolean;
  disabled?: boolean;
  icon?: ReactNode;
  onClick?: () => void;
  ariaLabel?: string;
}

function SolidButton({
  variant,
  size,
  children,
  loading = false,
  disabled = false,
  icon,
  onClick,
  ariaLabel,
}: SolidButtonProps) {
  return (
    <button
      type="button"
      onClick={onClick}
      disabled={disabled || loading}
      aria-label={ariaLabel}
      aria-busy={loading}
      className={[
        "relative inline-flex select-none items-center justify-center font-semibold tracking-tight",
        "transition-[background-color,transform,box-shadow] duration-150 ease-out",
        "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2",
        "active:translate-y-px",
        "disabled:pointer-events-none disabled:opacity-50",
        SIZE_CLASSES[size],
        VARIANT_CLASSES[variant],
      ].join(" ")}
    >
      {loading && (
        <span
          className={[
            "bsbs-spin shrink-0 rounded-full border-2 border-current border-r-transparent",
            SPINNER_SIZE[size],
          ].join(" ")}
          aria-hidden="true"
        />
      )}
      {!loading && icon && (
        <span className={["shrink-0", ICON_SIZE[size]].join(" ")} aria-hidden="true">
          {icon}
        </span>
      )}
      <span className={loading ? "opacity-90" : ""}>{children}</span>
    </button>
  );
}

const BoltIcon = (
  <svg viewBox="0 0 24 24" fill="none" className="h-full w-full" stroke="currentColor" strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round">
    <path d="M13 2 4 14h6l-1 8 9-12h-6l1-8Z" />
  </svg>
);

const CartIcon = (
  <svg viewBox="0 0 24 24" fill="none" className="h-full w-full" stroke="currentColor" strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round">
    <circle cx="9" cy="20" r="1.4" />
    <circle cx="18" cy="20" r="1.4" />
    <path d="M2 3h2.2l2.1 12.2a1.5 1.5 0 0 0 1.5 1.3h9.1a1.5 1.5 0 0 0 1.5-1.2L20.5 7H5.3" />
  </svg>
);

const CheckIcon = (
  <svg viewBox="0 0 24 24" fill="none" className="h-full w-full" stroke="currentColor" strokeWidth={2.6} strokeLinecap="round" strokeLinejoin="round">
    <path d="m5 12.5 4.5 4.5L19 6.5" />
  </svg>
);

const TrashIcon = (
  <svg viewBox="0 0 24 24" fill="none" className="h-full w-full" stroke="currentColor" strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round">
    <path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13" />
  </svg>
);

const sizes: { key: Size; label: string }[] = [
  { key: "sm", label: "Small" },
  { key: "md", label: "Medium" },
  { key: "lg", label: "Large" },
];

const variants: {
  key: Variant;
  label: string;
  icon: ReactNode;
  cta: string;
}[] = [
  { key: "primary", label: "Primary", icon: BoltIcon, cta: "Deploy to production" },
  { key: "secondary", label: "Secondary", icon: CartIcon, cta: "Add to cart" },
  { key: "success", label: "Success", icon: CheckIcon, cta: "Confirm order" },
  { key: "danger", label: "Danger", icon: TrashIcon, cta: "Delete workspace" },
];

export default function BtnSolidSet() {
  const [savingKey, setSavingKey] = useState<Variant | null>(null);
  const [disabledMode, setDisabledMode] = useState(false);

  const handleClick = (key: Variant) => {
    setSavingKey(key);
    window.setTimeout(() => setSavingKey(null), 1600);
  };

  return (
    <section className="relative w-full bg-slate-50 px-6 py-20 dark:bg-slate-950 sm:px-8">
      <style>{`
        @keyframes bsbs-spin {
          to { transform: rotate(360deg); }
        }
        .bsbs-spin { animation: bsbs-spin 0.6s linear infinite; }
        @media (prefers-reduced-motion: reduce) {
          .bsbs-spin { animation-duration: 1.4s; }
        }
      `}</style>

      <div className="mx-auto max-w-4xl">
        <header className="mb-12">
          <span className="inline-flex items-center rounded-full bg-indigo-100 px-3 py-1 text-xs font-semibold text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300">
            Buttons
          </span>
          <h2 className="mt-4 text-3xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
            Solid button set
          </h2>
          <p className="mt-3 max-w-2xl text-base leading-relaxed text-slate-600 dark:text-slate-400">
            Four intents — primary, secondary, success and danger — each in three
            sizes. Tactile press, focus-visible rings, and real loading and
            disabled states.
          </p>
        </header>

        <div className="mb-10 flex flex-wrap items-center gap-3">
          <button
            type="button"
            onClick={() => setDisabledMode((v) => !v)}
            aria-pressed={disabledMode}
            className="inline-flex h-9 items-center gap-2 rounded-lg border border-slate-300 bg-white px-3 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800"
          >
            <span
              className={[
                "flex h-4 w-8 items-center rounded-full p-0.5 transition-colors",
                disabledMode ? "bg-indigo-500" : "bg-slate-300 dark:bg-slate-600",
              ].join(" ")}
              aria-hidden="true"
            >
              <span
                className={[
                  "h-3 w-3 rounded-full bg-white transition-transform",
                  disabledMode ? "translate-x-4" : "translate-x-0",
                ].join(" ")}
              />
            </span>
            Disabled preview
          </button>
          <p className="text-sm text-slate-500 dark:text-slate-500">
            Tap any button to watch its loading state.
          </p>
        </div>

        <div className="space-y-4">
          {variants.map((v) => (
            <div
              key={v.key}
              className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900"
            >
              <div className="mb-5 flex items-center justify-between">
                <h3 className="text-sm font-semibold text-slate-900 dark:text-white">
                  {v.label}
                </h3>
                <code className="rounded bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-500 dark:bg-slate-800 dark:text-slate-400">
                  {v.key}
                </code>
              </div>
              <div className="flex flex-wrap items-center gap-4">
                {sizes.map((s) => (
                  <SolidButton
                    key={s.key}
                    variant={v.key}
                    size={s.key}
                    icon={v.icon}
                    disabled={disabledMode}
                    loading={savingKey === v.key}
                    onClick={() => handleClick(v.key)}
                  >
                    {savingKey === v.key ? "Working…" : v.cta}
                  </SolidButton>
                ))}
              </div>
            </div>
          ))}
        </div>

        <div className="mt-8 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900">
          <h3 className="mb-5 text-sm font-semibold text-slate-900 dark:text-white">
            Icon-only
          </h3>
          <div className="flex flex-wrap items-center gap-4">
            <button
              type="button"
              aria-label="Confirm and save"
              className="inline-flex h-10 w-10 items-center justify-center rounded-lg bg-emerald-600 text-white shadow-sm shadow-emerald-600/25 transition-[background-color,transform] duration-150 hover:bg-emerald-500 active:translate-y-px active:bg-emerald-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-600 dark:bg-emerald-500 dark:hover:bg-emerald-400 dark:focus-visible:outline-emerald-400"
            >
              <span className="h-5 w-5" aria-hidden="true">
                {CheckIcon}
              </span>
            </button>
            <button
              type="button"
              aria-label="Delete item"
              className="inline-flex h-10 w-10 items-center justify-center rounded-lg bg-rose-600 text-white shadow-sm shadow-rose-600/25 transition-[background-color,transform] duration-150 hover:bg-rose-500 active:translate-y-px active:bg-rose-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rose-600 dark:bg-rose-500 dark:hover:bg-rose-400 dark:focus-visible:outline-rose-400"
            >
              <span className="h-5 w-5" aria-hidden="true">
                {TrashIcon}
              </span>
            </button>
            <button
              type="button"
              aria-label="Quick deploy"
              className="inline-flex h-10 w-10 items-center justify-center rounded-lg bg-indigo-600 text-white shadow-sm shadow-indigo-600/25 transition-[background-color,transform] duration-150 hover:bg-indigo-500 active:translate-y-px active:bg-indigo-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-400"
            >
              <span className="h-5 w-5" aria-hidden="true">
                {BoltIcon}
              </span>
            </button>
          </div>
        </div>
      </div>
    </section>
  );
}

Dependencies

None (React + Tailwind only).

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →