niceui

Animated CTA Buttons

Six attention-grabbing call-to-action buttons for hero sections, pricing pages and launch banners.

ReactTailwind CSSTypeScriptComponentsMIT · Free
Live preview

Installation

01

Install with the shadcn CLI. The NiceUI theme comes along the first time.

$ npx shadcn@latest add https://niceui.dev/r/components-cta-buttons.json
02

Or copy the source below. Dependencies: lucide-react.

Source

components/niceui/ui/core/CtaButtons.tsx
import { useRef, useState, type PointerEvent, type ReactNode } from 'react';
import { ArrowRight, Check, Loader2, Sparkles, Zap } from 'lucide-react';
import { cn } from '@/lib/cn';

type CtaProps = { children: ReactNode; onClick?: () => void; className?: string };

export function ShimmerBorderButton({ children, onClick, className }: CtaProps) {
  return (
    <button
      onClick={onClick}
      className={cn(
        'focus-ring group relative inline-flex h-11 items-center overflow-hidden rounded-full p-px transition-transform duration-200 active:scale-[0.97]',
        className,
      )}
    >
      <span
        aria-hidden
        className="absolute left-1/2 top-1/2 aspect-square w-[250%] -translate-x-1/2 -translate-y-1/2 animate-spin-slow bg-[conic-gradient(from_0deg,transparent_0%,transparent_65%,#7dd3fc_80%,#f7f7f8_86%,transparent_92%)] [animation-duration:3s]"
      />
      <span className="relative inline-flex h-full items-center gap-2 rounded-full bg-ink-950 px-5 text-sm font-medium text-ink-50 transition-colors group-hover:bg-ink-900">
        {children}
      </span>
    </button>
  );
}

export function GradientGlowButton({ children, onClick, className }: CtaProps) {
  return (
    <button onClick={onClick} className={cn('focus-ring group relative inline-flex rounded-xl active:scale-[0.97] transition-transform', className)}>
      <span
        aria-hidden
        className="absolute -inset-1 rounded-2xl bg-gradient-to-r from-brand-500 via-sky-400 to-teal-400 opacity-40 blur-lg transition-all duration-500 ease-smooth group-hover:-inset-2 group-hover:opacity-80"
      />
      <span className="relative inline-flex h-11 items-center gap-2 rounded-xl bg-gradient-to-r from-brand-600 via-brand-500 to-sky-500 px-5 text-sm font-medium text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.25)]">
        {children}
      </span>
    </button>
  );
}

export function ArrowSlideButton({ children, onClick, className }: CtaProps) {
  return (
    <button
      onClick={onClick}
      className={cn(
        'focus-ring group inline-flex h-11 items-center gap-3 rounded-full bg-ink-50 pl-5 pr-1.5 text-sm font-medium text-ink-950 transition-all duration-300 ease-smooth hover:bg-white hover:pr-2 active:scale-[0.97]',
        className,
      )}
    >
      {children}
      <span className="relative flex h-8 w-8 items-center justify-center overflow-hidden rounded-full bg-ink-950 text-ink-50">
        <ArrowRight className="h-4 w-4 transition-transform duration-500 ease-smooth group-hover:translate-x-8" />
        <ArrowRight className="absolute h-4 w-4 -translate-x-8 transition-transform duration-500 ease-smooth group-hover:translate-x-0" />
      </span>
    </button>
  );
}

export function MagneticButton({ children, onClick, className, strength = 0.35 }: CtaProps & { strength?: number }) {
  const ref = useRef<HTMLButtonElement>(null);
  const [pos, setPos] = useState({ x: 0, y: 0, active: false });

  const onMove = (e: PointerEvent<HTMLButtonElement>) => {
    const r = ref.current!.getBoundingClientRect();
    setPos({ x: (e.clientX - r.left - r.width / 2) * strength, y: (e.clientY - r.top - r.height / 2) * strength, active: true });
  };

  return (
    <button
      ref={ref}
      onClick={onClick}
      onPointerMove={onMove}
      onPointerLeave={() => setPos({ x: 0, y: 0, active: false })}
      style={{ transform: `translate3d(${pos.x}px, ${pos.y}px, 0)` }}
      className={cn(
        'focus-ring inline-flex h-11 items-center gap-2 rounded-xl px-5 text-sm font-medium text-ink-50 shadow-hairline',
        pos.active ? 'bg-ink-700 transition-[transform,background-color] duration-150 ease-out' : 'bg-ink-800 transition-transform duration-700 ease-spring',
        className,
      )}
    >
      <span className="inline-flex items-center gap-2" style={{ transform: `translate3d(${pos.x * 0.4}px, ${pos.y * 0.4}px, 0)` }}>
        {children}
      </span>
    </button>
  );
}

export function ShineSweepButton({ children, onClick, className }: CtaProps) {
  return (
    <button
      onClick={onClick}
      className={cn(
        'focus-ring group relative inline-flex h-11 items-center gap-2 overflow-hidden rounded-xl bg-amber-400 px-5 text-sm font-medium text-ink-950 shadow-[inset_0_1px_0_rgba(255,255,255,0.4),0_6px_20px_-6px_rgba(251,191,36,0.6)] transition-transform active:scale-[0.97]',
        className,
      )}
    >
      <span
        aria-hidden
        className="absolute inset-y-0 -left-1/2 w-1/3 -skew-x-12 bg-gradient-to-r from-transparent via-white/70 to-transparent transition-transform duration-700 ease-smooth group-hover:translate-x-[420%]"
      />
      <span className="relative inline-flex items-center gap-2">{children}</span>
    </button>
  );
}

export function SuccessMorphButton({ children, className }: Omit<CtaProps, 'onClick'>) {
  const [state, setState] = useState<'idle' | 'loading' | 'success'>('idle');

  const run = () => {
    if (state !== 'idle') return;
    setState('loading');
    window.setTimeout(() => setState('success'), 1400);
    window.setTimeout(() => setState('idle'), 3200);
  };

  return (
    <button
      onClick={run}
      aria-live="polite"
      className={cn(
        'focus-ring relative inline-flex h-11 items-center justify-center overflow-hidden text-sm font-medium text-white transition-all duration-500 ease-spring active:scale-[0.97]',
        state === 'idle' && 'w-40 rounded-xl bg-brand-600 hover:bg-brand-500',
        state === 'loading' && 'w-11 rounded-[22px] bg-brand-600',
        state === 'success' && 'w-11 rounded-[22px] bg-emerald-500 shadow-[0_0_0_6px_rgba(16,185,129,0.15)]',
        className,
      )}
    >
      <span className={cn('absolute whitespace-nowrap transition-all duration-300', state === 'idle' ? 'opacity-100' : 'scale-75 opacity-0')}>
        {children}
      </span>
      <Loader2 className={cn('absolute h-4 w-4 animate-spin transition-opacity duration-200', state === 'loading' ? 'opacity-100' : 'opacity-0')} />
      <Check
        className={cn('absolute h-5 w-5 transition-all duration-500 ease-spring', state === 'success' ? 'scale-100 opacity-100' : 'scale-0 opacity-0')}
        strokeWidth={2.5}
      />
    </button>
  );
}

function Cell({ label, children, i }: { label: string; children: ReactNode; i: number }) {
  return (
    <div
      className="flex animate-fade-up flex-col items-center justify-center gap-4 rounded-2xl border border-white/5 bg-ink-950/60 px-4 py-7"
      style={{ animationDelay: `${i * 60}ms` }}
    >
      <div className="flex h-12 items-center">{children}</div>
      <span className="text-xs text-ink-400">{label}</span>
    </div>
  );
}

export function CtaButtonsDemo() {
  return (
    <div className="mx-auto grid w-full max-w-2xl grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
      <Cell i={0} label="Shimmer border">
        <ShimmerBorderButton>
          <Sparkles className="h-4 w-4 text-brand-300" /> Try Nice Pro
        </ShimmerBorderButton>
      </Cell>
      <Cell i={1} label="Gradient glow">
        <GradientGlowButton>
          <Zap className="h-4 w-4" /> Upgrade now
        </GradientGlowButton>
      </Cell>
      <Cell i={2} label="Arrow slide">
        <ArrowSlideButton>Get started</ArrowSlideButton>
      </Cell>
      <Cell i={3} label="Magnetic hover">
        <MagneticButton>Book a demo</MagneticButton>
      </Cell>
      <Cell i={4} label="Shine sweep">
        <ShineSweepButton>Claim 40% off</ShineSweepButton>
      </Cell>
      <Cell i={5} label="Success morph (click)">
        <SuccessMorphButton>Save changes</SuccessMorphButton>
      </Cell>
    </div>
  );
}