Inline CTAs
A small offer inside the page: four shared parts, a media rail, and seven typed presets. DrumKit-authored from Figma page 127:2907 per ADR-0001.
Inline CTAs
A small offer inside the page: four shared parts, a media rail, and seven typed presets. DrumKit-authored from Figma page 127:2907 per ADR-0001.
Demo
Every preset takes real typed props rather than fixture copy, so what the playground emits is what a product writes.
Recently updated
We’ve just shipped a faster editor, a new command menu, and a handful of fixes.
Four shared parts, seven typed presets
InlineCTA.Root is the shell, InlineCTA.Text the title and supporting line, InlineCTA.Actions the 12px action row, and InlineCTA.Card the nested 8-radius card the three billing Types share. On top of them sit the seven presets the sheet draws. Reach for a preset first and drop to the parts when your content is not one of the seven — the same contract Table and SlideoutMenu offer.Contextual demo
An inline CTA is a block in a page, not a layer over one. The seven sit in two screens with no docs shell in the way.
The family
What the module exports: the four shared parts, the Media rail and the CheckIcon beside them, and the seven presets.
| Component | Description | Import from |
|---|---|---|
| InlineCTA.Root | The shell: bg-primary, a 12px radius, a border, shadow-xs, and an inner wrapper at px-4 py-5 sm:p-6 that always renders because the sheet gives the root no padding at Mobile. Passing `media` switches it to the Image Type’s row layout and moves the border onto the content. | @/components/application/inline-cta/inline-cta |
| InlineCTA.Media | The image rail: 200px tall and full width below sm, 239 wide and full height above, with the rounding following the same flip and a 1px rgba(0,0,0,0.08) hairline drawn as an overlay rather than a border on the img. | @/components/application/inline-cta/inline-cta |
| InlineCTA.Text | Title over supporting text on a gap-0.5 column: text-md 16/24 semibold text-primary over text-sm/20 text-tertiary. Identical on all seven Types and at both breakpoints. | @/components/application/inline-cta/inline-cta |
| InlineCTA.Actions | The 12px action row. Below sm it is flex-col-reverse with every child full width, so the primary sits on top while staying last in the DOM and last in the tab order — the pattern the modal family already uses. | @/components/application/inline-cta/inline-cta |
| InlineCTA.Card | The nested 8-radius surface the three billing Types share, which the read found spelled three times rather than once. `tone` picks the background and `chrome="sm-up"` drops the whole card below sm, which is what Receipt draws. | @/components/application/inline-cta/inline-cta |
| InlineCTA.CheckIcon | The 24px round success check on the Upgrade plan feature list: CheckCircle at size-6 in text-fg-success-primary, aria-hidden. A two-line wrapper, not a new primitive — the sheet’s own component resolves to exactly this. | @/components/application/inline-cta/inline-cta |
Built from
The pieces with their own docs page. Nothing in this family re-authors a primitive the kit already ships.
Component | Where it’s used here |
|---|---|
| Button | Every action in the family, at size sm — px-3 py-2 with an 8px radius — which is also Button’s own default, so the five Types that take their buttons as children pass no size prop. The Email field’s submit is the one md, drawn that way on the sheet. |
| FeaturedIcon | The Change plan card’s header glyph, at size sm color brand: a 32px rounded-full bg-brand-secondary box with a 16px icon, which is precisely the sheet’s measurement. |
| FileIcon | Receipt’s file glyph at 40px, from @untitledui/file-icons, rendered as the light/dark pair file-upload-base already uses. The sheet draws a page with a red PDF badge; that is what the kit’s own icon is. |
| @untitledui/icons | CheckCircle behind InlineCTA.CheckIcon, and Mail01 twice — in the Email field’s input and beside the Payment method’s billing address. |
Anatomy
Six of seven Types share one shell: bg-primary, hairline border, rounded-xl, shadow-xs — at the 768 the sheet draws.
New feature available
Check out the new dashboard view. Pages now load faster.
Part | What the sheet draws, and the class it resolves to |
|---|---|
| Shell | bg-primary, border border-secondary, rounded-xl (the sheet’s radius-xl 12), shadow-xs and overflow-hidden. The sheet draws 768 wide at Desktop and 343 at Mobile; the component is full width and takes its column from the layout around it. Image is the exception — no border on the root, and a row from sm up |
| Shell padding | px-4 py-5 sm:p-6 — 16/20 at Mobile, 24 from 640px — on an inner wrapper, never on the root. The sheet gives the root no padding at Mobile on any of the seven Types and hangs it on a Content div, so the wrapper always renders |
| Shell gap | gap-5 (the sheet’s spacing-2xl 20) between the text block and whatever follows it, at both breakpoints |
| Text block | flex w-full flex-col gap-0.5 (spacing-xxs 2); title text-md 16/24 semibold text-primary over supporting text-sm/20 text-tertiary. Unchanged across all seven Types and both breakpoints |
| Action row | flex w-full flex-col-reverse gap-3 with *:w-full below sm, sm:w-auto sm:flex-row sm:items-start above. The gap is 12 at both breakpoints; the reversal is what puts the primary on top at Mobile from a DOM order of secondary-then-primary |
| Buttons | Button size sm — px-3 py-2 with an 8px radius — everywhere except the Email field’s submit, which the sheet draws md at px-3.5 py-2.5. sm is Button’s own default, so the five Types that take their buttons as children pass no size at all. Secondary is bg-primary with border-primary; the primary is bg-brand-solid |
| Image rail | h-50 w-full below sm (200 tall, full width), sm:h-auto sm:w-[239px] above — the sheet’s 239 exactly, as an arbitrary value, because the theme scale does not carry it. The rounding flips with it: rounded-t-xl below sm, sm:rounded-l-xl above |
| Image hairline | A 1px rgba(0,0,0,0.08) border on an aria-hidden overlay span rather than on the img, so it survives object-cover. The value is the sheet’s own and is deliberately not a token: it sits over arbitrary artwork rather than a surface |
| Image border | The content, not the root, carries border-r border-b border-l with rounded-bl-xl rounded-br-xl below sm, and sm:border-t sm:border-l-0 with sm:rounded-tr-xl sm:rounded-br-xl sm:rounded-bl-none above — so the border meets the image’s edge instead of crossing it. Per-corner rather than rounded-r/rounded-b, because those two both set border-bottom-right-radius and tailwind-merge does not treat them as one group, so emission order would decide the corner |
| Nested card | border border-secondary rounded-lg (the sheet’s radius-md 8). bg-primary_alt on Change plan, bg-primary on Payment method and Receipt; Receipt’s chrome arrives only at sm |
| Change plan header | border-b border-secondary, py-3 pr-5 pl-4 (12/20/16), gap-3 items-center, with FeaturedIcon size="sm" color="brand" — a 32px rounded-full bg-brand-secondary box with a 16px glyph — beside a text-md semibold text-secondary name. The header takes border-b only: the sheet draws a full stroke on it inside an already-bordered card, where Figma’s inside-aligned strokes coincide and CSS would double to 2px on three sides |
| Change plan body | p-4 gap-5 (16/20). The price is text-display-sm (30/38) semibold in the display family, with the period as text-sm text-tertiary on a pb-[3px] baseline nudge, items-end gap-1 |
| Upgrade plan list | flex w-full flex-col gap-4 (16); each row is flex gap-3 (12) items-start with a title text-md medium text-secondary directly over support text-md text-tertiary, no gap between them. Byte-identical at both breakpoints — the only part of the family that does not move |
| Check icon | CheckCircle at size-6 (24) in text-fg-success-primary, aria-hidden and shrink-0. The sheet models it as its own component (3488:546959, Type=Line / Size=sm / Color=Success); Line is CheckCircle and Success is the existing foreground token, so no new primitive and no new token |
| Payment method card | p-4 gap-4, a row from items-start. The brand mark sits in an h-8 w-[46px] slot with *:size-full; icon-to-text is gap-3 at both breakpoints, so the card’s own gap-4 is inert either way. The content column is gap-2 (8), with the billing line at gap-1.5 (6) items-center over a 16px Mail01 |
| Payment method’s Edit | One button whose chrome is nulled below sm with max-sm:[box-shadow:none], text-tertiary, bg-transparent, px-0 py-0 and hover:bg-transparent, and left to the secondary variant above it. Neither border-0 nor shadow-none works here: secondary draws its outline as ring-1 ring-inset PLUS shadow-xs-skeuomorphic (button.tsx:81), there is no border to remove, and cx extends only the text theme so tailwind-merge does not treat shadow-xs-skeuomorphic as a shadow-group class to replace. Measured: with shadow-none the Mobile button still computed a 1px rgba(0,0,0,0.18) inset ring; with max-sm:[box-shadow:none] it computes none. A prop cannot change at a breakpoint, and two rendered buttons would put two identically-named controls in the tab order. Below sm the hit target is extended by an invisible before: pseudo-element at -inset-x-2 -inset-y-1.5 — measured 2026-08-28 at about 46×32 around the visible 30×20 box, clearing WCAG 2.5.8’s 24×24 floor — the same technique Dropdown.DotsButton uses |
| Receipt card | flex flex-col gap-3 below sm, sm:flex-row sm:gap-4 sm:p-4 above, with the chrome arriving at the same breakpoint. The file glyph is FileIcon at size-10 (40), rendered as the light/dark pair file-upload already uses |
| Email field | flex w-full flex-col gap-3 below sm, sm:w-100 (400) sm:flex-row sm:items-end sm:gap-4 above. The Input is the kit’s md — px-3 py-2, an 8px radius, border-primary and a 20px Mail01 — under a text-sm medium label at the kit’s own gap-1.5 (6). The submit is the family’s only md button and goes full width below sm |
The root has no padding; the wrapper does
Content div instead. InlineCTA.Root therefore always renders that wrapper and puts the responsive padding on it: px-4 py-5 sm:p-6, at gap-5. That is also what makes the Image Type expressible, because the wrapper is the thing the border moves onto.The text block is the one part that never moves
gap-0.5, a text-md 16/24 semibold title in text-primary over text-sm/20 support in text-tertiary — identical in all fourteen symbols, Types and breakpoints alike. Nothing about it steps, which is worth stating because the slideout family’s title does step and the modal family’s does not.The widths are honest; the layout inside them is the window’s
sm: prefixes are the exception — they draw the Mobile form at any window, which is the only way to put it beside the Desktop one on a page.The seven Types
All seven, live, at the 768 the sheet draws. They differ only in what sits under the text block.
New feature available
Check out the new dashboard view. Pages now load faster.

New feature available
Check out the new dashboard view. Pages now load faster.
Subscribe to our newsletter
Get the latest posts and product news, once a month.
Change your plan
Upgrade or downgrade at any time. Changes are prorated.
Basic plan
$10per month
Includes up to 10 users, 20GB of storage and access to every core feature.
Upgrade your plan
You’re on the free plan. Here is what the team plan adds.
Unlimited projects
Create as many boards as your team needs.
Advanced analytics
See where every activation drops off.
Priority support
A named contact and a one-hour first response.
Payment method
Change how you pay for your plan.
Visa ending in 1234
Expiry 06/2026
billing@untitledui.com
Your receipt is ready
Your invoice for August 2026 has been issued.
Invoice-Aug-2026.pdf
1.2 MB
Type | Component | What it draws |
|---|---|---|
| Actions | InlineCTAActions | The plainest of the seven and the shape the others extend: a title, a supporting line, and two sm buttons. 1255:131889 / 1255:132361. |
| Image | InlineCTAImage | The only Type that restructures the shell, and it does three things at once: the direction, the border’s owner, and which corners round. That is why the shell takes a media prop rather than padding and gap props. 1255:131888 / 1255:132370. |
| Email field | InlineCTAEmailField | A labelled email input and a submit button, wrapped in a real form so Enter works. Its button is the family’s only md — drawn that way, not an oversight. 1255:131908 / 1255:132354. |
| Change plan | InlineCTAChangePlan | The nested card with a header: a 32px featured icon and the plan name over a display-sm price, a description, and two buttons. Two of the three normalisations land here. 1255:131909 / 1255:132334. |
| Upgrade plan | InlineCTAUpgradePlan | A feature list of check-marked title-over-support pairs, rendered as a real ul, with the action row as a sibling of the list rather than inside it. 1255:131912 / 1255:132311. |
| Payment method | InlineCTAPaymentMethod | The card on file: a 46×32 brand mark, the name and expiry, a billing line, and a single Edit — the only Type with one action rather than two, and the only one whose button changes chrome across the breakpoint. 1255:131910 / 1255:132295. |
| Receipt | InlineCTAReceipt | A downloadable document: the kit’s own FileIcon at 40px, the filename and size, and two buttons. Its nested card disappears entirely below sm, which is a chrome change rather than a structural one. 1255:131911 / 1255:132811. |
These are components a product configures, not demos it copies
features, price, cardName, fileName — and their buttons are your children, so a product renders one rather than duplicating it. That is why this page can put all seven on screen at once and the modal page could not put any of its 51.What the breakpoint changes
Two Types restructure below sm. The Mobile cells strip sm: prefixes to draw the 343 form; the Desktop cells are live.

New feature available
Check out the new dashboard view. Pages now load faster.
New feature available
Check out the new dashboard view. Pages now load faster.
The hairline on the image is not a token
rgba(0,0,0,0.08) border, drawn as an overlaid span so it sits above the artwork rather than being covered by it. It is written as an arbitrary value on purpose: it sits over an arbitrary photograph rather than over a surface, so none of the border tokens is the right answer for it. It follows the same rounding flip as the image itself.Your receipt is ready
Your invoice for August 2026 has been issued.
Invoice-Aug-2026.pdf
1.2 MB
Your receipt is ready
Your invoice for August 2026 has been issued.
Invoice-Aug-2026.pdf
1.2 MB
One element, two very different pictures
InlineCTA.Card chrome="sm-up" plus flex-col gap-3 sm:flex-row sm:gap-4 expresses both, with no DOM reparenting and no second component.The nested card is one primitive used three times
border-secondary at an 8px radius, differing only in background — bg-primary_alt for Change plan, bg-primary for the other two — and in whether they carry a header. That is InlineCTA.Card’s tone and chrome props and nothing else. Writing it three times is exactly the drift the modal batch spent a day documenting, so it was not written three times.The action row
Five Types end in a button pair, stacking below sm with the primary on top — CSS, not DOM order. Email field ends in its single md submit, Payment method in a single Edit.
flex-col-reverse is the whole trick
flex-col-reverse at its base and picks up sm:flex-row above it — the primary sits on top at a phone width while staying last in the DOM, last in the tab order and last for a screen reader. The modal family arrived at the same answer for the same reason, and this family adopted it rather than inventing a second one.Size | What it draws | Which Types |
|---|---|---|
sm | px-3 py-2, 8px radius | Six of the seven Types: Actions, Image, Change plan, Upgrade plan, Payment method and Receipt. |
md | px-3.5 py-2.5 | Email field only, and drawn that way on the sheet at both breakpoints — it sits beside an md Input, so the pair lines up on the bottom edge. |
The one size that is not sm is drawn, not an oversight
size="sm" except the Email field, whose submit is size="md" at both breakpoints. It is the easiest thing in the family to normalise by accident and wrong to: the button is baseline-aligned with an md Input on items-end, and an sm button would sit 4px short of the field it belongs to.Why the chrome is classes rather than a prop
color prop cannot change at a breakpoint, and the obvious workaround — rendering the button twice behind sm:hidden and hidden sm:flex — puts two identically-named controls in the tab order and duplicates the click target. That is the exact defect the previous batch spent a review round removing, so it is not repeated here: one button, with sm: classes that restore the secondary variant’s chrome above the breakpoint and null it below. Everything else in the family, including the Image Type’s full row-to-column flip, needs nothing but sm: classes.Payment method is also the only Type with one action
Edit. Because it is one control rather than a row, it does not use InlineCTA.Actions at all — it sits inside the card’s text row, aligned to the end, which is why the stacking rule above does not apply to it.An uncertainty the measurement resolved
VisaIcon has a 34 × 24 viewBox, an aspect of 1.4167. Those are not the same ratio — but SVG resolves that without distorting anything: preserveAspectRatio defaults to xMidYMid meet, so the viewBox is letterboxed and centred inside the slot rather than stretched. Measured against a production build on 2026-08-25 at exactly 46 × 32, with roughly 0.7px of horizontal padding and no distortion at all.