# Star Button

> Like/star toggle with a spring-scaled star, particle burst and live count.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/star-button
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/star-button.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/star-button-demo.tsx"
"use client"

import * as React from "react"

import { StarButton } from "@/components/star-button"

export default function StarButtonDemo() {
  const [last, setLast] = React.useState<string>("unstarred")

  return (
    <div className="flex flex-col items-center gap-4 p-8">
      <div className="flex flex-wrap items-center justify-center gap-4">
        <StarButton onPressedChange={(pressed) => setLast(pressed ? "starred" : "unstarred")} />
        <StarButton defaultPressed count={2048} />
      </div>
      <p className="text-xs text-muted-foreground">First button: {last}</p>
    </div>
  )
}
```

## Source

```tsx title="components/star-button.tsx"
"use client"

import * as React from "react"
import { Star } from "lucide-react"

import { cn } from "@/lib/utils"

const css = `@keyframes star-button-burst{0%{transform:rotate(var(--a)) translateY(0) scale(1);opacity:1}100%{transform:rotate(var(--a)) translateY(-26px) scale(0);opacity:0}}@keyframes star-button-pop{0%{transform:scale(1)}40%{transform:scale(1.45)}100%{transform:scale(1)}}@media (prefers-reduced-motion:reduce){.star-button-particle,.star-button-pop{animation:none!important}}`

const PARTICLES = 8

export interface StarButtonProps extends Omit<React.ComponentProps<"button">, "onChange"> {
  defaultPressed?: boolean
  onPressedChange?: (pressed: boolean) => void
  /** Base count shown next to the star */
  count?: number
}

export function StarButton({
  defaultPressed = false,
  onPressedChange,
  count = 128,
  className,
  onClick,
  ...props
}: StarButtonProps) {
  const [pressed, setPressed] = React.useState(defaultPressed)
  const [burst, setBurst] = React.useState(0)

  return (
    <>
      <style href="star-button" precedence="default">
        {css}
      </style>
      <button
        type="button"
        aria-pressed={pressed}
        className={cn(
          "group relative inline-flex h-10 items-center gap-2 rounded-full border border-border bg-background px-4 text-sm font-medium text-foreground",
          "transition-colors duration-200 hover:bg-muted",
          pressed && "border-amber-400/60 bg-amber-400/10",
          "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:outline-none",
          "disabled:pointer-events-none disabled:opacity-50",
          className
        )}
        onClick={(event) => {
          const next = !pressed
          setPressed(next)
          if (next) setBurst((value) => value + 1)
          onPressedChange?.(next)
          onClick?.(event)
        }}
        {...props}
      >
        <span className="relative inline-flex size-5 items-center justify-center">
          {pressed &&
            Array.from({ length: PARTICLES }, (_, index) => (
              <span
                key={`${burst}-${index}`}
                aria-hidden
                className="star-button-particle pointer-events-none absolute top-1/2 left-1/2 -mt-[2px] -ml-[2px] size-1 rounded-full bg-amber-400"
                style={{
                  ["--a" as string]: `${(360 / PARTICLES) * index}deg`,
                  animation: "star-button-burst 500ms ease-out forwards",
                }}
              />
            ))}
          <Star
            key={pressed ? `on-${burst}` : "off"}
            className={cn(
              "star-button-pop size-5 transition-colors duration-200",
              pressed ? "fill-amber-400 text-amber-400" : "text-muted-foreground group-hover:text-foreground"
            )}
            style={pressed ? { animation: "star-button-pop 450ms cubic-bezier(0.34,1.56,0.64,1)" } : undefined}
          />
        </span>
        <span className="tabular-nums">{pressed ? count + 1 : count}</span>
      </button>
    </>
  )
}
```
