DoorDash (iOS) — Jetpack Compose Implementation Guide
Companion to DESIGN.md — the framework-neutral spec. This file ports DoorDash’s visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, paste-ready @Composables, Modifiers, 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 (DoorDash’s appetizing white canvas, single aggressive red CTA, photo-first merchant cards, the floating checkout button) while making everything idiomatic Android —
NavigationBarinstead of a UITabBar, Coil instead ofAsyncImage,sp/dpinstead ofpt.
Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, and Coil 2.6+ — DoorDash is photo-first, so Coil for food imagery is required.
1. Color Tokens
// ui/theme/DoorDashColors.kt
import androidx.compose.ui.graphics.Color
object DoorDashColors {
// Brand — the "spend money" color
val Red = Color(0xFFEB1700)
val RedPressed = Color(0xFFC31500)
val RedTint = Color(0xFFFFEBE8) // promo banners — NOT full red
// Canvas & Text
val Canvas = Color(0xFFFFFFFF)
val Charcoal = Color(0xFF191919) // primary text — NOT pure black
val TextSecondary = Color(0xFF757575)
val TextTertiary = Color(0xFFAFAFAF)
val Divider = Color(0xFFE5E5E5)
val SurfaceMuted = Color(0xFFF7F7F7)
val SurfaceTint = Color(0xFFFAFAFA)
// Semantic — orange + green are the ONLY semantic chromas
val RatingOrange = Color(0xFFFF8000)
val FeeGreen = Color(0xFF008B4A)
val Warning = Color(0xFFF5B800)
val Error = Color(0xFFD1350F)
val InfoBlue = Color(0xFF0066E3)
val DashPass = Color(0xFF006B82)
// Dark mode (NOT true black — keeps food appetizing)
val DarkBackground = Color(0xFF191919)
val DarkSurface1 = Color(0xFF262626)
val DarkSurface2 = Color(0xFF303030)
val DarkDivider = Color(0xFF3A3A3A)
}
DoorDash is white-canvas first; wire a lightColorScheme with a warm-#191919 dark counterpart. DoorDash Red is reserved for interactive commit moments — never a default container fill on non-actionable surfaces.
// ui/theme/Theme.kt
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
private val DoorDashLight = lightColorScheme(
primary = DoorDashColors.Red,
onPrimary = DoorDashColors.Canvas,
background = DoorDashColors.Canvas,
onBackground = DoorDashColors.Charcoal,
surface = DoorDashColors.Canvas,
onSurface = DoorDashColors.Charcoal,
surfaceVariant = DoorDashColors.SurfaceMuted,
outline = DoorDashColors.Divider,
error = DoorDashColors.Error,
)
private val DoorDashDark = darkColorScheme(
primary = DoorDashColors.Red,
onPrimary = DoorDashColors.Canvas,
background = DoorDashColors.DarkBackground,
onBackground = DoorDashColors.Canvas,
surface = DoorDashColors.DarkSurface1,
onSurface = DoorDashColors.Canvas,
surfaceVariant = DoorDashColors.DarkSurface2,
outline = DoorDashColors.DarkDivider,
error = DoorDashColors.Error,
)
@Composable
fun DoorDashTheme(useDark: Boolean = false, content: @Composable () -> Unit) =
MaterialTheme(
colorScheme = if (useDark) DoorDashDark else DoorDashLight,
typography = DoorDashTypography,
content = content,
)
2. Typography
TT Norms Pro is proprietary (DoorDash’s brand face — no separate display family). Drop the TTFs in res/font/ (lowercase, snake_case). Fall back to the system sans (Roboto) and system monospace for order numbers / promo codes. Money is never light — every currency amount is weight 700.
// ui/theme/DoorDashType.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 TTNormsPro = FontFamily(
Font(R.font.tt_norms_pro_regular, FontWeight.Normal),
Font(R.font.tt_norms_pro_medium, FontWeight.Medium),
Font(R.font.tt_norms_pro_semibold, FontWeight.SemiBold),
Font(R.font.tt_norms_pro_bold, FontWeight.Bold),
)
private const val TNUM = "tnum" // tabular figures on prices
// Named ramp — mirrors DESIGN.md §3 exactly (pt → sp 1:1)
object DoorDashText {
val HeroTitle = TextStyle(TTNormsPro, fontWeight = FontWeight.Bold, fontSize = 28.sp, lineHeight = 32.sp, letterSpacing = (-0.4).sp)
val SectionTitle = TextStyle(TTNormsPro, fontWeight = FontWeight.Bold, fontSize = 22.sp, lineHeight = 26.sp, letterSpacing = (-0.3).sp)
val Merchant = TextStyle(TTNormsPro, fontWeight = FontWeight.Bold, fontSize = 18.sp, lineHeight = 23.sp, letterSpacing = (-0.2).sp)
val MenuItem = TextStyle(TTNormsPro, fontWeight = FontWeight.SemiBold, fontSize = 17.sp, lineHeight = 22.sp, letterSpacing = (-0.15).sp)
val Body = TextStyle(TTNormsPro, fontWeight = FontWeight.Normal, fontSize = 15.sp, lineHeight = 21.sp)
val PriceLarge = TextStyle(TTNormsPro, fontWeight = FontWeight.Bold, fontSize = 20.sp, lineHeight = 20.sp, letterSpacing = (-0.2).sp, fontFeatureSettings = TNUM)
val PriceBody = TextStyle(TTNormsPro, fontWeight = FontWeight.SemiBold, fontSize = 15.sp, lineHeight = 18.sp, fontFeatureSettings = TNUM)
val Meta = TextStyle(TTNormsPro, fontWeight = FontWeight.Normal, fontSize = 13.sp, lineHeight = 17.sp)
val Chip = TextStyle(TTNormsPro, fontWeight = FontWeight.SemiBold, fontSize = 14.sp, lineHeight = 14.sp)
val ButtonCTA = TextStyle(TTNormsPro, fontWeight = FontWeight.Bold, fontSize = 16.sp, lineHeight = 16.sp)
val ButtonSmall = TextStyle(TTNormsPro, fontWeight = FontWeight.SemiBold, fontSize = 14.sp, lineHeight = 14.sp)
val Tab = TextStyle(TTNormsPro, fontWeight = FontWeight.SemiBold, fontSize = 11.sp, lineHeight = 11.sp, letterSpacing = 0.1.sp)
val PromoBadge = TextStyle(TTNormsPro, fontWeight = FontWeight.Bold, fontSize = 11.sp, lineHeight = 11.sp, letterSpacing = 0.3.sp)
val NavTitle = TextStyle(TTNormsPro, fontWeight = FontWeight.Bold, fontSize = 17.sp, lineHeight = 19.sp, letterSpacing = (-0.1).sp)
val NavLarge = TextStyle(TTNormsPro, fontWeight = FontWeight.Bold, fontSize = 34.sp, lineHeight = 37.sp, letterSpacing = (-0.5).sp)
}
val DoorDashTypography = Typography(
displaySmall = DoorDashText.NavLarge,
headlineLarge = DoorDashText.HeroTitle,
titleLarge = DoorDashText.SectionTitle,
titleMedium = DoorDashText.Merchant,
bodyMedium = DoorDashText.Body,
labelSmall = DoorDashText.Tab,
)
3. Signature Components
Floating Checkout CTA (the app’s whole point)
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
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.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp
import java.text.NumberFormat
@Composable
fun FloatingCheckoutCTA(
itemCount: Int,
total: Double,
onTap: () -> 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.7f, stiffness = 600f), label = "cta")
val usd = NumberFormat.getCurrencyInstance()
Row(
modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.scale(scale)
.shadow(24.dp, RoundedCornerShape(28.dp), spotColor = DoorDashColors.Red.copy(alpha = 0.35f))
.clip(RoundedCornerShape(28.dp))
.background(if (pressed) DoorDashColors.RedPressed else DoorDashColors.Red)
.clickable(interaction, indication = null, onClick = onTap)
.height(52.dp)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (itemCount >= 1) {
Box(
Modifier.defaultMinSize(22.dp, 22.dp).clip(CircleShape).background(Color.White).padding(horizontal = 4.dp),
contentAlignment = Alignment.Center,
) { Text("$itemCount", style = DoorDashText.PromoBadge, color = DoorDashColors.Red) }
Spacer(Modifier.width(8.dp))
}
Text("Checkout", style = DoorDashText.ButtonCTA, color = Color.White)
Spacer(Modifier.weight(1f))
Text(usd.format(total), style = DoorDashText.ButtonCTA, color = Color.White)
}
}
Merchant Card (photo-first — never a placeholder)
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.Icon
import androidx.compose.ui.layout.ContentScale
import coil.compose.AsyncImage
@Composable
fun MerchantCard(
name: String,
rating: Double,
deliveryMinutes: Int,
deliveryFee: String,
hasFreeDelivery: Boolean,
promoBadge: String?,
photoUrl: String,
isSaved: Boolean,
onToggleSave: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier
.width(260.dp)
.shadow(12.dp, RoundedCornerShape(12.dp), spotColor = Color.Black.copy(alpha = 0.06f))
.background(DoorDashColors.Canvas),
) {
Box {
AsyncImage(
model = photoUrl,
contentDescription = name,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(16f / 10f)
.clip(RoundedCornerShape(12.dp)),
)
promoBadge?.let {
Text(
it.uppercase(),
style = DoorDashText.PromoBadge,
color = DoorDashColors.Red,
modifier = Modifier
.padding(8.dp)
.clip(RoundedCornerShape(4.dp))
.background(DoorDashColors.RedTint)
.padding(horizontal = 7.dp, vertical = 3.dp),
)
}
Icon(
imageVector = if (isSaved) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
contentDescription = if (isSaved) "Remove from saved" else "Save",
tint = if (isSaved) DoorDashColors.Red else Color.Black,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(8.dp)
.size(32.dp)
.clip(CircleShape)
.background(Color.White.copy(alpha = 0.85f)) // white scrim keeps icon legible
.clickable(onClick = onToggleSave)
.padding(7.dp),
)
}
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(name, style = DoorDashText.Merchant, color = DoorDashColors.Charcoal)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Icon(Icons.Filled.Star, null, tint = DoorDashColors.RatingOrange, modifier = Modifier.size(12.dp))
Text("%.1f".format(rating), style = DoorDashText.Meta, color = DoorDashColors.Charcoal)
Text("· $deliveryMinutes min", style = DoorDashText.Meta, color = DoorDashColors.TextSecondary)
if (hasFreeDelivery) {
Text("· \$0 Delivery", style = DoorDashText.Meta.copy(fontWeight = FontWeight.SemiBold), color = DoorDashColors.FeeGreen)
} else {
Text("· $deliveryFee", style = DoorDashText.Meta, color = DoorDashColors.TextSecondary)
}
}
}
}
}
Category Chip
@Composable
fun CategoryChip(emoji: String, label: String, isActive: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
Row(
modifier
.clip(RoundedCornerShape(20.dp))
.background(if (isActive) DoorDashColors.Charcoal else DoorDashColors.SurfaceMuted)
.clickable(onClick = onClick)
.padding(horizontal = 14.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
Text(emoji, style = DoorDashText.Chip)
Text(label, style = DoorDashText.Chip, color = if (isActive) Color.White else DoorDashColors.Charcoal)
}
}
Promo Badge
@Composable
fun PromoBadge(text: String, modifier: Modifier = Modifier) {
Text(
text.uppercase(),
style = DoorDashText.PromoBadge,
color = DoorDashColors.Red,
modifier = modifier
.clip(RoundedCornerShape(4.dp))
.background(DoorDashColors.RedTint)
.padding(horizontal = 8.dp, vertical = 4.dp),
)
}
Quantity Stepper
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Remove
@Composable
fun QuantityStepper(count: Int, onCountChange: (Int) -> Unit, min: Int = 0, max: Int = 99, modifier: Modifier = Modifier) {
Row(modifier, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Box(
Modifier.size(28.dp).clip(CircleShape).background(DoorDashColors.SurfaceMuted)
.clickable(enabled = count > min) { onCountChange(count - 1) },
contentAlignment = Alignment.Center,
) {
Icon(Icons.Filled.Remove, "Decrease", tint = if (count > min) DoorDashColors.Charcoal else DoorDashColors.TextTertiary, modifier = Modifier.size(14.dp))
}
Text("$count", style = DoorDashText.PriceLarge.copy(fontSize = 16.sp), color = DoorDashColors.Charcoal, modifier = Modifier.defaultMinSize(20.dp))
Box(
Modifier.size(28.dp).clip(CircleShape).background(DoorDashColors.SurfaceMuted)
.clickable(enabled = count < max) { onCountChange(count + 1) },
contentAlignment = Alignment.Center,
) {
Icon(Icons.Filled.Add, "Increase", tint = DoorDashColors.Charcoal, modifier = Modifier.size(14.dp))
}
}
}
4. Add-to-Cart Pulse & Order-Placed Confetti (the signature dynamic)
DoorDash has no color extraction — its distinctive dynamic system is the add-to-cart pulse driving the floating CTA into existence, and the full-screen red/orange confetti + success haptic on order placed. Animate the cart badge with a spring bounce, then explode confetti from the top on confirmation.
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.Canvas
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.drawscope.rotate
import kotlinx.coroutines.launch
import kotlin.random.Random
// Cart-badge bounce on add-to-cart
@Composable
fun rememberCartBadgeBump(itemCount: Int): Float {
val scale = remember { Animatable(1f) }
LaunchedEffect(itemCount) {
if (itemCount > 0) {
scale.animateTo(1.25f, spring(dampingRatio = 0.45f, stiffness = 700f))
scale.animateTo(1f, spring(dampingRatio = 0.6f, stiffness = 500f))
}
}
return scale.value
}
// Full-screen order-placed confetti — DoorDash red + rating orange
@Composable
fun OrderPlacedConfetti(visible: Boolean, modifier: Modifier = Modifier) {
if (!visible) return
val haptics = LocalHapticFeedback.current
LaunchedEffect(Unit) { haptics.performHapticFeedback(HapticFeedbackType.LongPress) } // ~iOS .success
data class Piece(val x: Float, val color: Color, val delay: Int, val spin: Float)
val palette = listOf(DoorDashColors.Red, DoorDashColors.RatingOrange, DoorDashColors.FeeGreen)
val pieces = remember {
List(14) { Piece(Random.nextFloat(), palette.random(), Random.nextInt(0, 220), Random.nextFloat() * 360f) }
}
val progress = remember { Animatable(0f) }
LaunchedEffect(Unit) { progress.animateTo(1f, androidx.compose.animation.core.tween(1200)) }
Canvas(modifier.fillMaxSize()) {
pieces.forEach { p ->
val t = ((progress.value * 1200f - p.delay) / 1200f).coerceIn(0f, 1f)
val y = t * (size.height + 40f) - 20f
rotate(p.spin * t, pivot = Offset(p.x * size.width, y)) {
drawRoundRect(
color = p.color,
topLeft = Offset(p.x * size.width - 4f, y),
size = androidx.compose.ui.geometry.Size(8.dp.toPx(), 14.dp.toPx()),
cornerRadius = androidx.compose.ui.geometry.CornerRadius(2.dp.toPx()),
)
}
}
}
}
Add-to-cart button pulse: scale 0.95 → 1.05 → 1.0 over 250ms with a success haptic — pair it with the cart-badge bump above.
5. Navigation
DoorDash uses a 5-tab bottom bar (Home / Grocery / Offers / Orders / Account) with always-visible labels — DoorDash labels its tabs. Active tint is DoorDash Red with a filled icon variant. The bar is solid (not translucent — Android has no live blur anyway); the floating checkout CTA hovers above it whenever the cart is non-empty.
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.LocalGroceryStore
import androidx.compose.material.icons.filled.LocalOffer
import androidx.compose.material.icons.filled.Receipt
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults
@Composable
fun DoorDashBottomBar(selected: Int, cartCount: Int, onSelect: (Int) -> Unit) {
NavigationBar(containerColor = DoorDashColors.Canvas, tonalElevation = 0.dp) {
val items = listOf(
"Home" to Icons.Filled.Home,
"Grocery" to Icons.Filled.LocalGroceryStore,
"Offers" to Icons.Filled.LocalOffer,
"Orders" to Icons.Filled.Receipt,
"Account" to Icons.Filled.Person,
)
items.forEachIndexed { i, (label, icon) ->
NavigationBarItem(
selected = selected == i,
onClick = { onSelect(i) },
icon = {
Box(contentAlignment = Alignment.TopEnd) {
Icon(icon, contentDescription = label, modifier = Modifier.size(24.dp))
if (label == "Orders" && cartCount > 0) {
Box(
Modifier.offset(x = 8.dp, y = (-4).dp).defaultMinSize(16.dp, 16.dp)
.clip(CircleShape).background(DoorDashColors.Red).padding(horizontal = 4.dp),
contentAlignment = Alignment.Center,
) { Text("$cartCount", style = DoorDashText.Tab, color = Color.White) }
}
}
},
label = { Text(label, style = DoorDashText.Tab) },
colors = NavigationBarItemDefaults.colors(
selectedIconColor = DoorDashColors.Red,
selectedTextColor = DoorDashColors.Red,
unselectedIconColor = DoorDashColors.TextSecondary,
unselectedTextColor = DoorDashColors.TextSecondary,
indicatorColor = Color.Transparent,
),
)
}
}
}
6. Motion
| Moment | Compose recipe |
|---|---|
| Card entrance | fadeIn + slideInVertically { it / 8 }, ≈300ms ease-out |
| Add-to-cart | button scale 0.95 → 1.05 → 1.0 via Animatable (~250ms); HapticFeedbackType.LongPress (~iOS .success) |
| Cart badge update | Animatable spring bounce 1 → 1.25 → 1 (dampingRatio = 0.45f) |
| Carousel snap | LazyRow + rememberSnapFlingBehavior (rubber-band edge) |
| Order placed | full-screen Canvas confetti for 1.2s + success haptic + checkmark scale-in |
| CTA press | animateFloatAsState 1 → 0.98 with spring(stiffness ≈ 600f) |
// Carousel with snap + cards bleeding past the right edge ("there's more, swipe")
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
import androidx.compose.foundation.lazy.rememberLazyListState
val state = rememberLazyListState()
LazyRow(
state = state,
flingBehavior = rememberSnapFlingBehavior(state),
contentPadding = PaddingValues(start = 16.dp, end = 48.dp), // last card peeks ~30%
horizontalArrangement = Arrangement.spacedBy(12.dp),
) { /* MerchantCard items */ }
Haptics: prefer LocalHapticFeedback. For richer control use LocalView.current.performHapticFeedback(HapticFeedbackConstants.CONFIRM) (API 30+) or a Vibrator VibrationEffect.createOneShot(...) for the order-placed .success notification feel.
7. Icons
DoorDash leans on SF Symbol equivalents; the closest first-party set is androidx.compose.material:material-icons-extended. The red DoorDash delivery pin / logo ships as a vector drawable loaded via ImageVector.vectorResource(R.drawable.…).
| Purpose | SF Symbol (iOS) | Material Icon (Compose) |
|---|---|---|
| Search | magnifyingglass |
Icons.Filled.Search |
| Heart (save) | heart / heart.fill |
Icons.Outlined.FavoriteBorder / Icons.Filled.Favorite |
| Star (rating) | star.fill |
Icons.Filled.Star (tinted #FF8000) |
| Home tab | house.fill |
Icons.Filled.Home |
| Grocery tab | bag.fill |
Icons.Filled.LocalGroceryStore |
| Offers tab | percent |
Icons.Filled.LocalOffer |
| Orders tab | bag |
Icons.Filled.Receipt |
| Account tab | person.fill |
Icons.Filled.Person |
| Cart | bag / bag.fill |
Icons.Filled.ShoppingBag |
| Location | mappin.and.ellipse |
Icons.Filled.LocationOn |
| More | ellipsis |
Icons.Filled.MoreHoriz |
| Delivery | bicycle / car.fill |
Icons.Filled.DirectionsBike / Icons.Filled.DirectionsCar |
| DashPass | sparkles |
Icons.Filled.AutoAwesome |
| Stepper minus / plus | minus / plus |
Icons.Filled.Remove / Icons.Filled.Add |
| Share | square.and.arrow.up |
Icons.Filled.IosShare |
| Delivery pin | (custom red pin) | Vector drawable — DoorDash logo embedded |
8. Minimum SDK & Accessibility Notes
- minSdk 24 (Compose floor is 21; Coil + modern motion are comfortable at 24).
compileSdk 34+, Compose BOM2024.09+, Kotlin2.0+. - Edge-to-edge: call
enableEdgeToEdge()in theActivity. White canvas wants dark-content system bars (isAppearanceLightStatusBars = true); the floating checkout CTA must useModifier.windowInsetsPadding(WindowInsets.navigationBars)so it hovers 16dp above the gesture nav and never collides with it. - Font scaling:
sphonors the user’s font scale — keep it on merchant names, prices, body. Cap price numerals at 140% (fontScale.coerceAtMost(1.4f)) and category chips at 140% to prevent wrap breakage; tab labels scale to 14sp max. Prices carryfontFeatureSettings = "tnum"so amounts stay column-aligned. - TalkBack: the checkout CTA is one node —
Modifier.semantics(mergeDescendants = true) { contentDescription = "Checkout, 2 items, total \$23.40" }. Merchant card merges name + rating + delivery meta; the save heart is a separate toggle with state. - Touch targets: Material guidance is 48dp minimum. The 52dp CTA is huge; the 28dp stepper circles and 32dp save heart need 48dp hit area via padding; category chips clear with
minHeight = 32.dpplus padding. - Contrast:
#191919on white exceeds WCAG AAA; DoorDash Red#EB1700on white clears AA at 16sp bold (button text only). The real low-contrast pair is#757575TextSecondary on white — passes AA at 14sp+, validate at 11sp tab/promo labels and darken if targeting compliance. Pair#FF8000rating and#008B4A$0-fee with their glyphs so they read without color. - Reduce motion: when
Settings.Global.ANIMATOR_DURATION_SCALE == 0f, skip the order-placed confetti and the cart-badge bounce — show a static checkmark instead. - Dynamic color: do not enable Material You
dynamicLightColorScheme()— DoorDash Red#EB1700is the brand’s “spend money” signal and must not shift with wallpaper. Strong-brand app: keep the curated scheme.