# Button

> Solid, outline, soft, ghost and link buttons that light up on hover.

Category: Forms. Docs: https://get-neonkit.vercel.app/components/button

## Installation

```bash
npx shadcn@latest add https://get-neonkit.vercel.app/r/button.json
```

With the `@neonkit` namespace configured: `npx shadcn@latest add @neonkit/button`.

npm dependencies: `@radix-ui/react-slot`, `lucide-react`. Also needs `lib/utils.ts` (`cn`) and `lib/neon.ts` (`neonVars`), which the CLI adds.

## Usage

```tsx
import { Button } from "@/components/ui/button"

<Button color="magenta" variant="solid">Ship it</Button>
```

## Examples

### Variants

```tsx
import { ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button";

export default function ButtonDemo() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-3">
      <Button variant="solid">
        Deploy <ArrowRight />
      </Button>
      <Button>Outline</Button>
      <Button variant="soft">Soft</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="link">Link</Button>
    </div>
  );
}
```

### Colors

Pass a palette name or any hex value to color.

```tsx
import { Button } from "@/components/ui/button";

const colors = ["cyan", "blue", "violet", "magenta", "pink", "red", "orange", "amber", "lime", "green"] as const;

export default function ButtonColors() {
  return (
    <div className="grid w-full max-w-xl gap-3">
      <div className="flex flex-wrap justify-center gap-2">
        {colors.map((color) => (
          <Button key={color} color={color} size="sm" className="capitalize">
            {color}
          </Button>
        ))}
      </div>
      <div className="flex flex-wrap justify-center gap-2">
        {colors.map((color) => (
          <Button key={color} color={color} variant="solid" size="sm" className="capitalize">
            {color}
          </Button>
        ))}
      </div>
      <div className="flex justify-center">
        <Button color="#ff6a00" variant="solid" size="sm">
          Any hex works · #ff6a00
        </Button>
      </div>
    </div>
  );
}
```

### Sizes

```tsx
import { Plus } from "lucide-react";
import { Button } from "@/components/ui/button";

export default function ButtonSizes() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-3">
      <Button size="sm">Small</Button>
      <Button size="md">Medium</Button>
      <Button size="lg">Large</Button>
      <Button size="icon" aria-label="Add item">
        <Plus />
      </Button>
    </div>
  );
}
```

### Loading and disabled

loading swaps in a spinner and blocks clicks until it clears.

```tsx
"use client";

import * as React from "react";
import { Button } from "@/components/ui/button";

export default function ButtonLoading() {
  const [loading, setLoading] = React.useState(false);

  function save() {
    setLoading(true);
    setTimeout(() => setLoading(false), 1800);
  }

  return (
    <div className="flex flex-wrap items-center justify-center gap-3">
      <Button variant="solid" color="lime" loading={loading} onClick={save}>
        {loading ? "Saving" : "Save changes"}
      </Button>
      <Button disabled>Disabled</Button>
    </div>
  );
}
```

### As a link

asChild hands the styles to its child, so links keep their semantics.

```tsx
import { ArrowUpRight } from "lucide-react";
import { Button } from "@/components/ui/button";

export default function ButtonAsLink() {
  return (
    <Button asChild color="magenta">
      <a href="https://github.com/AadiXC0DE/NeonKit" target="_blank" rel="noreferrer">
        Star on GitHub <ArrowUpRight />
      </a>
    </Button>
  );
}
```

## API reference

### Button

Also accepts every native button attribute.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"solid" \| "outline" \| "soft" \| "ghost" \| "link"` | `"outline"` | Visual style. |
| `color` | `NeonColor` | `inherited` | Palette name or hex. Falls back to --neon-accent, then cyan. |
| `size` | `"sm" \| "md" \| "lg" \| "icon"` | `"md"` | Height and padding. |
| `loading` | `boolean` | `false` | Shows a spinner and disables the button. |
| `asChild` | `boolean` | `false` | Merges props onto the single child element instead of rendering a button. |

## Accessibility

- Icon-only buttons need an aria-label.
- loading sets aria-busy so screen readers announce the pending state.

## Source

`components/ui/button.tsx`

```tsx
"use client";

import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { LoaderCircle } from "lucide-react";

import { cn } from "@/lib/utils";
import { neonVars, type NeonColor } from "@/lib/neon";

const variants = {
  solid:
    "bg-[rgb(var(--neon))] text-[#05060a] shadow-[0_0_0_1px_rgb(var(--neon)),0_0_18px_rgb(var(--neon)/0.45),inset_0_1px_0_rgb(255_255_255/0.45)] hover:shadow-[0_0_0_1px_rgb(var(--neon)),0_0_32px_rgb(var(--neon)/0.7),inset_0_1px_0_rgb(255_255_255/0.45)] hover:brightness-110",
  outline:
    "border border-[rgb(var(--neon)/0.7)] bg-[rgb(var(--neon)/0.06)] text-[rgb(var(--neon))] shadow-[0_0_12px_rgb(var(--neon)/0.25),inset_0_0_12px_rgb(var(--neon)/0.12)] [text-shadow:0_0_10px_rgb(var(--neon)/0.6)] hover:border-[rgb(var(--neon))] hover:bg-[rgb(var(--neon)/0.12)] hover:shadow-[0_0_24px_rgb(var(--neon)/0.5),inset_0_0_16px_rgb(var(--neon)/0.2)]",
  soft:
    "bg-[rgb(var(--neon)/0.12)] text-[rgb(var(--neon))] hover:bg-[rgb(var(--neon)/0.2)]",
  ghost:
    "text-white/70 hover:bg-[rgb(var(--neon)/0.1)] hover:text-[rgb(var(--neon))]",
  link: "h-auto px-0 text-[rgb(var(--neon))] underline-offset-4 hover:underline hover:[text-shadow:0_0_12px_rgb(var(--neon)/0.7)]",
} as const;

const sizes = {
  sm: "h-8 gap-1.5 rounded-md 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",
  icon: "size-10 rounded-lg",
} as const;

export interface ButtonProps
  extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "color"> {
  /** Palette name or any hex color. Inherits `--neon-accent` when omitted. */
  color?: NeonColor;
  variant?: keyof typeof variants;
  size?: keyof typeof sizes;
  /** Shows a spinner and disables the button. */
  loading?: boolean;
  /** Render the child element (e.g. a link) with button styles. */
  asChild?: boolean;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  (
    {
      className,
      style,
      color,
      variant = "outline",
      size = "md",
      loading = false,
      asChild = false,
      disabled,
      children,
      ...props
    },
    ref,
  ) => {
    const Comp = asChild ? Slot : "button";

    return (
      <Comp
        ref={ref}
        style={neonVars(color, style)}
        disabled={disabled || loading}
        aria-busy={loading || undefined}
        className={cn(
          "[--neon:var(--neon-accent,34_211_238)]",
          "relative inline-flex shrink-0 cursor-pointer select-none items-center justify-center whitespace-nowrap font-medium outline-none",
          "transition-[background-color,border-color,color,box-shadow,filter,transform] duration-200 ease-out active:scale-[0.97]",
          "focus-visible:ring-2 focus-visible:ring-[rgb(var(--neon)/0.6)] focus-visible:ring-offset-2 focus-visible:ring-offset-[#05060a]",
          "disabled:pointer-events-none disabled:opacity-50",
          "[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
          sizes[size],
          variants[variant],
          className,
        )}
        {...props}
      >
        {asChild ? (
          children
        ) : (
          <>
            {loading && <LoaderCircle className="animate-spin" aria-hidden />}
            {children}
          </>
        )}
      </Comp>
    );
  },
);
Button.displayName = "Button";

export { Button };
```
