Tawny
All components

Effects

Border & Counters

BorderBeam, AnimatedGradientText, and NumberTicker in one panel.

components/ui/effects
New components

0 designs

Headers, motion, and map primitives — rendered live, not screenshots.

Source

Copy the files below into your project.

packages/ui/src/components/border-beam.tsx
import { cn } from '../lib/utils'

interface BorderBeamProps {
  className?: string
  size?: number
  duration?: number
  delay?: number
  colorFrom?: string
  colorTo?: string
  borderWidth?: number
}

/**
 * Animated conic-gradient border beam. Mount inside a `relative overflow-hidden rounded-*` container.
 */
export function BorderBeam({
  className,
  size = 200,
  duration = 10,
  delay = 0,
  colorFrom = 'rgba(255,255,255,0.6)',
  colorTo = 'transparent',
  borderWidth = 1,
}: BorderBeamProps) {
  return (
    <div
      aria-hidden="true"
      className={cn('pointer-events-none absolute inset-0 rounded-[inherit]', className)}
      style={
        {
          '--size': `${size}px`,
          '--duration': `${duration}s`,
          '--delay': `${delay}s`,
          '--color-from': colorFrom,
          '--color-to': colorTo,
          '--border-width': `${borderWidth}px`,
        } as React.CSSProperties
      }
    >
      {/* The beam travels around the border via a mask + background trick */}
      <div
        className="absolute inset-0 rounded-[inherit]"
        style={{
          padding: borderWidth,
          background: `conic-gradient(from calc(var(--angle, 0deg)), ${colorTo}, ${colorTo} 40%, ${colorFrom} 60%, ${colorTo})`,
          WebkitMask: `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
          WebkitMaskComposite: 'xor',
          maskComposite: 'exclude',
          animation: `border-beam-rotate var(--duration) linear calc(var(--delay) * -1) infinite`,
        }}
      />
      <style>{`
        @keyframes border-beam-rotate {
          from { --angle: 0deg; }
          to   { --angle: 360deg; }
        }
        @property --angle {
          syntax: '<angle>';
          initial-value: 0deg;
          inherits: false;
        }
      `}</style>
    </div>
  )
}
packages/ui/src/components/animated-gradient-text.tsx
import { cn } from '../lib/utils'
import type { ReactNode } from 'react'

interface AnimatedGradientTextProps {
  children: ReactNode
  className?: string
}

/**
 * Pill badge with an animated conic-gradient border and shimmer fill.
 */
export function AnimatedGradientText({ children, className }: AnimatedGradientTextProps) {
  return (
    <div
      className={cn(
        'group relative mx-auto flex max-w-fit cursor-default items-center justify-center',
        'rounded-full px-4 py-1.5 text-sm',
        'bg-background/80 backdrop-blur-sm',
        'shadow-[inset_0_-6px_10px_#ffffff1f]',
        'transition-shadow duration-500 ease-out',
        'hover:shadow-[inset_0_-6px_10px_#ffffff3f]',
        // animated border via background-clip trick
        'before:absolute before:inset-0 before:-z-10 before:rounded-full',
        'before:bg-[conic-gradient(from_var(--border-angle),transparent_25%,rgba(255,255,255,0.18)_50%,transparent_75%)]',
        '[--border-angle:0turn]',
        'before:animate-[border-beam_4s_linear_infinite]',
        className
      )}
    >
      <span
        className={cn(
          'inline-flex items-center gap-1.5',
          'bg-gradient-to-r from-white/90 via-white to-white/60 bg-clip-text text-transparent'
        )}
      >
        {children}
      </span>
    </div>
  )
}
packages/ui/src/components/number-ticker.tsx
'use client'

import { useEffect, useRef } from 'react'
import { useInView, useMotionValue, useSpring, animate } from 'motion/react'
import { cn } from '../lib/utils'

interface NumberTickerProps {
  value: number
  /** Prefix string, e.g. "$" */
  prefix?: string
  /** Suffix string, e.g. "+" or "k+" */
  suffix?: string
  className?: string
  delay?: number
  decimalPlaces?: number
}

export function NumberTicker({
  value,
  prefix = '',
  suffix = '',
  className,
  delay = 0,
  decimalPlaces = 0,
}: NumberTickerProps) {
  const ref = useRef<HTMLSpanElement>(null)
  const motionValue = useMotionValue(0)
  const springValue = useSpring(motionValue, { damping: 60, stiffness: 100 })
  const isInView = useInView(ref, { once: true, margin: '0px' })

  useEffect(() => {
    if (!isInView) return
    const timer = setTimeout(() => {
      animate(motionValue, value, { duration: 1.2, ease: 'easeOut' })
    }, delay * 1000)
    return () => clearTimeout(timer)
  }, [isInView, value, delay, motionValue])

  useEffect(() => {
    return springValue.on('change', (latest) => {
      if (ref.current) {
        ref.current.textContent =
          prefix +
          Intl.NumberFormat('en-US', {
            minimumFractionDigits: decimalPlaces,
            maximumFractionDigits: decimalPlaces,
          }).format(Number(latest.toFixed(decimalPlaces))) +
          suffix
      }
    })
  }, [springValue, prefix, suffix, decimalPlaces])

  return (
    <span
      ref={ref}
      className={cn('inline-block tabular-nums tracking-tight', className)}
    >
      {prefix}0{suffix}
    </span>
  )
}