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.

Type
import { InlineCTAActions } from "@/components/application/inline-cta/inline-cta-actions"; import { Button } from "@/components/base/buttons/button"; <InlineCTAActions title="Recently updated" description="We’ve just shipped a faster editor, a new command menu, and a handful of fixes." > <Button color="secondary">Dismiss</Button> <Button>See what’s new</Button> </InlineCTAActions>

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.

The components in this family, with their descriptions and import paths
ComponentDescriptionImport from
InlineCTA.RootThe 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.MediaThe 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.TextTitle 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.ActionsThe 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.CardThe 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.CheckIconThe 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
ButtonEvery 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.
FeaturedIconThe 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.
FileIconReceipt’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/iconsCheckCircle 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.

The Actions Type at 768, the plainest of the seven: a title, a supporting line, and a two-button row at gap-3
Part
What the sheet draws, and the class it resolves to
Shellbg-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 paddingpx-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 gapgap-5 (the sheet’s spacing-2xl 20) between the text block and whatever follows it, at both breakpoints
Text blockflex 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 rowflex 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
ButtonsButton 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 railh-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 hairlineA 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 borderThe 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 cardborder 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 headerborder-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 bodyp-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 listflex 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 iconCheckCircle 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 cardp-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 EditOne 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 cardflex 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 fieldflex 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

At Mobile the sheet gives the root no padding and no gap at all — every one of the fourteen symbols hangs both on an inner 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

The 768 cell is the card at that width, not a 768 viewport. Narrow the browser, or open the contextual preview, to resolve the pairs. The four cells further down that are written out without their 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.

Actions — a title, a line of support, and two sm buttons at gap-3, written secondary then primary

New feature available

Check out the new dashboard view. Pages now load faster.

Image — the only Type that restructures the shell: a 239px rail, no border on the root, and the border moved onto the content so it can meet the image’s edge

Subscribe to our newsletter

Get the latest posts and product news, once a month.

Email field — a real form at w-100, whose submit is the family’s only md button. The label is visible and tied to the input; the placeholder is not doing that job

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.

Change plan — the nested card in its bg-primary_alt tone, with a header, a display-sm price in the display family, and the family’s only non-stacking action row at Mobile before the 2026-08-25 ruling

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.

Upgrade plan — a feature list at gap-4, each row a 24px success CheckIcon beside a text-md medium title over text-md regular support with no gap between them. The list is byte-identical at both breakpoints

Payment method

Change how you pay for your plan.

Visa ending in 1234

Expiry 06/2026

billing@untitledui.com

Payment method — the nested card in its plain tone, a 46x32 brand mark, and the family’s only single action rather than a pair

Your receipt is ready

Your invoice for August 2026 has been issued.

Invoice-Aug-2026.pdf

1.2 MB

Receipt — the nested card as a row: the kit’s own FileIcon at 40, the filename and size, and the action row sharing the card rather than sitting under it
Type
Component
What it draws
ActionsInlineCTAActionsThe plainest of the seven and the shape the others extend: a title, a supporting line, and two sm buttons. 1255:131889 / 1255:132361.
ImageInlineCTAImageThe 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 fieldInlineCTAEmailFieldA 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 planInlineCTAChangePlanThe 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 planInlineCTAUpgradePlanA 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 methodInlineCTAPaymentMethodThe 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.
ReceiptInlineCTAReceiptA 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

The slideout and modal catalogues are fixture files: each one hard-codes its copy and several open themselves on mount, so the only way to use one is to copy it. The seven here take real typed props — 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.

Image at 768: a row. The root drops its border, the 239px rail is self-stretch with the rounding on its left corners only, and the content carries border-t, border-r and border-b with the rounding on its right

New feature available

Check out the new dashboard view. Pages now load faster.

Image at 343: a column. The rail moves on top at 200px tall and full width, the rounding flips from left-only to top-only, and the content’s border flips from t/r/b to b/l/r — three changes at once, which is why this is a prop on Root rather than a padding class

The hairline on the image is not a token

The rail carries a 1px 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

Receipt at 768: a nested card with bg-primary, a border-secondary hairline, an 8px radius and 16px of padding, laid out as a row with the actions beside the file

Your receipt is ready

Your invoice for August 2026 has been issued.

Invoice-Aug-2026.pdf

1.2 MB

Receipt at 343: the card is gone entirely — no background, no border, no radius, no padding. The same two children stack, so nothing is reparented; only the chrome is dropped

One element, two very different pictures

The Receipt pair looked, on the Mobile read alone, like the one thing that might force splitting the Type in two. It does not. At Desktop the card is a row holding the file block and the action row side by side; at Mobile the same two children stack with the chrome removed. 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

Change plan, Payment method and Receipt all draw 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.

At 640 and up: a row, content-sized, items-start, in the order it is written — secondary first, primary last, reading left to right
The same row with its sm: classes removed, so it draws its 343 form at any window: full width, and Changelog on top while still being second in the DOM and second in the tab order

flex-col-reverse is the whole trick

The sheet puts the primary above the secondary at Mobile and to the right of it at Desktop. Reordering the DOM per breakpoint is not possible; reordering it once would put the committing button first in the tab order on every screen. So the row is 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
smpx-3 py-2, 8px radiusSix of the seven Types: Actions, Image, Change plan, Upgrade plan, Payment method and Receipt.
mdpx-3.5 py-2.5Email 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

Every Type uses 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.
Edit at 640 and up: a secondary button with its full chrome — border, background, padding and shadow-xs
The same button with the chrome its sm: classes add stripped out: text-sm semibold in text-tertiary, no border, background, padding or shadow. One control, two chromes — not two controls. The real control also grows an invisible before: hit target here, which this chrome-only replica leaves out

Why the chrome is classes rather than a prop

This is the family’s one genuine API constraint. A 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

Six Types end in a pair; this one ends in a single 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

The sheet draws the card brand mark at 46 × 32, an aspect of 1.4375. The kit’s 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.