coderef

Bumble

Bumble's iOS app is unapologetically loud. Where Hinge whispers on cream paper and Tinder neon-flames at you, Bumble paints the screen Bumble Yellow (#FFC629) — a saturated, sun-bright honey-yellow — and lets it bake.
DownloadRaw
Bumble

Build with the Bumble look.

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

Palette

At a glance

Theme
light
Typefaces
Brando, Bricolage Grotesque, Tab labels
Sections
8
Length
4,955 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · Bumble

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

Bumble (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports Bumble’s visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, paste-ready @Composables, the Hexagon shape, the glowing heart action button, the It’s-a-Match celebration, and the 24-hour countdown chip.

Why a Compose guide for an iOS-referenced app? The DESIGN.md tokens are platform-neutral. This file keeps the visual identity (Bumble’s stadium-loud yellow, pure-black-on-yellow contrast, the hexagon spatial signature, the heavy Brando ladder) while making everything idiomatic Android — a custom Shape for the hex, NavigationBar instead of a UITabBar, Brush glow approximated with elevation + a radial scrim, sp/dp instead of pt.

Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, and Coil 2.6+ for profile photos. No color extraction, so no androidx.palette.

1. Color Tokens

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

object BumbleColors {
    // Brand
    val Yellow      = Color(0xFFFFC629) // the brand — CTAs, tab indicator, match, heart
    val HoneyDeep   = Color(0xFFF5B616) // pressed state on yellow
    val YellowLight = Color(0xFFFFE9A1) // soft chip fills, premium gradients

    // Mode colors (Date / BFF / Bizz)
    val BFFTeal     = Color(0xFF11AAA8)
    val BizzOrange  = Color(0xFFFF8000)

    // Canvas & Surfaces
    val Canvas      = Color(0xFFFFFFFF) // Date mode
    val BFFCream    = Color(0xFFFFFCF2) // BFF mode tint
    val Surface1    = Color(0xFFF5F5F5) // input fills, settings rows
    val Surface2    = Color(0xFFEDEDED) // pressed states, chip fills
    val Divider     = Color(0xFFE5E5E5) // 0.5dp hairlines

    // Text
    val Black       = Color(0xFF1F1F1F) // primary on white (warm, NOT pure)
    val Slate       = Color(0xFF5A5A5A) // secondary
    val Mist        = Color(0xFF9C9C9C) // tertiary, placeholders
    val OnYellow    = Color(0xFF000000) // pure black — required on yellow for WCAG AA

    // Semantic
    val MatchPink   = Color(0xFFE94B7B) // ONLY the It's-a-Match heart
    val Verified    = Color(0xFF0066FF) // verified checkmark
    val Warning     = Color(0xFFFF9500) // incomplete-profile warning
    val Error       = Color(0xFFD72638) // validation errors, expired countdown
    val Success     = Color(0xFF00A86B) // confirmation toasts

    // Dark mode (warm OLED — yellow unchanged)
    val DarkCanvas   = Color(0xFF0F0F0F)
    val DarkSurface1 = Color(0xFF1A1A1A)
    val DarkSurface2 = Color(0xFF2A2A2A)
    val DarkDivider  = Color(0xFF2F2F2F)
    val DarkText     = Color(0xFFF2F2F2)
    val DarkTextSec  = Color(0xFF9C9C9C)
    val HoneyDeepDk  = Color(0xFFFFD45C) // brightened for OLED pressed states
}

Wire it into a Material 3 lightColorScheme (Date mode) — Bumble’s primary surface is white. Provide a parallel dark scheme; the yellow stays identical because it reads loud on OLED.

// ui/theme/Theme.kt
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.MaterialTheme

private val BumbleLight = lightColorScheme(
    primary        = BumbleColors.Yellow,
    onPrimary      = BumbleColors.OnYellow,   // pure black on yellow — non-negotiable
    background     = BumbleColors.Canvas,
    onBackground   = BumbleColors.Black,
    surface        = BumbleColors.Canvas,
    onSurface      = BumbleColors.Black,
    surfaceVariant = BumbleColors.Surface1,
    outline        = BumbleColors.Divider,
    error          = BumbleColors.Error,
)

private val BumbleDark = darkColorScheme(
    primary        = BumbleColors.Yellow,     // unchanged on dark
    onPrimary      = BumbleColors.OnYellow,
    background     = BumbleColors.DarkCanvas,
    onBackground   = BumbleColors.DarkText,
    surface        = BumbleColors.DarkSurface1,
    onSurface      = BumbleColors.DarkText,
    surfaceVariant = BumbleColors.DarkSurface2,
    outline        = BumbleColors.DarkDivider,
    error          = BumbleColors.Error,
)

@Composable
fun BumbleTheme(content: @Composable () -> Unit) =
    MaterialTheme(
        colorScheme = if (isSystemInDarkTheme()) BumbleDark else BumbleLight,
        typography = BumbleTypography,
        content = content,
    )

2. Typography

Brando is the proprietary Bumble face — slab-influenced, humanist, heavy. Drop the TTFs in res/font/ (lowercase, snake_case). Bumble has no Light or Thin — only 400/500/700/900. Fall back to Roboto (the closest free Android substitute); for a tighter brand match consider bundling Bricolage Grotesque / Manrope Heavy.

// ui/theme/BumbleType.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 Brando = FontFamily(
    Font(R.font.brando_regular, FontWeight.Normal),   // 400
    Font(R.font.brando_medium,  FontWeight.Medium),   // 500
    Font(R.font.brando_bold,    FontWeight.Bold),     // 700
    Font(R.font.brando_black,   FontWeight.Black),    // 900
)

// Named ramp — mirrors DESIGN.md §3 exactly (pt → sp 1:1, same weights/tracking)
object BumbleText {
    val MatchHero    = TextStyle(Brando, fontWeight = FontWeight.Black,  fontSize = 44.sp, lineHeight = 46.sp, letterSpacing = (-0.8).sp)
    val Display      = TextStyle(Brando, fontWeight = FontWeight.Black,  fontSize = 32.sp, lineHeight = 35.sp, letterSpacing = (-0.6).sp)
    val ScreenTitle  = TextStyle(Brando, fontWeight = FontWeight.Black,  fontSize = 24.sp, lineHeight = 28.sp, letterSpacing = (-0.4).sp)
    val CardName     = TextStyle(Brando, fontWeight = FontWeight.Bold,   fontSize = 28.sp, lineHeight = 31.sp, letterSpacing = (-0.4).sp)
    val Section      = TextStyle(Brando, fontWeight = FontWeight.Bold,   fontSize = 18.sp, lineHeight = 22.sp, letterSpacing = (-0.1).sp)
    val Body         = TextStyle(Brando, fontWeight = FontWeight.Medium, fontSize = 16.sp, lineHeight = 22.sp)
    val BodyBold     = TextStyle(Brando, fontWeight = FontWeight.Bold,   fontSize = 16.sp, lineHeight = 22.sp)
    val BodySmall    = TextStyle(Brando, fontWeight = FontWeight.Medium, fontSize = 14.sp, lineHeight = 20.sp)
    val Button       = TextStyle(Brando, fontWeight = FontWeight.Bold,   fontSize = 16.sp, lineHeight = 16.sp)
    val ButtonLarge  = TextStyle(Brando, fontWeight = FontWeight.Bold,   fontSize = 18.sp, lineHeight = 18.sp)
    val Tab          = TextStyle(Brando, fontWeight = FontWeight.Bold,   fontSize = 10.sp, lineHeight = 10.sp, letterSpacing = 0.2.sp)
    val Chip         = TextStyle(Brando, fontWeight = FontWeight.Medium, fontSize = 13.sp, lineHeight = 13.sp)
    val Meta         = TextStyle(Brando, fontWeight = FontWeight.Medium, fontSize = 13.sp, lineHeight = 17.sp)
    val Counter      = TextStyle(Brando, fontWeight = FontWeight.Bold,   fontSize = 11.sp, lineHeight = 11.sp, letterSpacing = 0.2.sp)
    val Compliment   = TextStyle(Brando, fontWeight = FontWeight.Black,  fontSize = 22.sp, lineHeight = 26.sp, letterSpacing = (-0.3).sp)
}

// Map onto Material 3 slots so stock components inherit the brand
val BumbleTypography = Typography(
    displayLarge  = BumbleText.Display,
    headlineLarge = BumbleText.ScreenTitle,
    titleLarge    = BumbleText.CardName,
    titleMedium   = BumbleText.Section,
    bodyMedium    = BumbleText.Body,
    labelSmall    = BumbleText.Tab,
)

The 24-hour countdown is the only place numerals must hold column width — pin tabular figures on Meta (countdown) and Counter (badges) with TextStyle(fontFeatureSettings = "tnum").

3. Signature Components

Hexagon Shape (the brand spatial signature)

Bumble’s hex is point-up. This is the SwiftUI Hexagon: Shape ported to a Compose Shape — reuse it for match avatars, mode-toggle buttons, and the brand mark.

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import kotlin.math.cos
import kotlin.math.min
import kotlin.math.sin

val HexagonShape = object : Shape {
    override fun createOutline(
        size: androidx.compose.ui.geometry.Size,
        layoutDirection: LayoutDirection,
        density: Density,
    ): Outline {
        val r = min(size.width, size.height) / 2f
        val cx = size.width / 2f
        val cy = size.height / 2f
        val path = Path()
        for (i in 0 until 6) {
            val angle = (Math.PI / 3 * i - Math.PI / 2).toFloat() // point-up
            val x = cx + r * cos(angle)
            val y = cy + r * sin(angle)
            if (i == 0) path.moveTo(x, y) else path.lineTo(x, y)
        }
        path.close()
        return Outline.Generic(path)
    }
}

Yellow Primary CTA

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
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.draw.scale
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.foundation.clickable
import androidx.compose.ui.unit.dp

@Composable
fun BumblePrimaryButton(
    label: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    hasGlow: Boolean = false,
) {
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    val scale by animateFloatAsState(if (pressed) 0.97f else 1f, label = "ctaScale")
    val haptics = LocalHapticFeedback.current

    Box(
        modifier = modifier
            .fillMaxWidth()
            .height(56.dp)
            .scale(scale)
            // Yellow glow halo — Android has no colored blur shadow, so a tinted
            // ambient/spot shadow approximates the brand glow on hero CTAs.
            .then(
                if (hasGlow)
                    Modifier.shadow(24.dp, CircleShape, ambientColor = BumbleColors.Yellow,
                        spotColor = BumbleColors.Yellow)
                else Modifier
            )
            .clip(CircleShape) // 28dp capsule == full pill at 56dp height
            .background(if (pressed) BumbleColors.HoneyDeep else BumbleColors.Yellow)
            .clickable(interaction, indication = null) {
                haptics.performHapticFeedback(HapticFeedbackType.LongPress) // ≈ .impact(.medium)
                onClick()
            },
        contentAlignment = Alignment.Center,
    ) {
        Text(label, style = BumbleText.Button, color = BumbleColors.OnYellow) // pure black
    }
}

Swipe Action Row (under the swipe card)

Five circular buttons; the yellow heart is the hero with a glow halo. Press scales to 0.9; the haptic varies per button.

import androidx.compose.foundation.border
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.automirrored.filled.Undo
import androidx.compose.material3.Icon
import androidx.compose.ui.graphics.vector.ImageVector

@Composable
fun SwipeActionRow(
    onRewind: () -> Unit,
    onPass: () -> Unit,
    onLike: () -> Unit,
    onSuper: () -> Unit,
    onCompliment: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Row(
        modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterHorizontally),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        ActionCircle(48.dp, BumbleColors.Surface1, null, HapticFeedbackType.TextHandleMove, onRewind) {
            Icon(Icons.AutoMirrored.Filled.Undo, "Rewind", tint = BumbleColors.Mist, modifier = Modifier.size(20.dp))
        }
        ActionCircle(56.dp, BumbleColors.Canvas, BumbleColors.Black, HapticFeedbackType.TextHandleMove, onPass) {
            Icon(Icons.Filled.Close, "Pass", tint = BumbleColors.Black, modifier = Modifier.size(22.dp))
        }
        // Heart Yes — 64dp hero with yellow glow halo
        HeartActionButton(onLike)
        ActionCircle(56.dp, BumbleColors.Yellow, null, HapticFeedbackType.LongPress, onSuper) {
            Icon(Icons.Filled.Star, "SuperSwipe", tint = BumbleColors.OnYellow, modifier = Modifier.size(22.dp))
        }
        ActionCircle(56.dp, BumbleColors.Canvas, BumbleColors.Yellow, HapticFeedbackType.TextHandleMove, onCompliment) {
            // Bee glyph is proprietary — ship as a vector drawable; placeholder star here
            Icon(Icons.Filled.Star, "Compliment", tint = BumbleColors.Yellow, modifier = Modifier.size(22.dp))
        }
    }
}

@Composable
private fun ActionCircle(
    diameter: androidx.compose.ui.unit.Dp,
    fill: Color,
    stroke: Color?,
    haptic: HapticFeedbackType,
    onClick: () -> Unit,
    content: @Composable () -> Unit,
) {
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    val scale by animateFloatAsState(if (pressed) 0.9f else 1f, label = "actionScale")
    val haptics = LocalHapticFeedback.current
    Box(
        Modifier
            .size(diameter)
            .scale(scale)
            .clip(CircleShape)
            .background(fill)
            .then(if (stroke != null) Modifier.border(1.5.dp, stroke, CircleShape) else Modifier)
            .clickable(interaction, indication = null) { haptics.performHapticFeedback(haptic); onClick() },
        contentAlignment = Alignment.Center,
    ) { content() }
}

@Composable
private fun HeartActionButton(onLike: () -> Unit) {
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    val scale by animateFloatAsState(
        targetValue = if (pressed) 1.2f else 1f,
        animationSpec = spring(dampingRatio = 0.6f, stiffness = 600f),
        label = "heartScale",
    )
    val haptics = LocalHapticFeedback.current
    Box(
        Modifier
            .size(64.dp)
            .scale(scale)
            .shadow(16.dp, CircleShape, ambientColor = BumbleColors.Yellow, spotColor = BumbleColors.Yellow)
            .clip(CircleShape)
            .background(BumbleColors.Yellow)
            .clickable(interaction, indication = null) {
                haptics.performHapticFeedback(HapticFeedbackType.LongPress) // ≈ .impact(.medium)
                onLike()
            },
        contentAlignment = Alignment.Center,
    ) { Icon(Icons.Filled.Favorite, "Like", tint = BumbleColors.OnYellow, modifier = Modifier.size(28.dp)) }
}

Swipe Card (the hero component)

3:4 photo, 12dp radius (firmer edge than Hinge), 4-segment Stories-style progress bar at the top, bottom gradient carrying name + verified check + bio.

import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.material.icons.filled.Verified
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import coil.compose.AsyncImage

@Composable
fun SwipeCard(
    photoUrls: List<String>,
    name: String,
    age: Int,
    bio: String,
    isVerified: Boolean,
    onSwiped: (liked: Boolean) -> Unit,
    modifier: Modifier = Modifier,
) {
    var current by remember { mutableStateOf(0) }
    var dragX by remember { mutableStateOf(0f) }

    Box(
        modifier = modifier
            .fillMaxWidth()
            .aspectRatio(3f / 4f)
            .shadow(16.dp, RoundedCornerShape(12.dp), spotColor = Color.Black.copy(alpha = 0.12f))
            .clip(RoundedCornerShape(12.dp))
            .pointerInput(Unit) {
                detectHorizontalDragGestures(
                    onDragEnd = { if (kotlin.math.abs(dragX) > size.width * 0.3f) onSwiped(dragX > 0); dragX = 0f },
                ) { _, drag -> dragX += drag }
            },
    ) {
        AsyncImage(
            model = photoUrls[current],
            contentDescription = "$name, $age",
            modifier = Modifier.fillMaxSize(),
            contentScale = ContentScale.Crop,
        )
        // Bottom gradient overlay (bottom 30%, transparent → 0.7 black)
        Box(
            Modifier
                .align(Alignment.BottomCenter)
                .fillMaxWidth()
                .fillMaxHeight(0.3f)
                .background(Brush.verticalGradient(listOf(Color.Transparent, Color.Black.copy(alpha = 0.7f))))
        )
        // Photo progress bar — 4 segments, 3dp tall, 4dp gap
        Row(
            Modifier.fillMaxWidth().padding(12.dp),
            horizontalArrangement = Arrangement.spacedBy(4.dp),
        ) {
            repeat(photoUrls.size) { i ->
                Box(
                    Modifier
                        .weight(1f)
                        .height(3.dp)
                        .clip(CircleShape)
                        .background(if (i == current) Color.White else Color.White.copy(alpha = 0.4f))
                )
            }
        }
        // Name + age + verified + bio
        Column(
            Modifier.align(Alignment.BottomStart).padding(horizontal = 20.dp, vertical = 20.dp),
            verticalArrangement = Arrangement.spacedBy(4.dp),
        ) {
            Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
                Text("$name, $age", style = BumbleText.CardName, color = Color.White)
                if (isVerified) {
                    Icon(Icons.Filled.Verified, "Verified", tint = BumbleColors.Verified, modifier = Modifier.size(18.dp))
                }
            }
            Text(bio, style = BumbleText.Body, color = Color.White, maxLines = 1, overflow = TextOverflow.Ellipsis)
        }
    }
}

24-Hour Countdown Chip (the core mechanic)

import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.ui.draw.alpha

@Composable
fun CountdownChip(remainingSeconds: Long, modifier: Modifier = Modifier) {
    val expired = remainingSeconds <= 0
    val label = if (expired) "Time's up — extend?"
        else "Your turn: ${remainingSeconds / 3600}h ${(remainingSeconds % 3600) / 60}m"

    val t = rememberInfiniteTransition(label = "countdownPulse")
    val a by t.animateFloat(
        initialValue = 0.95f, targetValue = 1f,
        animationSpec = infiniteRepeatable(tween(1500), RepeatMode.Reverse),
        label = "pulse",
    )

    Box(
        modifier
            .fillMaxWidth()
            .height(32.dp)
            .alpha(a)
            .clip(CircleShape)
            .background(if (expired) BumbleColors.Error else BumbleColors.Yellow)
            .border(1.dp, if (expired) BumbleColors.Error else BumbleColors.HoneyDeep, CircleShape)
            .padding(horizontal = 16.dp),
        contentAlignment = Alignment.Center,
    ) {
        Text(
            label,
            style = BumbleText.Meta.copy(fontWeight = FontWeight.Bold, fontFeatureSettings = "tnum"),
            color = if (expired) Color.White else BumbleColors.OnYellow, // pure black on yellow
        )
    }
}

It’s a Match! Celebration

import androidx.compose.animation.core.Animatable
import androidx.compose.ui.draw.scale

@Composable
fun MatchCelebration(
    myAvatarUrl: String,
    theirAvatarUrl: String,
    theirName: String,
    onSendMessage: () -> Unit,
    onKeepSwiping: () -> Unit,
) {
    val heartScale = remember { Animatable(0.2f) }
    val haptics = LocalHapticFeedback.current

    LaunchedEffect(Unit) {
        haptics.performHapticFeedback(HapticFeedbackType.LongPress) // ≈ .notification(.success)
        heartScale.animateTo(1f, spring(dampingRatio = 0.55f, stiffness = 500f)) // bounce in
    }

    Box(Modifier.fillMaxSize().background(BumbleColors.Yellow)) { // full-screen yellow canvas
        Column(
            Modifier.fillMaxSize().padding(horizontal = 24.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center,
        ) {
            Box(contentAlignment = Alignment.Center) {
                Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) {
                    HexAvatar(myAvatarUrl)
                    HexAvatar(theirAvatarUrl)
                }
                Icon(
                    Icons.Filled.Favorite, contentDescription = null,
                    tint = BumbleColors.MatchPink, // ONLY place this pink appears
                    modifier = Modifier.size(48.dp).scale(heartScale.value),
                )
            }
            Spacer(Modifier.height(24.dp))
            Text("It's a Match!", style = BumbleText.MatchHero, color = BumbleColors.Black)
            Text("You and $theirName want to chat", style = BumbleText.Body, color = BumbleColors.Black)
            Spacer(Modifier.height(12.dp))
            Text("She has 24 hours to make the first move", style = BumbleText.Meta,
                color = BumbleColors.Black.copy(alpha = 0.8f))
            Spacer(Modifier.height(48.dp))
            // Primary CTA is WHITE here (yellow canvas) with warm-black text
            Box(
                Modifier.fillMaxWidth().height(56.dp).clip(CircleShape).background(Color.White)
                    .clickable { onSendMessage() },
                contentAlignment = Alignment.Center,
            ) { Text("Send a Message", style = BumbleText.ButtonLarge, color = BumbleColors.Black) }
            Spacer(Modifier.height(16.dp))
            Text("Keep Swiping", style = BumbleText.Button, color = BumbleColors.Black,
                modifier = Modifier.clickable { onKeepSwiping() })
        }
    }
}

@Composable
fun HexAvatar(url: String, size: androidx.compose.ui.unit.Dp = 120.dp) {
    AsyncImage(
        model = url,
        contentDescription = null,
        modifier = Modifier
            .size(size)
            .clip(HexagonShape)
            .border(4.dp, Color.White, HexagonShape),
        contentScale = ContentScale.Crop,
    )
}

Chat Input with Send Button

import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.ui.text.input.TextFieldValue

@Composable
fun BumbleChatInput(
    value: TextFieldValue,
    onValueChange: (TextFieldValue) -> Unit,
    onSend: () -> Unit,
    modifier: Modifier = Modifier,
) {
    val canSend = value.text.isNotBlank()
    val haptics = LocalHapticFeedback.current
    Row(
        modifier.fillMaxWidth().padding(16.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(10.dp),
    ) {
        Box(
            Modifier.weight(1f).clip(CircleShape).background(BumbleColors.Surface1)
                .padding(horizontal = 20.dp, vertical = 14.dp),
        ) {
            BasicTextField(
                value = value, onValueChange = onValueChange,
                textStyle = BumbleText.Body.copy(color = BumbleColors.Black),
                decorationBox = { inner ->
                    if (value.text.isEmpty()) {
                        Text("Type your message…", style = BumbleText.Body, color = BumbleColors.Mist)
                    }
                    inner()
                },
            )
        }
        Box(
            Modifier
                .size(40.dp)
                .clip(CircleShape)
                .background(if (canSend) BumbleColors.Yellow else BumbleColors.Surface1)
                .clickable(enabled = canSend) {
                    haptics.performHapticFeedback(HapticFeedbackType.LongPress)
                    onSend()
                },
            contentAlignment = Alignment.Center,
        ) {
            Icon(Icons.Filled.ArrowUpward, "Send",
                tint = if (canSend) BumbleColors.OnYellow else BumbleColors.Mist,
                modifier = Modifier.size(18.dp))
        }
    }
}

4. Mode Switching (the distinctive system)

Bumble’s defining structural feature is one app, three products: Date (yellow) / BFF (teal) / Bizz (orange). Switching modes recolors all brand chrome while the hexagon shape stays constant. Drive this with a CompositionLocal so every composable reads the active accent without prop-drilling.

enum class BumbleMode(val accent: Color, val pressed: Color) {
    Date(BumbleColors.Yellow, BumbleColors.HoneyDeep),
    BFF(BumbleColors.BFFTeal, Color(0xFF0E908E)),
    Bizz(BumbleColors.BizzOrange, Color(0xFFE57300)),
}

val LocalBumbleMode = androidx.compose.runtime.staticCompositionLocalOf { BumbleMode.Date }

@Composable
fun BumbleModeProvider(mode: BumbleMode, content: @Composable () -> Unit) {
    androidx.compose.runtime.CompositionLocalProvider(LocalBumbleMode provides mode, content = content)
}

// A hexagonal mode-toggle button — literally hex-shaped, not rectangular
@Composable
fun ModeHexButton(mode: BumbleMode, active: Boolean, onClick: () -> Unit) {
    val haptics = LocalHapticFeedback.current
    Box(
        Modifier
            .size(64.dp)
            .clip(HexagonShape)
            .then(
                if (active) Modifier.background(mode.accent)
                else Modifier.border(2.dp, BumbleColors.Mist, HexagonShape)
            )
            .clickable {
                haptics.performHapticFeedback(HapticFeedbackType.TextHandleMove) // ≈ .selection
                onClick()
            },
        contentAlignment = Alignment.Center,
    ) {
        Icon(Icons.Filled.Star, mode.name, // mode glyph (hive/heart/briefcase) — vector drawable in prod
            tint = if (active) BumbleColors.OnYellow else BumbleColors.Mist, modifier = Modifier.size(24.dp))
    }
}

Then read LocalBumbleMode.current.accent anywhere yellow appears (CTAs, tab indicator, match ring) so the whole app retints on switch. The hex is never replaced with a circle.

5. Navigation

Bumble has 5 bottom tabs: People / Hives / Matches / Chats / Profile — the Hives icon is a literal honeycomb. Use Material 3 NavigationBar. The iOS tab bar is opaque (Bumble likes hard surfaces, no blur) — Android has no live blur anyway, so use a solid Canvas container with a 0.5dp top divider. Active tint is warm Bumble Black with a 4dp yellow indicator dot below the label — not the Material pill.

import androidx.compose.material.icons.filled.Chat
import androidx.compose.material.icons.filled.Hexagon
import androidx.compose.material.icons.filled.People
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults

@Composable
fun BumbleBottomBar(selected: Int, onSelect: (Int) -> Unit) {
    val accent = LocalBumbleMode.current.accent
    NavigationBar(containerColor = BumbleColors.Canvas, tonalElevation = 0.dp) {
        val items = listOf(
            "People"  to Icons.Filled.People,
            "Hives"   to Icons.Filled.Hexagon,
            "Matches" to Icons.Filled.Favorite,
            "Chats"   to Icons.Filled.Chat,
            "Profile" to Icons.Filled.Person,
        )
        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 = BumbleText.Tab) },
                colors = NavigationBarItemDefaults.colors(
                    selectedIconColor   = BumbleColors.Black,  // warm black, not yellow
                    selectedTextColor   = BumbleColors.Black,
                    unselectedIconColor = BumbleColors.Mist,
                    unselectedTextColor = BumbleColors.Mist,
                    indicatorColor      = accent.copy(alpha = 0.0f), // no Material pill — Bumble has none
                ),
            )
        }
    }
}

Render the 4dp yellow (or mode-accent) indicator dot manually 4dp beneath the selected item’s label if you want exact parity — NavigationBar has no built-in dot affordance. The top header is a slim 56dp row (no large-title) with ScreenTitle text leading and trailing action glyphs.

6. Motion

Moment Compose recipe
Heart tap animateFloatAsState 1 → 1.2 with spring(dampingRatio = 0.6f); HapticFeedbackType.LongPress (.impact medium)
SuperSwipe (star) full-screen yellow flash 200ms via Animatable alpha 0→1→0; HapticFeedbackType.LongPress (.success)
Swipe commit animateFloatAsState rotationZ → 8° + offset 500dp off-screen over 350ms (spring)
Yes/No confetti rememberInfiniteTransition / Animatable driving falling yellow hexes for 800ms
Match celebration photos slide in from edges; pink heart Animatable.animateTo(1f, spring(dampingRatio = 0.55f))
24h chip pulse rememberInfiniteTransition alpha 0.95 → 1.0 over 1500ms RepeatMode.Reverse
Hex loading state rememberInfiniteTransition rotationZ 0 → 360 over 1200ms on a 6-hex honeycomb
Tab switch 200ms tween; indicator dot fadeIn; HapticFeedbackType.TextHandleMove (.selection)
// SuperSwipe full-screen yellow flash
@Composable
fun SuperSwipeFlash(trigger: Boolean) {
    val flash = remember { Animatable(0f) }
    val haptics = LocalHapticFeedback.current
    LaunchedEffect(trigger) {
        if (trigger) {
            haptics.performHapticFeedback(HapticFeedbackType.LongPress) // ≈ .notification(.success)
            flash.animateTo(1f, tween(200))
            flash.animateTo(0f, tween(200))
        }
    }
    if (flash.value > 0f) {
        Box(Modifier.fillMaxSize().alpha(flash.value).background(BumbleColors.Yellow))
    }
}

Honor Reduce Motion: when Settings.Global.ANIMATOR_DURATION_SCALE == 0f, skip the hex confetti and the heart scale-bounce on match — keep the haptics. For richer haptics use LocalView.current.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) (API 30+) or a Vibrator VibrationEffect.createOneShot(10, ...) to approximate iOS’s .soft Compliment tap. For the match-celebration shared photo, prefer SharedTransitionLayout + Modifier.sharedElement() (Compose 1.7+).

7. Icons

Bumble uses outline/filled pairs; the closest first-party set is androidx.compose.material:material-icons-extended. The bee glyph and the hexagonal “B” logomark are proprietary — ship them as vector drawables and load via ImageVector.vectorResource(R.drawable.…).

Purpose SF Symbol (iOS) Material Icon (Compose)
Heart action (filled) heart.fill Icons.Filled.Favorite
X Pass xmark Icons.Filled.Close
Star SuperSwipe star.fill Icons.Filled.Star
Rewind arrow.uturn.backward Icons.AutoMirrored.Filled.Undo
Bee Compliment ant.fill (placeholder) custom vector drawable (proprietary bee)
Match heart heart.fill (Match Pink) Icons.Filled.Favorite tinted MatchPink
Send (chat input) arrow.up Icons.Filled.ArrowUpward
Verified check checkmark.seal.fill Icons.Filled.Verified (tint Verified)
People tab person.2 / .fill Icons.Filled.People
Hives tab hexagon / .fill Icons.Filled.Hexagon
Matches tab heart / .fill Icons.Filled.Favorite
Chats tab bubble.left / .fill Icons.Filled.Chat
Profile tab person.crop.circle / .fill Icons.Filled.Person
Filters slider.horizontal.3 Icons.Filled.Tune
Settings gearshape Icons.Filled.Settings
Brand “B” logomark (hex bee mark) custom vector drawable

8. Minimum SDK & Accessibility Notes

  • minSdk 24 (Compose floor is 21; modern motion + SharedTransitionLayout comfortable at 24). compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+.
  • Edge-to-edge: call enableEdgeToEdge() in the Activity. The light Date canvas wants dark system-bar icons (WindowCompat light appearance); the dark scheme wants light icons. Pin the chat input above the IME with Modifier.imePadding() and the tab bar above the gesture nav with Scaffold insets.
  • Font scaling: sp honors the user’s font scale automatically — keep it on match hero, display, screen titles, body, bio, section headers. Pin layout-sensitive text (10sp tab labels, 11sp counter badges, 13sp chip labels, the 24-hour countdown) by deriving size from dp or wrapping in CompositionLocalProvider(LocalDensity provides Density(density, fontScale = 1f)).
  • TalkBack: announce the match screen as one node — Modifier.semantics(mergeDescendants = true) { contentDescription = "It's a Match. You and $name want to chat. She has 24 hours. Send a Message button." }. Give the swipe card an accessibility action set ("Like", "Pass", "SuperSwipe", "Compliment") so users can act without dragging. Re-announce the countdown chip when it crosses a 30-minute boundary.
  • Touch targets: Material guidance is 48.dp minimum. The 64dp heart and 56dp CTA clear it; the 48dp Rewind and 40dp send button are at/above the floor — keep the send button’s 40dp visual inside a 48dp hit area via padding.
  • Contrast: Bumble Yellow #FFC629 requires pure black (#000000) text — warm #1F1F1F on yellow fails WCAG AA at small sizes. Use BumbleColors.OnYellow on every yellow surface, no exceptions. #9C9C9C Mist on white is borderline at body size — keep it to ≥14sp metadata and validate with a contrast checker.
  • Dynamic color: do not enable Material You dynamicColorScheme() — Bumble’s identity is the fixed #FFC629 yellow (and the teal/orange mode accents) regardless of wallpaper. (Material You suits Google/Material-first apps; Bumble is a strong-brand exception.) The yellow stays identical across light and dark; only canvas/surface/text tokens invert.
All mobile designs →

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