coderef

Uber Eats

Uber Eats' iOS app is a bright, photo-first marketplace built on a pure white canvas (#FFFFFF) where food photography does the selling.
DownloadRaw
Uber Eats

Build with the Uber Eats look.

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

Palette

At a glance

Theme
light
Typefaces
Uber Move
Sections
8
Length
4,025 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · Uber Eats

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

Uber Eats (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports Uber Eats’ visual language to Android with Jetpack Compose (Material 3): color token objects (light + dark), 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 (Uber Eats’ bright photo-first white canvas, single green accent, sticky cart bar, live map tracking) while making everything idiomatic Android — NavigationBar instead of a UITabBar, Google Maps Compose for tracking, sp/dp instead of pt.

Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, Coil 2.6+ for images, and maps-compose4.3+ for the tracking map.

1. Color Tokens

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

object UEColors {
    // Light — default
    val Canvas    = Color(0xFFFFFFFF)
    val Surface   = Color(0xFFF3F3F3)
    val Surface2  = Color(0xFFEEEEEE)
    val Divider   = Color(0xFFE8E8E8)
    val TextPrimary   = Color(0xFF000000)
    val TextSecondary = Color(0xFF6B6B6B)
    val TextTertiary  = Color(0xFFA6A6A6)

    // Dark
    val DarkCanvas    = Color(0xFF000000)
    val DarkSurface   = Color(0xFF1C1C1E)
    val DarkSurface2  = Color(0xFF2C2C2E)
    val DarkDivider   = Color(0xFF2C2C2E)
    val DarkTextPrimary   = Color(0xFFFFFFFF)
    val DarkTextSecondary = Color(0xFFA6A6A6)

    // Brand — identical in light & dark
    val Green        = Color(0xFF06C167)
    val GreenPressed = Color(0xFF05A658)
    val GreenTint    = Color(0xFFE7F8EF)
    val ErrorRed     = Color(0xFFE11900)
    val BusyAmber    = Color(0xFFFF8A00)
}

Provide both Material 3 schemes; the green is the primary in both (only neutrals invert).

// ui/theme/Theme.kt
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.*

private val UELight = lightColorScheme(
    primary       = UEColors.Green,
    onPrimary     = Color.White,
    background     = UEColors.Canvas,
    onBackground   = UEColors.TextPrimary,
    surface        = UEColors.Surface,
    onSurface      = UEColors.TextPrimary,
    surfaceVariant = UEColors.Surface2,
    outline        = UEColors.Divider,
    error          = UEColors.ErrorRed,
)

private val UEDark = darkColorScheme(
    primary       = UEColors.Green,            // unchanged in dark
    onPrimary     = Color.White,
    background     = UEColors.DarkCanvas,
    onBackground   = UEColors.DarkTextPrimary,
    surface        = UEColors.DarkSurface,
    onSurface      = UEColors.DarkTextPrimary,
    surfaceVariant = UEColors.DarkSurface2,
    outline        = UEColors.DarkDivider,
    error          = UEColors.ErrorRed,
)

@Composable
fun UETheme(content: @Composable () -> Unit) =
    MaterialTheme(
        colorScheme = if (isSystemInDarkTheme()) UEDark else UELight,
        typography = UETypography,
        content = content,
    )

2. Typography

Uber Move is a licensed brand typeface. Drop the TTFs in res/font/ (lowercase, snake_case) and build a FontFamily. Fall back to the system font (Roboto).

// ui/theme/UEType.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 UberMove = FontFamily(
    Font(R.font.uber_move_regular, FontWeight.Normal), // 400
    Font(R.font.uber_move_medium,  FontWeight.Medium), // 500
    Font(R.font.uber_move_bold,    FontWeight.Bold),   // 700
)

// Named ramp — mirrors DESIGN.md §3 exactly (pt → sp 1:1)
object UEText {
    val RestaurantHeader = TextStyle(UberMove, fontWeight = FontWeight.Bold,   fontSize = 30.sp, lineHeight = 33.sp, letterSpacing = (-0.4).sp)
    val TitleLarge       = TextStyle(UberMove, fontWeight = FontWeight.Bold,   fontSize = 28.sp, lineHeight = 32.sp, letterSpacing = (-0.4).sp)
    val SectionHeader    = TextStyle(UberMove, fontWeight = FontWeight.Bold,   fontSize = 22.sp, lineHeight = 26.sp, letterSpacing = (-0.3).sp)
    val Subsection       = TextStyle(UberMove, fontWeight = FontWeight.Bold,   fontSize = 20.sp, lineHeight = 24.sp, letterSpacing = (-0.3).sp)
    val RestaurantName   = TextStyle(UberMove, fontWeight = FontWeight.Bold,   fontSize = 17.sp, lineHeight = 21.sp, letterSpacing = (-0.1).sp)
    val MenuItemTitle    = TextStyle(UberMove, fontWeight = FontWeight.Medium, fontSize = 16.sp, lineHeight = 21.sp, letterSpacing = (-0.1).sp)
    val Body             = TextStyle(UberMove, fontWeight = FontWeight.Normal, fontSize = 15.sp, lineHeight = 22.sp)
    val Price            = TextStyle(UberMove, fontWeight = FontWeight.Bold,   fontSize = 16.sp, lineHeight = 20.sp)
    val Meta             = TextStyle(UberMove, fontWeight = FontWeight.Normal, fontSize = 14.sp, lineHeight = 18.sp)
    val Pill             = TextStyle(UberMove, fontWeight = FontWeight.Medium, fontSize = 14.sp, lineHeight = 18.sp)
    val Caption          = TextStyle(UberMove, fontWeight = FontWeight.Normal, fontSize = 13.sp, lineHeight = 17.sp)
    val LabelUpper       = TextStyle(UberMove, fontWeight = FontWeight.Bold,   fontSize = 11.sp, lineHeight = 13.sp, letterSpacing = 0.6.sp)
    val Button           = TextStyle(UberMove, fontWeight = FontWeight.Bold,   fontSize = 16.sp, lineHeight = 20.sp, letterSpacing = 0.2.sp)
    val Tab              = TextStyle(UberMove, fontWeight = FontWeight.Medium, fontSize = 10.sp, lineHeight = 12.sp, letterSpacing = 0.2.sp)
    val CartBadge        = TextStyle(UberMove, fontWeight = FontWeight.Bold,   fontSize = 12.sp, lineHeight = 14.sp)
}

// Map onto Material 3 slots so stock components inherit the brand
val UETypography = Typography(
    headlineLarge = UEText.TitleLarge,
    headlineSmall = UEText.SectionHeader,
    titleMedium   = UEText.RestaurantName,
    bodyMedium    = UEText.Body,
    labelSmall    = UEText.Tab,
)

3. Signature Components

Primary Green Button

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.*
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.*
import androidx.compose.ui.unit.dp

@Composable
fun UEPrimaryButton(
    title: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    trailing: String? = null,
) {
    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 = "ctaScale",
    )
    val haptics = LocalHapticFeedback.current

    Row(
        modifier = modifier
            .fillMaxWidth()
            .height(52.dp)
            .scale(scale)
            .clip(RoundedCornerShape(12.dp))
            .background(if (pressed) UEColors.GreenPressed else UEColors.Green)
            .clickable(interaction, indication = null) {
                haptics.performHapticFeedback(HapticFeedbackType.LongPress) // ~iOS light impact
                onClick()
            }
            .padding(horizontal = if (trailing != null) 20.dp else 0.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = if (trailing != null) Arrangement.SpaceBetween
                                 else Arrangement.Center,
    ) {
        Text(title, style = UEText.Button, color = Color.White)
        if (trailing != null) Text(trailing, style = UEText.Button, color = Color.White)
    }
}

Photo-First Restaurant Card

@Composable
fun UERestaurantCard(
    name: String,
    rating: String,
    eta: String,
    fee: String,
    freeDelivery: Boolean,
    photoUrl: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    val scheme = MaterialTheme.colorScheme
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    val scale by animateFloatAsState(if (pressed) 0.98f else 1f, label = "cardScale")

    Column(modifier.clickable(interaction, indication = null, onClick = onClick)) {
        Box(
            Modifier
                .fillMaxWidth()
                .aspectRatio(16f / 9f)
                .scale(scale)
                .clip(RoundedCornerShape(12.dp)),
        ) {
            AsyncImage(model = photoUrl, contentDescription = name,
                modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop)
            if (freeDelivery) {
                Text(
                    "$0 Delivery Fee",
                    style = UEText.Caption.copy(fontWeight = FontWeight.Bold),
                    color = Color.White,
                    modifier = Modifier
                        .align(Alignment.TopStart).padding(10.dp)
                        .clip(RoundedCornerShape(50)).background(UEColors.Green)
                        .padding(horizontal = 8.dp, vertical = 4.dp),
                )
            }
            Box(
                Modifier
                    .align(Alignment.TopEnd).padding(10.dp)
                    .size(36.dp).clip(CircleShape).background(Color.White.copy(alpha = 0.9f)),
                contentAlignment = Alignment.Center,
            ) { Icon(Icons.Outlined.FavoriteBorder, contentDescription = "Favorite",
                      tint = Color.Black, modifier = Modifier.size(18.dp)) }
        }
        Spacer(Modifier.height(8.dp))
        Text(name, style = UEText.RestaurantName, color = scheme.onBackground)
        Row(verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(6.dp),
            modifier = Modifier.padding(top = 4.dp)) {
            Icon(Icons.Filled.Star, contentDescription = null,
                tint = scheme.onBackground, modifier = Modifier.size(12.dp)) // monochrome, not yellow
            Text(rating, style = UEText.Meta, color = scheme.onBackground)
            Text("· $eta ·", style = UEText.Meta, color = UEColors.TextSecondary)
            Text(fee, style = UEText.Meta, color = if (freeDelivery) UEColors.Green else UEColors.TextSecondary)
        }
    }
}

Sticky Cart Bar

@Composable
fun UEStickyCartBar(
    count: Int,
    total: String,
    onView: () -> Unit,
    modifier: Modifier = Modifier,
) {
    AnimatedVisibility(
        visible = count > 0,
        enter = slideInVertically { it } + fadeIn(),
        exit = slideOutVertically { it } + fadeOut(),
        modifier = modifier,
    ) {
        Row(
            Modifier
                .fillMaxWidth()
                .padding(16.dp)
                .shadow(12.dp, RoundedCornerShape(12.dp), spotColor = Color.Black.copy(alpha = 0.12f))
                .clip(RoundedCornerShape(12.dp))
                .background(UEColors.Green)
                .clickable(onClick = onView)
                .height(56.dp)
                .padding(horizontal = 16.dp),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Box(Modifier.size(28.dp).clip(CircleShape).background(Color.White),
                contentAlignment = Alignment.Center) {
                Text("$count", style = UEText.CartBadge, color = UEColors.Green)
            }
            Text("View cart", style = UEText.Button, color = Color.White,
                modifier = Modifier.weight(1f), textAlign = TextAlign.Center)
            Text(total, style = UEText.Button, color = Color.White)
        }
    }
}

Menu Item Row + Cart-Count Bump

@Composable
fun UEMenuItemRow(
    name: String, desc: String, price: String, photoUrl: String,
    onAdd: () -> Unit, modifier: Modifier = Modifier,
) {
    val scheme = MaterialTheme.colorScheme
    val haptics = LocalHapticFeedback.current
    Row(
        modifier
            .fillMaxWidth()
            .padding(vertical = 12.dp)
            .drawBehind {
                drawLine(UEColors.Divider, Offset(0f, size.height), Offset(size.width, size.height), 1.dp.toPx())
            },
        horizontalArrangement = Arrangement.spacedBy(12.dp),
    ) {
        Column(Modifier.weight(1f)) {
            Text(name, style = UEText.MenuItemTitle, color = scheme.onBackground)
            Text(desc, style = UEText.Meta, color = UEColors.TextSecondary, maxLines = 2,
                overflow = TextOverflow.Ellipsis, modifier = Modifier.padding(top = 4.dp))
            Text(price, style = UEText.Body, color = scheme.onBackground, modifier = Modifier.padding(top = 2.dp))
        }
        Box(Modifier.size(88.dp)) {
            AsyncImage(model = photoUrl, contentDescription = name,
                modifier = Modifier.size(88.dp).clip(RoundedCornerShape(12.dp)),
                contentScale = ContentScale.Crop)
            Box(
                Modifier
                    .align(Alignment.BottomEnd)
                    .offset(x = 6.dp, y = 6.dp)
                    .size(28.dp).clip(CircleShape).background(UEColors.Green)
                    .clickable {
                        haptics.performHapticFeedback(HapticFeedbackType.LongPress)
                        onAdd()
                    },
                contentAlignment = Alignment.Center,
            ) { Icon(Icons.Filled.Add, contentDescription = "Add", tint = Color.White,
                      modifier = Modifier.size(16.dp)) }
        }
    }
}

@Composable
fun UECartCountBadge(count: Int) {
    val scale = remember { Animatable(1f) }
    LaunchedEffect(count) {
        scale.animateTo(1.25f, spring(dampingRatio = 0.5f)); scale.animateTo(1f, spring())
    }
    Box(
        Modifier.scale(scale.value).defaultMinSize(18.dp, 18.dp)
            .clip(CircleShape).background(UEColors.Green).padding(horizontal = 4.dp),
        contentAlignment = Alignment.Center,
    ) { Text("$count", style = UEText.CartBadge, color = Color.White) }
}

4. Live Map Order Tracking

import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.compose.*

@Composable
fun UEOrderTracking(route: List<LatLng>, etaText: String, step: Int) {
    val scheme = MaterialTheme.colorScheme
    var courier by remember { mutableStateOf(route.first()) }
    LaunchedEffect(route) {
        route.forEach { p ->
            courier = p          // Marker eases via MarkerState; or animate a custom LatLng tween
            delay(3000)
        }
    }

    Box(Modifier.fillMaxSize()) {
        GoogleMap(
            modifier = Modifier.fillMaxSize(),
            cameraPositionState = rememberCameraPositionState {
                position = com.google.maps.android.compose.CameraPositionState().position
            },
        ) {
            Polyline(points = route, color = Color.Black, width = 12f) // near-black route
            Marker(state = MarkerState(position = courier))            // green pin via custom icon in production
        }

        Column(
            Modifier
                .align(Alignment.BottomCenter)
                .fillMaxWidth()
                .shadow(28.dp, RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
                        spotColor = Color.Black.copy(alpha = 0.16f))
                .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp))
                .background(scheme.background)
                .padding(20.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp),
        ) {
            Box(Modifier.align(Alignment.CenterHorizontally)
                .width(40.dp).height(5.dp).clip(RoundedCornerShape(3.dp)).background(UEColors.Divider))
            Text(etaText, style = UEText.SectionHeader, color = scheme.onBackground)
            Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
                repeat(4) { i ->
                    Box(
                        Modifier.weight(1f).height(4.dp).clip(RoundedCornerShape(2.dp))
                            .background(if (i <= step) UEColors.Green else UEColors.Surface2)
                    )
                }
            }
        }
    }
}

5. Bottom Navigation (Tab Bar)

Use Material 3 NavigationBar. Active tint is near-black/white, NOT green — green is reserved for action surfaces (cart bar, CTAs, badges).

@Composable
fun UEBottomBar(selected: Int, cartCount: Int, onSelect: (Int) -> Unit) {
    val scheme = MaterialTheme.colorScheme
    NavigationBar(containerColor = scheme.background, tonalElevation = 0.dp) {
        val items = listOf(
            "Home"    to Icons.Filled.Home,
            "Browse"  to Icons.Filled.GridView,
            "Search"  to Icons.Filled.Search,
            "Cart"    to Icons.Filled.ShoppingCart,
            "Account" to Icons.Filled.AccountCircle,
        )
        items.forEachIndexed { i, (label, icon) ->
            NavigationBarItem(
                selected = selected == i,
                onClick = { onSelect(i) },
                icon = {
                    if (i == 3 && cartCount > 0) {
                        BadgedBox(badge = {
                            Badge(containerColor = UEColors.Green, contentColor = Color.White) {
                                Text("$cartCount")
                            }
                        }) { Icon(icon, contentDescription = label, modifier = Modifier.size(24.dp)) }
                    } else {
                        Icon(icon, contentDescription = label, modifier = Modifier.size(24.dp))
                    }
                },
                label = { Text(label, style = UEText.Tab) },
                colors = NavigationBarItemDefaults.colors(
                    selectedIconColor   = scheme.onBackground,   // near-black/white, NOT green
                    selectedTextColor   = scheme.onBackground,
                    unselectedIconColor = UEColors.TextTertiary,
                    unselectedTextColor = UEColors.TextTertiary,
                    indicatorColor      = Color.Transparent,
                ),
            )
        }
    }
}

6. Motion

Moment Compose recipe
Cart-count bump Animatable 1 → 1.25 → 1 (spring(dampingRatio = 0.5f)) keyed on count
Sticky cart bar appear AnimatedVisibility + slideInVertically { it } + fadeIn()
Map courier marker MarkerState position update; for smooth easing tween a LatLng via animateValueAsState (~1s) and feed it to the marker
Card press animateFloatAsState 1 → 0.98, tween(150) ease-out
Pill select animateColorAsState on background (tween(150)); scale 0.97 on press
// Smoothly tween the courier between two LatLngs
@Composable
fun rememberEasedLatLng(target: LatLng): State<LatLng> {
    val lat by animateFloatAsState(target.latitude.toFloat(),
        tween(1000, easing = FastOutSlowInEasing), label = "lat")
    val lng by animateFloatAsState(target.longitude.toFloat(),
        tween(1000, easing = FastOutSlowInEasing), label = "lng")
    return remember { derivedStateOf { LatLng(lat.toDouble(), lng.toDouble()) } }
}

Haptics: prefer LocalHapticFeedback. For richer control use LocalView.current.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) (API 30+) or a Vibrator VibrationEffect.createOneShot(8, ...) to approximate iOS’s .light impact.

7. Icons

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

Purpose SF Symbol (iOS) Material Icon (Compose)
Add plus Icons.Filled.Add
Quantity − / + minus / plus Icons.Filled.Remove / Icons.Filled.Add
Rating star star.fill Icons.Filled.Star (monochrome)
Favorite heart / heart.fill Icons.Outlined.FavoriteBorder / Icons.Filled.Favorite
Share square.and.arrow.up Icons.Filled.IosShare
Search magnifyingglass Icons.Filled.Search
Address chevron chevron.down Icons.Filled.KeyboardArrowDown
Courier message message.fill Icons.Filled.Chat
Courier call phone.fill Icons.Filled.Call
Home (tab) house.fill Icons.Filled.Home
Browse (tab) square.grid.2x2.fill Icons.Filled.GridView
Search (tab) magnifyingglass Icons.Filled.Search
Cart (tab) cart.fill Icons.Filled.ShoppingCart
Account (tab) person.crop.circle.fill Icons.Filled.AccountCircle

8. Minimum SDK & Accessibility Notes

  • minSdk 24 (Compose floor is 21; Maps Compose + modern motion comfortable at 24). compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+. Requires a Google Maps API key for the tracking screen.
  • Edge-to-edge: call enableEdgeToEdge(); the white canvas wants dark-content status bar in light mode (and light-content in dark). Apply Scaffold insets so the sticky cart bar and map sheet clear gesture nav.
  • Light-first: default to the light scheme; isSystemInDarkTheme() swaps neutrals but UEColors.Green is the primary in both schemes.
  • Font scaling: sp honors the user’s font scale automatically — keep it on restaurant names, menu items, descriptions, ETA. Pin layout-sensitive text (tab labels, cart badge, map ETA chip) via dp-derived sizes.
  • TalkBack: announce cards (“Sunrise Diner, 4.8 stars, 25 minute delivery, 99 cent delivery fee”); the cart bar as a single button (“View cart, 3 items, $28.40”); wrap the map ETA in Modifier.semantics { liveRegion = LiveRegionMode.Polite }.
  • Touch targets: Material guidance is 48.dp minimum. The 52–56.dp CTA/cart bar clear it; pad the 28.dp + add button to a 48.dp hit area.
  • Contrast: #6B6B6B on #FFFFFF passes WCAG AA at 14sp+. White on #06C167 passes AA at 16sp bold (the CTA size) — validate any smaller green text. In dark mode keep #06C167 and re-verify green-on-#1C1C1E for green text/icons.
  • Dynamic color: do not enable Material You dynamicColorScheme() — Uber Eats’ brand requires the fixed white (or near-black) canvas and the single #06C167 green 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