CTA Bar
Sticky bottom action bar — status ribbon, price line, consent checkbox, payment method and buttons above the home indicator.
cta-barOpen in Figma ↗A sticky footer container that holds the primary call-to-action area at the bottom of a screen, combining a button group, optional status messaging, optional pricing, and the iOS Home Indicator.
Use for: Any screen that requires a persistent bottom action — such as confirming a plan selection, completing a purchase, or proceeding through a flow. Always the last element on a screen, pinned above the navbar or directly at the screen edge when no navbar is present.
Not for: Actions that belong inline within content — use a button directly inside a section body slot instead. Not for navigation between views — use the navbar.
Choosing: Set surface=true to apply the white card surface and drop shadow, lifting the bar above the screen content. Set surface=false when the bar sits on a surface that already provides the visual separation. Choose the button group composition based on the number of actions needed: primary for a single full-width CTA; primary+secondary for a primary action with a supporting option; primary+tertiary for a primary with a low-weight inline alternative; primary+secondary+tertiary for three-level action hierarchy. Use vertical grouping when actions stack, horizontal when they sit side by side. Add the status ribbon above the buttons when the screen needs to surface a success, alert, warning, or info message directly in the footer. Add the price line when the user needs to see the cost before confirming. Toggle payment=true on the action block to include payment method detail alongside the buttons.
Rules: Always place at the bottom of the screen, outside any scroll container or section. The Home Indicator is always present — do not remove it. In RTL, all button layouts and status ribbon icons mirror automatically. Use at most one CTA Bar per screen.
import SwiftUI import EandTokens // cta-bar: white card, top radius 24, shadow/md (0 0 44, midnight 10%). Action block: // status ribbon → price line → consent line (16/20/0) → button group (20/20/0, gap 20). // The 34pt home-indicator block is the bar's only bottom spacing — keep it. struct CtaFooter: View { @State private var agreed = false var body: some View { VStack(spacing: 0) { StatusRibbon(status: .success, message: "Payment method saved", action: "View") PriceLine(label: "Total amount", value: "AED 1,250", actionLabel: "View breakdown") ConsentCheckbox(on: $agreed) .padding(.top, EandSpacing.lg).padding(.horizontal, EandSpacing.xl) VStack(spacing: EandSpacing.xl) { EandButton(title: "Continue") {} // lg, block EandButton(title: "Not now", variant: .tertiary) {} .padding(.vertical, EandSpacing.sm) } .padding(.top, EandSpacing.xl).padding(.horizontal, EandSpacing.xl) Color.clear.frame(height: 34) // home-indicator block; on device the safe area } .background(EandColor.surfaceBaseInverse) .clipShape(UnevenRoundedRectangle(topLeadingRadius: EandRadius.radius8, topTrailingRadius: EandRadius.radius8)) .shadow(color: EandColor.midnightA10, radius: 22) // shadow/md: blur 44 } }
When to use
- Use at the bottom of a purchase, checkout or confirmation screen, to hold the screen's committing action together with what the user is committing to.
- Don't use for actions inside the page — place a Button. Not for a compact inline action row (ActionBar) or for app navigation (NavBar).
Anatomy
CtaFooter is Figma's cta-bar: a surface, an ActionBlock and the iOS home
indicator. The action block stacks its slots top to bottom:
ribbon— a full-bleedStatusRibbon:success,alert,warningorinfo.price— rendered as a PriceLine:{ label, value, actionLabel?, onAction?, actionIcon? }.noteis deprecated.terms— a ConsentCheckbox. Pass it controlled, so its state stays with you.payment+actions— with aPaymentRow, the payment method and the confirm button take half the width each. Without one,actions(aButtonGroupor a blockButton) spans the bar.homeIndicator— on by default. It is also the bar's only bottom spacing, so leave it on.
surface={false} drops the white card and shadow, for a bar that sits on a surface which
already separates it. rounded={false} squares the top corners.
<CtaFooter
price={{ label: 'Total', value: 'AED 120.00' }}
terms={
<ConsentCheckbox
checked={agreed}
onChange={(e) => setAgreed(e.currentTarget.checked)}
prefix="Agree to the"
links={[{ label: 'Terms' }, { label: 'Privacy Policy' }]}
suffix="before proceeding"
/>
}
actions={<ButtonGroup primary={<Button size="lg" block disabled={!agreed}>Confirm</Button>} />}
/>ButtonGroup stacks buttons 20px apart, or with orientation="horizontal" puts secondary and
primary side by side, 12px apart (secondary first). A tertiary action always sits on its own
centred row.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
actions | ReactNode | — | CTA buttons (use <ButtonGroup/> or a block <Button/>). |
homeIndicator? | boolean | true | iOS home-indicator block below the actions (Figma home-indicator, default true).
It is also the bar's only bottom spacing: with it off the actions sit flush, as in
the symbol. |
payment? | ReactNode | — | Payment-method summary (use <PaymentRow/>); renders beside the actions, half each. |
price? | CtaFooterPrice | — | Price summary, rendered as a <PriceLine/>. |
ribbon? | ReactNode | — | Status strip across the very top (use <StatusRibbon/>). |
rounded? | boolean | true | Round the top corners (24px). Figma's cta-bar always does; pass false for a flush edge. |
surface? | boolean | true | White card + shadow/md (Figma surface, default true). false = transparent, no shadow. |
terms? | ReactNode | — | Checkbox line — pass a <ConsentCheckbox/> (or a <Checkbox/>) so state stays with the caller. |
| Prop | Type | Default | Description |
|---|---|---|---|
action? | ReactNode | — | Trailing action label (Figma show-action); the tap target spans the label + icon. |
leadingIcon? | ReactNode | — | Leading glyph, drawn in a 24px box (Figma show-icon). |
onAction? | (() => void) | — | |
status? | 'alert' | 'success' | 'warning' | 'info' | info | |
trailingIcon? | ReactNode | — |
| Prop | Type | Default | Description |
|---|---|---|---|
primary | ReactNode | — | |
orientation? | 'horizontal' | 'vertical' | vertical | vertical stacks the buttons; horizontal puts secondary+primary side by side
(Figma button-stack horizontal false|true, 31617:1409). |
secondary? | ReactNode | — | |
tertiary? | ReactNode | — | Tertiary/link action — always renders on its own centered row. |
| Prop | Type | Default | Description |
|---|---|---|---|
label | ReactNode | — | Masked identifier, e.g. "•••• 4326". |
actionLabel? | ReactNode | Change | |
icon? | ReactNode | — | Payment method tile (logo/image); clipped to a 40x40 rounded square. |
onAction? | (() => void) | — |
Design
Figma source: cta-bar — etisalat Consumer App DSL V1.2
- Top corners
border-radius/8(24). Surfacesurface/base/inversewithshadow/md(0 0 44 0, midnight at 10%). - Action block padding: 16/20/0/20 for the checkbox line; 20/20/0/20 for the actions, or
16/20/0/20 beside a payment method, with a
spacing/2xsgap. - Status ribbon: 40px (
scale/40), padding 4/20,alert-message/{surface,text}/<status>, a 24px icon box and atitle/xslabel. Its action isbody/mdinbutton/link/text/midnight/default. - Payment row: a 40px tile at
border-radius/3, abody/mdlabel, and “Change” as a tertiarymdbutton. - Home indicator: a 34px block with a 144×5 bar 8px from the bottom. The bar uses the iOS
kit's black
Labels/Primary, which is not a DSL token. - Every row mirrors in RTL.