Chipotle (iOS) — Jetpack Compose Implementation Guide
Companion to DESIGN.md — the framework-neutral spec. This file ports Chipotle’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 (Chipotle’s cream kraft canvas, single brick-red accent, ALL-CAPS condensed headers, the build-your-burrito ingredient stepper, the rewards ring) while making everything idiomatic Android —
NavigationBarinstead of a UITabBar,dp/spinstead ofpt, Compose haptics instead ofUIImpactFeedbackGenerator.
Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, Coil 2.6+ for images.
1. Color Tokens
// ui/theme/ChipotleColors.kt
import androidx.compose.ui.graphics.Color
object ChipotleColors {
// Canvas & Surfaces
val Cream = Color(0xFFFFF5E1)
val Surface = Color(0xFFFFFFFF)
val CreamDeep = Color(0xFFF4E8D0)
val Divider = Color(0xFFE8DCC4)
// Text (brown, never black)
val TextPrimary = Color(0xFF451400)
val TextSecondary = Color(0xFF8A6B4F)
val TextTertiary = Color(0xFFB49A82)
// Brand
val Red = Color(0xFFA81612)
val RedPressed = Color(0xFF8C1210)
val RedTint = Color(0xFFF4E2E1)
// Support / Semantic
val Tan = Color(0xFFAC8C5B)
val Success = Color(0xFF3C7A3C)
val Spice = Color(0xFFE07B26)
}
Wire it into a Material 3 lightColorScheme so ripples, dividers, and default component colors inherit the brand. Chipotle 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 ChipotleScheme = lightColorScheme(
primary = ChipotleColors.Red,
onPrimary = Color.White,
background = ChipotleColors.Cream,
onBackground = ChipotleColors.TextPrimary,
surface = ChipotleColors.Surface,
onSurface = ChipotleColors.TextPrimary,
surfaceVariant = ChipotleColors.CreamDeep,
onSurfaceVariant = ChipotleColors.TextSecondary,
outline = ChipotleColors.Divider,
error = ChipotleColors.Red,
)
@Composable
fun ChipotleTheme(content: @Composable () -> Unit) =
MaterialTheme(colorScheme = ChipotleScheme, typography = ChipotleTypography, content = content)
2. Typography
Headers use Archivo (ALL CAPS); body uses a calm sans (Avenir is iOS-only — substitute the bundled brand body face or fall back to Roboto). Drop the TTFs in res/font/ (lowercase, snake_case).
// ui/theme/ChipotleType.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.style.TextStyle as _
import androidx.compose.ui.unit.sp
val Archivo = FontFamily(
Font(R.font.archivo_bold, FontWeight.Bold), // 700
Font(R.font.archivo_extrabold, FontWeight.ExtraBold), // 800
)
val BodySans = FontFamily(
Font(R.font.body_regular, FontWeight.Normal), // 400 (Avenir substitute)
Font(R.font.body_bold, FontWeight.Bold), // 700
)
// Named ramp — mirrors DESIGN.md §3 exactly (pt → sp 1:1). Apply ALL CAPS at call site.
object ChipotleText {
val Hero = TextStyle(Archivo, fontWeight = FontWeight.ExtraBold, fontSize = 32.sp, lineHeight = 34.sp, letterSpacing = 0.4.sp)
val ScreenTitle = TextStyle(Archivo, fontWeight = FontWeight.ExtraBold, fontSize = 26.sp, lineHeight = 29.sp, letterSpacing = 0.3.sp)
val Section = TextStyle(Archivo, fontWeight = FontWeight.Bold, fontSize = 18.sp, lineHeight = 21.sp, letterSpacing = 0.6.sp)
val Button = TextStyle(Archivo, fontWeight = FontWeight.ExtraBold, fontSize = 16.sp, lineHeight = 18.sp, letterSpacing = 0.8.sp)
val Points = TextStyle(Archivo, fontWeight = FontWeight.ExtraBold, fontSize = 34.sp, lineHeight = 36.sp)
val Badge = TextStyle(Archivo, fontWeight = FontWeight.Bold, fontSize = 11.sp, lineHeight = 13.sp, letterSpacing = 0.6.sp)
val ItemName = TextStyle(BodySans, fontWeight = FontWeight.Bold, fontSize = 17.sp, lineHeight = 22.sp)
val Price = TextStyle(BodySans, fontWeight = FontWeight.Bold, fontSize = 16.sp, lineHeight = 19.sp)
val Body = TextStyle(BodySans, fontWeight = FontWeight.Normal, fontSize = 15.sp, lineHeight = 22.sp)
val Meta = TextStyle(BodySans, fontWeight = FontWeight.Normal, fontSize = 13.sp, lineHeight = 17.sp)
val Tab = TextStyle(BodySans, fontWeight = FontWeight.Bold, fontSize = 11.sp, lineHeight = 13.sp, letterSpacing = 0.2.sp)
val Caption = TextStyle(BodySans, fontWeight = FontWeight.Normal, fontSize = 11.sp, lineHeight = 14.sp)
}
// Map onto Material 3 slots so stock components inherit the brand
val ChipotleTypography = Typography(
headlineLarge = ChipotleText.ScreenTitle,
headlineSmall = ChipotleText.Section,
titleMedium = ChipotleText.ItemName,
bodyMedium = ChipotleText.Body,
labelSmall = ChipotleText.Tab,
)
Render prices, calories, and points with tabular figures via TextStyle(fontFeatureSettings = "tnum"). Apply ALL CAPS with text = title.uppercase().
3. Signature Components
Section Header (ALL CAPS)
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun SectionHeader(title: String, modifier: Modifier = Modifier) {
Column(modifier.padding(top = 24.dp)) {
Text(title.uppercase(), style = ChipotleText.Section, color = ChipotleColors.TextPrimary)
Spacer(Modifier.height(8.dp))
HorizontalDivider(color = ChipotleColors.Divider, thickness = 1.dp)
}
}
Build-Your-Burrito Ingredient Row
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
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.ui.Alignment
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
@Composable
fun IngredientRow(
name: String,
meta: String?,
selected: Boolean,
multiSelect: Boolean = false,
onClick: () -> Unit,
) {
val haptics = LocalHapticFeedback.current
val shape = if (multiSelect) RoundedCornerShape(6.dp) else CircleShape
val size = if (multiSelect) 26.dp else 28.dp
Row(
Modifier
.fillMaxWidth()
.heightIn(min = 60.dp)
.background(if (selected) ChipotleColors.RedTint else ChipotleColors.Surface)
.then(if (selected) Modifier.drawLeftRule(ChipotleColors.Red) else Modifier)
.clickable {
haptics.performHapticFeedback(HapticFeedbackType.TextHandleMove)
onClick()
}
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Column(Modifier.weight(1f)) {
Text(name, style = ChipotleText.ItemName, color = ChipotleColors.TextPrimary)
if (meta != null) Text(meta, style = ChipotleText.Meta, color = ChipotleColors.TextSecondary)
}
Box(
Modifier
.size(size)
.clip(shape)
.background(if (selected) ChipotleColors.Red else Color.Transparent)
.border(2.dp, if (selected) ChipotleColors.Red else ChipotleColors.Divider, shape),
contentAlignment = Alignment.Center,
) {
AnimatedVisibility(selected, enter = scaleIn(), exit = scaleOut()) {
Icon(Icons.Filled.Check, contentDescription = null, tint = Color.White,
modifier = Modifier.size(16.dp))
}
}
}
}
// Small modifier: draw a 1.5dp red rule on the leading edge of a selected row
fun Modifier.drawLeftRule(color: Color) = drawBehind {
drawRect(color, size = androidx.compose.ui.geometry.Size(1.5.dp.toPx(), size.height))
}
Primary CTA (ALL CAPS)
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.ui.draw.scale
@Composable
fun ChipotleCTA(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(54.dp)
.scale(scale)
.clip(RoundedCornerShape(8.dp))
.background(if (pressed) ChipotleColors.RedPressed else ChipotleColors.Red)
.clickable(interaction, indication = null, onClick = onClick),
contentAlignment = Alignment.Center,
) {
Text(label.uppercase(), style = ChipotleText.Button, color = Color.White)
}
}
Rewards Points Ring
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.Canvas
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
@Composable
fun RewardsRing(points: Int, goal: Int, modifier: Modifier = Modifier) {
val target = (points.toFloat() / goal).coerceAtMost(1f)
val sweep = remember { Animatable(0f) }
LaunchedEffect(target) { sweep.animateTo(target, tween(700, easing = FastOutSlowInEasing)) }
Box(modifier.size(120.dp), contentAlignment = Alignment.Center) {
Canvas(Modifier.fillMaxSize()) {
val sw = 10.dp.toPx()
drawArc(ChipotleColors.Divider, 0f, 360f, false,
style = Stroke(sw))
drawArc(ChipotleColors.Red, -90f, 360f * sweep.value, false,
style = Stroke(sw, cap = StrokeCap.Round))
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("%,d".format(points),
style = ChipotleText.Points.copy(fontFeatureSettings = "tnum"),
color = ChipotleColors.Red)
Text("POINTS", style = ChipotleText.Badge, color = ChipotleColors.TextSecondary)
}
}
}
Step Progress Bar
@Composable
fun StepProgress(total: Int, current: Int, modifier: Modifier = Modifier) {
Row(
modifier.fillMaxWidth().padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(2.dp),
) {
repeat(total) { i ->
val color by animateColorAsState(
if (i <= current) ChipotleColors.Red else ChipotleColors.Divider,
tween(250), label = "seg$i",
)
Box(
Modifier
.weight(1f)
.height(4.dp)
.clip(CircleShape)
.background(color),
)
}
}
}
4. Build-Flow State
data class OrderBuild(
val rice: String? = null, // single-select
val beans: String? = null, // single-select
val protein: String? = null, // single-select
val toppings: Set<String> = emptySet(), // multi-select
val salsas: Set<String> = emptySet(), // multi-select
val extras: Set<String> = emptySet(), // each adds a "+$2.00" meta tag
) {
val runningTotal: Double get() = 7.65 + extras.size * 2.0 // base + extras
}
5. Bottom Navigation (Tab Bar)
Use Material 3 NavigationBar. Chipotle’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 ChipotleBottomBar(selected: Int, onSelect: (Int) -> Unit) {
NavigationBar(containerColor = ChipotleColors.Surface, tonalElevation = 0.dp) {
val items = listOf(
"Order" to Icons.Filled.Restaurant,
"Scan" to Icons.Filled.QrCode,
"Delivery" to Icons.Filled.DirectionsBike,
"More" to Icons.Filled.MoreHoriz,
)
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.uppercase(), style = ChipotleText.Tab) },
colors = NavigationBarItemDefaults.colors(
selectedIconColor = ChipotleColors.Red,
selectedTextColor = ChipotleColors.Red,
unselectedIconColor = ChipotleColors.TextSecondary,
unselectedTextColor = ChipotleColors.TextSecondary,
indicatorColor = ChipotleColors.RedTint,
),
)
}
}
}
The persistent “ADD TO BAG” footer sits above the bottom bar during the build flow — render ChipotleCTA (with the running total appended) in the Scaffold bottomBar slot stacked above ChipotleBottomBar, on a ChipotleColors.Surface band with a top hairline.
6. Motion
| Moment | Compose recipe |
|---|---|
| Ingredient select | check AnimatedVisibility scaleIn()/scaleOut(); row bg crossfade; HapticFeedbackType.TextHandleMove |
| Step progress slide | animateColorAsState(tween(250)) per segment as current advances |
| Rewards ring fill | Animatable 0 → target tween(700, FastOutSlowInEasing) driving drawArc sweep; number counts |
| Add to Bag | CTA press Modifier.scale 1 → 0.98; bag badge spring 1 → 1.2 → 1; success haptic |
| Card tap | Modifier.scale 1 → 0.98 via collectIsPressedAsState() |
| Sheet present | ModalBottomSheet default slide; scrim ChipotleColors.TextPrimary.copy(alpha = 0.40f) |
// Bag-count badge bounce after add
val badge = remember { Animatable(1f) }
LaunchedEffect(bagCount) {
if (bagCount > 0) { badge.animateTo(1.2f, spring(dampingRatio = 0.5f)); badge.animateTo(1f, spring()) }
}
Haptics: prefer LocalHapticFeedback. For a softer “select” tick on ingredient taps, LocalView.current.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK) (API 30+) approximates iOS .light impact.
7. Icons
Chipotle ships custom glyphs; the closest first-party set is androidx.compose.material:material-icons-extended. For exact parity, export Chipotle’s glyphs as vector drawables and load via ImageVector.vectorResource(R.drawable.…).
| Purpose | SF Symbol (iOS) | Material Icon (Compose) |
|---|---|---|
| Selected check | checkmark |
Icons.Filled.Check |
| Order (tab) | fork.knife |
Icons.Filled.Restaurant |
| Scan (tab) | qrcode |
Icons.Filled.QrCode |
| Delivery (tab) | bicycle |
Icons.Filled.DirectionsBike |
| More (tab) | ellipsis |
Icons.Filled.MoreHoriz |
| Bag | bag / bag.fill |
Icons.Filled.ShoppingBag |
| Search | magnifyingglass |
Icons.Filled.Search |
| Location chevron | chevron.down |
Icons.Filled.KeyboardArrowDown |
| Quantity minus / plus | minus / plus |
Icons.Filled.Remove / Icons.Filled.Add |
| Rewards / star | star.fill |
Icons.Filled.Star |
| Spicy | flame.fill |
Icons.Filled.LocalFireDepartment |
| Back | chevron.left |
Icons.Filled.ArrowBackIosNew |
8. Minimum SDK & Accessibility Notes
- minSdk 24 (Compose floor is 21; modern motion + drawArc are comfortable at 24).
compileSdk 34+, Compose BOM2024.09+, Kotlin2.0+. - Edge-to-edge: call
enableEdgeToEdge()in theActivity; the cream canvas wants dark system-bar icons viaWindowCompat. ApplyScaffoldinsets so the “ADD TO BAG” footer clears the gesture nav. - Font scaling:
sphonors the user’s font scale automatically — keep it on item names and body. ALL-CAPS condensed headers absorb growth; pin tab labels and step segments viadp-derived sizing orCompositionLocalProvider(LocalDensity provides Density(density, fontScale = 1f)). - TalkBack: ingredient rows use
Modifier.semantics { role = Role.Checkbox /* or RadioButton */; stateDescription = if (selected) "Selected" else "Not selected" }; merge the name + meta; the CTA announces “Add to bag, total $X”. - Touch targets: Material guidance is 48.dp minimum. Ingredient rows are 60.dp (clear); ensure the quantity − / + glyphs get 48.dp hit areas via padding.
- Contrast: espresso
#451400on cream#FFF5E1is very high contrast.#8A6B4Fon cream passes WCAG AA at 13sp+ — validate 11sp captions and darken toward#6E5238for strict compliance. - Dynamic color: do not enable Material You
dynamicLightColorScheme()— Chipotle’s brand requires the fixed cream canvas, brick-red accent, and brown text regardless of wallpaper.