Promo Pill

Translucent offer marker with a low-frequency glow pulse and optional dismiss.

Add to your project

Pick the way you like to build.

Let your AI assistant add it

Copy this prompt and paste it into the AI tool building your site.

# Integrate this UI component

Add this component to my project.

1. Install any missing dependencies.
2. Make sure Tailwind CSS is set up and scans the new file.
3. Prefer passing props over editing the component source — changing the component itself may break it.
4. Adjust colors, size, and layout via props or wrapper classes to match my project. You know my codebase better than this snippet does.

## Component source

```tsx
'use client';

import { AnimatePresence, motion } from 'framer-motion';
import { useState, type ReactNode } from 'react';

// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------
//
// label         Pill copy
// icon          Optional leading mark (node or emoji string)
// tone          Color mood: neutral | accent | success
// href          Optional link — renders as <a>
// pulse         Soft luminous pulse behind the pill
// dismissible   Show a quiet dismiss control
// onDismiss     Called after dismiss animation
// className     Styles for the outer shell
//
type PromoPillProps = {
  label?: string;
  icon?: ReactNode;
  tone?: 'neutral' | 'accent' | 'success';
  href?: string;
  pulse?: boolean;
  dismissible?: boolean;
  onDismiss?: () => void;
  className?: string;
};

const TONE = {
  neutral: {
    shell: 'border-gray-200/80 bg-white/70 text-gray-800',
    dot: 'bg-gray-400',
    glow: 'rgba(0,0,0,0.06)',
  },
  accent: {
    shell: 'border-rose-200/70 bg-rose-50/70 text-rose-900',
    dot: 'bg-rose-400',
    glow: 'rgba(251,113,133,0.28)',
  },
  success: {
    shell: 'border-emerald-200/70 bg-emerald-50/70 text-emerald-900',
    dot: 'bg-emerald-400',
    glow: 'rgba(52,211,153,0.28)',
  },
};

export default function PromoPill({
  label = 'Early access — 20% off',
  icon,
  tone = 'accent',
  href,
  pulse = true,
  dismissible = false,
  onDismiss,
  className = '',
}: PromoPillProps) {
  const [visible, setVisible] = useState(true);
  const t = TONE[tone];

  const inner = (
    <>
      {pulse ? (
        <motion.span
          className='pointer-events-none absolute inset-0 -z-10 rounded-full'
          style={{ background: t.glow }}
          animate={{ opacity: [0.35, 0.7, 0.35], scale: [1, 1.04, 1] }}
          transition={{ duration: 2.8, repeat: Infinity, ease: 'easeInOut' }}
          aria-hidden
        />
      ) : null}

      <span
        className={['h-1.5 w-1.5 shrink-0 rounded-full', t.dot].join(' ')}
        aria-hidden
      />

      {icon ? (
        <span className='shrink-0 text-[13px] leading-none' aria-hidden>
          {icon}
        </span>
      ) : null}

      <span className='text-xs font-medium tracking-wide'>{label}</span>

      {href ? (
        <motion.span
          className='text-xs opacity-60'
          aria-hidden
          whileHover={{ x: 2 }}
        >
          →
        </motion.span>
      ) : null}
    </>
  );

  const sharedClass = [
    'group relative inline-flex items-center gap-2 rounded-full border px-3.5 py-1.5 shadow-[0_1px_2px_rgba(0,0,0,0.04)] backdrop-blur-md transition-colors',
    t.shell,
    className,
  ]
    .filter(Boolean)
    .join(' ');

  return (
    <AnimatePresence>
      {visible ? (
        <motion.div
          className='inline-flex items-center gap-1.5'
          initial={{ opacity: 0, y: 6, filter: 'blur(4px)' }}
          animate={{ opacity: 1, y: 0, filter: 'blur(0px)' }}
          exit={{ opacity: 0, scale: 0.96, filter: 'blur(4px)' }}
          transition={{ duration: 0.4, ease: [0.22, 1, 0.36, 1] }}
        >
          {href ? (
            <a href={href} className={sharedClass}>
              {inner}
            </a>
          ) : (
            <div className={sharedClass}>{inner}</div>
          )}

          {dismissible ? (
            <button
              type='button'
              aria-label='Dismiss'
              onClick={() => {
                setVisible(false);
                window.setTimeout(() => onDismiss?.(), 280);
              }}
              className='inline-flex h-6 w-6 items-center justify-center rounded-full text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-700'
            >
              <svg viewBox='0 0 12 12' className='h-3 w-3' aria-hidden>
                <path
                  d='M3 3l6 6M9 3L3 9'
                  stroke='currentColor'
                  strokeWidth='1.4'
                  strokeLinecap='round'
                />
              </svg>
            </button>
          ) : null}
        </motion.div>
      ) : null}
    </AnimatePresence>
  );
}

```

Works with your AI tools

ClaudeClaude*
CodexCodex*
LovableLovable
Base44Base44
Figma MakeFigma Make
ReplitReplit*
CursorCursor*
v0v0

* Compatible with most projects that use the default React/Next.js framework.

Built With

ReactTailwind CSSFramer Motion

Props

PropTypeDescription
labelstringPill copy
iconReactNodeOptional leading mark (node or emoji string)
tone'neutral' | 'accent' | 'success'Color mood
hrefstringOptional link — renders as an anchor
pulsebooleanSoft luminous pulse behind the pill
dismissiblebooleanShow a quiet dismiss control
onDismiss() => voidCalled after the dismiss animation
classNamestringStyles for the outer shell

More Conversion Components

Primary and secondary actions as one unit — depth on hover, underline reveal on the quiet CTA.