coderef

Zomato

Zomato's iOS app is a dual-purpose restaurant platform: it is both a food-delivery app and a dining-out / discovery app, and the entire design system is organized around that split.
DownloadRaw
Zomato

Build with the Zomato look.

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

Palette

At a glance

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

Add it to your project

Then prompt your agent

Design System · Zomato

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

Zomato (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports Zomato’s visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, the semantic rating pill, the veg/non-veg mark, the bordered ADD button, the Delivery/Dining toggle, 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 (white canvas, Zomato Red, the semantic rating pill, the veg/non-veg mark, the bordered ADD, the Delivery/Dining toggle) while making everything idiomatic Android — NavigationBar instead of a UITabBar, ModalBottomSheet instead of a UIKit sheet, sp/dp instead of pt.

Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, and Coil 2.6+ for hero photos and dish thumbnails. Zomato’s palette is a fixed brand + semantic rating scale, so dynamic color extraction (Palette / Material You) is not used.

1. Color Tokens

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

object ZomatoColors {
    // Brand (interactive)
    val Red          = Color(0xFFE23744)
    val RedPressed   = Color(0xFFC42531)

    // Rating scale (semantic)
    val RatingGreen  = Color(0xFF267E3E)
    val RatingAmber  = Color(0xFFDB7C38)
    val RatingRed    = Color(0xFFE23744) // = brand
    val RatingGrey   = Color(0xFF9C9C9C)

    // Membership & dining
    val Gold         = Color(0xFFEFC75E)
    val DiningBlue   = Color(0xFF256FEF)

    // Canvas & Surfaces (Light)
    val Canvas       = Color(0xFFFFFFFF)
    val Surface1     = Color(0xFFF4F4F5)
    val Surface2     = Color(0xFFEBEBEC)
    val Divider      = Color(0xFFE8E8E8)

    // Canvas & Surfaces (Dark)
    val DarkCanvas   = Color(0xFF121212) // warm near-black — NOT pure black
    val DarkSurface1 = Color(0xFF1C1C1E)
    val DarkSurface2 = Color(0xFF262629)
    val DarkDivider  = Color(0xFF2C2C2E)

    // Text
    val TextPrimary     = Color(0xFF1C1C1C)
    val TextSecondary   = Color(0xFF696969)
    val TextTertiary    = Color(0xFF9C9C9C)
    val DarkTextPrimary = Color(0xFFF2F2F2)

    // Veg / non-veg
    val VegGreen     = Color(0xFF267E3E)
    val NonVegMaroon = Color(0xFFB91C1C)

    // Semantic
    val Error        = Color(0xFFE03A3A)
}

// Semantic rating → fill color
fun ratingFill(score: Double): Color = when {
    score >= 4.0 -> ZomatoColors.RatingGreen
    score >= 3.0 -> ZomatoColors.RatingAmber
    score > 0.0  -> ZomatoColors.RatingRed
    else         -> ZomatoColors.RatingGrey
}

Wire it into both schemes. Zomato is light-first (white, photo-forward, dense); the dark scheme uses a warm #121212, 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 ZomatoLight = lightColorScheme(
    primary        = ZomatoColors.Red,
    onPrimary      = Color.White,
    background      = ZomatoColors.Canvas,
    onBackground   = ZomatoColors.TextPrimary,
    surface        = ZomatoColors.Surface1,
    onSurface      = ZomatoColors.TextPrimary,
    surfaceVariant = ZomatoColors.Surface2,
    outline        = ZomatoColors.Divider,
    error          = ZomatoColors.Error,
)

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

@Composable
fun ZomatoTheme(
    dark: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit,
) = MaterialTheme(
    colorScheme = if (dark) ZomatoDark else ZomatoLight,
    typography = ZomatoTypography,
    content = content,
)

2. Typography (Material 3)

Zomato’s brand face is a custom Okra-family grotesque (proprietary); use Inter (SIL OFL) — drop the TTFs in res/font/. The app is dense and heavy: names 800, screen titles 900; numbers tabular.

// ui/theme/ZomatoType.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 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),
    Font(R.font.inter_black,     FontWeight.Black),
)

// Named ramp — mirrors DESIGN.md §3 exactly (pt → sp 1:1)
object ZomatoText {
    val ScreenTitle    = TextStyle(Inter, fontWeight = FontWeight.Black,     fontSize = 32.sp, lineHeight = 37.sp, letterSpacing = (-0.5).sp)
    val RestaurantHero = TextStyle(Inter, fontWeight = FontWeight.ExtraBold, fontSize = 24.sp, lineHeight = 29.sp, letterSpacing = (-0.4).sp)
    val Section        = TextStyle(Inter, fontWeight = FontWeight.ExtraBold, fontSize = 20.sp, lineHeight = 26.sp, letterSpacing = (-0.3).sp)
    val MenuCategory   = TextStyle(Inter, fontWeight = FontWeight.ExtraBold, fontSize = 17.sp, lineHeight = 22.sp, letterSpacing = (-0.2).sp)
    val ItemName       = TextStyle(Inter, fontWeight = FontWeight.Bold,      fontSize = 15.sp, lineHeight = 20.sp)
    val Body           = TextStyle(Inter, fontWeight = FontWeight.Medium,    fontSize = 14.sp, lineHeight = 20.sp)
    val Price          = TextStyle(Inter, fontWeight = FontWeight.Bold,      fontSize = 14.sp, lineHeight = 18.sp)
    val Meta           = TextStyle(Inter, fontWeight = FontWeight.Normal,    fontSize = 13.sp, lineHeight = 18.sp)
    val RatingPill     = TextStyle(Inter, fontWeight = FontWeight.ExtraBold, fontSize = 14.sp, lineHeight = 14.sp)
    val RatingCaption  = TextStyle(Inter, fontWeight = FontWeight.Normal,    fontSize = 11.sp, lineHeight = 14.sp)
    val Tag            = TextStyle(Inter, fontWeight = FontWeight.ExtraBold, fontSize = 11.sp, lineHeight = 11.sp, letterSpacing = 0.4.sp)
    val Button         = TextStyle(Inter, fontWeight = FontWeight.ExtraBold, fontSize = 16.sp, lineHeight = 16.sp)
    val AddLabel       = TextStyle(Inter, fontWeight = FontWeight.ExtraBold, fontSize = 14.sp, lineHeight = 14.sp, letterSpacing = 0.3.sp)
    val Tab            = TextStyle(Inter, fontWeight = FontWeight.SemiBold,  fontSize = 10.sp, lineHeight = 10.sp, letterSpacing = 0.1.sp)
    val Caption        = TextStyle(Inter, fontWeight = FontWeight.Medium,    fontSize = 12.sp, lineHeight = 16.sp)
}

// Map onto Material 3 slots so stock components inherit the brand
val ZomatoTypography = Typography(
    headlineLarge  = ZomatoText.ScreenTitle,
    headlineMedium = ZomatoText.RestaurantHero,
    titleLarge     = ZomatoText.Section,
    titleMedium    = ZomatoText.MenuCategory,
    bodyMedium     = ZomatoText.Body,
    labelSmall     = ZomatoText.Tab,
)

For tabular figures on prices/ratings, use TextStyle.copy(fontFeatureSettings = "tnum").

3. Signature Components

Semantic Rating Pill

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp

@Composable
fun RatingPill(score: Double, count: String? = null) {
    Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(5.dp)) {
        Row(
            Modifier.clip(RoundedCornerShape(7.dp)).background(ratingFill(score))
                .padding(horizontal = 9.dp, vertical = 4.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(4.dp),
        ) {
            Icon(Icons.Filled.Star, null, tint = Color.White, modifier = Modifier.size(11.dp))
            Text(String.format("%.1f", score),
                style = ZomatoText.RatingPill.copy(fontFeatureSettings = "tnum"), color = Color.White)
        }
        if (count != null) {
            Text(count, style = ZomatoText.RatingCaption,
                color = ZomatoColors.TextSecondary, textDecoration = TextDecoration.Underline)
        }
    }
}

Veg / Non-Veg Mark

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.border
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Path

@Composable
fun VegMark(isVeg: Boolean) {
    val c = if (isVeg) ZomatoColors.VegGreen else ZomatoColors.NonVegMaroon
    Box(
        Modifier.size(16.dp).border(1.5.dp, c, RoundedCornerShape(3.dp)),
        contentAlignment = Alignment.Center,
    ) {
        if (isVeg) {
            Box(Modifier.size(7.dp).clip(RoundedCornerShape(50)).background(c))
        } else {
            Canvas(Modifier.size(9.dp)) {
                val p = Path().apply {
                    moveTo(size.width / 2f, 0f)
                    lineTo(size.width, size.height)
                    lineTo(0f, size.height)
                    close()
                }
                drawPath(p, c)
            }
        }
    }
}

Menu Item Row (bordered ADD → stepper)

import androidx.compose.foundation.clickable
import androidx.compose.foundation.shadow
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Remove
import androidx.compose.material.icons.filled.Star
import androidx.compose.runtime.*
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.IntOffset
import coil.compose.AsyncImage

@Composable
fun MenuItemRow(
    isVeg: Boolean, bestseller: Boolean, name: String, price: String,
    rating: Double, ratingCount: Int, imageUrl: String,
) {
    var qty by remember { mutableIntStateOf(0) }
    val haptics = LocalHapticFeedback.current
    fun tick(n: Int) { haptics.performHapticFeedback(HapticFeedbackType.TextHandleMove); qty = n }

    Row(
        Modifier.fillMaxWidth().padding(vertical = 14.dp),
        horizontalArrangement = Arrangement.spacedBy(14.dp),
    ) {
        Column(Modifier.weight(1f)) {
            VegMark(isVeg)
            if (bestseller) {
                Row(Modifier.padding(top = 7.dp, bottom = 3.dp),
                    verticalAlignment = Alignment.CenterVertically,
                    horizontalArrangement = Arrangement.spacedBy(4.dp)) {
                    Icon(Icons.Filled.Star, null, tint = ZomatoColors.RatingAmber, modifier = Modifier.size(9.dp))
                    Text("Bestseller", style = ZomatoText.Tag, color = ZomatoColors.RatingAmber)
                }
            }
            Text(name, style = ZomatoText.ItemName, color = ZomatoColors.TextPrimary,
                modifier = if (bestseller) Modifier else Modifier.padding(top = 7.dp))
            Text(price, style = ZomatoText.Price, color = ZomatoColors.TextPrimary,
                modifier = Modifier.padding(top = 5.dp))
            Row(Modifier.padding(top = 6.dp),
                verticalAlignment = Alignment.CenterVertically,
                horizontalArrangement = Arrangement.spacedBy(4.dp)) {
                Icon(Icons.Filled.Star, null, tint = ZomatoColors.RatingGreen, modifier = Modifier.size(10.dp))
                Text("${"%.1f".format(rating)} ($ratingCount)",
                    style = ZomatoText.Caption, color = ZomatoColors.TextSecondary)
            }
        }

        Column(horizontalAlignment = Alignment.CenterHorizontally) {
            AsyncImage(model = imageUrl, contentDescription = null,
                modifier = Modifier.size(width = 96.dp, height = 90.dp).clip(RoundedCornerShape(12.dp)),
                contentScale = ContentScale.Crop)
            Box(
                Modifier
                    .offset { IntOffset(0, -16.dp.roundToPx()) }   // overlaps the thumbnail
                    .shadow(8.dp, RoundedCornerShape(10.dp))
                    .clip(RoundedCornerShape(10.dp))
                    .background(ZomatoColors.Canvas)
                    .border(1.dp, ZomatoColors.Red, RoundedCornerShape(10.dp)),
            ) {
                if (qty == 0) {
                    Text("ADD", style = ZomatoText.AddLabel, color = ZomatoColors.Red,
                        modifier = Modifier.clickable { qty = 1 }
                            .padding(horizontal = 22.dp, vertical = 8.dp))
                } else {
                    Row(verticalAlignment = Alignment.CenterVertically) {
                        Icon(Icons.Filled.Remove, "Decrease", tint = ZomatoColors.Red,
                            modifier = Modifier.size(36.dp).clickable { if (qty > 1) tick(qty - 1) else qty = 0 }
                                .padding(11.dp))
                        Text("$qty", style = ZomatoText.ItemName.copy(fontFeatureSettings = "tnum"),
                            color = ZomatoColors.Red, modifier = Modifier.widthIn(min = 24.dp))
                        Icon(Icons.Filled.Add, "Increase", tint = ZomatoColors.Red,
                            modifier = Modifier.size(36.dp).clickable { tick(qty + 1) }.padding(11.dp))
                    }
                }
            }
        }
    }
    HorizontalDivider(color = ZomatoColors.Divider, thickness = 1.dp)
}

Delivery / Dining Out Toggle

import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.layout.fillMaxWidth

@Composable
fun DeliveryDiningToggle(isDelivery: Boolean, onChange: (Boolean) -> Unit) {
    Row(
        Modifier.fillMaxWidth().clip(RoundedCornerShape(50))
            .background(ZomatoColors.Surface1).padding(4.dp),
    ) {
        Seg("Delivery",   isDelivery,  Modifier.weight(1f)) { onChange(true) }
        Seg("Dining Out", !isDelivery, Modifier.weight(1f)) { onChange(false) }
    }
}

@Composable
private fun Seg(label: String, active: Boolean, modifier: Modifier, onClick: () -> Unit) {
    Box(
        modifier.clip(RoundedCornerShape(50))
            .background(if (active) ZomatoColors.Red else Color.Transparent)
            .clickable { onClick() }
            .padding(vertical = 9.dp),
        contentAlignment = Alignment.Center,
    ) {
        Text(label,
            style = ZomatoText.RatingPill.copy(fontSize = 13.sp, fontWeight = FontWeight.Bold),
            color = if (active) Color.White else ZomatoColors.TextSecondary)
    }
}

Restaurant Detail Header (hero + pull-up card)

import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.Search

@Composable
fun RestaurantDetailHeader(
    imageUrl: String, name: String, meta: String,
    deliveryRating: Double, diningRating: Double,
) {
    Column {
        Box(Modifier.fillMaxWidth().height(200.dp)) {
            AsyncImage(model = imageUrl, contentDescription = name,
                modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop)
            Row(
                Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, top = 14.dp),
                verticalAlignment = Alignment.CenterVertically,
            ) {
                HeroBtn(Icons.Filled.ArrowBack)
                Spacer(Modifier.weight(1f))
                Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
                    HeroBtn(Icons.Filled.Search)
                    HeroBtn(Icons.Filled.FavoriteBorder)
                }
            }
        }
        Column(
            Modifier.fillMaxWidth()
                .offset { IntOffset(0, -22.dp.roundToPx()) }
                .clip(RoundedCornerShape(topStart = 22.dp, topEnd = 22.dp))
                .background(ZomatoColors.Canvas)
                .padding(horizontal = 18.dp).padding(top = 20.dp),
        ) {
            Text(name, style = ZomatoText.RestaurantHero, color = ZomatoColors.TextPrimary)
            Text(meta, style = ZomatoText.Meta, color = ZomatoColors.TextSecondary,
                modifier = Modifier.padding(top = 5.dp))
            Row(Modifier.padding(top = 14.dp), horizontalArrangement = Arrangement.spacedBy(18.dp)) {
                RatingPill(deliveryRating, "12.4K Delivery")
                RatingPill(diningRating, "3.1K Dining")
            }
        }
    }
}

@Composable
private fun HeroBtn(icon: androidx.compose.ui.graphics.vector.ImageVector) {
    Box(
        Modifier.size(34.dp).clip(RoundedCornerShape(50)).background(Color.Black.copy(alpha = 0.4f)),
        contentAlignment = Alignment.Center,
    ) { Icon(icon, null, tint = Color.White, modifier = Modifier.size(17.dp)) }
}

Primary Button

import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.ui.draw.scale

@Composable
fun ZButton(title: String, onClick: () -> Unit) {
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    Box(
        Modifier.fillMaxWidth().heightIn(min = 52.dp)
            .scale(if (pressed) 0.98f else 1f)
            .clip(RoundedCornerShape(12.dp))
            .background(if (pressed) ZomatoColors.RedPressed else ZomatoColors.Red)
            .clickable(interaction, indication = null) { onClick() },
        contentAlignment = Alignment.Center,
    ) { Text(title, style = ZomatoText.Button, color = Color.White) }
}
// Embeds price: ZButton("Add item · ₹320", onClick = {})

4. Navigation

Zomato has a 4-tab bottom strip and no Material tint pill — active is just Zomato Red.

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

@Composable
fun ZomatoBottomBar(selected: Int, onSelect: (Int) -> Unit) {
    NavigationBar(containerColor = ZomatoColors.Canvas, tonalElevation = 0.dp) {
        val items = listOf(
            "Delivery" to Icons.Filled.Home,
            "Dining"   to Icons.Filled.Restaurant,
            "Live"     to Icons.Filled.GraphicEq,
            "Profile"  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 = ZomatoText.Tab) },
                colors = NavigationBarItemDefaults.colors(
                    selectedIconColor   = ZomatoColors.Red,
                    selectedTextColor   = ZomatoColors.Red,
                    unselectedIconColor = Color(0xFF888888),
                    unselectedTextColor = Color(0xFF888888),
                    indicatorColor      = Color.Transparent, // no Material pill — Zomato has none
                ),
            )
        }
    }
}

The customize, cart and address surfaces are ModalBottomSheet with a 22.dp top radius (matching the detail pull-up card). The rider map uses Google Maps Compose with a Zomato-Red route + rider marker; the sticky cart bar floats above the menu with the item count + total embedded in a ZButton.

5. Motion

Zomato motion is functional — the rating color is the signal, not animation.

Moment Compose recipe
ADD → stepper AnimatedContent swapping the ADD label for the stepper tween(180); cart bar slideInVertically tween(280) + HapticFeedbackType.LongPress
Quantity change Crossfade / AnimatedContent on the count + TextHandleMove haptic
Delivery/Dining toggle active fill animateDpAsState x-offset spring(dampingRatio = 0.85f); content Crossfade(tween(200))
Hero pull-up parallax LazyColumn + nestedScroll; hero graphicsLayer { translationY = scroll * 0.5f }; nav alpha animates as the hero scrolls away
Card → detail shared element via SharedTransitionLayout (Compose 1.7+) tween(320)
Rating pill appear fadeIn + slight scaleIn tween(150) — color is the signal, keep it subtle
Sheet present ModalBottomSheet default slide; scrim fades in parallel
Order tracking advance status node animateDpAsState tween(250) + HapticFeedbackType.LongPress (medium analog)
// ADD → stepper — the canonical Zomato motion
AnimatedContent(targetState = qty == 0, label = "addToStepper",
    transitionSpec = { fadeIn(tween(180)) togetherWith fadeOut(tween(120)) }) { isAdd ->
    if (isAdd) /* ADD label */ else /* bordered red stepper */
}

Haptics: HapticFeedbackType.TextHandleMove for the stepper tick; HapticFeedbackType.LongPress for ADD / stage-complete. Order-placed success can use VibrationEffect.createPredefined(EFFECT_HEAVY_CLICK) via the system Vibrator.

6. Icons

The closest first-party set to Zomato’s iconography is androidx.compose.material:material-icons-extended. The veg/non-veg mark is custom-drawn (Canvas), not an icon.

Purpose SF Symbol (iOS) Material Icon (Compose)
Delivery (tab) house / house.fill Icons.Filled.Home
Dining (tab) fork.knife Icons.Filled.Restaurant
Live (tab) dot.radiowaves.left.and.right Icons.Filled.GraphicEq
Profile (tab) person.crop.circle Icons.Filled.AccountCircle
Rating star star.fill Icons.Filled.Star
Hero back chevron.left Icons.Filled.ArrowBack
Hero search magnifyingglass Icons.Filled.Search
Hero save heart / heart.fill Icons.Filled.FavoriteBorder / Icons.Filled.Favorite
ADD minus minus Icons.Filled.Remove
ADD plus plus Icons.Filled.Add
Share square.and.arrow.up Icons.Filled.IosShare
Dining — book calendar Icons.Filled.CalendarMonth
Offer tag.fill Icons.Filled.LocalOffer
Pro / Gold crown.fill Icons.Filled.WorkspacePremium
Rider (tracking) bicycle Icons.Filled.DirectionsBike

7. Minimum SDK & Accessibility Notes

  • minSdk 24 (Compose floor is 21; modern motion and SharedTransitionLayout are comfortable at 24). compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+.
  • Edge-to-edge: call enableEdgeToEdge(); the white canvas wants dark-content system bars (light-content over the hero photo and in dark mode). The hero nav scrims respect the camera cutout; the bottom tab + sticky cart bar use Modifier.navigationBarsPadding().
  • Semantic rating: always derive the pill fill from ratingFill(score) — never hard-code one color. The color carrying the score’s quality is the core of Zomato.
  • Veg/non-veg is not color-only: the VegMark must carry a contentDescription (“Vegetarian”/“Non-vegetarian”); it is legally significant in India and must not depend on color perception.
  • Font scaling: sp honors the user’s font scale — keep it on screen titles, restaurant names, body and prices. Pin layout-sensitive text (the rating pill, veg mark, 11sp Bestseller tag, 10sp tab labels, the ADD label) by deriving from dp or CompositionLocalProvider(LocalDensity provides Density(density, fontScale = 1f)).
  • Tabular numerals: apply fontFeatureSettings = "tnum" to ratings, prices, review counts and quantity counters.
  • TalkBack: the rating pill announces score and quality (“4.3 stars, rated great”); the menu row is one focusable node combining veg status + name + price + rating; the ADD button is “Add {dish}” and after tap becomes a stepper with state.
  • Touch targets: Material guidance is 48.dp. Give the ADD button and the 36.dp stepper controls a 48.dp hit area via padding; the segmented toggle halves are ≥ 44.dp tall; primary buttons ≥ 52.dp.
  • Contrast: #1C1C1C on #FFFFFF passes WCAG AAA; white on the rating greens/ambers/reds passes AA for the pill score; the semantic scale is always paired with the star + an accessible label so it is never color-only.
  • Reduce motion: when Settings.Global.ANIMATOR_DURATION_SCALE == 0, disable the ADD→stepper transition and the toggle’s sliding fill (substitute a Crossfade); keep the rating pill static.
  • Dark mode: invert via the Dark* palette — #121212, NOT true black; #1C1C1C text becomes #F2F2F2. Brand red, the full rating scale, veg colors, gold and dining blue are constant so a pill’s color always means the same thing. Shadows are nearly invisible on dark, so add a 0.5dp DarkDivider border on floating surfaces (ADD button, sticky cart). Do not enable Material You dynamicColorScheme() — Zomato’s red identity and the semantic rating scale 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