Design System
Components

CTA Bar

Sticky bottom action bar — status ribbon, price line, consent checkbox, payment method and buttons above the home indicator.

Figma description · 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.

Total amountAED 1,250.00
Props
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:

  1. ribbon — a full-bleed StatusRibbon: success, alert, warning or info.
  2. price — rendered as a PriceLine: { label, value, actionLabel?, onAction?, actionIcon? }. note is deprecated.
  3. terms — a ConsentCheckbox. Pass it controlled, so its state stays with you.
  4. payment + actions — with a PaymentRow, the payment method and the confirm button take half the width each. Without one, actions (a ButtonGroup or a block Button) spans the bar.
  5. 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

PropTypeDefaultDescription
actionsReactNode—CTA buttons (use <ButtonGroup/> or a block <Button/>).
homeIndicator?booleantrueiOS 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?booleantrueRound the top corners (24px). Figma's cta-bar always does; pass false for a flush edge.
surface?booleantrueWhite 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.
PropTypeDefaultDescription
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—
PropTypeDefaultDescription
primaryReactNode—
orientation?'horizontal' | 'vertical'verticalvertical 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.
PropTypeDefaultDescription
labelReactNode—Masked identifier, e.g. "•••• 4326".
actionLabel?ReactNodeChange
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). Surface surface/base/inverse with shadow/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/2xs gap.
  • Status ribbon: 40px (scale/40), padding 4/20, alert-message/{surface,text}/<status>, a 24px icon box and a title/xs label. Its action is body/md in button/link/text/midnight/default.
  • Payment row: a 40px tile at border-radius/3, a body/md label, and “Change” as a tertiary md button.
  • 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.

On this page