8.2 KiB
Cta
Function
The panel that asks for the next step: a title, a supporting line and the
buttons, on a finished surface that lifts it off the page. Either the closing
beat of a page (center) or a mid-page nudge that must not stop the reading
(justified).
Composition map
| Slot | Composes | Notes |
|---|---|---|
| root | bare <section aria-labelledby> |
landmark; id names it from the title the block renders |
| padding · measure | Section + Container |
size / container |
| entrance | Motion (trigger="viewport", scale-fade) |
the panel arrives WHOLE — it is one statement, not a list, so it does not stagger its parts |
| panel | Surface (always solid · color · gradient · rounded) |
the look is the system's finish, not paint of the block's own |
| layout | Stack (center) · Grid 2-col (justified) |
justified stacks on narrow, where one row would crush both halves |
eyebrow |
— (app: Badge / text) |
above the title |
title |
Heading at level (default 2) |
the block wraps the app's words and owns the landmark id; on-solid ink |
description |
Text as="p" (60ch measure) |
on-solid ink. A span would ignore align — text-align is inert on an inline box |
actions |
Flex (column → row at sm) |
the app drops Buttons / Links |
Form: snippet slots, not sub-components
The tier's rule is that a compound API is earned when parts repeat
(feature-grid.Item) or coordinate (pricing.Switch ↔ PlanPrice). A CTA's
parts do neither: they are fixed positional slots the root arranges. So it takes
the same shape as hero. (The plan sketched .Title/.Description/.Actions;
this is the same registered deviation as hero.)
Coordination
Position under the 2026-07-31 doctrine (architecture/blocks.md §«Coordination»).
Owns nothing. A closing panel places copy and actions; there is no state, no data shape and no word of its own. Layout blocks do not coordinate, and giving one a machine would be the opposite mistake.
Decisions
2026-07-29 — reference floor (dossier §P1: Tailwind Plus «CTA 11» · PrimeBlocks «CTA 12» · Untitled UI):
- Adopted: the centred panel (parity) and the
justifiedarrangement — copy at the inline-start, actions at the end — which the dossier names as the gap to close. - The finish is the system's: a CTA is distinguished by treatment, and
treatment is already framework vocabulary.
Surfacewithgradienton by default (the one section that earns it), so a theme or palette swap carries the panel along. The block paints nothing of its own. - Ink follows the panel: the title and description take on-solid, the
contrast ink the palette guarantees for that canvas. No hand-picked colour, so
a
coloror theme swap carries them along. - It reveals whole: one
Motion, no stagger. A CTA is a single statement; dealing its three parts in one by one would read as hesitation. - No
variantprop — the panel is always solid. It shipped as'solid' | 'soft'and the soft state was measured out in the browser: the primary track lands atoklch(0.9932 …)against a--color-surface-defaultofoklch(0.9911 …)— 0.002 L apart, i.e. no panel at all in light mode, andSurfacehas no border to fall back on. Cutting the prop is cheaper than shipping a state that vanishes. - The demo offers only dark canvases (
primary·indigo·plum). Measured body-copy contrast on the panel: 5.18 · 5.21 · 4.75 in both modes, versusneutral3.32 /secondary3.30 /slate3.30 /teal3.07 in light. The palette's contrast slot is#fffffffor every solid step, so a mid-lightness canvas cannot carry body copy at AA. The block forwards anycolorthe app passes — it just does not advertise the ones that fail.
Demo
web/routes/blocks/cta/ — the block full-bleed on the page, with live control of
layout, colour, finish and direction, and the device widths served from
preview/. Mini-page in CtaSite.svelte, shared by both surfaces.
Gaps
| Gap | Disposition |
|---|---|
| Split with media (panel with a screenshot beside the copy) | deferred — recurs in the refs; it is a third arrangement, and Mockup already exists for the media. Enters when a demo asks |
| Full-bleed panel (edge to edge, no container) | app-land — the app sets container="full" |
| Dismissible / sticky CTA | out — that is the banner block's job (F2.11), not this one |
| A quiet BOUNDED panel | canon — Surface soft does not bound (0.002 L from the page in light) and has no border; Card outline bounds but takes no gradient finish. No primitive covers "quiet CTA panel with an edge" |
contrast slot is white on every solid step |
canon — so a mid-L solid canvas (neutral 3.32 · secondary 3.30 · slate 3.30 · teal 3.07 in light) puts body copy below AA. The pairing guarantee holds for the dark canvases only. Measured with a luminance probe, not eyeballed |
| On-solid chip treatment | canon — Badge has no on-solid variant; the demo uses variant="soft", whose near-white track happens to read against a saturated canvas. A Badge that knows it sits on a solid panel would be the real answer |
Found while composing
Text alignis inert by default.Textrenders aspan;text-aligndoes nothing on an inline box, soalign="center"silently kept the copy left-aligned inside the centred layout. Fixed here withas="p"(which is what a paragraph is anyway), but the prop advertises an effect it does not have until the consumer changes the element. Canon candidate.Surface softcannot bound a panel — measured above; see Gaps.Groupdoes not stack. The actions row neededFlex direction={{ base: 'column', sm: 'row' }}: in aGroupat 420px the secondary action's label breaks mid-phrase against the primary button.herocomposes its actions withGrouptoo, so it has the same narrow behaviour — noted for its next pass.