coderef

Marriott Bonvoy

Marriott Bonvoy's iOS app is a premium hotel-booking and loyalty product, and its visual language is engineered to feel like a high-end travel concierge in your pocket. It is not a discount-forward marketplace — it is a membership.
DownloadRaw
Marriott Bonvoy

Build with the Marriott Bonvoy look.

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

Palette

At a glance

Theme
dark
Typefaces
SF Pro Text, SF Pro Display
Sections
8
Length
4,473 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · Marriott Bonvoy

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

Marriott Bonvoy (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports Marriott Bonvoy’s visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, the signature Bonvoy points panel, points-aware rate cards, the hotel hero, the Mobile Key, the bottom bar, 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 (Bonvoy’s deep navy base, disciplined gold for member value, the points panel, points-aware rate select, the Mobile Key) while making everything idiomatic Android — NavigationBar instead of a UITabBar, ModalBottomSheet for the date picker, NFC/HCE for the real Mobile Key, sp/dp instead of pt.

Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, Coil 2.6+ for hero/thumbnails. Bonvoy has a fixed brand palette (no Material You extraction). It is light-mode-capable but renders premium in both; a full dark scheme is provided.

1. Color Tokens

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

object BonvoyColors {
    // Canvas & Surfaces (Light)
    val Canvas         = Color(0xFFFFFFFF)
    val SurfaceCream   = Color(0xFFF6F4EF)
    val SurfacePressed = Color(0xFFECE9E1)
    val Divider        = Color(0xFFE3E1DA)

    // Canvas & Surfaces (Dark)
    val DarkCanvas   = Color(0xFF121214) // near-black, slightly warm
    val DarkSurface1 = Color(0xFF1A1A1D)
    val DarkSurface2 = Color(0xFF232327)
    val DarkDivider  = Color(0xFF2E2E33)

    // Structure (Navy) — constant across schemes
    val Navy        = Color(0xFF16264A)
    val NearBlack   = Color(0xFF1C1C1C)
    val NavyLine    = Color(0xFF243A66)
    val NavyPressed = Color(0xFF1F3157)

    // Member value (Gold) — used sparingly to mark earned value
    val Gold        = Color(0xFFB3852A)
    val GoldBright  = Color(0xFFD2A23E) // dark-mode points/tier/Book
    val GoldPressed = Color(0xFF8F6A1F)
    val OnGold      = Color(0xFF1C1206)

    // Text
    val TextPrimary       = Color(0xFF1A1A1C) // near-black
    val TextSecondary     = Color(0xFF5C5C63)
    val TextTertiary      = Color(0xFF8E8E96)
    val DarkTextPrimary   = Color(0xFFEDEDEF)
    val DarkTextSecondary = Color(0xFFA0A0A8)

    // Semantic
    val Success = Color(0xFF2E8B57)
    val Error   = Color(0xFFC7453B)
    val Warning = Color(0xFFC8901C)
}

enum class BonvoyTier(val label: String) {
    Member("MEMBER"), Silver("SILVER ELITE"), Gold("GOLD ELITE"),
    Platinum("PLATINUM ELITE"), Titanium("TITANIUM ELITE"), Ambassador("AMBASSADOR ELITE");
}

Wire it into both schemes. Navy is the constant structure color; gold marks member value and brightens to #D2A23E on dark.

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

private val BonvoyLight = lightColorScheme(
    primary        = BonvoyColors.Gold,    // the "Book" CTA / value
    onPrimary      = BonvoyColors.OnGold,
    secondary      = BonvoyColors.Navy,    // structure / secondary CTA
    background      = BonvoyColors.Canvas,
    onBackground   = BonvoyColors.TextPrimary,
    surface        = BonvoyColors.SurfaceCream,
    onSurface      = BonvoyColors.TextPrimary,
    surfaceVariant = BonvoyColors.SurfacePressed,
    outline        = BonvoyColors.Divider,
    error          = BonvoyColors.Error,
)

private val BonvoyDark = darkColorScheme(
    primary        = BonvoyColors.GoldBright,
    onPrimary      = BonvoyColors.OnGold,
    secondary      = BonvoyColors.Navy,
    background      = BonvoyColors.DarkCanvas,
    onBackground   = BonvoyColors.DarkTextPrimary,
    surface        = BonvoyColors.DarkSurface1,
    onSurface      = BonvoyColors.DarkTextPrimary,
    surfaceVariant = BonvoyColors.DarkSurface2,
    outline        = BonvoyColors.DarkDivider,
    error          = BonvoyColors.Error,
)

@Composable
fun BonvoyTheme(
    dark: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit,
) = MaterialTheme(
    colorScheme = if (dark) BonvoyDark else BonvoyLight,
    typography  = BonvoyTypography,
    content     = content,
)

2. Typography

Marriott Bonvoy ships no custom typeface — it uses the platform system font with the OS font-scale. Eyebrows are letter-spaced uppercase small-caps; the points balance is an 800-weight numeral.

// ui/theme/BonvoyType.kt
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.sp

private val Sys = FontFamily.Default // system font — no bundled face

object BonvoyText {
    val LargeTitle    = TextStyle(Sys, fontWeight = FontWeight.ExtraBold, fontSize = 32.sp, lineHeight = 37.sp, letterSpacing = (-0.4).sp)
    val PointsBalance = TextStyle(Sys, fontWeight = FontWeight.ExtraBold, fontSize = 26.sp, lineHeight = 31.sp, letterSpacing = (-0.3).sp)
    val HotelName     = TextStyle(Sys, fontWeight = FontWeight.Bold,      fontSize = 22.sp, lineHeight = 27.sp, letterSpacing = (-0.3).sp)
    val Section       = TextStyle(Sys, fontWeight = FontWeight.Bold,      fontSize = 18.sp, lineHeight = 23.sp, letterSpacing = (-0.2).sp)
    val Body          = TextStyle(Sys, fontWeight = FontWeight.Normal,    fontSize = 16.sp, lineHeight = 24.sp)
    val RateTitle     = TextStyle(Sys, fontWeight = FontWeight.SemiBold,  fontSize = 15.sp, lineHeight = 20.sp)
    val Price         = TextStyle(Sys, fontWeight = FontWeight.ExtraBold, fontSize = 17.sp, lineHeight = 20.sp)
    val Meta          = TextStyle(Sys, fontWeight = FontWeight.Normal,    fontSize = 14.sp, lineHeight = 19.sp)
    val Eyebrow       = TextStyle(Sys, fontWeight = FontWeight.Bold,      fontSize = 11.sp, lineHeight = 13.sp, letterSpacing = 1.2.sp)
    val PointsEarn    = TextStyle(Sys, fontWeight = FontWeight.Bold,      fontSize = 11.sp, lineHeight = 13.sp, letterSpacing = 0.3.sp)
    val Button        = TextStyle(Sys, fontWeight = FontWeight.Bold,      fontSize = 16.sp, lineHeight = 16.sp)
    val Tab           = TextStyle(Sys, fontWeight = FontWeight.Medium,    fontSize = 10.sp, lineHeight = 11.sp, letterSpacing = 0.1.sp)
    val Caption       = TextStyle(Sys, fontWeight = FontWeight.Medium,    fontSize = 12.sp, lineHeight = 15.sp)
}

val BonvoyTypography = Typography(
    headlineLarge  = BonvoyText.LargeTitle,
    headlineMedium = BonvoyText.PointsBalance,
    titleMedium    = BonvoyText.HotelName,
    bodyMedium     = BonvoyText.Body,
    labelSmall     = BonvoyText.Tab,
)

3. Signature Components

Bonvoy Points Panel (the core component)

import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import java.text.NumberFormat

@Composable
fun BonvoyPointsPanel(
    tier: BonvoyTier,
    memberName: String,
    points: Int,
    progress: Float,             // 0f..1f toward next free night
    footnote: String,
) {
    val animPoints by animateIntAsState(points, tween(600, easing = EaseOutCubic), label = "points")
    val animFill by animateFloatAsState(progress, tween(500, easing = EaseOutCubic), label = "fill")
    val fmt = remember { NumberFormat.getInstance() }

    Column(
        Modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(14.dp))
            .background(BonvoyColors.Navy)
            .border(1.dp, BonvoyColors.NavyLine, RoundedCornerShape(14.dp))
            .padding(16.dp),
    ) {
        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
            Column {
                Text(tier.label, style = BonvoyText.Eyebrow, color = BonvoyColors.GoldBright)
                Text(memberName, fontSize = 13.sp, color = Color.White.copy(alpha = 0.7f),
                    modifier = Modifier.padding(top = 2.dp))
            }
            Column(horizontalAlignment = Alignment.End) {
                Text(fmt.format(animPoints), style = BonvoyText.PointsBalance, color = Color.White)
                Text("POINTS", fontSize = 11.sp, letterSpacing = 0.4.sp, color = Color.White.copy(alpha = 0.6f))
            }
        }
        Box(
            Modifier
                .fillMaxWidth().height(6.dp).padding(top = 14.dp)
        ) {
            Box(Modifier.fillMaxSize().clip(RoundedCornerShape(3.dp)).background(Color.White.copy(alpha = 0.14f)))
            Box(
                Modifier
                    .fillMaxHeight().fillMaxWidth(animFill)
                    .clip(RoundedCornerShape(3.dp))
                    .background(Brush.horizontalGradient(listOf(BonvoyColors.Gold, BonvoyColors.GoldBright)))
            )
        }
        Text(footnote, fontSize = 11.sp, color = Color.White.copy(alpha = 0.6f),
            modifier = Modifier.padding(top = 8.dp))
    }
}

Points-Aware Rate Card

import androidx.compose.foundation.clickable

@Composable
fun RateCard(
    name: String, detail: String, priceText: String, pointsLine: String,
    selected: Boolean, onClick: () -> Unit,
) {
    Row(
        Modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(10.dp))
            .background(if (selected) BonvoyColors.Gold.copy(alpha = 0.10f) else BonvoyColors.DarkSurface1)
            .border(1.dp, if (selected) BonvoyColors.Gold else BonvoyColors.DarkDivider, RoundedCornerShape(10.dp))
            .clickable(onClick = onClick)
            .padding(14.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.SpaceBetween,
    ) {
        Column(Modifier.weight(1f)) {
            Text(name, style = BonvoyText.RateTitle, color = BonvoyColors.DarkTextPrimary)
            Text(detail, fontSize = 12.sp, color = BonvoyColors.DarkTextSecondary,
                modifier = Modifier.padding(top = 3.dp))
        }
        Column(horizontalAlignment = Alignment.End) {
            Text(priceText, style = BonvoyText.Price, color = BonvoyColors.DarkTextPrimary)
            Text(pointsLine, style = BonvoyText.PointsEarn, color = BonvoyColors.GoldBright,
                modifier = Modifier.padding(top = 2.dp))
        }
    }
}

Hotel Hero

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.layout.ContentScale
import coil.compose.AsyncImage

@Composable
fun HotelHero(
    imageUrl: String, subBrand: String, name: String, location: String,
    saved: Boolean, onBack: () -> Unit, onShare: () -> Unit, onToggleSave: () -> Unit,
) {
    Box(Modifier.fillMaxWidth().height(256.dp)) {
        AsyncImage(model = imageUrl, contentDescription = name,
            modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop)
        Box(
            Modifier.align(Alignment.BottomCenter).fillMaxWidth().height(145.dp)
                .background(Brush.verticalGradient(listOf(Color.Transparent, BonvoyColors.DarkCanvas.copy(alpha = 0.7f))))
        )
        Row(
            Modifier.align(Alignment.TopCenter).fillMaxWidth().padding(horizontal = 16.dp).padding(top = 54.dp),
            horizontalArrangement = Arrangement.SpaceBetween,
        ) {
            HeroButton(Icons.Filled.ArrowBack, onBack)
            Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
                HeroButton(Icons.Filled.IosShare, onShare)
                HeroButton(if (saved) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder, onToggleSave)
            }
        }
        Column(Modifier.align(Alignment.BottomStart).padding(18.dp)) {
            Text(subBrand.uppercase(), style = BonvoyText.Eyebrow.copy(letterSpacing = 1.4.sp), color = BonvoyColors.GoldBright)
            Text(name, fontSize = 23.sp, fontWeight = FontWeight.ExtraBold, color = Color.White,
                modifier = Modifier.padding(top = 5.dp))
            Text(location, fontSize = 13.sp, color = Color.White.copy(alpha = 0.82f),
                modifier = Modifier.padding(top = 5.dp))
        }
    }
}

@Composable
private fun HeroButton(icon: androidx.compose.ui.graphics.vector.ImageVector, onClick: () -> Unit) {
    IconButton(
        onClick = onClick,
        modifier = Modifier.size(36.dp).clip(CircleShape).background(BonvoyColors.DarkCanvas.copy(alpha = 0.55f)),
    ) { Icon(icon, contentDescription = null, tint = Color.White, modifier = Modifier.size(18.dp)) }
}

Mobile Key

import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback

@Composable
fun MobileKeyCard(room: String, checkIn: String, onUnlock: () -> Unit) {
    val haptics = LocalHapticFeedback.current
    Row(
        Modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(14.dp))
            .background(BonvoyColors.Navy)
            .border(1.dp, BonvoyColors.NavyLine, RoundedCornerShape(14.dp))
            .clickable { haptics.performHapticFeedback(HapticFeedbackType.LongPress); onUnlock() }
            .padding(16.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(14.dp),
    ) {
        Box(
            Modifier.size(48.dp).clip(RoundedCornerShape(12.dp)).background(BonvoyColors.GoldBright.copy(alpha = 0.16f)),
            contentAlignment = Alignment.Center,
        ) { Icon(Icons.Filled.Lock, contentDescription = null, tint = BonvoyColors.GoldBright, modifier = Modifier.size(22.dp)) }
        Column(Modifier.weight(1f)) {
            Text("Mobile Key ready", fontSize = 14.sp, fontWeight = FontWeight.Bold, color = Color.White)
            Text("Skip the front desk · Tap your phone to unlock", fontSize = 12.sp,
                color = Color.White.copy(alpha = 0.65f), modifier = Modifier.padding(top = 3.dp))
            Text("ROOM $room · CHECK-IN $checkIn", style = BonvoyText.Eyebrow.copy(letterSpacing = 0.5.sp),
                color = BonvoyColors.GoldBright, modifier = Modifier.padding(top = 6.dp))
        }
    }
}

Elite Tier Badge + Booking Bar

@Composable
fun TierBadge(tier: BonvoyTier) {
    val (fg, bg, border) = when (tier) {
        BonvoyTier.Member     -> Triple(BonvoyColors.TextSecondary, BonvoyColors.DarkSurface2, Color.White.copy(alpha = 0.06f))
        BonvoyTier.Silver     -> Triple(Color(0xFFC7C7CE), Color(0xFF2A2A2E), Color(0xFF3A3A40))
        BonvoyTier.Gold       -> Triple(BonvoyColors.GoldBright, BonvoyColors.Gold.copy(alpha = 0.16f), BonvoyColors.GoldBright.copy(alpha = 0.40f))
        BonvoyTier.Platinum   -> Triple(Color(0xFF9FB6E0), BonvoyColors.Navy.copy(alpha = 0.40f), BonvoyColors.NavyLine)
        BonvoyTier.Titanium   -> Triple(BonvoyColors.GoldBright, BonvoyColors.Navy, BonvoyColors.NavyLine)
        BonvoyTier.Ambassador -> Triple(BonvoyColors.GoldBright, BonvoyColors.Navy, BonvoyColors.Gold)
    }
    Box(
        Modifier
            .clip(RoundedCornerShape(999.dp)).background(bg)
            .border(1.dp, border, RoundedCornerShape(999.dp))
            .padding(horizontal = 12.dp, vertical = 6.dp),
    ) { Text(tier.label, style = BonvoyText.Eyebrow.copy(letterSpacing = 0.6.sp), color = fg) }
}

@Composable
fun BonvoyBookingBar(price: String, pointsEarned: String, onBook: () -> Unit) {
    val haptics = LocalHapticFeedback.current
    Row(
        Modifier
            .fillMaxWidth().height(76.dp)
            .background(BonvoyColors.DarkCanvas.copy(alpha = 0.97f))
            .padding(horizontal = 18.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.SpaceBetween,
    ) {
        Column {
            Row(verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
                Text(price, fontSize = 19.sp, fontWeight = FontWeight.ExtraBold, color = BonvoyColors.DarkTextPrimary)
                Text("/ night", fontSize = 13.sp, color = BonvoyColors.DarkTextSecondary, modifier = Modifier.padding(bottom = 3.dp))
            }
            Text(pointsEarned, style = BonvoyText.PointsEarn, color = BonvoyColors.GoldBright)
        }
        Button(
            onClick = { haptics.performHapticFeedback(HapticFeedbackType.LongPress); onBook() },
            shape = RoundedCornerShape(8.dp),
            colors = ButtonDefaults.buttonColors(containerColor = BonvoyColors.Gold, contentColor = BonvoyColors.OnGold),
            contentPadding = PaddingValues(horizontal = 26.dp, vertical = 13.dp),
        ) { Text("Book", style = BonvoyText.Button) }
    }
}

4. Bottom Navigation Bar

There is no Material tint pill — active is gold (loyalty is the spine of the app).

@Composable
fun BonvoyBottomBar(selected: Int, onSelect: (Int) -> Unit, keyActive: Boolean = false) {
    NavigationBar(containerColor = BonvoyColors.DarkCanvas, tonalElevation = 0.dp) {
        val items = listOf(
            "Home"       to Icons.Filled.Home,
            "Search"     to Icons.Filled.Search,
            "Trips"      to Icons.Filled.Work,
            "Mobile Key" to Icons.Filled.Key,
            "Account"    to Icons.Filled.AccountCircle,
        )
        items.forEachIndexed { i, (label, icon) ->
            NavigationBarItem(
                selected = selected == i,
                onClick = { onSelect(i) },
                icon = {
                    if (i == 3 && keyActive) {
                        BadgedBox(badge = { Badge(containerColor = BonvoyColors.GoldBright) }) {
                            Icon(icon, contentDescription = label, modifier = Modifier.size(22.dp))
                        }
                    } else Icon(icon, contentDescription = label, modifier = Modifier.size(22.dp))
                },
                label = { Text(label, style = BonvoyText.Tab) },
                colors = NavigationBarItemDefaults.colors(
                    selectedIconColor = BonvoyColors.GoldBright,
                    selectedTextColor = BonvoyColors.GoldBright,
                    unselectedIconColor = BonvoyColors.TextTertiary,
                    unselectedTextColor = BonvoyColors.TextTertiary,
                    indicatorColor = Color.Transparent, // no Material pill — Bonvoy has none
                ),
            )
        }
    }
}

5. Navigation

Bonvoy’s loop is Search → hotel detail → rate select → confirmation → Mobile Key. Model it with Navigation-Compose / Nav3: a 300ms slide push; the bottom bar persists on the five primary tabs and is hidden on the hotel detail (which has its own hero overlay nav + sticky booking bar). The date/room picker and filters are ModalBottomSheets. The real Mobile Key uses NFC/HCE (android.nfc) — the card in §3 is the trigger surface; the actual unlock goes through the lock vendor SDK.

6. Motion

Bonvoy motion is calm and premium — the loudest moment is the points count-up. 180–600ms ease-out.

Moment Compose recipe
Points count-up animateIntAsState(points, tween(600, EaseOutCubic))
Progress bar fill animateFloatAsState(progress, tween(500, EaseOutCubic)) → fillMaxWidth(animFill)
Rate select gold border/tint switch + animateColorAsState(tween(180)); booking bar AnimatedContent cross-dissolve 250ms
Mobile Key unlock gold ring animateFloatAsState pulse + HapticFeedbackType.LongPress / success
Hero parallax Modifier.graphicsLayer { translationY = scroll * 0.5f }
Card appear AnimatedVisibility fadeIn(tween(220)) + slideInVertically { it / 6 }, 60ms delay/item
Page navigation Nav3/NavHost slide push tween(300)
Sheet present ModalBottomSheet default slide-up; scrim fades 250ms
// Points count-up — the canonical Bonvoy motion
val animPoints by animateIntAsState(points, tween(600, easing = EaseOutCubic), label = "points")
Text(NumberFormat.getInstance().format(animPoints), style = BonvoyText.PointsBalance, color = Color.White)

Haptics: prefer LocalHapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) for the soft impact on “Book”; a success cue (HapticFeedbackConstants.CONFIRM on API 30+ via LocalView.current.performHapticFeedback(...)) for Mobile Key unlock and booking confirmed; HapticFeedbackType.TextHandleMove for rate select and date endpoints.

7. Icons

Use androidx.compose.material:material-icons-extended. The points progress bar and tier badges are drawn with Box+shape; the gold sub-brand eyebrow is text, not a glyph.

Purpose SF Symbol (iOS) Material Icon (Compose)
Home (tab) house Icons.Filled.Home
Search (tab) magnifyingglass Icons.Filled.Search
Trips (tab) suitcase Icons.Filled.Work
Mobile Key (tab) key Icons.Filled.Key
Account (tab) person.crop.circle Icons.Filled.AccountCircle
Back (hero) chevron.left Icons.Filled.ArrowBack
Share square.and.arrow.up Icons.Filled.IosShare
Save heart / heart.fill Icons.Filled.FavoriteBorder / Favorite
Rating star star.fill Icons.Filled.Star
Mobile Key glyph lock.fill / key.fill Icons.Filled.Lock / Icons.Filled.Key
Points / rewards star.circle.fill Icons.Filled.Stars
Free night moon.stars.fill Icons.Filled.Nightlight
Member tier crown.fill Icons.Filled.WorkspacePremium
Calendar / dates calendar Icons.Filled.CalendarMonth
Room & guests person.2 Icons.Filled.Group
Location mappin.and.ellipse Icons.Filled.LocationOn
Amenities checkmark.circle Icons.Filled.CheckCircle
Filters slider.horizontal.3 Icons.Filled.Tune
Confirmed checkmark.seal.fill Icons.Filled.Verified

8. Minimum SDK & Accessibility Notes

  • minSdk 24 (Compose floor is 21; NFC/HCE for Mobile Key needs API 19+, comfortable at 24). compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+.
  • Edge-to-edge: call enableEdgeToEdge(); the hero extends under the status bar with white overlay controls (light-content system bars while a hero is visible). The booking bar respects the navigation bar inset.
  • Font scaling: sp honors the user’s font scale — keep it on titles, points balance, hotel name, headers, body. Pin layout-sensitive text (eyebrows, 10sp tab labels, points-earn lines, progress legends) via a fixed-density wrapper: CompositionLocalProvider(LocalDensity provides Density(density, fontScale = 1f)).
  • No bundled font: use FontFamily.Default (system) everywhere. Do not ship a custom typeface.
  • TalkBack: announce the member panel “Titanium Elite, 128,450 points, 64% toward your next free night”; rate cards “{name}, {detail}, {price}, earn {points} points, double-tap to select”; the Mobile Key “Mobile Key ready, Room 1408, double-tap to unlock”; tier badges carry text labels (not color alone).
  • Touch targets: Material guidance is 48.dp. Give the 36.dp hero overlay buttons a ≥ 48.dp hit area; rate cards and the Mobile Key card are fully tappable; the “Book” button is ≥ 48.dp tall.
  • Contrast: #1A1A1C on #FFFFFF and #EDEDEF on #121214 pass WCAG AA. Gold-on-navy and the #1C1206-on-gold CTA are carefully paired; the gold sub-brand eyebrow is paired with the hotel name (not the sole channel).
  • Reduce motion: when Settings.Global.ANIMATOR_DURATION_SCALE == 0, set the points value and bar fill directly (no count-up); cross-dissolve rate/price changes; remove hero parallax.
  • Dark mode: navy #16264A stays constant (panels, secondary CTA, brand frame); gold brightens to #D2A23E for points/tier/Book; canvas is #121214, NOT pure black; #1A1A1C text becomes #EDEDEF. Shadows nearly vanish on dark — navy panels already use a 1dp NavyLine border; floating panels add a 1dp DarkDivider border. Do not enable Material You dynamicColorScheme(): Bonvoy’s navy + disciplined-gold identity must hold regardless of wallpaper.
  • Loyalty/security: never log full membership numbers or Mobile Key tokens; treat them as sensitive. The Mobile Key unlock must require an explicit user action (no auto-unlock); gate the real unlock behind device authentication where the lock SDK requires it.
All mobile designs →

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