coderef

Gmail

Gmail's iOS app is a Material You inbox that inherits Google's multicolor brand DNA while staying clean enough to spend all day in.
DownloadRaw
Gmail

Build with the Gmail look.

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

Palette

At a glance

Theme
light
Typefaces
Google Sans, Roboto Mono
Sections
8
Length
4,685 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · Gmail

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

Gmail (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports Gmail’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? Gmail is a Material app — the iOS build is Material You ported onto SF Pro. On Android this guide goes home: a true FloatingActionButton and ExtendedFloatingActionButton, SwipeToDismissBox for archive/delete, a NavigationBar with the real Material active-indicator pill, and sp/dp instead of pt. Because it is Material-native, this is one of the few apps where you should embrace dynamicColorScheme() (see §8).

Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, and Coil 2.6+ for sender avatar photos.

1. Color Tokens

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

object GmailColors {
    // Canvas & Surfaces
    val Canvas         = Color(0xFFFFFFFF)
    val Surface        = Color(0xFFF6F8FC)
    val SurfacePressed = Color(0xFFF1F3F4)
    val Divider        = Color(0xFFDADCE0)

    // Text
    val TextPrimary   = Color(0xFF202124)
    val TextSecondary = Color(0xFF5F6368)
    val TextDisabled  = Color(0xFF9AA0A6)
    val TextLink      = Color(0xFF1A73E8)

    // Gmail Red — FAB + destructive ONLY
    val Red        = Color(0xFFD93025)
    val RedPressed = Color(0xFFB3261E)
    val RedSubtle  = Color(0xFFFCE8E6)

    // Google 4-color brand (avatars, chips, Smart Reply)
    val GoogleRed    = Color(0xFFEA4335)
    val GoogleYellow = Color(0xFFFBBC04)
    val GoogleGreen  = Color(0xFF34A853)
    val GoogleBlue   = Color(0xFF4285F4)

    // Semantic
    val StarYellow   = Color(0xFFF5BA18)
    val ArchiveTeal  = Color(0xFF1E8E3E)
    val SelectedRow  = Color(0xFFE8F0FE)
    val SnoozePurple = Color(0xFFA142F4)

    // Dark Mode (Google's standard charcoal — NOT pure black)
    val DarkCanvas      = Color(0xFF202124)
    val DarkSurface1    = Color(0xFF28292C)
    val DarkSurface2    = Color(0xFF303134)
    val DarkDivider     = Color(0xFF3C4043)
    val DarkTextPrimary = Color(0xFFE8EAED)
    val DarkTextSecondary = Color(0xFFBDC1C6)
    val DarkRed         = Color(0xFFF28B82) // FAB shifts lighter on dark
}

Wire it into a Material 3 scheme. Gmail’s dark mode is #202124 charcoal — never #000.

// ui/theme/Theme.kt
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import android.os.Build

private val GmailLight = lightColorScheme(
    primary           = GmailColors.Red,         // the FAB
    onPrimary         = Color.White,
    secondary         = GmailColors.TextLink,    // tonal buttons / active tab
    background        = GmailColors.Canvas,
    onBackground      = GmailColors.TextPrimary,
    surface           = GmailColors.Canvas,
    onSurface         = GmailColors.TextPrimary,
    surfaceVariant    = GmailColors.Surface,
    secondaryContainer = GmailColors.SelectedRow, // Material active-indicator pill
    outline           = GmailColors.Divider,
    error             = GmailColors.Red,
)

private val GmailDark = darkColorScheme(
    primary           = GmailColors.DarkRed,
    onPrimary         = GmailColors.DarkCanvas,
    secondary         = GmailColors.TextLink,
    background        = GmailColors.DarkCanvas,
    onBackground      = GmailColors.DarkTextPrimary,
    surface           = GmailColors.DarkSurface1,
    onSurface         = GmailColors.DarkTextPrimary,
    surfaceVariant    = GmailColors.DarkSurface2,
    secondaryContainer = GmailColors.TextLink.copy(alpha = 0.24f),
    outline           = GmailColors.DarkDivider,
    error             = GmailColors.DarkRed,
)

@Composable
fun GmailTheme(
    dark: Boolean = isSystemInDarkTheme(),
    dynamic: Boolean = true,  // Gmail is Material-native — Material You ON by default
    content: @Composable () -> Unit,
) {
    val ctx = LocalContext.current
    val scheme = when {
        dynamic && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ->
            if (dark) dynamicDarkColorScheme(ctx) else dynamicLightColorScheme(ctx)
        dark -> GmailDark
        else -> GmailLight
    }
    MaterialTheme(colorScheme = scheme, typography = GmailTypography, content = content)
}

2. Typography

Two families: Google Sans (titles, buttons, structural — geometric humanist) and Roboto (body, metadata — the Android system default). Drop Google Sans TTFs in res/font/; Roboto is the platform fallback so you can rely on the system family or bundle it (Apache 2.0).

// ui/theme/GmailType.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 GoogleSans = FontFamily(
    Font(R.font.google_sans_regular, FontWeight.Normal),  // 400
    Font(R.font.google_sans_medium,  FontWeight.Medium),  // 500
    Font(R.font.google_sans_bold,    FontWeight.Bold),    // 700
)
val Roboto = FontFamily(
    Font(R.font.roboto_regular, FontWeight.Normal),
    Font(R.font.roboto_medium,  FontWeight.Medium),
)

// Named ramp — mirrors DESIGN.md §3 exactly (pt → sp 1:1, line-height = size × ratio)
object GmailText {
    val TitleLarge    = TextStyle(GoogleSans, fontWeight = FontWeight.Normal, fontSize = 28.sp, lineHeight = 34.sp)
    val TitleCompact  = TextStyle(GoogleSans, fontWeight = FontWeight.Medium, fontSize = 20.sp, lineHeight = 24.sp, letterSpacing = 0.15.sp)
    val SectionHeader = TextStyle(GoogleSans, fontWeight = FontWeight.Medium, fontSize = 13.sp, lineHeight = 16.sp, letterSpacing = 0.8.sp)
    val ThreadSubject = TextStyle(GoogleSans, fontWeight = FontWeight.Normal, fontSize = 22.sp, lineHeight = 28.sp)
    val SenderUnread  = TextStyle(GoogleSans, fontWeight = FontWeight.Medium, fontSize = 14.sp, lineHeight = 17.sp)
    val SubjectUnread = TextStyle(GoogleSans, fontWeight = FontWeight.Medium, fontSize = 14.sp, lineHeight = 18.sp)
    val SenderRead    = TextStyle(Roboto,     fontWeight = FontWeight.Normal, fontSize = 14.sp, lineHeight = 17.sp)
    val Snippet       = TextStyle(Roboto,     fontWeight = FontWeight.Normal, fontSize = 13.sp, lineHeight = 17.sp)
    val Body          = TextStyle(Roboto,     fontWeight = FontWeight.Normal, fontSize = 15.sp, lineHeight = 23.sp)
    val FabButton     = TextStyle(GoogleSans, fontWeight = FontWeight.Medium, fontSize = 16.sp, lineHeight = 16.sp, letterSpacing = 0.1.sp)
    val TonalButton   = TextStyle(GoogleSans, fontWeight = FontWeight.Medium, fontSize = 14.sp, lineHeight = 14.sp, letterSpacing = 0.1.sp)
    val SmartReply    = TextStyle(GoogleSans, fontWeight = FontWeight.Medium, fontSize = 14.sp, lineHeight = 14.sp)
    val LabelChip     = TextStyle(GoogleSans, fontWeight = FontWeight.Medium, fontSize = 12.sp, lineHeight = 12.sp, letterSpacing = 0.2.sp)
    val Tab           = TextStyle(GoogleSans, fontWeight = FontWeight.Medium, fontSize = 12.sp, lineHeight = 12.sp, letterSpacing = 0.1.sp)
    val Timestamp     = TextStyle(Roboto,     fontWeight = FontWeight.Normal, fontSize = 13.sp, lineHeight = 16.sp)
    val Meta          = TextStyle(Roboto,     fontWeight = FontWeight.Normal, fontSize = 12.sp, lineHeight = 16.sp)
}

// Map onto Material 3 slots so stock components inherit the brand
val GmailTypography = Typography(
    headlineLarge = GmailText.TitleLarge,
    headlineSmall = GmailText.ThreadSubject,
    titleMedium   = GmailText.SenderUnread,
    bodyMedium    = GmailText.Body,
    labelLarge    = GmailText.TonalButton,
    labelSmall    = GmailText.Tab,
)

3. Signature Components

Compose FAB (the Gmail-red squircle)

Use the real Material FloatingActionButton / ExtendedFloatingActionButton. The Gmail FAB is a 16.dp-rounded squircle — override the default circular shape.

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.shrinkHorizontally
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp

@Composable
fun ComposeFab(
    extended: Boolean,            // true on tablet / scroll-up — shows "Compose"
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    val haptics = LocalHapticFeedback.current
    ExtendedFloatingActionButton(
        onClick = {
            haptics.performHapticFeedback(HapticFeedbackType.LongPress) // ≈ .impact(.medium)
            onClick()
        },
        expanded = extended,
        icon = { Icon(Icons.Filled.Edit, contentDescription = "Compose") },
        text = { Text("Compose", style = GmailText.FabButton) },
        containerColor = MaterialTheme.colorScheme.primary, // Gmail red / dark red
        contentColor = Color.White,
        shape = RoundedCornerShape(16.dp),                  // Material You squircle, NOT a circle
        modifier = modifier,
    )
}

Place it in Scaffold(floatingActionButton = { ComposeFab(...) }, floatingActionButtonPosition = FabPosition.End) so it floats bottom-right with 16.dp clearance above the bottom bar.

Email List Row (with swipe actions)

import androidx.compose.material3.SwipeToDismissBox
import androidx.compose.material3.SwipeToDismissBoxValue
import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.filled.Archive
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.outlined.StarBorder
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextOverflow

@Composable
fun EmailRow(
    sender: String,
    subject: String,
    snippet: String,
    timestamp: String,
    isUnread: Boolean,
    isStarred: Boolean,
    avatarColor: Color,
    avatarInitial: String,
    onOpen: () -> Unit,
    onArchive: () -> Unit,
    onDelete: () -> Unit,
) {
    val dismiss = rememberSwipeToDismissBoxState(
        confirmValueChange = {
            when (it) {
                SwipeToDismissBoxValue.StartToEnd -> { onArchive(); true } // swipe →: archive (teal)
                SwipeToDismissBoxValue.EndToStart -> { onDelete(); true }  // swipe ←: delete (red)
                else -> false
            }
        },
        positionalThreshold = { it * 0.4f }, // 40% commit threshold
    )

    SwipeToDismissBox(
        state = dismiss,
        backgroundContent = {
            val (color, icon, align) = when (dismiss.dismissDirection) {
                SwipeToDismissBoxValue.StartToEnd -> Triple(GmailColors.ArchiveTeal, Icons.Filled.Archive, Alignment.CenterStart)
                else -> Triple(GmailColors.Red, Icons.Filled.Delete, Alignment.CenterEnd)
            }
            Box(Modifier.fillMaxSize().background(color).padding(horizontal = 24.dp), contentAlignment = align) {
                Icon(icon, contentDescription = null, tint = Color.White)
            }
        },
    ) {
        Row(
            Modifier
                .fillMaxWidth()
                .height(72.dp)
                .background(MaterialTheme.colorScheme.surface)
                .clickable(onClick = onOpen)
                .padding(horizontal = 16.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(12.dp),
        ) {
            // Avatar — colored fallback with initial
            Box(Modifier.size(36.dp).clip(CircleShape).background(avatarColor), contentAlignment = Alignment.Center) {
                Text(avatarInitial, style = GmailText.SenderUnread.copy(fontSize = 16.sp), color = Color.White)
            }
            Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
                Text(
                    sender,
                    style = if (isUnread) GmailText.SenderUnread else GmailText.SenderRead,
                    color = GmailColors.TextPrimary,
                    maxLines = 1, overflow = TextOverflow.Ellipsis,
                )
                Row {
                    Text(
                        subject,
                        style = if (isUnread) GmailText.SubjectUnread else GmailText.SenderRead,
                        color = GmailColors.TextPrimary,
                        maxLines = 1, overflow = TextOverflow.Ellipsis,
                    )
                    Text(
                        " — $snippet",
                        style = GmailText.Snippet,
                        color = GmailColors.TextSecondary,
                        maxLines = 1, overflow = TextOverflow.Ellipsis,
                    )
                }
            }
            Column(horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.spacedBy(4.dp)) {
                Text(
                    timestamp,
                    style = GmailText.Timestamp,
                    color = if (isUnread) GmailColors.TextPrimary else GmailColors.TextSecondary,
                )
                Icon(
                    if (isStarred) Icons.Filled.Star else Icons.Outlined.StarBorder,
                    contentDescription = if (isStarred) "Starred" else "Not starred",
                    tint = if (isStarred) GmailColors.StarYellow else GmailColors.TextSecondary,
                    modifier = Modifier.size(18.dp),
                )
            }
        }
    }
}

// Avatar color — deterministic from sender email's first letter
fun gmailAvatarColor(email: String): Pair<Color, String> {
    val initial = email.firstOrNull()?.uppercaseChar() ?: '?'
    val color = when (initial) {
        in 'A'..'F' -> GmailColors.GoogleRed
        in 'G'..'L' -> GmailColors.GoogleYellow
        in 'M'..'R' -> GmailColors.GoogleGreen
        else        -> GmailColors.GoogleBlue
    }
    return color to initial.toString()
}

Tonal Button (Reply / Reply all / Forward)

@Composable
fun TonalButton(text: String, icon: androidx.compose.ui.graphics.vector.ImageVector, onClick: () -> Unit) {
    FilledTonalButton(
        onClick = onClick,
        shape = CircleShape, // 500pt pill
        colors = ButtonDefaults.filledTonalButtonColors(
            containerColor = GmailColors.Surface,
            contentColor = GmailColors.TextLink, // the one place link blue is button text
        ),
        contentPadding = PaddingValues(horizontal = 24.dp, vertical = 10.dp),
    ) {
        Icon(icon, contentDescription = null, modifier = Modifier.size(18.dp))
        Spacer(Modifier.width(6.dp))
        Text(text, style = GmailText.TonalButton)
    }
}

Smart Reply Chip + Label Chip

@Composable
fun SmartReplyRow(suggestions: List<String>, onPick: (String) -> Unit) {
    val haptics = LocalHapticFeedback.current
    Row(
        Modifier.horizontalScroll(rememberScrollState()),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
    ) {
        suggestions.forEach { text ->
            AssistChip(
                onClick = {
                    haptics.performHapticFeedback(HapticFeedbackType.LongPress)
                    onPick(text)
                },
                label = { Text(text, style = GmailText.SmartReply) },
                shape = CircleShape,
                colors = AssistChipDefaults.assistChipColors(labelColor = GmailColors.TextLink),
                border = AssistChipDefaults.assistChipBorder(enabled = true, borderColor = GmailColors.Divider),
            )
        }
    }
}

@Composable
fun LabelChip(label: String, color: Color) {
    Box(
        Modifier
            .clip(CircleShape)
            .background(color.copy(alpha = 0.1f))
            .border(1.dp, color, CircleShape)
            .padding(horizontal = 10.dp, vertical = 4.dp),
    ) {
        Text(label, style = GmailText.LabelChip, color = color)
    }
}

4. Gmail-Specific Feature: Compose FAB Scroll Behavior + Swipe Actions

The FAB is THE Gmail. It must always be reachable: on scroll-down it collapses to icon-only (56.dp); on scroll-up it re-expands to ExtendedFloatingActionButton with the “Compose” label. Drive extended from a NestedScrollConnection so width animates over ~200ms (the ExtendedFloatingActionButton expanded flag animates this for you).

import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.geometry.Offset

@Composable
fun InboxScreen(emails: List<EmailModel>) {
    var fabExtended by remember { mutableStateOf(true) }
    val scrollConn = remember {
        object : NestedScrollConnection {
            override fun onPreScroll(available: Offset, source: androidx.compose.ui.input.nestedscroll.NestedScrollSource): Offset {
                if (available.y < -4f) fabExtended = false // scrolling down → collapse
                if (available.y >  4f) fabExtended = true  // scrolling up → expand
                return Offset.Zero
            }
        }
    }

    Scaffold(
        floatingActionButton = { ComposeFab(extended = fabExtended, onClick = { /* open compose */ }) },
        floatingActionButtonPosition = FabPosition.End,
    ) { padding ->
        LazyColumn(Modifier.padding(padding).nestedScroll(scrollConn)) {
            items(emails, key = { it.id }) { e ->
                val (avColor, initial) = gmailAvatarColor(e.senderEmail)
                EmailRow(
                    sender = e.sender, subject = e.subject, snippet = e.snippet,
                    timestamp = e.timestamp, isUnread = e.isUnread, isStarred = e.isStarred,
                    avatarColor = avColor, avatarInitial = initial,
                    onOpen = { /* open thread */ },
                    onArchive = { /* archive + animate row height 72 → 0 */ },
                    onDelete = { /* delete */ },
                )
            }
        }
    }
}

SwipeToDismissBox (in EmailRow) is the Material-idiomatic equivalent of iOS .swipeActions: swipe-right reveals the teal archive pane, swipe-left the red delete pane, with a 40% positional commit threshold and an automatic spring snap-back below it. On archive, animate the row’s height 72.dp → 0 via animateContentSize() so adjacent rows shift up.

5. Navigation

Use Material 3 NavigationBar — Gmail’s iOS active-indicator pill is literally the Material You secondaryContainer pill, so this is the one app where the stock component matches the source exactly. 2–4 configurable tabs: Mail, Chat, Spaces, Meet. Android has no live blur, so the bar is the opaque canvas surface with a 0.5dp top divider.

@Composable
fun GmailBottomBar(selected: Int, onSelect: (Int) -> Unit) {
    Column {
        HorizontalDivider(color = GmailColors.Divider, thickness = 0.5.dp)
        NavigationBar(containerColor = MaterialTheme.colorScheme.surface, tonalElevation = 0.dp) {
            val items = listOf(
                "Mail"   to Icons.Filled.Email,
                "Chat"   to Icons.Filled.ChatBubble,
                "Spaces" to Icons.Filled.Groups,
                "Meet"   to Icons.Filled.Videocam,
            )
            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 = GmailText.Tab) },
                    colors = NavigationBarItemDefaults.colors(
                        selectedIconColor   = GmailColors.TextLink,
                        selectedTextColor   = GmailColors.TextLink,
                        unselectedIconColor = GmailColors.TextSecondary,
                        unselectedTextColor = GmailColors.TextSecondary,
                        indicatorColor      = GmailColors.SelectedRow, // the #E8F0FE Material pill — keep it
                    ),
                )
            }
        }
    }
}

The top app bar uses TopAppBar with a leading hamburger (opens a ModalNavigationDrawer of mailboxes), a centered pill search field (#F6F8FC, 500pt radius), and a trailing 28.dp account avatar.

6. Motion

Moment Compose recipe
FAB tap FloatingActionButton ripple + HapticFeedbackType.LongPress (≈ .impact(.medium))
FAB scroll collapse/expand ExtendedFloatingActionButton(expanded = …) animates width 120 → 56.dp over ~200ms
Swipe action SwipeToDismissBox, 40% positional threshold; below it springs back ~250ms
Star tap Animatable 1.0 → 1.2 → 1.0 spring, 300ms; HapticFeedbackType.LongPress (≈ .impact(.light))
Archive completion row animateContentSize() height 72 → 0 over 300ms; adjacent rows shift up
Smart Reply pick chip ripple + text fadeIn into compose box, 200ms
// Star toggle bounce
@Composable
fun StarToggle(starred: Boolean, onToggle: () -> Unit) {
    val scale = remember { Animatable(1f) }
    val haptics = LocalHapticFeedback.current
    val scope = rememberCoroutineScope()
    Icon(
        if (starred) Icons.Filled.Star else Icons.Outlined.StarBorder,
        contentDescription = if (starred) "Starred" else "Not starred",
        tint = if (starred) GmailColors.StarYellow else GmailColors.TextSecondary,
        modifier = Modifier
            .size(18.dp)
            .scale(scale.value)
            .clickable {
                scope.launch {
                    scale.animateTo(1.2f, tween(120)); scale.animateTo(1f, spring(dampingRatio = 0.5f))
                }
                haptics.performHapticFeedback(HapticFeedbackType.LongPress)
                onToggle()
            },
    )
}

Haptics: prefer LocalHapticFeedback. HapticFeedbackType.LongPress approximates iOS impact; for richer control use LocalView.current.performHapticFeedback(HapticFeedbackConstants.CONFIRM) (API 30+) or a Vibrator VibrationEffect.createOneShot(12, …).

7. Icons

Gmail uses Material Symbols natively; androidx.compose.material:material-icons-extended is the first-party match. The Gmail “M” logo and the pull-to-refresh swirl ship as vector drawables (ImageVector.vectorResource(R.drawable.…)).

Purpose SF Symbol (iOS) Material Icon (Compose)
Compose FAB square.and.pencil Icons.Filled.Edit
Mail (tab) envelope / .fill Icons.Filled.Email
Chat (tab) bubble.left Icons.Filled.ChatBubble
Spaces (tab) person.3 Icons.Filled.Groups
Meet (tab) video Icons.Filled.Videocam
Search magnifyingglass Icons.Filled.Search
Menu hamburger line.3.horizontal Icons.Filled.Menu
Archive archivebox Icons.Filled.Archive
Delete trash Icons.Filled.Delete
Mark unread envelope.badge Icons.Filled.MarkEmailUnread
Snooze clock Icons.Filled.Schedule
Star star / .fill Icons.Outlined.StarBorder / Icons.Filled.Star
Reply arrowshape.turn.up.left Icons.AutoMirrored.Filled.Reply
Reply all arrowshape.turn.up.left.2 Icons.AutoMirrored.Filled.ReplyAll
Forward arrowshape.turn.up.right Icons.AutoMirrored.Filled.Forward
Attachment paperclip Icons.Filled.AttachFile
Send paperplane.fill Icons.AutoMirrored.Filled.Send
Back chevron.left Icons.AutoMirrored.Filled.ArrowBack

8. Minimum SDK & Accessibility Notes

  • minSdk 24 (Compose floor is 21; SwipeToDismissBox and modern motion 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 status-bar icons (charcoal #202124 in dark wants light icons). Use Scaffold insets so the FAB floats 16.dp above the gesture nav and the top bar clears the camera cutout.
  • Font scaling: sp honors the user’s font scale automatically — keep it on email body, subjects, snippets, thread content. Pin layout-sensitive text (13sp section headers, 13sp timestamps, 12sp tab labels, 14sp Smart Reply / 12sp label chips) by deriving size from dp or wrapping in CompositionLocalProvider(LocalDensity provides Density(density, fontScale = 1f)) so the fixed 72.dp rows never clip.
  • TalkBack: collapse each email row into one node with Modifier.semantics(mergeDescendants = true) — “Sarah Chen, unread, Q4 planning update. Wanted to share the latest thinking. 10:42 AM. Not starred. Double-tap to open.” Expose archive/delete as custom accessibility actions since swipe is not discoverable via TalkBack.
  • Touch targets: Material guidance is 48.dp minimum. The FAB (56.dp) is well clear; ensure the 18.dp star and 24.dp top-bar icons sit in ≥48.dp hit areas via padding.
  • Contrast: #202124 on #FFFFFF exceeds AAA. #5F6368 secondary on white meets AA at 13sp+ — boost the snippet/timestamp toward #3C4043 when the system “increase contrast” setting (or isHighTextContrastEnabled) is on. In dark mode, switch the layered shadows for a tonal surfaceColorAtElevation() tint since shadows vanish on #202124 (Material’s dark-mode depth model).
  • Dynamic color: RECOMMENDED — embrace dynamicColorScheme(). Gmail is a Material-native Google app; on Android 12+ it should adopt Material You and tint chrome from the user’s wallpaper. Keep only the semantic anchors fixed: the Gmail-red FAB (primary), the Google 4-color avatar palette, the star yellow #F5BA18, and the teal/red swipe panes — these carry meaning and must not drift with the wallpaper. Everything else (surfaces, the active-indicator pill, tonal-button container) should flow from dynamicLightColorScheme() / dynamicDarkColorScheme().
All mobile designs →

Brand names describe the visual style only; coderef is not affiliated with or endorsed by any brand listed. Licenses