coderef

Facebook

Facebook's iOS app is a utility dashboard — dense, function-first, colorful, and recognizable from 25 paces by its one signature element: the Facebook Blue bar and f logo at the top of the screen.
DownloadRaw
Facebook

Build with the Facebook look.

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

Palette

At a glance

Theme
light
Typefaces
SF Pro Text, SF Pro Display
Sections
8
Length
5,281 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · Facebook

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

Facebook (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports Facebook’s visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, paste-ready @Composables, Modifiers, motion, and haptics.

Why a Compose guide for an iOS-referenced app? The DESIGN.md tokens are platform-neutral. This file keeps the visual identity (Facebook’s soft-gray canvas, white floating cards, single-blue brand, and the 7-emoji Reactions popover) while making everything idiomatic Android — NavigationBar instead of a UITabBar, a translucent Surface instead of .regularMaterial, sp/dp instead of pt.

Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, and Coil 2.6+ for remote avatars and post media.

1. Color Tokens

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

object FacebookColors {
    // Canvas & Surfaces (Light)
    val Canvas       = Color(0xFFF0F2F5)
    val Card         = Color(0xFFFFFFFF)
    val SurfaceTint  = Color(0xFFF7F8FA)
    val Divider      = Color(0xFFE4E6EB)
    val Separator    = Color(0xFFCED0D4)

    // Canvas & Surfaces (Dark)
    val DarkCanvas      = Color(0xFF18191A)
    val DarkCard        = Color(0xFF242526)
    val DarkSurfaceTint = Color(0xFF3A3B3C)
    val DarkDivider     = Color(0xFF3E4042)

    // Text
    val TextPrimaryLight   = Color(0xFF050505)
    val TextPrimaryDark    = Color(0xFFE4E6EB)
    val TextSecondaryLight = Color(0xFF65676B)
    val TextSecondaryDark  = Color(0xFFB0B3B8)
    val TextTertiary       = Color(0xFF8A8D91)

    // Brand
    val Blue        = Color(0xFF1877F2)
    val BluePressed = Color(0xFF0A5FC8)
    val BlueLight   = Color(0xFFE7F3FF)

    // Reactions
    val LikeBlue    = Color(0xFF1877F2) // same as Blue
    val LovePink    = Color(0xFFF3425F)
    val CareYellow  = Color(0xFFF7B928)
    val HahaYellow  = Color(0xFFF7B928)
    val WowYellow   = Color(0xFFF7B928)
    val SadYellow   = Color(0xFFF7B928)
    val AngryOrange = Color(0xFFE9710F)

    // Semantic
    val LiveRed      = Color(0xFFFA3E3E)
    val ErrorRed     = Color(0xFFFA383E)
    val SuccessGreen = Color(0xFF42B72A)
    val PhotoGreen   = Color(0xFF45BD62) // compose-bar photo picker glyph
}

Wire it into a Material 3 lightColorScheme so ripples, dividers, and default component colors inherit the brand. Facebook is light-first; provide a darkColorScheme too since the app ships a true dark mode.

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

private val FacebookLight = lightColorScheme(
    primary        = FacebookColors.Blue,
    onPrimary      = Color.White,
    background     = FacebookColors.Canvas,        // the between-card gray
    onBackground   = FacebookColors.TextPrimaryLight,
    surface        = FacebookColors.Card,          // the white card
    onSurface      = FacebookColors.TextPrimaryLight,
    surfaceVariant = FacebookColors.SurfaceTint,
    outline        = FacebookColors.Divider,
    error          = FacebookColors.ErrorRed,
)

private val FacebookDark = darkColorScheme(
    primary        = FacebookColors.Blue,
    onPrimary      = Color.White,
    background     = FacebookColors.DarkCanvas,
    onBackground   = FacebookColors.TextPrimaryDark,
    surface        = FacebookColors.DarkCard,
    onSurface      = FacebookColors.TextPrimaryDark,
    surfaceVariant = FacebookColors.DarkSurfaceTint,
    outline        = FacebookColors.DarkDivider,
    error          = FacebookColors.ErrorRed,
)

@Composable
fun FacebookTheme(
    dark: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit,
) = MaterialTheme(
    colorScheme = if (dark) FacebookDark else FacebookLight,
    typography  = FacebookTypography,
    content     = content,
)

2. Typography

Facebook uses SF Pro on iOS — the Apple system font. On Android the platform default is Roboto, the closest free grotesque. You can ship Roboto Flex from res/font/ for tighter weight control; the system fallback is automatic if you omit the family.

// ui/theme/FacebookType.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 FacebookSans = FontFamily(
    Font(R.font.roboto_regular,  FontWeight.Normal),   // 400
    Font(R.font.roboto_medium,   FontWeight.Medium),   // 500
    Font(R.font.roboto_semibold, FontWeight.SemiBold), // 600
    Font(R.font.roboto_bold,     FontWeight.Bold),     // 700
    Font(R.font.roboto_black,    FontWeight.Black),    // 900 — the "f" logo only
)

// Named ramp — mirrors DESIGN.md §3 exactly (pt → sp 1:1, line-height = size × ratio)
object FacebookText {
    val FLogo        = TextStyle(FacebookSans, fontWeight = FontWeight.Black,    fontSize = 22.sp, lineHeight = 22.sp)
    val ScreenTitle  = TextStyle(FacebookSans, fontWeight = FontWeight.Bold,     fontSize = 24.sp, lineHeight = 28.sp, letterSpacing = (-0.2).sp)
    val SectionHead  = TextStyle(FacebookSans, fontWeight = FontWeight.Bold,     fontSize = 20.sp, lineHeight = 24.sp, letterSpacing = (-0.1).sp)
    val DisplayName  = TextStyle(FacebookSans, fontWeight = FontWeight.SemiBold, fontSize = 15.sp, lineHeight = 20.sp)
    val PostBody     = TextStyle(FacebookSans, fontWeight = FontWeight.Normal,   fontSize = 15.sp, lineHeight = 20.sp)
    val PostBodyBig  = TextStyle(FacebookSans, fontWeight = FontWeight.SemiBold, fontSize = 24.sp, lineHeight = 30.sp)
    val CommentBody  = TextStyle(FacebookSans, fontWeight = FontWeight.Normal,   fontSize = 14.sp, lineHeight = 18.sp)
    val CommentName  = TextStyle(FacebookSans, fontWeight = FontWeight.SemiBold, fontSize = 13.sp, lineHeight = 17.sp)
    val Timestamp    = TextStyle(FacebookSans, fontWeight = FontWeight.Normal,   fontSize = 13.sp, lineHeight = 17.sp)
    val ReactionCnt  = TextStyle(FacebookSans, fontWeight = FontWeight.Normal,   fontSize = 13.sp, lineHeight = 17.sp)
    val ActionLabel  = TextStyle(FacebookSans, fontWeight = FontWeight.SemiBold, fontSize = 15.sp, lineHeight = 15.sp)
    val Cta          = TextStyle(FacebookSans, fontWeight = FontWeight.SemiBold, fontSize = 17.sp, lineHeight = 17.sp)
    val TabLabel     = TextStyle(FacebookSans, fontWeight = FontWeight.Medium,   fontSize = 11.sp, lineHeight = 11.sp)
    val Sponsored    = TextStyle(FacebookSans, fontWeight = FontWeight.Normal,   fontSize = 13.sp, lineHeight = 17.sp)
    val LiveBadge    = TextStyle(FacebookSans, fontWeight = FontWeight.Bold,     fontSize = 10.sp, lineHeight = 10.sp, letterSpacing = 0.5.sp)
}

// Map onto Material 3 slots so stock components inherit the brand
val FacebookTypography = Typography(
    headlineMedium = FacebookText.ScreenTitle,
    headlineSmall  = FacebookText.SectionHead,
    titleMedium    = FacebookText.DisplayName,
    bodyMedium     = FacebookText.PostBody,
    labelMedium    = FacebookText.ActionLabel,
    labelSmall     = FacebookText.TabLabel,
)

3. Signature Components

Post Card (the feed unit)

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage

@Composable
fun FBPostCard(
    displayName: String,
    timestamp: String,
    audience: String,          // "Public", "Friends"
    avatarUrl: String,
    body: String,
    mediaUrl: String?,
    modifier: Modifier = Modifier,
) {
    var userReaction by remember { mutableStateOf<FBReaction?>(null) }
    var showReactions by remember { mutableStateOf(false) }

    Column(
        modifier = modifier
            .fillMaxWidth()
            .padding(horizontal = 8.dp, vertical = 4.dp)
            .clip(RoundedCornerShape(8.dp))
            .background(MaterialTheme.colorScheme.surface),
    ) {
        // Header
        Row(
            Modifier.fillMaxWidth().padding(12.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(8.dp),
        ) {
            AsyncImage(
                model = avatarUrl,
                contentDescription = null,
                modifier = Modifier.size(40.dp).clip(CircleShape),
            )
            Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
                Text(displayName, style = FacebookText.DisplayName, color = FacebookColors.TextPrimaryLight)
                Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
                    Text(timestamp, style = FacebookText.Timestamp, color = FacebookColors.TextSecondaryLight)
                    Text("·", color = FacebookColors.TextSecondaryLight)
                    Icon(Icons.Filled.Public, null, tint = FacebookColors.TextSecondaryLight, modifier = Modifier.size(12.dp))
                    Text(audience, style = FacebookText.Timestamp, color = FacebookColors.TextSecondaryLight)
                }
            }
            Icon(
                Icons.Filled.MoreHoriz,
                contentDescription = "More options",
                tint = FacebookColors.TextSecondaryLight,
                modifier = Modifier.size(20.dp),
            )
        }

        // Body — full card width, NOT indented after avatar
        Text(
            body,
            style = FacebookText.PostBody,
            color = FacebookColors.TextPrimaryLight,
            modifier = Modifier.padding(horizontal = 12.dp).padding(bottom = 12.dp),
        )

        // Media — edge-to-edge inside card (card already clips corners)
        if (mediaUrl != null) {
            AsyncImage(
                model = mediaUrl,
                contentDescription = null,
                modifier = Modifier.fillMaxWidth(),
                contentScale = ContentScale.FillWidth,
            )
        }

        // Reaction summary
        Row(
            Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(6.dp),
        ) {
            Box {
                Row(horizontalArrangement = Arrangement.spacedBy((-4).dp)) {
                    ReactionDot(FacebookColors.LikeBlue, Icons.Filled.ThumbUp)
                    ReactionDot(FacebookColors.LovePink, Icons.Filled.Favorite)
                }
            }
            Text("You, Sarah, and 45 others", style = FacebookText.ReactionCnt, color = FacebookColors.TextSecondaryLight)
            Spacer(Modifier.weight(1f))
            Text("12 comments · 3 shares", style = FacebookText.ReactionCnt, color = FacebookColors.TextSecondaryLight)
        }

        HorizontalDivider(color = FacebookColors.Divider, thickness = 1.dp)

        // Action row — Like / Comment / Share
        Box {
            Row(Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
                FBActionButton(
                    icon = userReaction?.icon ?: Icons.Outlined.ThumbUp,
                    label = userReaction?.label ?: "Like",
                    color = userReaction?.color ?: FacebookColors.TextSecondaryLight,
                    modifier = Modifier.weight(1f),
                    onTap = { userReaction = if (userReaction == null) FBReaction.Like else null },
                    onLongPress = { showReactions = true },
                )
                FBActionButton(Icons.Outlined.ChatBubbleOutline, "Comment", FacebookColors.TextSecondaryLight, Modifier.weight(1f), {}, {})
                FBActionButton(Icons.Outlined.Share, "Share", FacebookColors.TextSecondaryLight, Modifier.weight(1f), {}, {})
            }
            if (showReactions) {
                ReactionsPopover(
                    onSelect = { userReaction = it; showReactions = false },
                    modifier = Modifier.align(Alignment.TopCenter).offset(y = (-56).dp),
                )
            }
        }
    }
}

@Composable
private fun ReactionDot(color: Color, icon: androidx.compose.ui.graphics.vector.ImageVector) {
    Box(Modifier.size(18.dp).clip(CircleShape).background(color), contentAlignment = Alignment.Center) {
        Icon(icon, null, tint = Color.White, modifier = Modifier.size(9.dp))
    }
}

Action Button (Like / Comment / Share)

import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalHapticFeedback

@Composable
fun FBActionButton(
    icon: androidx.compose.ui.graphics.vector.ImageVector,
    label: String,
    color: Color,
    modifier: Modifier = Modifier,
    onTap: () -> Unit,
    onLongPress: () -> Unit,
) {
    val haptics = LocalHapticFeedback.current
    Row(
        modifier = modifier
            .heightIn(min = 40.dp)
            .pointerInput(Unit) {
                detectTapGestures(
                    onTap = {
                        haptics.performHapticFeedback(HapticFeedbackType.LongPress) // ≈ iOS .soft impact
                        onTap()
                    },
                    onLongPress = {
                        haptics.performHapticFeedback(HapticFeedbackType.LongPress)
                        onLongPress()
                    },
                )
            },
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally),
    ) {
        Icon(icon, contentDescription = null, tint = color, modifier = Modifier.size(20.dp))
        Text(label, style = FacebookText.ActionLabel, color = color)
    }
}

Reactions model + popover

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*

enum class FBReaction(
    val label: String,
    val icon: androidx.compose.ui.graphics.vector.ImageVector,
    val color: Color,
) {
    Like ("Like",  Icons.Filled.ThumbUp,        FacebookColors.LikeBlue),
    Love ("Love",  Icons.Filled.Favorite,       FacebookColors.LovePink),
    Care ("Care",  Icons.Filled.VolunteerActivism, FacebookColors.CareYellow),
    Haha ("Haha",  Icons.Filled.SentimentVerySatisfied, FacebookColors.HahaYellow),
    Wow  ("Wow",   Icons.Filled.SentimentSatisfied,     FacebookColors.WowYellow),
    Sad  ("Sad",   Icons.Filled.SentimentDissatisfied,  FacebookColors.SadYellow),
    Angry("Angry", Icons.Filled.SentimentVeryDissatisfied, FacebookColors.AngryOrange),
}

@Composable
fun ReactionsPopover(
    onSelect: (FBReaction) -> Unit,
    modifier: Modifier = Modifier,
    hoveredIndex: Int? = null,
) {
    Surface(
        modifier = modifier,
        shape = CircleShape,                       // 500pt-radius pill
        color = MaterialTheme.colorScheme.surface,
        shadowElevation = 16.dp,                   // rgba(0,0,0,0.2) 0 4px 16px
    ) {
        Row(
            Modifier.padding(horizontal = 8.dp, vertical = 6.dp),
            horizontalArrangement = Arrangement.spacedBy(4.dp),
        ) {
            FBReaction.entries.forEachIndexed { i, reaction ->
                val scale by animateFloatAsState(
                    if (hoveredIndex == i) 1.3f else 1f,
                    spring(dampingRatio = 0.55f, stiffness = 700f),
                    label = "react$i",
                )
                Box(
                    Modifier
                        .size(48.dp)                // 48dp tap area, 28dp glyph
                        .clickable(
                            interactionSource = remember { MutableInteractionSource() },
                            indication = null,
                        ) { onSelect(reaction) },
                    contentAlignment = Alignment.Center,
                ) {
                    Icon(
                        reaction.icon,
                        contentDescription = "${reaction.label} reaction",
                        tint = reaction.color,
                        modifier = Modifier.size(28.dp).scale(scale),
                    )
                }
            }
        }
    }
}

Top Nav Bar (with blue “f” logo)

Android has no live blur, so the bar is an opaque card surface with a hairline divider — Facebook’s iOS top bar is opaque too (no longer the historical blue bar).

@Composable
fun FBTopNav() {
    Surface(color = MaterialTheme.colorScheme.surface) {
        Column {
            Row(
                Modifier.fillMaxWidth().height(56.dp).padding(horizontal = 16.dp),
                verticalAlignment = Alignment.CenterVertically,
            ) {
                // Blue "f" logo in a rounded square
                Box(
                    Modifier.size(36.dp).clip(RoundedCornerShape(8.dp)).background(FacebookColors.Blue),
                    contentAlignment = Alignment.Center,
                ) {
                    Text("f", style = FacebookText.FLogo, color = Color.White, modifier = Modifier.offset(y = (-1).dp))
                }
                Spacer(Modifier.weight(1f))
                Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
                    FBCircleIconButton(Icons.Filled.Search, "Search")
                    FBCircleIconButton(Icons.AutoMirrored.Filled.Message, "Messenger")
                }
            }
            HorizontalDivider(color = FacebookColors.Divider, thickness = 1.dp)
        }
    }
}

@Composable
fun FBCircleIconButton(icon: androidx.compose.ui.graphics.vector.ImageVector, label: String) {
    Box(
        Modifier.size(36.dp).clip(CircleShape).background(FacebookColors.Divider)
            .clickable { },
        contentAlignment = Alignment.Center,
    ) {
        Icon(icon, contentDescription = label, tint = FacebookColors.TextPrimaryLight, modifier = Modifier.size(18.dp))
    }
}

4. Facebook-Specific Feature: Reactions Popover with Drag-Select

The signature Facebook micro-interaction. Long-press the Like button → a floating pill of 7 emoji pops in → the user drags horizontally; the hovered icon scales to 1.3 with a TextButton-style tooltip; release selects that reaction. Drive everything from one pointerInput so the long-press and the drag share a gesture stream.

import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.changedToUp
import androidx.compose.ui.layout.onGloballyPositioned
import kotlinx.coroutines.delay

@Composable
fun ReactionsHost(
    onReaction: (FBReaction) -> Unit,
    content: @Composable () -> Unit,
) {
    var show by remember { mutableStateOf(false) }
    var hovered by remember { mutableStateOf<Int?>(null) }
    var rowWidthPx by remember { mutableStateOf(1) }
    val haptics = LocalHapticFeedback.current
    val slot = FBReaction.entries.size

    Box {
        Box(
            Modifier.pointerInput(Unit) {
                awaitEachGesture {
                    val down = awaitFirstDown()
                    // Long-press threshold ≈ 400ms before the popover opens
                    val longPress = try {
                        withTimeout(400) { waitForUpOrCancellation(); false }
                    } catch (_: PointerEventTimeoutCancellationException) { true }
                    if (!longPress) return@awaitEachGesture

                    show = true
                    haptics.performHapticFeedback(HapticFeedbackType.LongPress) // .impact(.light)
                    // Track the finger across the 7 slots
                    do {
                        val event = awaitPointerEvent()
                        val x = event.changes.first().position.x
                        val idx = (x / (rowWidthPx / slot)).toInt().coerceIn(0, slot - 1)
                        if (idx != hovered) {
                            hovered = idx
                            haptics.performHapticFeedback(HapticFeedbackType.TextHandleMove) // .selection
                        }
                    } while (event.changes.none { it.changedToUp() })

                    hovered?.let {
                        onReaction(FBReaction.entries[it])
                        haptics.performHapticFeedback(HapticFeedbackType.LongPress)          // .impact(.medium)
                    }
                    show = false
                    hovered = null
                }
            },
        ) { content() }

        if (show) {
            ReactionsPopover(
                onSelect = { onReaction(it); show = false },
                hoveredIndex = hovered,
                modifier = Modifier
                    .align(Alignment.TopCenter)
                    .offset(y = (-56).dp)
                    .onGloballyPositioned { rowWidthPx = it.size.width },
            )
        }
    }
}

Wrap the Like button: ReactionsHost(onReaction = { vm.setReaction(it) }) { FBActionButton(...) }. The popover entry uses a scale 0.5 → 1.0 + fade (see §6); each emoji springs to 1.3 on hover via the animateFloatAsState already in ReactionsPopover.

5. Bottom Navigation (Tab Bar)

Use Material 3 NavigationBar. Facebook shows a label under every icon — not icon-only. The iOS bar is an opaque card; Android has no live blur, so use the solid card surface with a top hairline. Active tint is Facebook Blue.

@Composable
fun FBBottomBar(selected: Int, onSelect: (Int) -> Unit, notifCount: Int = 0) {
    Column {
        HorizontalDivider(color = FacebookColors.Divider, thickness = 1.dp)
        NavigationBar(
            containerColor = MaterialTheme.colorScheme.surface,
            tonalElevation = 0.dp,
        ) {
            val items = listOf(
                "Home"          to Icons.Filled.Home,
                "Video"         to Icons.Filled.SmartDisplay,
                "Marketplace"   to Icons.Filled.Storefront,
                "Notifications" to Icons.Filled.Notifications,
                "Menu"          to Icons.Filled.Menu,
            )
            items.forEachIndexed { i, (label, icon) ->
                NavigationBarItem(
                    selected = selected == i,
                    onClick = { onSelect(i) },
                    icon = {
                        if (label == "Notifications" && notifCount > 0) {
                            BadgedBox(badge = { Badge(containerColor = FacebookColors.ErrorRed) { Text("$notifCount") } }) {
                                Icon(icon, contentDescription = label, modifier = Modifier.size(26.dp))
                            }
                        } else {
                            Icon(icon, contentDescription = label, modifier = Modifier.size(26.dp))
                        }
                    },
                    label = { Text(label, style = FacebookText.TabLabel) },
                    alwaysShowLabel = true, // Facebook always shows labels
                    colors = NavigationBarItemDefaults.colors(
                        selectedIconColor   = FacebookColors.Blue,
                        selectedTextColor   = FacebookColors.Blue,
                        unselectedIconColor = FacebookColors.TextSecondaryLight,
                        unselectedTextColor = FacebookColors.TextSecondaryLight,
                        indicatorColor      = Color.Transparent, // no Material pill — Facebook has none
                    ),
                )
            }
        }
    }
}

6. Motion

Moment Compose recipe
Like tap Animatable keyframes 0.9 → 1.15 → 1.0 over 300ms; fill turns #1877F2; HapticFeedbackType.LongPress (≈ .impact(.light))
Reactions open AnimatedVisibility scaleIn(initialScale = 0.5f) + fadeIn, 400ms; each emoji springs to 1.3 on hover
Reactions drag-select HapticFeedbackType.TextHandleMove on each hover; selected icon bounces 1.5 → 1.0 + LongPress haptic
Card tap background darken 3% (Color.Black.copy(alpha = 0.03f) overlay) for 150ms on press
Stories ring tap ring scale 1.0 → 0.9 → 1.0 then open viewer with a crossfade
Reaction swap into Like slot AnimatedContent with fadeIn() togetherWith fadeOut(), 200ms
// Like-tap bounce on the thumbs-up
@Composable
fun LikeThumb(liked: Boolean) {
    val scale = remember { Animatable(1f) }
    LaunchedEffect(liked) {
        if (liked) {
            scale.animateTo(1.15f, tween(150))
            scale.animateTo(1f, spring(dampingRatio = 0.5f, stiffness = 500f))
        }
    }
    Icon(
        if (liked) Icons.Filled.ThumbUp else Icons.Outlined.ThumbUp,
        contentDescription = if (liked) "Liked" else "Like",
        tint = if (liked) FacebookColors.Blue else FacebookColors.TextSecondaryLight,
        modifier = Modifier.size(20.dp).scale(scale.value),
    )
}

Haptics: prefer LocalHapticFeedback. HapticFeedbackType.LongPress approximates iOS impact; HapticFeedbackType.TextHandleMove approximates .selection. For finer control use LocalView.current.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) (API 30+) or a Vibrator VibrationEffect.createOneShot(10, …).

7. Icons

Facebook’s reaction emoji are illustration-style and should ship as vector drawables (ImageVector.vectorResource(R.drawable.…)) for true parity; the table below maps the rest onto androidx.compose.material:material-icons-extended.

Purpose SF Symbol (iOS) Material Icon (Compose)
Like (outline / fill) hand.thumbsup / .fill Icons.Outlined.ThumbUp / Icons.Filled.ThumbUp
Love heart.fill Icons.Filled.Favorite
Care hands.sparkles.fill Icons.Filled.VolunteerActivism
Haha / Wow face.smiling.fill / mouth.fill Icons.Filled.SentimentVerySatisfied / SentimentSatisfied
Sad / Angry face.dashed.fill / flame.fill Icons.Filled.SentimentDissatisfied / SentimentVeryDissatisfied
Comment bubble.left Icons.Outlined.ChatBubbleOutline
Share arrowshape.turn.up.right Icons.Outlined.Share
Overflow ellipsis Icons.Filled.MoreHoriz
Search magnifyingglass Icons.Filled.Search
Messenger ellipsis.message.fill Icons.AutoMirrored.Filled.Message
Home tab house.fill / house Icons.Filled.Home
Video tab play.rectangle.fill Icons.Filled.SmartDisplay
Marketplace tab cart.fill Icons.Filled.Storefront
Notifications tab bell.fill Icons.Filled.Notifications
Menu tab line.3.horizontal Icons.Filled.Menu
Photo picker photo.on.rectangle Icons.Filled.Photo
Live video dot.radiowaves.left.and.right Icons.Filled.SensorsOff (or custom LIVE badge)
Audience (Public) globe.americas.fill Icons.Filled.Public
Add Friend person.badge.plus Icons.Filled.PersonAddAlt1

8. Minimum SDK & Accessibility Notes

  • minSdk 24 (Compose floor is 21; modern motion is comfortable at 24). compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+.
  • Edge-to-edge: call enableEdgeToEdge() in the Activity; the light canvas wants dark system-bar icons (WindowCompat light status bar). Apply Scaffold insets so the bottom bar clears the gesture nav and the top nav clears the status bar / camera cutout (the f logo stays left of the cutout).
  • Font scaling: sp honors the user’s font scale automatically — keep it on post body, comment body, names, headers. Pin layout-sensitive text (the 11sp tab labels, the 13sp reaction count, the 10sp LIVE badge) by deriving size from dp or wrapping in CompositionLocalProvider(LocalDensity provides Density(density, fontScale = 1f)).
  • TalkBack: make each post card its own container with Modifier.semantics(mergeDescendants = true) so the feed reads post-by-post; mark the trailing overflow and the action row as separate buttons. Announce reactions as “Liked by you and 45 others, double-tap to see breakdown”.
  • Reactions popover reachability: long-press is not discoverable for switch/TalkBack users — add a Modifier.semantics { customActions = listOf(CustomAccessibilityAction("Choose reaction") { … }) } on the Like button that opens the popover, and label each emoji as a button (“Love reaction”).
  • Touch targets: Material guidance is 48.dp minimum. The reaction icons already use a 48.dp box; ensure the 20.dp action-row glyphs and the 12.dp audience icon sit inside ≥48.dp rows (the action buttons are 40.dp tall — bump to 48.dp for strict compliance).
  • Contrast: #050505 on #FFFFFF exceeds WCAG AAA. Validate #65676B secondary gray on #FFFFFF (passes AA at 13sp+) and especially #8A8D91 “Sponsored” tertiary on white — bump toward #6B6E73 if your build targets strict AA.
  • Dynamic color: do not enable Material You dynamicColorScheme() — Facebook’s brand requires the fixed #1877F2 blue, the soft-gray canvas, and the seven fixed reaction colors regardless of wallpaper.
All mobile designs →

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