shadimage
ScrollX UI
uianimated-buttonsource: ok · hit

Animated Button

AnimatedButton is a highly customizable animated button component with shimmer effects, glow, and visual styling options.

npx shadcn add "https://www.scrollxui.dev/registry/animated-button.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
ui-primitives@radix-ui/react-slot

Files2 · source cached 10/8/2026, 8:13:52 PM

components/ui/animated-button.tsx
'use client';

import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';

interface CustomCSSProperties extends React.CSSProperties {
  '--shimmer-color'?: string;
  '--radius'?: string;
  '--speed'?: string;
  '--cut'?: string;
  '--bg'?: string;
  '--spread'?: string;
  '--border-width'?: string;
  '--border-segment'?: string;
}

const buttonVariants = cva(
  'group relative z-0 bg-white dark:bg-[rgba(0,0,0,1)] flex cursor-pointer items-center justify-center overflow-hidden whitespace-nowrap  transform-gpu transition-all duration-300 ease-in-out active:translate-y-px',
  {
    variants: {
      variant: {
        default: 'text-cyan-400 border border-cyan-400/20 hover:text-cyan-300',
        outline:
          'bg-transparent text-cyan-400 border border-cyan-400 hover:text-cyan-300',
        ghost: 'bg-transparent text-cyan-400 hover:bg-cyan-950/30',
        glow: 'text-cyan-400 border border-cyan-400/30 hover:text-cyan-300 hover:shadow-glow',
      },
      size: {
        default: 'h-10 px-6 py-2',
        sm: 'h-8 px-4 py-1 text-xs',
        lg: 'h-12 px-8 py-3 text-base',
        icon: 'h-10 w-10',
      },
      glow: {
        true: 'hover:shadow-[0_0_5px_#03e9f4,0_0_25px_#03e9f4]',
        false: '',
      },
      textEffect: {
        normal: 'group-hover:tracking-normal',
        spread: 'group-hover:tracking-wider',
      },
      uppercase: {
        true: '',
        false: '',
      },
      rounded: {
        default: 'rounded-md',
        full: 'rounded-full',
        none: 'rounded-none',
        custom: 'rounded-[0.95rem]',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
      glow: false,
      textEffect: 'normal',
      uppercase: true,
      rounded: 'custom',
    },
  },
);

export interface AnimatedButtonProps
  extends
    React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean;
  hideAnimations?: boolean;
  shimmerColor?: string;
  shimmerSize?: string;
  borderRadius?: string;
  shimmerDuration?: string;
  background?: string;
  style?: CustomCSSProperties;
}

const AnimatedButton = React.forwardRef<HTMLButtonElement, AnimatedButtonProps>(
  (
    {
      className,
      variant,
      size,
      glow,
      textEffect,
      uppercase,
      rounded,
      asChild = false,
      hideAnimations = false,
      shimmerColor = '#03e9f4',
      shimmerSize = '0.05em',
      shimmerDuration = '3s',
      borderRadius = '100px',
      background = 'rgba(0, 0, 0, 1)',
      style,
      children,
      ...props
    },
    ref,
  ) => {
    const Comp = asChild ? Slot : 'button';

    const combinedStyle: CustomCSSProperties = {
      ...style,
      '--shimmer-color': shimmerColor,
      '--radius': borderRadius,
      '--speed': shimmerDuration,
      '--cut': shimmerSize,
      '--bg': background,
      '--spread': '90deg',
      borderRadius: rounded === 'custom' ? borderRadius : undefined,
    };

    const buttonStyle = `
      @keyframes animatedButton-shimmer-slide {
        to {
          transform: translate(calc(100cqw - 100%), 0);
        }
      }
      
      @keyframes animatedButton-spin-around {
        0% {
          transform: translateZ(0) rotate(0);
        }
        15%, 35% {
          transform: translateZ(0) rotate(90deg);
        }
        65%, 85% {
          transform: translateZ(0) rotate(270deg);
        }
        100% {
          transform: translateZ(0) rotate(360deg);
        }
      }
      
      @keyframes animatedButton-spread {
        0% {
          letter-spacing: normal;
          transform: perspective(var(--radius)) rotateY(0deg);
        }
        50% {
          letter-spacing: var(--cut);
          transform: perspective(var(--radius)) rotateY(var(--spread));
        }
        100% {
          letter-spacing: normal;
          transform: perspective(var(--radius)) rotateY(0deg);
        }
      }
      
      .animate-shimmer-slide-scoped {
        animation: animatedButton-shimmer-slide var(--speed) ease-in-out infinite alternate;
      }
      
      .animate-spin-around-scoped {
        animation: animatedButton-spin-around calc(var(--speed) * 2) infinite linear;
      }
      
      .has-animate-spread > span {
        animation: animatedButton-spread calc(var(--speed) * 3) ease-in-out infinite;
      }
      
      .shadow-glow-scoped {
        box-shadow: 0 0 5px var(--shimmer-color),
                    0 0 25px var(--shimmer-color),
                    0 0 50px var(--shimmer-color);
      }

      /* Mobile optimization scoped to this component */
      @media (max-width: 768px) {
        .animated-button-mobile {
          --radius: 60px;
          --speed: 2.5s;
          --cut: 0.03em;
        }
      }
    `;

    return (
      <Comp
        className={cn(
          'animated-button animated-button-mobile',
          buttonVariants({
            variant,
            size,
            glow,
            textEffect,
            uppercase,
            rounded,
            className,
          }),
          glow && 'shadow-glow-scoped',
        )}
        style={combinedStyle}
        ref={ref}
        {...props}
      >
        <style jsx>{buttonStyle}</style>

        {!hideAnimations && (
          <div className='absolute inset-0 overflow-visible -z-30 blur-[2px] @container-[size]'>
            <div className='absolute inset-0 h-[100cqh] animate-shimmer-slide-scoped aspect-[1]'>
              <div className='absolute -inset-full w-auto rotate-0 animate-spin-around-scoped [background:conic-gradient(from_calc(270deg-(var(--spread)*0.5)),transparent_0,var(--shimmer-color)_var(--spread),transparent_var(--spread))]' />
            </div>
          </div>
        )}

        <div className='absolute size-full rounded-2xl px-4 py-1.5 text-sm font-medium' />

        <div
          className='absolute -z-20 [background:var(--bg)]'
          style={{ inset: shimmerSize, borderRadius }}
        />

        <span
          className={cn(
            'relative z-10 transition-all duration-300 flex items-center justify-center',
            textEffect === 'spread' && 'group-hover:tracking-wider',
          )}
        >
          {children}
        </span>
      </Comp>
    );
  },
);

AnimatedButton.displayName = 'AnimatedButton';

export { AnimatedButton, buttonVariants };
lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

More from ScrollX UI

Browse all components in this library →