CTA Button

A prominent call-to-action button — the highest-emphasis action on a page. Full-width by default, taller hit area than the base Button, heavier label weight, and a slightly softer tap response so the press feels weightier. Picks the surface treatment (variant) and the semantic tint (intent) independently so the same component covers brand, success, danger, and quiet "maybe later" CTAs.

Customize
Style
solid
brand
Content
Continue

Installation

npx shadcn@latest add https://craftbits.dev/r/cta-button.json

Usage

import { CTAButton } from "@craft-bits/core";
 
<CTAButton onClick={onContinue}>Continue</CTAButton>
 
<CTAButton variant="outline" intent="brand">
  Learn more
</CTAButton>
 
<CTAButton variant="solid" intent="danger" fullWidth={false}>
  Delete account
</CTAButton>

Render through any element via Radix Slot:

<CTAButton asChild>
  <a href="/get-started">Get started</a>
</CTAButton>

Understanding the component

  1. Two-axis CVA recipe. ctaButtonVariants composes a base block (size, type, border, focus ring) and walks a compoundVariants matrix of variant × intent so the twelve cells are explicit. The prop signature is derived from the recipe via VariantProps<typeof ctaButtonVariants> — adding a new intent or variant updates the types automatically.
  2. Full-width by default. Unlike Button, CTAButton defaults to fullWidth={true} because the canonical use case is the dominant action slot at the bottom of a card, modal, or section. Set fullWidth={false} for inline CTAs that should hug their label.
  3. Heavier visual weight. Taller hit area (h-12, min-h-[2.75rem]) and font-bold label set this button apart from the lighter base Button so on a page with both, the eye reads CTAButton as the primary action.
  4. Motion on press. The default render is a motion.button. whileTap={TAP_SCALE} scales to 0.96 on press; transition={SPRINGS.snap} springs it back. When disabled is set, whileTap is dropped so disabled buttons feel inert.
  5. Focus ring on accent. Keyboard focus shows a 2px ring in var(--cb-accent) with a 2px offset against cb-bg — preserves contrast across both themes regardless of the chosen intent.
  6. asChild slot. When asChild is true, content is rendered into the parent via Radix Slot. The branch renders a static element (no motion gestures) because Slot cannot reliably forward motion's gesture handlers — useful for wrapping a <Link> or <a> while keeping CTAButton's styling.

Variants

// Surface treatment
<CTAButton variant="solid">Solid</CTAButton>
<CTAButton variant="outline">Outline</CTAButton>
<CTAButton variant="subtle">Subtle</CTAButton>
 
// Semantic intent
<CTAButton intent="brand">Brand</CTAButton>
<CTAButton intent="success">Success</CTAButton>
<CTAButton intent="danger">Danger</CTAButton>
<CTAButton intent="muted">Muted</CTAButton>
 
// Inline width (e.g. inside a horizontal toolbar)
<CTAButton fullWidth={false}>Inline CTA</CTAButton>
 
// With icons
<CTAButton icon={<PlusIcon />}>Add item</CTAButton>
<CTAButton iconRight={<ArrowRightIcon />}>Continue</CTAButton>

Props

PropTypeDefaultDescription
variant'solid' | 'outline' | 'subtle''solid'Surface treatment.
intent'brand' | 'success' | 'danger' | 'muted''brand'Semantic tint.
fullWidthbooleantrueStretch to the parent's width. Set false for inline CTAs.
asChildbooleanfalseRender content into the parent via Radix Slot. Disables motion gestures.
iconReactNodeLeading icon node. Wrapped with aria-hidden.
iconRightReactNodeTrailing icon node. Wrapped with aria-hidden.
disabledbooleanfalseDisables the button and drops the tap gesture.
classNamestringMerged onto the rendered element via cn().
...restHTMLMotionProps<'button'>Any other motion.button prop (onClick, aria-*, data-*, etc.).

Accessibility

  • Renders a real <button> by default — receives focus, fires on Enter / Space, participates in form submission.
  • A visible :focus-visible ring uses var(--cb-accent) for keyboard users with a 2px offset against cb-bg so it preserves contrast regardless of the chosen intent.
  • disabled sets the native disabled attribute (no click, removed from tab order) and drops the tap gesture so the button feels inert.
  • Icon slots are wrapped in an aria-hidden span — provide a visible label or aria-label so the button has an accessible name even when only an icon is rendered.
  • Minimum hit area: min-h-[2.75rem] (44px), meeting WCAG 2.5.8.
  • Motion respects prefers-reduced-motion: TAP_SCALE is a sub-300ms 0.04 spring via SPRINGS.snap. For zero motion, override transition to a step.

Credits

  • Extracted from: algoflashcards (src/platform/ui/CTAButton.tsx). The original wired in a project-specific playSound, a raw color override prop, and a hand-rolled bevel-tile-strong class. craft-bits generalizes the API to a two-axis CVA matrix (variant × intent), re-paints through the cb-* token system, drops the sound coupling, adds asChild + icon slots, and forwards a real ref — bringing it in line with the base Button.