Button Group

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

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 { motion } from 'framer-motion';
import type { ReactNode } from 'react';

// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------
//
// primary      Main CTA — label, optional href / onClick / icon
// secondary    Quiet companion action
// size         sm | md | lg
// alignment    left | center | right
// loading      Disable and show a refined spinner on primary
// className    Styles for the outer shell
//
type Action = {
  label: string;
  href?: string;
  onClick?: () => void;
  icon?: ReactNode;
};

type ButtonGroupProps = {
  primary?: Action;
  secondary?: Action;
  size?: 'sm' | 'md' | 'lg';
  alignment?: 'left' | 'center' | 'right';
  loading?: boolean;
  className?: string;
};

const SIZE = {
  sm: 'px-4 py-2 text-xs',
  md: 'px-5 py-2.5 text-sm',
  lg: 'px-6 py-3 text-base',
};

const ALIGN = {
  left: 'justify-start',
  center: 'justify-center',
  right: 'justify-end',
};

function ActionEl({
  action,
  className,
  children,
  disabled,
}: {
  action: Action;
  className: string;
  children: ReactNode;
  disabled?: boolean;
}) {
  if (action.href && !disabled) {
    return (
      <a href={action.href} className={className} onClick={action.onClick}>
        {children}
      </a>
    );
  }
  return (
    <button
      type='button'
      className={className}
      onClick={action.onClick}
      disabled={disabled}
    >
      {children}
    </button>
  );
}

export default function ButtonGroup({
  primary = { label: 'Start free', href: '#' },
  secondary = { label: 'View demo', href: '#' },
  size = 'md',
  alignment = 'left',
  loading = false,
  className = '',
}: ButtonGroupProps) {
  const pad = SIZE[size];

  return (
    <motion.div
      className={[
        'inline-flex flex-wrap items-center gap-3',
        ALIGN[alignment],
        className,
      ]
        .filter(Boolean)
        .join(' ')}
      initial={{ opacity: 0, y: 8 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
    >
      <motion.div
        whileHover={{ y: -1 }}
        whileTap={{ scale: 0.985 }}
        transition={{ type: 'spring', stiffness: 420, damping: 28 }}
      >
        <ActionEl
          action={primary}
          disabled={loading}
          className={[
            'relative inline-flex items-center justify-center gap-2 overflow-hidden rounded-full bg-gray-900 font-medium tracking-wide text-white shadow-[0_1px_0_rgba(255,255,255,0.12)_inset,0_8px_24px_rgba(0,0,0,0.18)] transition-[background-color,box-shadow] duration-300 hover:bg-gray-800 hover:shadow-[0_1px_0_rgba(255,255,255,0.16)_inset,0_12px_32px_rgba(0,0,0,0.22)] focus:outline-hidden focus-visible:ring-2 focus-visible:ring-gray-900/30 focus-visible:ring-offset-2 disabled:cursor-wait disabled:opacity-70',
            pad,
          ].join(' ')}
        >
          {loading ? (
            <span
              className='h-3.5 w-3.5 animate-spin rounded-full border border-white/30 border-t-white'
              aria-hidden
            />
          ) : (
            primary.icon
          )}
          {primary.label}
        </ActionEl>
      </motion.div>

      {secondary ? (
        <ActionEl
          action={secondary}
          className={[
            'group relative inline-flex items-center gap-1.5 font-medium tracking-wide text-gray-600 transition-colors hover:text-gray-900 focus:outline-hidden focus-visible:ring-2 focus-visible:ring-gray-900/20 focus-visible:ring-offset-2',
            pad.replace(/px-\d+(\.\d+)?/, 'px-1'),
          ].join(' ')}
        >
          {secondary.icon}
          <span className='relative'>
            {secondary.label}
            <span className='absolute -bottom-0.5 left-0 h-px w-full origin-left scale-x-0 bg-gray-900 transition-transform duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:scale-x-100' />
          </span>
        </ActionEl>
      ) : null}
    </motion.div>
  );
}

```

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
primaryActionMain CTA — label, optional href / onClick / icon
secondaryActionQuiet companion action
size'sm' | 'md' | 'lg'Control size
alignment'left' | 'center' | 'right'Horizontal alignment of the pair
loadingbooleanDisable and show a spinner on the primary action
classNamestringStyles for the outer shell

More Conversion Components

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