coderef

Lyft

Lyft's iOS app is a full-screen map with a friendly card floating over it. There is no tab bar — the map is the app. A live, edge-to-edge map fills the entire viewport; everything else (the "Where to?" search, the ride-type picker, the…
DownloadRaw
Lyft

Build with the Lyft look.

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

Palette

At a glance

Theme
light
Typefaces
Inter
Sections
8
Length
3,570 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · Lyft

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

Lyft (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports Lyft’s visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, paste-ready @Composables, Modifiers, motion, and haptics — including the full-screen map + rounded bottom sheet model and the ride-type selector.

Why a Compose guide for an iOS-referenced app? The DESIGN.md tokens are platform-neutral. This file keeps the visual identity (Lyft’s full-screen map, the soft rounded bottom sheet, single pink accent, very-rounded geometry, the ride-type selector) while making everything idiomatic Android — ModalBottomSheet instead of UIKit detents, dp/sp instead of pt, Compose haptics instead of UIImpactFeedbackGenerator.

Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, the Maps Compose library (com.google.maps.android:maps-compose), and Coil 2.6+ for images.

1. Color Tokens

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

object LyftColors {
    // Light
    val Canvas      = Color(0xFFFFFFFF)
    val Surface     = Color(0xFFF4F4F7)
    val SurfaceDeep = Color(0xFFE9E9EF)
    val Divider     = Color(0xFFE5E5EA)
    val TextPrimary   = Color(0xFF11111F)
    val TextSecondary = Color(0xFF6B6B7B)
    val TextTertiary  = Color(0xFF9A9AA8)

    // Dark
    val CanvasDark      = Color(0xFF11111F)
    val SurfaceDark     = Color(0xFF1C1C2B)
    val SurfaceDeepDark = Color(0xFF26263A)
    val DividerDark     = Color(0xFF2C2C40)
    val TextPrimaryDark = Color(0xFFFFFFFF)
    val TextSecondaryDark = Color(0xFFA0A0B4)

    // Brand
    val Pink         = Color(0xFFFF00BF)
    val PinkPressed  = Color(0xFFD500A0)
    val PinkTint     = Color(0xFFFFE5F7)
    val PinkTintDark = Color(0xFF33102B)

    // Semantic
    val Success = Color(0xFF00A862)
    val Gold    = Color(0xFFFFB400)
    val Error   = Color(0xFFE5484D)
}

Lyft ships both light and dark — wire two Material 3 schemes and pick by system theme. The pink stays identical on both.

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

private val LyftLight = lightColorScheme(
    primary = LyftColors.Pink, onPrimary = Color.White,
    background = LyftColors.Canvas, onBackground = LyftColors.TextPrimary,
    surface = LyftColors.Canvas, onSurface = LyftColors.TextPrimary,
    surfaceVariant = LyftColors.Surface, onSurfaceVariant = LyftColors.TextSecondary,
    outline = LyftColors.Divider, error = LyftColors.Error,
)
private val LyftDark = darkColorScheme(
    primary = LyftColors.Pink, onPrimary = Color.White,
    background = LyftColors.CanvasDark, onBackground = LyftColors.TextPrimaryDark,
    surface = LyftColors.CanvasDark, onSurface = LyftColors.TextPrimaryDark,
    surfaceVariant = LyftColors.SurfaceDark, onSurfaceVariant = LyftColors.TextSecondaryDark,
    outline = LyftColors.DividerDark, error = LyftColors.Error,
)

@Composable
fun LyftTheme(content: @Composable () -> Unit) =
    MaterialTheme(
        colorScheme = if (isSystemInDarkTheme()) LyftDark else LyftLight,
        typography = LyftTypography, content = content,
    )

2. Typography

Lyft uses Inter with a friendly, rounded character. Drop the TTFs in res/font/ (lowercase, snake_case). Fall back to the system font (Roboto).

// ui/theme/LyftType.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),   // 400
    Font(R.font.inter_semibold, FontWeight.SemiBold), // 600
    Font(R.font.inter_bold,     FontWeight.Bold),     // 700
)

// Named ramp — mirrors DESIGN.md §3 exactly (pt → sp 1:1)
object LyftText {
    val SheetTitle = TextStyle(Inter, fontWeight = FontWeight.Bold,     fontSize = 22.sp, lineHeight = 26.sp, letterSpacing = (-0.3).sp)
    val WhereTo    = TextStyle(Inter, fontWeight = FontWeight.SemiBold, fontSize = 20.sp, lineHeight = 25.sp, letterSpacing = (-0.2).sp)
    val RideName   = TextStyle(Inter, fontWeight = FontWeight.Bold,     fontSize = 17.sp, lineHeight = 21.sp, letterSpacing = (-0.1).sp)
    val Price      = TextStyle(Inter, fontWeight = FontWeight.Bold,     fontSize = 17.sp, lineHeight = 20.sp)
    val Body       = TextStyle(Inter, fontWeight = FontWeight.Normal,   fontSize = 15.sp, lineHeight = 22.sp)
    val Button     = TextStyle(Inter, fontWeight = FontWeight.Bold,     fontSize = 17.sp, lineHeight = 20.sp)
    val DriverName = TextStyle(Inter, fontWeight = FontWeight.Bold,     fontSize = 18.sp, lineHeight = 22.sp, letterSpacing = (-0.1).sp)
    val Meta       = TextStyle(Inter, fontWeight = FontWeight.Normal,   fontSize = 14.sp, lineHeight = 18.sp)
    val RideSub    = TextStyle(Inter, fontWeight = FontWeight.Normal,   fontSize = 13.sp, lineHeight = 17.sp)
    val Badge      = TextStyle(Inter, fontWeight = FontWeight.Bold,     fontSize = 11.sp, lineHeight = 13.sp, letterSpacing = 0.3.sp)
    val Caption    = TextStyle(Inter, fontWeight = FontWeight.Normal,   fontSize = 12.sp, lineHeight = 15.sp)
    val PinLabel   = TextStyle(Inter, fontWeight = FontWeight.Bold,     fontSize = 13.sp, lineHeight = 15.sp)
}

// Map onto Material 3 slots so stock components inherit the brand
val LyftTypography = Typography(
    headlineSmall = LyftText.SheetTitle,
    titleMedium   = LyftText.RideName,
    bodyMedium    = LyftText.Body,
    labelSmall    = LyftText.Badge,
)

Render prices, ETAs, and fare breakdowns with tabular figures via TextStyle(fontFeatureSettings = "tnum").

3. Signature Components

Full-Screen Map + Rounded Bottom Sheet

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.maps.android.compose.GoogleMap

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RideHomeScreen() {
    val sheetState = rememberStandardBottomSheetState(
        initialValue = SheetValue.PartiallyExpanded,
        skipHiddenState = true,
    )
    val scaffold = rememberBottomSheetScaffoldState(bottomSheetState = sheetState)

    BottomSheetScaffold(
        scaffoldState = scaffold,
        sheetPeekHeight = 140.dp,
        sheetShape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp), // very rounded
        sheetContainerColor = MaterialTheme.colorScheme.surface,
        sheetDragHandle = {
            Box(Modifier.padding(top = 8.dp).size(width = 36.dp, height = 4.dp)
                .clip(RoundedCornerShape(2.dp)).background(LyftColors.Divider))
        },
        sheetContent = { ChooseRideSheet() },
    ) {
        Box(Modifier.fillMaxSize()) {
            GoogleMap(modifier = Modifier.fillMaxSize())  // full-bleed map
            MapIconButton(
                "menu",
                Modifier.padding(16.dp).align(Alignment.TopStart),
            ) { /* open menu */ }
        }
    }
}

BottomSheetScaffold snaps with a spring. For the signature slight overshoot, wrap programmatic snaps in animateTo(target, spring(dampingRatio = 0.85f, stiffness = 220f)).

Ride-Type Selector Row

import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.ui.Alignment
import androidx.compose.ui.draw.clip
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback

@Composable
fun RideTypeRow(
    iconUrl: String,
    name: String,
    subtitle: String,
    capacityEta: String,
    price: String,
    selected: Boolean,
    onClick: () -> Unit,
) {
    val haptics = LocalHapticFeedback.current
    val bg by animateColorAsState(if (selected) LyftColors.PinkTint else Color.Transparent,
        label = "rowBg")
    val border by animateColorAsState(if (selected) LyftColors.Pink else Color.Transparent,
        label = "rowBorder")

    Row(
        Modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(16.dp))
            .background(bg)
            .border(2.dp, border, RoundedCornerShape(16.dp))
            .clickable {
                haptics.performHapticFeedback(HapticFeedbackType.TextHandleMove)
                onClick()
            }
            .padding(horizontal = 16.dp, vertical = 14.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(12.dp),
    ) {
        AsyncImage(model = iconUrl, contentDescription = null,
            modifier = Modifier.size(width = 56.dp, height = 40.dp))
        Column(Modifier.weight(1f)) {
            Text(name, style = LyftText.RideName, color = MaterialTheme.colorScheme.onSurface)
            Text(subtitle, style = LyftText.RideSub, color = MaterialTheme.colorScheme.onSurfaceVariant)
            Text(capacityEta, style = LyftText.Meta, color = MaterialTheme.colorScheme.onSurfaceVariant)
        }
        Text(price, style = LyftText.Price.copy(fontFeatureSettings = "tnum"),
            color = MaterialTheme.colorScheme.onSurface)
    }
}

Primary CTA

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 LyftCTA(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(56.dp)
            .scale(scale)
            .clip(RoundedCornerShape(16.dp))
            .background(if (pressed) LyftColors.PinkPressed else LyftColors.Pink)
            .clickable(interaction, indication = null, onClick = onClick),
        contentAlignment = Alignment.Center,
    ) {
        Text(label, style = LyftText.Button, color = Color.White)
    }
}

Pickup Pin (with drop)

import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.Canvas
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Path

@Composable
fun PickupPin(modifier: Modifier = Modifier) {
    val drop = remember { Animatable(0f) }
    val haptics = LocalHapticFeedback.current
    LaunchedEffect(Unit) {
        haptics.performHapticFeedback(HapticFeedbackType.LongPress)
        drop.animateTo(1f, spring(dampingRatio = 0.5f, stiffness = 200f)) // bounce
    }
    Box(
        modifier
            .size(28.dp, 38.dp)
            .scale(0.6f + 0.4f * drop.value)
            .offset(y = (-40 * (1 - drop.value)).dp),
        contentAlignment = Alignment.TopCenter,
    ) {
        Canvas(Modifier.matchParentSize()) {
            val w = size.width
            val path = Path().apply {
                addOval(androidx.compose.ui.geometry.Rect(0f, 0f, w, w))
                moveTo(w / 2, w * 0.5f)
                lineTo(w / 2, size.height)
                close()
            }
            drawPath(path, LyftColors.Pink)
            drawCircle(Color.White, radius = w * 0.18f, center = Offset(w / 2, w * 0.42f))
        }
    }
}

“Where to?” Search Bar

import androidx.compose.ui.draw.shadow

@Composable
fun WhereToBar(modifier: Modifier = Modifier) {
    Row(
        modifier
            .fillMaxWidth()
            .padding(horizontal = 20.dp)
            .shadow(12.dp, RoundedCornerShape(16.dp),
                spotColor = LyftColors.TextPrimary.copy(alpha = 0.16f))
            .clip(RoundedCornerShape(16.dp))
            .background(MaterialTheme.colorScheme.surface)
            .padding(horizontal = 18.dp)
            .height(56.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(12.dp),
    ) {
        Box(Modifier.size(10.dp).clip(RoundedCornerShape(3.dp)).background(LyftColors.Pink))
        Text("Where to?", style = LyftText.WhereTo,
            color = MaterialTheme.colorScheme.onSurfaceVariant)
    }
}

4. Sheet State Progression

enum class TripState { Idle, ChooseRide, ConfirmPickup, EnRoute, Rate }
// Hold TripState in a ViewModel. Map each to a target sheet value + content:
// Idle → PartiallyExpanded "Where to?"; ChooseRide → Expanded ride list;
// ConfirmPickup → a mid anchor + address/Confirm; EnRoute → driver card; Rate → rating+tip.
// On state change call sheetState.expand()/partialExpand() (or animateTo a custom anchor).

5. Navigation (No Tab Bar)

Lyft has no NavigationBar. The root is the map + BottomSheetScaffold; trip phases are state-driven. The floating menu button opens account/history as a separate destination (navController.navigate(...) to a full-screen route or a ModalBottomSheet), not a tab.

@Composable
fun MapIconButton(system: String, modifier: Modifier = Modifier, onClick: () -> Unit) {
    val icon = when (system) {
        "menu" -> Icons.Filled.Menu
        "back" -> Icons.Filled.ArrowBackIosNew
        "locate" -> Icons.Filled.MyLocation
        else -> Icons.Filled.Layers
    }
    Box(
        modifier
            .size(44.dp)
            .shadow(12.dp, CircleShape, spotColor = LyftColors.TextPrimary.copy(alpha = 0.16f))
            .clip(CircleShape)
            .background(MaterialTheme.colorScheme.surface)
            .clickable(onClick = onClick),
        contentAlignment = Alignment.Center,
    ) {
        Icon(icon, contentDescription = system, tint = MaterialTheme.colorScheme.onSurface,
            modifier = Modifier.size(20.dp))
    }
}

6. Motion

Moment Compose recipe
Sheet detent snap BottomSheetScaffold default spring; programmatic animateTo(spring(dampingRatio = 0.85f, stiffness = 220f)) for the slight overshoot
Ride-type select animateColorAsState for bg/border ring; HapticFeedbackType.TextHandleMove
Pickup pin drop Animatable 0 → 1 spring(dampingRatio = 0.5f, stiffness = 200f); scale + offset; HapticFeedbackType.LongPress (~soft)
CTA press Modifier.scale 1 → 0.98 via collectIsPressedAsState()
Map recenter cameraPositionState.animate(update, durationMs = 400)
ETA bubble update AnimatedContent cross-fade on the number; tabular figures prevent layout jump
// Map recenter
LaunchedEffect(recenterTrigger) {
    cameraPositionState.animate(
        CameraUpdateFactory.newLatLngZoom(userLatLng, 16f), durationMs = 400)
}

Haptics: prefer LocalHapticFeedback. For the pickup-pin landing, LocalView.current.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) (API 30+) approximates iOS .soft impact.

7. Icons

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

Purpose SF Symbol (iOS) Material Icon (Compose)
Menu (map) line.3.horizontal Icons.Filled.Menu
Back / dismiss chevron.left Icons.Filled.ArrowBackIosNew
Recenter / locate location.fill Icons.Filled.MyLocation
Layers square.3.layers.3d Icons.Filled.Layers
Search magnifyingglass Icons.Filled.Search
Saved: Home house.fill Icons.Filled.Home
Saved: Work briefcase.fill Icons.Filled.Work
Rating star star.fill Icons.Filled.Star
Contact driver phone.fill / message.fill Icons.Filled.Call / Icons.Filled.Message
Share trip square.and.arrow.up Icons.Filled.IosShare
Safety shield.fill Icons.Filled.Shield
Where-to dot (drawn pink square) —

8. Minimum SDK & Accessibility Notes

  • minSdk 24 (Compose floor is 21; BottomSheetScaffold + Maps Compose are comfortable at 24). compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+.
  • Edge-to-edge: call enableEdgeToEdge(); the map renders full-bleed behind the system bars. System-bar icon contrast must flip with the map’s day/night tiles — use WindowCompat and update on theme change. Inset floating controls with Modifier.windowInsetsPadding(WindowInsets.systemBars).
  • Font scaling: sp honors the user’s font scale — keep it on sheet titles, ride names, body. Pin badges and pin labels; the “Where to?” prompt scales to a max and never wraps.
  • TalkBack: ride-type rows use Modifier.semantics { role = Role.RadioButton; stateDescription = if (selected) "Selected" else "Not selected" } and merge name/sub/price; the CTA announces “Select Lyft”; the map exposes a summary contentDescription.
  • Touch targets: Material guidance is 48.dp minimum. The CTA (56) and ride rows (~72) clear it; floating icon buttons are 44 — bump to 48 if your audit requires, keeping the visual circle at 44 via inner padding.
  • Reduce Motion: check Settings.Global.ANIMATOR_DURATION_SCALE (or accessibility manager) and swap sheet/pin springs for short tween animations.
  • Contrast: #6B6B7B on white and #A0A0B4 on #11111F pass WCAG AA at 14sp+. Lyft Pink #FF00BF on white passes AA for bold 17sp+ — keep the CTA bold and verify the pressed #D500A0 too.
  • Dynamic color: do not enable Material You dynamicLightColorScheme()/dynamicDarkColorScheme() — Lyft Pink must stay fixed across both themes regardless of wallpaper.

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