coderef

Monzo

Monzo's iOS app is built around one unforgettable object: the Hot Coral debit card (#FF3464). That coral is the brand — it appears the moment you open the app as a full-width card hero at the top of the Home screen, and it threads through…
DownloadRaw
Monzo

Build with the Monzo look.

Palette, type and components from this DESIGN.md, applied to a sample hero.

Palette

At a glance

Theme
dark
Typefaces
Inter
Sections
7
Length
4,023 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · Monzo

Light and dark previews, tokens and a ready-to-use package, generated from this DESIGN.md. No AI involved: the same file always produces the same output.

Generating design system…
DownloadRaw

Monzo (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports Monzo’s visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, paste-ready @Composables, the Hot Coral card hero + transaction feed + Pots, motion, and haptics.

Why a Compose guide for an iOS-referenced app? The DESIGN.md tokens are platform-neutral. This file keeps the visual identity (the Hot Coral card hero, Monzo Navy, the emoji transaction feed, round Pot coins) while making everything idiomatic Android — NavigationBar instead of a UITabBar, LazyColumn for the feed, LazyRow for Pots, graphicsLayer for the card flip, sp/dp instead of pt.

Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, and Coil 2.6+ for merchant logos. Do not enable Material You dynamicColorScheme — Hot Coral is the brand and must hold regardless of wallpaper. Monzo is dark-and-navy friendly; a full dark scheme is provided.

1. Color Tokens

// ui/theme/MonzoColors.kt
import androidx.compose.ui.graphics.Color

object MonzoColors {
    // Brand
    val Coral        = Color(0xFFFF3464)
    val CoralPressed = Color(0xFFE02855)
    val Navy         = Color(0xFF14233C)
    val NavyDeep     = Color(0xFF0E1620)

    // Canvas & Surfaces (Light)
    val Canvas         = Color(0xFFF6F7F9)
    val Surface        = Color(0xFFFFFFFF)
    val SurfacePressed = Color(0xFFEEF0F3)
    val Divider        = Color(0xFFE4E7EB)

    // Canvas & Surfaces (Dark)
    val DarkCanvas   = Color(0xFF0E1620) // deep navy — NOT pure black
    val DarkSurface1 = Color(0xFF14233C) // the brand navy
    val DarkSurface2 = Color(0xFF1B2F4D)
    val DarkDivider  = Color(0xFF243A57)

    // Text
    val TextPrimary       = Color(0xFF14233C)
    val TextSecondary     = Color(0xFF5C6B82)
    val TextTertiary      = Color(0xFF94A1B5)
    val DarkTextPrimary   = Color(0xFFF4F6F9)
    val DarkTextSecondary = Color(0xFF9FB0C6)
    val DarkTextTertiary  = Color(0xFF61748E)

    // Pot / category accents (light / dark)
    val MintLight = Color(0xFF2FB59A); val Mint = Color(0xFF5CE0C4)
    val SkyLight  = Color(0xFF3E7FC4); val Sky  = Color(0xFF5AA9F0)
    val GoldLight = Color(0xFFE0A23A); val Gold = Color(0xFFFFC75F)
    val VioletLight = Color(0xFF7A4FD0); val Violet = Color(0xFFA98AE6)
    val TealLight = Color(0xFF1F9E8F); val Teal = Color(0xFF46C7B7)

    // Semantic
    val IncomeLight = Color(0xFF1FA971); val Income = Color(0xFF2FCB8F)
    val ErrorLight  = Color(0xFFE0354A); val Error  = Color(0xFFFF5A6E)
    val WarningLight = Color(0xFFD98A1F); val Warning = Color(0xFFFFB347)
}

Wire it into both schemes. Monzo is happiest dark-and-navy; the dark scheme uses #0E1620 deep navy, never true black.

// ui/theme/Theme.kt
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.MaterialTheme

private val MonzoLight = lightColorScheme(
    primary        = MonzoColors.Coral,
    onPrimary      = Color.White,
    background     = MonzoColors.Canvas,
    onBackground   = MonzoColors.TextPrimary,
    surface        = MonzoColors.Surface,
    onSurface      = MonzoColors.TextPrimary,
    surfaceVariant = MonzoColors.SurfacePressed,
    outline        = MonzoColors.Divider,
    error          = MonzoColors.ErrorLight,
)

private val MonzoDark = darkColorScheme(
    primary        = MonzoColors.Coral,
    onPrimary      = Color.White,
    background     = MonzoColors.DarkCanvas,
    onBackground   = MonzoColors.DarkTextPrimary,
    surface        = MonzoColors.DarkSurface1,
    onSurface      = MonzoColors.DarkTextPrimary,
    surfaceVariant = MonzoColors.DarkSurface2,
    outline        = MonzoColors.DarkDivider,
    error          = MonzoColors.Error,
)

@Composable
fun MonzoTheme(
    dark: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit,
) = MaterialTheme(
    colorScheme = if (dark) MonzoDark else MonzoLight,
    typography = MonzoTypography,
    content = content,
)

2. Typography

Monzo’s product face is Inter (SIL OFL) — drop the TTFs in res/font/. Money values use tabular figures via a font feature setting. Body 400; titles 700; balance/screen-title 800.

// ui/theme/MonzoType.kt
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.FontFeature
import androidx.compose.ui.text.style.TextGeometricTransform
import androidx.compose.ui.unit.sp

val Inter = FontFamily(
    Font(R.font.inter_regular,    FontWeight.Normal),
    Font(R.font.inter_medium,     FontWeight.Medium),
    Font(R.font.inter_semibold,   FontWeight.SemiBold),
    Font(R.font.inter_bold,       FontWeight.Bold),
    Font(R.font.inter_extrabold,  FontWeight.ExtraBold),
)

// Tabular-figures feature for money
val Tnum = androidx.compose.ui.text.font.FontFeatureSettings("tnum")

object MonzoText {
    val ScreenTitle = TextStyle(Inter, fontWeight = FontWeight.ExtraBold, fontSize = 32.sp, lineHeight = 37.sp, letterSpacing = (-0.6).sp)
    val Balance     = TextStyle(Inter, fontWeight = FontWeight.ExtraBold, fontSize = 30.sp, lineHeight = 33.sp, letterSpacing = (-0.5).sp, fontFeatureSettings = "tnum")
    val Section     = TextStyle(Inter, fontWeight = FontWeight.Bold,      fontSize = 22.sp, lineHeight = 26.sp, letterSpacing = (-0.3).sp)
    val Subsection  = TextStyle(Inter, fontWeight = FontWeight.Bold,      fontSize = 18.sp, lineHeight = 23.sp, letterSpacing = (-0.2).sp)
    val Body        = TextStyle(Inter, fontWeight = FontWeight.Normal,    fontSize = 16.sp, lineHeight = 23.sp)
    val Merchant    = TextStyle(Inter, fontWeight = FontWeight.SemiBold,  fontSize = 15.sp, lineHeight = 20.sp, letterSpacing = (-0.1).sp)
    val Amount      = TextStyle(Inter, fontWeight = FontWeight.Bold,      fontSize = 15.sp, lineHeight = 20.sp, letterSpacing = (-0.1).sp, fontFeatureSettings = "tnum")
    val Meta        = TextStyle(Inter, fontWeight = FontWeight.Medium,    fontSize = 13.sp, lineHeight = 18.sp)
    val Label       = TextStyle(Inter, fontWeight = FontWeight.Medium,    fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.1.sp)
    val Button      = TextStyle(Inter, fontWeight = FontWeight.Bold,      fontSize = 16.sp, lineHeight = 16.sp)
    val Tab         = TextStyle(Inter, fontWeight = FontWeight.Medium,    fontSize = 10.sp, lineHeight = 10.sp, letterSpacing = 0.1.sp)
    val CardNumber  = TextStyle(Inter, fontWeight = FontWeight.SemiBold,  fontSize = 13.sp, lineHeight = 13.sp, letterSpacing = 2.sp)
}

// Map onto Material 3 slots so stock components inherit the brand
val MonzoTypography = Typography(
    headlineLarge = MonzoText.ScreenTitle,
    headlineMedium = MonzoText.Section,
    titleMedium   = MonzoText.Subsection,
    bodyMedium    = MonzoText.Body,
    labelSmall    = MonzoText.Tab,
)

3. Signature Components

Hot Coral Card Hero

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp

@Composable
fun MonzoCardHero(
    balance: String,
    maskedNumber: String,
    modifier: Modifier = Modifier,
) {
    var flipped by remember { mutableStateOf(false) }
    val rotation by animateFloatAsState(if (flipped) 180f else 0f, tween(350), label = "cardFlip")

    Box(
        modifier
            .padding(horizontal = 20.dp)
            .fillMaxWidth()
            .height(132.dp)
            .graphicsLayer { rotationY = rotation; cameraDistance = 12f * density }
            .shadow(30.dp, RoundedCornerShape(18.dp), spotColor = MonzoColors.Coral.copy(alpha = 0.4f))
            .clip(RoundedCornerShape(18.dp))
            .background(
                Brush.linearGradient(listOf(Color(0xFFFF3464), Color(0xFFFF5A7F), Color(0xFFE02855)))
            )
            .clickable { flipped = !flipped }
            .padding(20.dp),
    ) {
        Text("MONZO", style = MonzoText.Label.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp),
            color = Color.White.copy(alpha = 0.9f), modifier = Modifier.align(Alignment.TopEnd))
        Column(Modifier.align(Alignment.CenterStart)) {
            Text(balance, style = MonzoText.Balance, color = Color.White)
            Text("Available to spend", style = MonzoText.Label, color = Color.White.copy(alpha = 0.85f))
        }
        Text(maskedNumber, style = MonzoText.CardNumber, color = Color.White.copy(alpha = 0.9f),
            modifier = Modifier.align(Alignment.BottomStart))
    }
}

Transaction Row

@Composable
fun TransactionRow(
    emoji: String,
    tint: Color,
    merchant: String,
    category: String,
    time: String,
    amount: String,
    isIncome: Boolean = false,
    isPending: Boolean = false,
) {
    Row(
        Modifier
            .fillMaxWidth()
            .alpha(if (isPending) 0.6f else 1f)
            .padding(horizontal = 20.dp, vertical = 10.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(14.dp),
    ) {
        Box(
            Modifier.size(42.dp).clip(CircleShape).background(tint.copy(alpha = 0.18f)),
            contentAlignment = Alignment.Center,
        ) { Text(emoji, fontSize = 20.sp) }

        Column(Modifier.weight(1f)) {
            Text(merchant, style = MonzoText.Merchant, color = MonzoColors.DarkTextPrimary)
            Text("$category · $time", style = MonzoText.Meta, color = MonzoColors.DarkTextSecondary)
        }
        Text(
            amount, style = MonzoText.Amount,
            color = if (isIncome) MonzoColors.Income else MonzoColors.DarkTextPrimary,
        )
    }
}

Pot Coin

@Composable
fun PotCoin(emoji: String, name: String, amount: String, gradient: List<Color>) {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Box(
            Modifier
                .size(92.dp)
                .shadow(16.dp, RoundedCornerShape(16.dp), spotColor = Color.Black.copy(alpha = 0.2f))
                .clip(RoundedCornerShape(16.dp))
                .background(Brush.linearGradient(gradient)),
            contentAlignment = Alignment.Center,
        ) { Text(emoji, fontSize = 30.sp) }
        Spacer(Modifier.height(8.dp))
        Text(name, style = MonzoText.Label, color = MonzoColors.DarkTextPrimary)
        Text(amount, style = MonzoText.Label.copy(fontSize = 11.sp), color = MonzoColors.DarkTextSecondary)
    }
}

Pot Card (detail / list)

import androidx.compose.foundation.shape.CircleShape

@Composable
fun PotCard(emoji: String, name: String, saved: String, goal: String, progress: Float) {
    Row(
        Modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(16.dp))
            .background(MonzoColors.DarkSurface2)
            .padding(16.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(14.dp),
    ) {
        Box(
            Modifier.size(48.dp).clip(RoundedCornerShape(12.dp))
                .background(Brush.linearGradient(listOf(MonzoColors.Mint, MonzoColors.MintLight))),
            contentAlignment = Alignment.Center,
        ) { Text(emoji, fontSize = 22.sp) }

        Column(Modifier.weight(1f)) {
            Text(name, style = MonzoText.Merchant, color = MonzoColors.DarkTextPrimary)
            Text("$saved of $goal goal", style = MonzoText.Label, color = MonzoColors.DarkTextSecondary)
            Spacer(Modifier.height(8.dp))
            Box(Modifier.fillMaxWidth().height(6.dp).clip(RoundedCornerShape(3.dp)).background(MonzoColors.DarkDivider)) {
                Box(Modifier.fillMaxWidth(progress).height(6.dp).clip(RoundedCornerShape(3.dp)).background(MonzoColors.Mint))
            }
        }
    }
}

Buttons

import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.OutlinedButton

@Composable
fun MonzoPrimaryButton(title: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
    Button(
        onClick = onClick,
        modifier = modifier.fillMaxWidth().heightIn(min = 48.dp),
        shape = RoundedCornerShape(14.dp),
        colors = ButtonDefaults.buttonColors(containerColor = MonzoColors.Coral, contentColor = Color.White),
    ) { Text(title, style = MonzoText.Button) }
}

@Composable
fun MonzoPillButton(title: String, onClick: () -> Unit) {
    OutlinedButton(
        onClick = onClick,
        shape = RoundedCornerShape(500.dp),
        border = androidx.compose.foundation.BorderStroke(1.5.dp, MonzoColors.Coral),
    ) { Text(title, style = MonzoText.Label.copy(fontWeight = FontWeight.SemiBold, fontSize = 14.sp), color = MonzoColors.Coral) }
}

4. Navigation

Monzo has a 5-tab bottom strip with a Hot Coral active state and no tint pill — active is just the coral icon + label.

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*

@Composable
fun MonzoBottomBar(selected: Int, onSelect: (Int) -> Unit) {
    NavigationBar(containerColor = MonzoColors.DarkCanvas, tonalElevation = 0.dp) {
        val items = listOf(
            "Home"     to Icons.Filled.Home,
            "Payments" to Icons.Filled.CreditCard,
            "Pots"     to Icons.Filled.GridView,
            "Trends"   to Icons.Filled.TrendingUp,
            "Account"  to Icons.Filled.AccountCircle,
        )
        items.forEachIndexed { i, (label, icon) ->
            NavigationBarItem(
                selected = selected == i,
                onClick = { onSelect(i) },
                icon = { Icon(icon, contentDescription = label, modifier = Modifier.size(22.dp)) },
                label = { Text(label, style = MonzoText.Tab) },
                colors = NavigationBarItemDefaults.colors(
                    selectedIconColor   = MonzoColors.Coral,
                    selectedTextColor   = MonzoColors.Coral,
                    unselectedIconColor = MonzoColors.DarkTextTertiary,
                    unselectedTextColor = MonzoColors.DarkTextTertiary,
                    indicatorColor      = Color.Transparent, // no Material pill — Monzo has none
                ),
            )
        }
    }
}

The Home screen is a LazyColumn: card hero (item), Pots LazyRow (item), then day-grouped transaction rows with sticky day-header rows (stickyHeader). The large-title header collapses on scroll via a TopAppBar + nestedScroll.

5. Motion

Monzo motion is confident and physical — the card flips, coins spring into Pots, numbers tick.

Moment Compose recipe
Card flip to controls animateFloatAsState 0 → 180° tween(350) on graphicsLayer { rotationY } with cameraDistance
Pot money move drag a coin via Modifier.draggable, animateOffsetAsState spring (dampingRatio = 0.7f) into the Pot; balance via animateIntAsState ticker
Feed insert new row AnimatedVisibility slideInVertically { -it } + fadeIn(tween(250)); highlight wash tween(800) fade-out
Pull-to-refresh PullToRefreshBox with MonzoColors.Coral indicator
Tab change instant color crossfade (NavigationBar handles); ~120ms feel
Page navigation Nav3/NavHost slide push tween(300)
Number ticker animateIntAsState(targetBalancePence, tween(250)) then format to currency
// Balance ticker — the canonical Monzo "money moved" motion
val shown by animateIntAsState(balancePence, tween(250), label = "balanceTick")
Text(formatCurrency(shown), style = MonzoText.Balance, color = Color.White)

Haptics: prefer LocalHapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) as the soft impact on Pot money move and card flip. For payment success use view.performHapticFeedback(HapticFeedbackConstants.CONFIRM). Tab change gets a light CLOCK_TICK.

6. Icons

Monzo’s category glyphs are emoji (user/merchant data), not icon-font icons. UI chrome maps to androidx.compose.material:material-icons-extended.

Purpose SF Symbol (iOS) Material Icon (Compose)
Home (tab) house.fill Icons.Filled.Home
Payments (tab) creditcard.fill Icons.Filled.CreditCard
Pots (tab) circle.grid.2x2.fill Icons.Filled.GridView
Trends (tab) chart.line.uptrend.xyaxis Icons.Filled.TrendingUp
Account (tab) person.crop.circle.fill Icons.Filled.AccountCircle
Search magnifyingglass Icons.Filled.Search
Add money plus Icons.Filled.Add
Freeze card snowflake Icons.Filled.AcUnit
Split bill person.2 Icons.Filled.Group
Map pin mappin.circle.fill Icons.Filled.LocationOn
Receipt doc.text Icons.Filled.Receipt
Notes square.and.pencil Icons.Filled.Edit
Back chevron.left Icons.Filled.ArrowBack
Settings gearshape Icons.Filled.Settings
Income arrow.down.left Icons.Filled.SouthWest
Pending clock Icons.Filled.Schedule

7. Minimum SDK & Accessibility Notes

  • minSdk 24 (Compose floor is 21; graphicsLayer rotation + modern motion comfortable at 24). compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+.
  • Edge-to-edge: call enableEdgeToEdge(); deep-navy canvas wants light-content system bars. The card hero and feed respect the camera cutout; sheets respect the IME with Modifier.imePadding().
  • Tabular figures: set fontFeatureSettings = "tnum" on balance and amount styles so money columns align; verify on your bundled Inter.
  • Font scaling: sp honors the user’s font scale — keep it on screen title, balance, section, body, merchant, meta. Pin layout-sensitive text (10sp tab labels, card number, Pot-coin captions) by deriving from dp or a fixed-fontScale CompositionLocalProvider.
  • TalkBack: label transaction rows “{merchant}, {category}, {amount}, {time}{, pending}”; the card hero is a button — “Current account, balance {amount}, double-tap to show card details”; expose Pot progress as a progressBarRangeInfo.
  • Touch targets: Material guidance is 48.dp. Transaction rows are full-row tappable (≥56.dp effective); give 22.dp tab icons a 48.dp hit area; primary buttons ≥ 48.dp.
  • Contrast: #14233C on #F6F7F9 and #F4F6F9 on #0E1620 pass WCAG AA for body. The category-tint circles use ~18% alpha fills behind a solid emoji — fine; validate any solid-colored chip text (especially gold) with a contrast checker.
  • Reduce motion: when Settings.Global.ANIMATOR_DURATION_SCALE == 0, disable the card flip (Crossfade between faces), the Pot coin spring (snap to target), and the balance ticker (set value directly); keep the income/spend color cue.
  • Dark mode: invert via the Dark* palette — #0E1620 deep navy, NOT true black; #14233C is the surface; #F4F6F9 text. Shadows are weak on dark, so add a 1dp DarkDivider border to sheets as the elevation cue. Do not enable dynamicColorScheme() — Hot Coral is the brand and must hold regardless of wallpaper.
All mobile designs →

Brand names describe the visual style only; coderef is not affiliated with or endorsed by any brand listed. Licenses