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.jsonUsage
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
- Two-axis CVA recipe.
ctaButtonVariantscomposes a base block (size, type, border, focus ring) and walks acompoundVariantsmatrix ofvariant × intentso the twelve cells are explicit. The prop signature is derived from the recipe viaVariantProps<typeof ctaButtonVariants>— adding a new intent or variant updates the types automatically. - Full-width by default. Unlike
Button,CTAButtondefaults tofullWidth={true}because the canonical use case is the dominant action slot at the bottom of a card, modal, or section. SetfullWidth={false}for inline CTAs that should hug their label. - Heavier visual weight. Taller hit area (
h-12,min-h-[2.75rem]) andfont-boldlabel set this button apart from the lighter baseButtonso on a page with both, the eye reads CTAButton as the primary action. - 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. Whendisabledis set,whileTapis dropped so disabled buttons feel inert. - Focus ring on accent. Keyboard focus shows a 2px ring in
var(--cb-accent)with a 2px offset againstcb-bg— preserves contrast across both themes regardless of the chosenintent. asChildslot. WhenasChildis true, content is rendered into the parent via RadixSlot. 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
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'solid' | 'outline' | 'subtle' | 'solid' | Surface treatment. |
intent | 'brand' | 'success' | 'danger' | 'muted' | 'brand' | Semantic tint. |
fullWidth | boolean | true | Stretch to the parent's width. Set false for inline CTAs. |
asChild | boolean | false | Render content into the parent via Radix Slot. Disables motion gestures. |
icon | ReactNode | — | Leading icon node. Wrapped with aria-hidden. |
iconRight | ReactNode | — | Trailing icon node. Wrapped with aria-hidden. |
disabled | boolean | false | Disables the button and drops the tap gesture. |
className | string | — | Merged onto the rendered element via cn(). |
...rest | HTMLMotionProps<'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-visiblering usesvar(--cb-accent)for keyboard users with a 2px offset againstcb-bgso it preserves contrast regardless of the chosenintent. disabledsets the nativedisabledattribute (no click, removed from tab order) and drops the tap gesture so the button feels inert.- Icon slots are wrapped in an
aria-hiddenspan — provide a visible label oraria-labelso 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_SCALEis a sub-300ms 0.04 spring viaSPRINGS.snap. For zero motion, overridetransitionto a step.
Credits
- Extracted from:
algoflashcards(src/platform/ui/CTAButton.tsx). The original wired in a project-specificplaySound, a rawcoloroverride prop, and a hand-rolledbevel-tile-strongclass. craft-bits generalizes the API to a two-axis CVA matrix (variant×intent), re-paints through thecb-*token system, drops the sound coupling, addsasChild+ icon slots, and forwards a real ref — bringing it in line with the baseButton.