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 —
NavigationBarinstead of a UITabBar,ModalBottomSheetfor the date picker, NFC/HCE for the real Mobile Key,sp/dpinstead ofpt.
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 BOM2024.09+, Kotlin2.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:
sphonors 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:
#1A1A1Con#FFFFFFand#EDEDEFon#121214pass 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
#16264Astays constant (panels, secondary CTA, brand frame); gold brightens to#D2A23Efor points/tier/Book; canvas is#121214, NOT pure black;#1A1A1Ctext becomes#EDEDEF. Shadows nearly vanish on dark — navy panels already use a 1dpNavyLineborder; floating panels add a 1dpDarkDividerborder. Do not enable Material YoudynamicColorScheme(): 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.