bottega

primitives

Aurora Border Button

A pill-shaped button with an animated aurora gradient border. Two blobs of primary-derived gradient (purple → pink → orange, remapped through color-mix to --primary tones) orbit the pill edge via CSS keyframes: one sweeps left-to-right via a translate animation, the other pulses in scale — together simulating a sweeping chromatic rim glow. A static corner blob gives ambient warmth at rest. The inner pill surface is a near-opaque --background fill so text stays crisp against the glow. On hover the embedded star SVG (filled with a linearGradient that references the same aurora stop custom properties) spins 360° with a springy cubic-bezier and the label scales up. All motion is CSS keyframe-driven; the mount-gate + useReducedMotion contract suppresses every animation before hydration and when the user prefers reduced motion, writing data-animate="false" so no layout/structure shifts occur.

Ported from Itskrish01 (MIT)


Install

1. Register the namespace (once per project):

json
// components.json — register the @bottega namespace once
{
  "registries": {
    "@bottega": { "url": "https://bottega.ariacode.ca/r/{name}.json" }
  }
}

2. Add the component:

bash
npx shadcn add @bottega/aurora-border-button

Usage

Usagetsx
<AuroraBorderButton />

Props

Standard props — see source.


Source

aurora-border-button.tsxtsx
"use client";

import { forwardRef, useEffect, useState } from "react";
import type React from "react";
import { useReducedMotion } from "motion/react";
import styles from "./aurora-border-button.module.css";

export type AuroraBorderButtonProps = {
  /** Button label or content. */
  children?: React.ReactNode;
  /** Extra class names merged onto the root button. */
  className?: string;
  /** Forwarded to <button type>. */
  type?: "button" | "submit" | "reset";
  /** Disables the button and suppresses animations. */
  disabled?: boolean;
  /** Click handler. */
  onClick?: React.MouseEventHandler<HTMLButtonElement>;
} & Omit<
  React.ButtonHTMLAttributes<HTMLButtonElement>,
  "type" | "disabled" | "onClick" | "children"
>;

/**
 * A pill-shaped button with a sweeping aurora gradient border glow.
 * Two animating blobs of primary-tinted gradient orbit the pill edge — one
 * translated horizontally, one scaled — creating a living chromatic rim.
 * On hover the embedded star icon spins 360° and scales up.
 * The inner surface is a near-opaque background swatch that keeps text crisp
 * against the glow. All motion is CSS keyframe-driven; no JS per-frame work.
 */
export const AuroraBorderButton = forwardRef<
  HTMLButtonElement,
  AuroraBorderButtonProps
>(function AuroraBorderButton(
  { children, className = "", type = "button", disabled, onClick, ...props },
  ref,
) {
  // Mount-gate + null-safe reduced-motion check (matches the golden contract).
  const [mounted, setMounted] = useState(false);
  useEffect(() => {
    setMounted(true);
  }, []);
  const prefersReducedMotion = useReducedMotion();
  const reduce = prefersReducedMotion === null || prefersReducedMotion;
  const animate = mounted && !reduce;

  return (
    <button
      ref={ref}
      type={type}
      disabled={disabled}
      onClick={onClick}
      data-animate={animate ? "true" : "false"}
      data-disabled={disabled ? "true" : undefined}
      className={[styles.root, className].filter(Boolean).join(" ")}
      {...props}
    >
      {/* Static corner glow — always visible, no animation */}
      <span aria-hidden="true" className={styles.cornerGlow} />

      {/* Sweeping border blob — translates horizontally */}
      <span aria-hidden="true" className={styles.borderBlobWrap}>
        <span aria-hidden="true" className={styles.borderBlob} />
      </span>

      {/* Inner pill surface + content */}
      <span className={styles.inner}>
        {/* Star icon that spins on hover */}
        <span aria-hidden="true" className={styles.iconWrap}>
          <svg
            width="18"
            height="18"
            viewBox="0 0 24 24"
            fill="none"
            xmlns="http://www.w3.org/2000/svg"
            className={styles.starSvg}
          >
            <path
              d="M11.5268 2.29489C11.5706 2.20635 11.6383 2.13183 11.7223 2.07972C11.8062 2.02761 11.903 2 12.0018 2C12.1006 2 12.1974 2.02761 12.2813 2.07972C12.3653 2.13183 12.433 2.20635 12.4768 2.29489L14.7868 6.97389C14.939 7.28186 15.1636 7.5483 15.4414 7.75035C15.7192 7.95239 16.0419 8.08401 16.3818 8.13389L21.5478 8.88989C21.6457 8.90408 21.7376 8.94537 21.8133 9.00909C21.8889 9.07282 21.9452 9.15644 21.9758 9.2505C22.0064 9.34456 22.0101 9.4453 21.9864 9.54133C21.9627 9.63736 21.9126 9.72485 21.8418 9.79389L18.1058 13.4319C17.8594 13.672 17.6751 13.9684 17.5686 14.2955C17.4622 14.6227 17.4369 14.9708 17.4948 15.3099L18.3768 20.4499C18.3941 20.5477 18.3835 20.6485 18.3463 20.7406C18.3091 20.8327 18.2467 20.9125 18.1663 20.9709C18.086 21.0293 17.9908 21.0639 17.8917 21.0708C17.7926 21.0777 17.6935 21.0566 17.6058 21.0099L12.9878 18.5819C12.6835 18.4221 12.345 18.3386 12.0013 18.3386C11.6576 18.3386 11.3191 18.4221 11.0148 18.5819L6.3978 21.0099C6.31013 21.0563 6.2112 21.0772 6.11225 21.0701C6.0133 21.0631 5.91832 21.0285 5.83809 20.9701C5.75787 20.9118 5.69563 20.8321 5.65846 20.7401C5.62128 20.6482 5.61066 20.5476 5.6278 20.4499L6.5088 15.3109C6.567 14.9716 6.54178 14.6233 6.43534 14.2959C6.32889 13.9686 6.14441 13.672 5.8978 13.4319L2.1618 9.79489C2.09039 9.72593 2.03979 9.63829 2.01576 9.54197C1.99173 9.44565 1.99524 9.34451 2.02588 9.25008C2.05652 9.15566 2.11307 9.07174 2.18908 9.00788C2.26509 8.94402 2.3575 8.90279 2.4558 8.88889L7.6208 8.13389C7.96106 8.08439 8.28419 7.95295 8.56238 7.75088C8.84058 7.54881 9.0655 7.28216 9.2178 6.97389L11.5268 2.29489Z"
              fill="url(#aurora-star-fill)"
              stroke="url(#aurora-star-stroke)"
              strokeLinecap="round"
              strokeLinejoin="round"
            />
            <defs>
              <linearGradient
                id="aurora-star-fill"
                x1="-0.5"
                y1="9"
                x2="15.5"
                y2="-1.5"
                gradientUnits="userSpaceOnUse"
              >
                <stop stopColor="var(--aurora-stop-a)" />
                <stop offset="0.575" stopColor="var(--aurora-stop-b)" />
                <stop offset="1" stopColor="var(--aurora-stop-c)" />
              </linearGradient>
              <linearGradient
                id="aurora-star-stroke"
                x1="-0.5"
                y1="9"
                x2="15.5"
                y2="-1.5"
                gradientUnits="userSpaceOnUse"
              >
                <stop stopColor="var(--aurora-stop-a)" />
                <stop offset="0.575" stopColor="var(--aurora-stop-b)" />
                <stop offset="1" stopColor="var(--aurora-stop-c)" />
              </linearGradient>
            </defs>
          </svg>
          {/* Star halo — decorative blur bloom behind the icon */}
          <span aria-hidden="true" className={styles.starHalo} />
        </span>

        {/* Label */}
        <span className={styles.label}>
          {children ?? "Star on Github"}
        </span>
      </span>
    </button>
  );
});

AuroraBorderButton.displayName = "AuroraBorderButton";

Dependencies

  • motion
  • @bottega/tokens