coderef

Domino's

Domino's iOS app is built around a single, famous moment of anticipation: the pizza tracker. The canvas is clean white (#FFFFFF) with a light gray surface (#F4F4F4) chunking content into clear, scannable blocks — the app's job is to get an…
DownloadRaw
Domino's

Build with the Domino's look.

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

Palette

At a glance

Theme
light
Typefaces
Archivo
Sections
8
Length
3,348 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · Domino's

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

Domino’s (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports Domino’s visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, paste-ready @Composables, Modifiers, motion, and haptics — including the signature five-stage pizza tracker.

Why a Compose guide for an iOS-referenced app? The DESIGN.md tokens are platform-neutral. This file keeps the visual identity (Domino’s bright white canvas, red-action / blue-info split, the five-stage tracker, the build-your-pizza preview, the domino-square mark) while making everything idiomatic Android — NavigationBar instead of a UITabBar, dp/sp instead of pt, Compose haptics instead of UIImpactFeedbackGenerator.

Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, Coil 2.6+ for images.

1. Color Tokens

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

object DominosColors {
    // Canvas & Surfaces
    val Canvas      = Color(0xFFFFFFFF)
    val Surface     = Color(0xFFF4F4F4)
    val SurfaceDeep = Color(0xFFEAEAEA)
    val Divider     = Color(0xFFE2E2E2)

    // Text
    val TextPrimary   = Color(0xFF1F1F1F)
    val TextSecondary = Color(0xFF6E6E6E)
    val TextTertiary  = Color(0xFF9A9A9A)

    // Brand
    val Red         = Color(0xFFE31837)
    val RedPressed  = Color(0xFFC2122E)
    val RedTint     = Color(0xFFFCE7EB)
    val Blue        = Color(0xFF006491)
    val BluePressed = Color(0xFF00547A)
    val BlueTint    = Color(0xFFE0EEF4)

    // Semantic
    val Success = Color(0xFF1E8E3E)
    val Warning = Color(0xFFF5A623)
}

Wire it into a Material 3 lightColorScheme so ripples, dividers, and default component colors inherit the brand. Domino’s is light-only by design; do not provide a dark scheme unless mirroring system dark with identical brand colors.

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

private val DominosScheme = lightColorScheme(
    primary          = DominosColors.Red,
    onPrimary        = Color.White,
    secondary         = DominosColors.Blue,
    onSecondary       = Color.White,
    background        = DominosColors.Canvas,
    onBackground      = DominosColors.TextPrimary,
    surface           = DominosColors.Canvas,
    onSurface         = DominosColors.TextPrimary,
    surfaceVariant    = DominosColors.Surface,
    onSurfaceVariant  = DominosColors.TextSecondary,
    outline           = DominosColors.Divider,
    error             = DominosColors.Red,
)

@Composable
fun DominosTheme(content: @Composable () -> Unit) =
    MaterialTheme(colorScheme = DominosScheme, typography = DominosTypography, content = content)

2. Typography

Domino’s uses Archivo. Drop the TTFs in res/font/ (lowercase, snake_case) and build a FontFamily. Fall back to the system font (Roboto) — its grotesque tone is the closest free substitute.

// ui/theme/DominosType.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.unit.sp

val Archivo = FontFamily(
    Font(R.font.archivo_regular, FontWeight.Normal), // 400
    Font(R.font.archivo_bold,    FontWeight.Bold),   // 700
)

// Named ramp — mirrors DESIGN.md §3 exactly (pt → sp 1:1)
object DominosText {
    val ScreenTitle  = TextStyle(Archivo, fontWeight = FontWeight.Bold,   fontSize = 26.sp, lineHeight = 31.sp, letterSpacing = (-0.3).sp)
    val TrackerStage = TextStyle(Archivo, fontWeight = FontWeight.Bold,   fontSize = 22.sp, lineHeight = 26.sp, letterSpacing = (-0.2).sp)
    val Section      = TextStyle(Archivo, fontWeight = FontWeight.Bold,   fontSize = 18.sp, lineHeight = 23.sp, letterSpacing = (-0.2).sp)
    val ItemName     = TextStyle(Archivo, fontWeight = FontWeight.Bold,   fontSize = 17.sp, lineHeight = 22.sp)
    val Price        = TextStyle(Archivo, fontWeight = FontWeight.Bold,   fontSize = 16.sp, lineHeight = 19.sp)
    val Body         = TextStyle(Archivo, fontWeight = FontWeight.Normal, fontSize = 15.sp, lineHeight = 22.sp)
    val Button       = TextStyle(Archivo, fontWeight = FontWeight.Bold,   fontSize = 16.sp, lineHeight = 20.sp, letterSpacing = 0.3.sp)
    val DealPrice    = TextStyle(Archivo, fontWeight = FontWeight.Bold,   fontSize = 28.sp, lineHeight = 30.sp, letterSpacing = (-0.4).sp)
    val Meta         = TextStyle(Archivo, fontWeight = FontWeight.Normal, fontSize = 13.sp, lineHeight = 17.sp)
    val Badge        = TextStyle(Archivo, fontWeight = FontWeight.Bold,   fontSize = 11.sp, lineHeight = 13.sp, letterSpacing = 0.4.sp)
    val Tab          = TextStyle(Archivo, fontWeight = FontWeight.Bold,   fontSize = 11.sp, lineHeight = 13.sp, letterSpacing = 0.2.sp)
    val Caption      = TextStyle(Archivo, fontWeight = FontWeight.Normal, fontSize = 11.sp, lineHeight = 14.sp)
}

// Map onto Material 3 slots so stock components inherit the brand
val DominosTypography = Typography(
    headlineLarge = DominosText.ScreenTitle,
    headlineSmall = DominosText.Section,
    titleMedium   = DominosText.ItemName,
    bodyMedium    = DominosText.Body,
    labelSmall    = DominosText.Tab,
)

Render prices, ETAs, and deal callouts with tabular figures via TextStyle(fontFeatureSettings = "tnum").

3. Signature Components

Five-Stage Pizza Tracker

import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp

enum class TrackerStage(val label: String, val headline: String) {
    Prep("Prep", "We're prepping your order"),
    Bake("Bake", "Your pizza is in the oven!"),
    Box("Box", "Boxing it up"),
    Quality("Quality Check", "Quality check in progress"),
    Out("Out for delivery", "Out for delivery!"),
}

@Composable
fun PizzaTracker(current: TrackerStage, eta: String, modifier: Modifier = Modifier) {
    val stages = TrackerStage.entries
    Column(
        modifier
            .fillMaxWidth()
            .shadow(14.dp, RoundedCornerShape(12.dp), spotColor = DominosColors.TextPrimary.copy(alpha = 0.12f))
            .clip(RoundedCornerShape(12.dp))
            .background(DominosColors.Canvas)
            .padding(16.dp),
    ) {
        Text(current.headline, style = DominosText.TrackerStage, color = DominosColors.TextPrimary)
        Spacer(Modifier.height(16.dp))
        Row(verticalAlignment = Alignment.CenterVertically) {
            stages.forEachIndexed { i, stage ->
                Node(stage, completed = i < current.ordinal, active = i == current.ordinal)
                if (i < stages.lastIndex) {
                    Box(
                        Modifier
                            .weight(1f)
                            .height(4.dp)
                            .background(
                                when {
                                    i < current.ordinal -> DominosColors.Blue
                                    i == current.ordinal -> DominosColors.Red
                                    else -> DominosColors.Divider
                                }
                            ),
                    )
                }
            }
        }
        Spacer(Modifier.height(16.dp))
        Text("Estimated delivery $eta", style = DominosText.Meta, color = DominosColors.TextSecondary)
        Spacer(Modifier.height(8.dp))
        Text("Track on the map",
            style = DominosText.Meta.copy(fontWeight = FontWeight.Bold), color = DominosColors.Blue)
    }
}

@Composable
private fun Node(stage: TrackerStage, completed: Boolean, active: Boolean) {
    val pulse = rememberInfiniteTransition(label = "pulse")
    val scale by pulse.animateFloat(
        1f, 1.06f,
        infiniteRepeatable(tween(800), RepeatMode.Reverse),
        label = "nodeScale",
    )
    val fill = when { completed -> DominosColors.Blue; active -> DominosColors.Red; else -> Color.Transparent }
    val stroke = when { completed -> DominosColors.Blue; active -> DominosColors.Red; else -> DominosColors.Divider }

    Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.width(56.dp)) {
        Box(
            Modifier
                .size(24.dp)
                .scale(if (active) scale else 1f)
                .clip(CircleShape)
                .background(fill)
                .then(Modifier.border(2.dp, stroke, CircleShape)),
            contentAlignment = Alignment.Center,
        ) {
            if (completed) Icon(Icons.Filled.Check, contentDescription = null,
                tint = Color.White, modifier = Modifier.size(13.dp))
        }
        Spacer(Modifier.height(6.dp))
        Text(
            stage.label, maxLines = 1,
            style = DominosText.Caption.copy(
                fontWeight = if (completed || active) FontWeight.Bold else FontWeight.Normal),
            color = when {
                active -> DominosColors.Red
                completed -> DominosColors.TextPrimary
                else -> DominosColors.TextTertiary
            },
        )
    }
}

Primary CTA

import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState

@Composable
fun DominosCTA(label: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    val scale by animateFloatAsState(if (pressed) 0.98f else 1f,
        spring(dampingRatio = 0.8f), label = "ctaScale")
    Box(
        modifier
            .fillMaxWidth()
            .height(52.dp)
            .scale(scale)
            .clip(RoundedCornerShape(8.dp))
            .background(if (pressed) DominosColors.RedPressed else DominosColors.Red)
            .clickable(interaction, indication = null, onClick = onClick),
        contentAlignment = Alignment.Center,
    ) {
        Text(label, style = DominosText.Button, color = Color.White)
    }
}

Deal Card + Domino-Square Mark

@Composable
fun DominoMark(modifier: Modifier = Modifier) {
    Row(modifier.size(22.dp).clip(RoundedCornerShape(4.dp))) {
        Box(Modifier.weight(1f).fillMaxHeight().background(DominosColors.Red))
        Box(Modifier.width(1.dp).fillMaxHeight().background(Color.White))
        Box(Modifier.weight(1f).fillMaxHeight().background(DominosColors.Blue))
    }
}

@Composable
fun DealCard(price: String, name: String, finePrint: String, onAdd: () -> Unit) {
    Column(
        Modifier
            .fillMaxWidth()
            .height(150.dp)
            .clip(RoundedCornerShape(8.dp))
            .background(DominosColors.Canvas)
            .border(2.dp, DominosColors.Red, RoundedCornerShape(8.dp))
            .padding(16.dp),
    ) {
        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically) {
            Text(price, style = DominosText.DealPrice.copy(fontFeatureSettings = "tnum"),
                color = DominosColors.Red)
            DominoMark()
        }
        Spacer(Modifier.height(6.dp))
        Text(name, style = DominosText.ItemName, color = DominosColors.TextPrimary)
        Text(finePrint, style = DominosText.Meta, color = DominosColors.TextSecondary)
        Spacer(Modifier.weight(1f))
        Box(
            Modifier
                .align(Alignment.End)
                .clip(RoundedCornerShape(8.dp))
                .background(DominosColors.Red)
                .clickable(onClick = onAdd)
                .padding(horizontal = 16.dp, vertical = 8.dp),
        ) { Text("ADD DEAL", style = DominosText.Badge, color = Color.White) }
    }
}

Build-Your-Pizza Live Preview

import androidx.compose.animation.core.animateDpAsState

@Composable
fun PizzaPreview(
    crustWidth: Dp,
    sauceColor: Color,
    hasCheese: Boolean,
    modifier: Modifier = Modifier,
) {
    val ring by animateDpAsState(crustWidth, tween(250), label = "crust")
    Box(modifier.size(160.dp), contentAlignment = Alignment.Center) {
        Box(Modifier.fillMaxSize().clip(CircleShape).background(Color(0xFFEDD9A8))
            .border(ring, Color(0xFFCC9E5C), CircleShape))
        Box(Modifier.padding(ring + 6.dp).fillMaxSize().clip(CircleShape).background(sauceColor))
        AnimatedVisibility(hasCheese, enter = fadeIn() + scaleIn(), exit = fadeOut() + scaleOut()) {
            Box(Modifier.padding(ring + 10.dp).fillMaxSize().clip(CircleShape)
                .background(Color(0x99FADC73)))
        }
        // topping garnishes scattered as small Boxes around the pie
    }
}

4. Tracker State Progression

// Hold `current: TrackerStage` in a ViewModel as state. Advancing recomputes
// node/rail colors; the rail segment color change animates implicitly. For the
// ~600ms red-fill hand-off, drive a per-segment Animatable width or color and
// fire LocalHapticFeedback (soft) when the stage flips.

5. Bottom Navigation (Tab Bar)

Use Material 3 NavigationBar. Domino’s iOS tab bar is an opaque white surface with a hairline divider and a red active tint.

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

@Composable
fun DominosBottomBar(selected: Int, onSelect: (Int) -> Unit) {
    NavigationBar(containerColor = DominosColors.Canvas, tonalElevation = 0.dp) {
        val items = listOf(
            "Order"   to Icons.Filled.Restaurant,
            "Tracker" to Icons.Filled.LocationOn,
            "Deals"   to Icons.Filled.LocalOffer,
            "Account" to Icons.Filled.Person,
        )
        items.forEachIndexed { i, (label, icon) ->
            NavigationBarItem(
                selected = selected == i,
                onClick = { onSelect(i) },
                icon = { Icon(icon, contentDescription = label, modifier = Modifier.size(24.dp)) },
                label = { Text(label, style = DominosText.Tab) },
                colors = NavigationBarItemDefaults.colors(
                    selectedIconColor = DominosColors.Red,
                    selectedTextColor = DominosColors.Red,
                    unselectedIconColor = DominosColors.TextSecondary,
                    unselectedTextColor = DominosColors.TextSecondary,
                    indicatorColor = DominosColors.RedTint,
                ),
            )
        }
    }
}

The persistent order footer (running total + red CTA) sits above this bar — render DominosCTA in the Scaffold bottomBar slot stacked above DominosBottomBar, on a DominosColors.Canvas band with a top hairline.

6. Motion

Moment Compose recipe
Tracker stage advance per-segment rail color/width Animatable tween(600, FastOutSlowInEasing); HapticFeedbackType.LongPress (~soft)
Active node pulse rememberInfiniteTransition animateFloat 1 → 1.06 infiniteRepeatable(tween(800), Reverse)
Build-pizza layer add crust animateDpAsState(tween(250)); cheese AnimatedVisibility fadeIn()+scaleIn()
Add to Order CTA Modifier.scale 1 → 0.98; cart badge spring 1 → 1.2 → 1; success haptic
Card tap Modifier.scale 1 → 0.98 via collectIsPressedAsState()
Sheet present ModalBottomSheet default slide; scrim DominosColors.TextPrimary.copy(alpha = 0.45f)
// Cart-count badge bounce after add
val badge = remember { Animatable(1f) }
LaunchedEffect(cartCount) {
    if (cartCount > 0) { badge.animateTo(1.2f, spring(dampingRatio = 0.5f)); badge.animateTo(1f, spring()) }
}

Haptics: prefer LocalHapticFeedback. For the tracker stage hand-off, LocalView.current.performHapticFeedback(HapticFeedbackConstants.CONFIRM) (API 30+) approximates iOS .soft impact.

7. Icons

Domino’s ships custom glyphs; the closest first-party set is androidx.compose.material:material-icons-extended. For exact parity, export Domino’s glyphs as vector drawables and load via ImageVector.vectorResource(R.drawable.…).

Purpose SF Symbol (iOS) Material Icon (Compose)
Completed stage check checkmark Icons.Filled.Check
Order (tab) menucard Icons.Filled.Restaurant
Tracker (tab) location.circle Icons.Filled.LocationOn
Deals (tab) tag Icons.Filled.LocalOffer
Account (tab) person Icons.Filled.Person
Cart cart Icons.Filled.ShoppingCart
Search magnifyingglass Icons.Filled.Search
Store chevron chevron.down Icons.Filled.KeyboardArrowDown
Quantity minus / plus minus / plus Icons.Filled.Remove / Icons.Filled.Add
Map / track map Icons.Filled.Map
Time / ETA clock Icons.Filled.Schedule
Back chevron.left Icons.Filled.ArrowBackIosNew

8. Minimum SDK & Accessibility Notes

  • minSdk 24 (Compose floor is 21; modern motion + drawArc-style work are comfortable at 24). compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+.
  • Edge-to-edge: call enableEdgeToEdge() in the Activity; the white canvas wants dark system-bar icons via WindowCompat. Apply Scaffold insets so the order footer clears the gesture nav.
  • Font scaling: sp honors the user’s font scale automatically — keep it on item names and body. The tracker headline scales but caps at 2 lines; pin tab labels and tracker node labels (abbreviate on small widths) via dp-derived sizing.
  • TalkBack: expose the tracker as a single node — Modifier.semantics { role = Role.Image; contentDescription = "Order status: Bake, stage 2 of 5, ETA 7:45 PM" } — and skip individual nodes; the CTA announces “Add to order, total $X”.
  • Touch targets: Material guidance is 48.dp minimum. Build option rows are ~56.dp (clear); ensure the quantity − / + glyphs get 48.dp hit areas via padding; the “Track on the map” link needs a 48.dp tappable area.
  • Contrast: #1F1F1F on #FFFFFF is maximal. #6E6E6E on white passes WCAG AA at 13sp+. Domino’s Red #E31837 text on white passes AA for bold 16sp+ — keep small red text bold.
  • Color is not the only signal: tracker completed nodes carry a check glyph and labels switch to bold, in addition to the blue/red/gray coding. Do not enable Material You dynamicLightColorScheme() — the red/blue domino identity must be fixed 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