coderef

WeChat

WeChat's iOS app is not really a messaging app — it's an operating system wearing a chat app's clothes, and the design reflects that.
DownloadRaw
WeChat

Build with the WeChat look.

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

Palette

At a glance

Theme
light
Typefaces
PingFang SC
Sections
8
Length
3,813 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · WeChat

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

WeChat (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports WeChat’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 (WeChat’s gray system, the super-app Discover grouped-list hub, pale-green tailed bubbles, rounded-square avatars, the red-packet card, the universal red dot) while making everything idiomatic Android — NavigationBar instead of a UITabBar, a custom tailed bubble Shape, sp/dp instead of pt. (WeChat’s real Android app is itself this UI; this maps the iOS spec onto modern Compose.)

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

1. Color Tokens

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

object WeChatColors {
    // Backdrop & canvas
    val Backdrop   = Color(0xFFEDEDED) // chat + grouped-list base
    val Canvas     = Color(0xFFFFFFFF)
    val Surface    = Color(0xFFF7F7F7)
    val Divider    = Color(0xFFD9D9D9)
    val PressedRow = Color(0xFFECECEC)

    // Text
    val TextPrimary   = Color(0xFF181818)
    val TextSecondary = Color(0xFF888888)
    val TextTertiary  = Color(0xFFB2B2B2)

    // Brand
    val Green        = Color(0xFF07C160)
    val GreenPressed = Color(0xFF06A050)

    // Message
    val Incoming = Color(0xFFFFFFFF)
    val Outgoing = Color(0xFF95EC69) // pale chat-green — NOT the brand green

    // Semantic
    val RedDot   = Color(0xFFFA5151)
    val LinkBlue = Color(0xFF576B95)
    val Gold     = Color(0xFFFBE3B3)

    // Discover icon chips (data-driven)
    val Moments    = Color(0xFF3F7DD5)
    val Scan       = Color(0xFF2EA0F8)
    val Channels   = Color(0xFFFA9D3B)
    val Mini       = Color(0xFF3CC51F)
    val Search     = Color(0xFF5C7CFA)
    val Nearby     = Color(0xFFF76F34)
    val TopStories = Color(0xFFEB6F6F)

    // Dark (system-dark users)
    val DarkCanvas        = Color(0xFF1A1A1A)
    val DarkBackdrop      = Color(0xFF111111)
    val DarkIncoming      = Color(0xFF2C2C2C)
    val DarkOutgoing      = Color(0xFF3EB575)
    val DarkTextPrimary   = Color(0xFFE8E8E8)
    val DarkTextSecondary = Color(0xFF9A9A9A)
}

Wire it into Material 3 schemes so ripples, dividers, and stock component colors inherit the brand. WeChat is light-first; provide a dark scheme for system-dark users.

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

private val WeChatLight = lightColorScheme(
    primary        = WeChatColors.Green,
    onPrimary      = Color.White,
    background      = WeChatColors.Backdrop,
    onBackground   = WeChatColors.TextPrimary,
    surface         = WeChatColors.Canvas,
    onSurface      = WeChatColors.TextPrimary,
    surfaceVariant = WeChatColors.Surface,
    outline        = WeChatColors.Divider,
    error          = WeChatColors.RedDot,
)

private val WeChatDark = darkColorScheme(
    primary        = WeChatColors.Green,
    onPrimary      = Color.White,
    background      = WeChatColors.DarkBackdrop,
    onBackground   = WeChatColors.DarkTextPrimary,
    surface         = WeChatColors.DarkCanvas,
    onSurface      = WeChatColors.DarkTextPrimary,
    surfaceVariant = WeChatColors.DarkCanvas,
    outline        = Color(0xFF333333),
    error          = WeChatColors.RedDot,
)

@Composable
fun WeChatTheme(content: @Composable () -> Unit) =
    MaterialTheme(
        colorScheme = if (isSystemInDarkTheme()) WeChatDark else WeChatLight,
        typography  = WeChatTypography,
        content     = content,
    )

2. Typography

WeChat uses PingFang SC (Apple CJK face — not on Android) / Noto Sans CJK at weights 400/600. On Android, drop Noto Sans CJK in res/font/.

// ui/theme/WeChatType.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

// Noto Sans CJK is the Android stand-in for PingFang SC
val WeChatSans = FontFamily(
    Font(R.font.noto_sans_cjk_regular,  FontWeight.Normal),   // 400
    Font(R.font.noto_sans_cjk_medium,   FontWeight.SemiBold),  // 600 (Noto "Medium")
)

// Named ramp — mirrors DESIGN.md §3 exactly (pt → sp 1:1)
object WeChatText {
    val NavTitle    = TextStyle(WeChatSans, fontWeight = FontWeight.SemiBold, fontSize = 17.sp, lineHeight = 22.sp)
    val ContactName = TextStyle(WeChatSans, fontWeight = FontWeight.Normal,   fontSize = 17.sp, lineHeight = 22.sp)
    val RowTitle    = TextStyle(WeChatSans, fontWeight = FontWeight.Normal,   fontSize = 17.sp, lineHeight = 22.sp)
    val MessageBody = TextStyle(WeChatSans, fontWeight = FontWeight.Normal,   fontSize = 17.sp, lineHeight = 24.sp) // CJK-friendly 1.4
    val Section     = TextStyle(WeChatSans, fontWeight = FontWeight.Normal,   fontSize = 14.sp, lineHeight = 17.sp)
    val Preview     = TextStyle(WeChatSans, fontWeight = FontWeight.Normal,   fontSize = 14.sp, lineHeight = 18.sp)
    val Timestamp   = TextStyle(WeChatSans, fontWeight = FontWeight.Normal,   fontSize = 13.sp, lineHeight = 16.sp)
    val TimeSep     = TextStyle(WeChatSans, fontWeight = FontWeight.Normal,   fontSize = 12.sp, lineHeight = 14.sp)
    val Button      = TextStyle(WeChatSans, fontWeight = FontWeight.SemiBold, fontSize = 17.sp, lineHeight = 17.sp)
    val ButtonSec   = TextStyle(WeChatSans, fontWeight = FontWeight.Normal,   fontSize = 16.sp, lineHeight = 16.sp)
    val Tab         = TextStyle(WeChatSans, fontWeight = FontWeight.Normal,   fontSize = 10.sp, lineHeight = 12.sp)
    val Badge       = TextStyle(WeChatSans, fontWeight = FontWeight.SemiBold, fontSize = 11.sp, lineHeight = 11.sp)
    val MomentsName = TextStyle(WeChatSans, fontWeight = FontWeight.SemiBold, fontSize = 15.sp, lineHeight = 20.sp)
    val Caption     = TextStyle(WeChatSans, fontWeight = FontWeight.Normal,   fontSize = 13.sp, lineHeight = 17.sp)
}

// Map onto Material 3 slots so stock components inherit the brand
val WeChatTypography = Typography(
    titleLarge  = WeChatText.NavTitle,
    bodyLarge   = WeChatText.MessageBody,
    titleMedium = WeChatText.ContactName,
    bodyMedium  = WeChatText.Preview,
    labelSmall  = WeChatText.Tab,
)

3. Signature Components

Discover Grouped-List Hub (Signature — the super-app)

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
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.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp

@Composable
fun DiscoverRow(
    icon: ImageVector,
    chipColor: Color,
    title: String,
    redDot: Boolean = false,
) {
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    Row(
        Modifier
            .fillMaxWidth()
            .height(56.dp)
            .background(if (pressed) WeChatColors.PressedRow else WeChatColors.Canvas)
            .clickable(interaction, indication = null) { }
            .padding(horizontal = 16.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(12.dp),
    ) {
        Box(
            Modifier.size(28.dp).clip(RoundedCornerShape(8.dp)).background(chipColor),
            contentAlignment = Alignment.Center,
        ) { Icon(icon, contentDescription = null, tint = Color.White, modifier = Modifier.size(15.dp)) }
        Text(title, style = WeChatText.RowTitle, color = WeChatColors.TextPrimary)
        Spacer(Modifier.weight(1f))
        if (redDot) Box(Modifier.size(8.dp).clip(RoundedCornerShape(4.dp)).background(WeChatColors.RedDot))
        Spacer(Modifier.width(8.dp))
        Icon(Icons.Filled.ChevronRight, contentDescription = null,
            tint = WeChatColors.TextTertiary, modifier = Modifier.size(18.dp))
    }
}

@Composable
fun DiscoverScreen() {
    LazyColumn(
        Modifier.fillMaxSize().background(WeChatColors.Backdrop),
        contentPadding = PaddingValues(vertical = 12.dp),
    ) {
        item { Cluster { DiscoverRow(Icons.Filled.PhotoLibrary, WeChatColors.Moments, "Moments", redDot = true) } }
        item {
            Cluster {
                DiscoverRow(Icons.Filled.PlayCircle, WeChatColors.Channels, "Channels")
                RowDivider()
                DiscoverRow(Icons.Filled.QrCodeScanner, WeChatColors.Scan, "Scan")
                RowDivider()
                DiscoverRow(Icons.Filled.Article, WeChatColors.TopStories, "Top Stories")
            }
        }
        item {
            Cluster {
                DiscoverRow(Icons.Filled.Search, WeChatColors.Search, "Search")
                RowDivider()
                DiscoverRow(Icons.Filled.LocationOn, WeChatColors.Nearby, "People Nearby")
            }
        }
        item { Cluster { DiscoverRow(Icons.Filled.GridView, WeChatColors.Mini, "Mini Programs") } }
    }
}

@Composable
private fun Cluster(content: @Composable ColumnScope.() -> Unit) {
    Column(
        Modifier.fillMaxWidth().background(WeChatColors.Canvas).padding(bottom = 0.dp),
        content = content,
    )
    Spacer(Modifier.height(12.dp)) // gray gutter between clusters
}

@Composable
private fun RowDivider() =
    HorizontalDivider(Modifier.padding(start = 56.dp), color = WeChatColors.Divider)

Tailed Message Bubble + Rounded-Square Avatar (Signature)

import androidx.compose.foundation.shape.GenericShape
import androidx.compose.ui.unit.Density
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.unit.LayoutDirection

@Composable
fun WCBubble(text: String, isOutgoing: Boolean) {
    Row(
        Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp),
        horizontalArrangement = if (isOutgoing) Arrangement.End else Arrangement.Start,
        verticalAlignment = Alignment.Top,
    ) {
        if (!isOutgoing) { Avatar(); Spacer(Modifier.width(8.dp)) }
        Text(
            text,
            style = WeChatText.MessageBody,
            color = WeChatColors.TextPrimary,
            modifier = Modifier
                .widthIn(max = 270.dp)
                .clip(bubbleTailShape(isOutgoing))
                .background(if (isOutgoing) WeChatColors.Outgoing else WeChatColors.Incoming)
                .padding(
                    start = if (isOutgoing) 12.dp else 16.dp, // extra room for the leading tail
                    end   = if (isOutgoing) 16.dp else 12.dp,
                    top = 9.dp, bottom = 9.dp,
                ),
        )
        if (isOutgoing) { Spacer(Modifier.width(8.dp)); Avatar() }
    }
}

// Rounded-SQUARE avatar — WeChat's constant brand tell (never a circle)
@Composable
private fun Avatar() =
    Box(Modifier.size(40.dp).clip(RoundedCornerShape(8.dp)).background(WeChatColors.Surface))

// 6dp body radius + a small solid triangular tail toward the sender's avatar
private fun bubbleTailShape(isOutgoing: Boolean) = GenericShape { size, _ ->
    val r = 6f; val tail = 12f; val ty = 22f
    if (isOutgoing) {
        addRoundRect(androidx.compose.ui.geometry.RoundRect(
            0f, 0f, size.width - tail, size.height,
            androidx.compose.ui.geometry.CornerRadius(r)))
        moveTo(size.width - tail, ty - 6f)
        lineTo(size.width, ty)
        lineTo(size.width - tail, ty + 6f)
        close()
    } else {
        addRoundRect(androidx.compose.ui.geometry.RoundRect(
            tail, 0f, size.width, size.height,
            androidx.compose.ui.geometry.CornerRadius(r)))
        moveTo(tail, ty - 6f)
        lineTo(0f, ty)
        lineTo(tail, ty + 6f)
        close()
    }
}

Chat List Row (rounded-square avatar + red badge)

@Composable
fun ChatRow(name: String, preview: String, time: String, unread: Int) {
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    Row(
        Modifier
            .fillMaxWidth()
            .height(64.dp)
            .background(if (pressed) WeChatColors.PressedRow else WeChatColors.Canvas)
            .clickable(interaction, indication = null) { }
            .padding(horizontal = 12.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(12.dp),
    ) {
        Box(Modifier.size(48.dp).clip(RoundedCornerShape(8.dp)).background(WeChatColors.Surface)) // rounded square
        Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
            Text(name, style = WeChatText.ContactName, color = WeChatColors.TextPrimary)
            Text(preview, style = WeChatText.Preview, color = WeChatColors.TextSecondary, maxLines = 1)
        }
        Column(horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.spacedBy(6.dp)) {
            Text(time, style = WeChatText.Timestamp, color = WeChatColors.TextSecondary)
            if (unread > 0) {
                Box(
                    Modifier.defaultMinSize(18.dp, 18.dp).clip(RoundedCornerShape(9.dp))
                        .background(WeChatColors.RedDot).padding(horizontal = 5.dp),
                    contentAlignment = Alignment.Center,
                ) { Text("$unread", style = WeChatText.Badge, color = Color.White) }
            }
        }
    }
}

Red Packet (Hóngbāo) Card (Signature)

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.filled.MailOutline
import androidx.compose.material.icons.filled.Paid
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.graphicsLayer

@Composable
fun RedPacketCard(sender: String) {
    var opened by remember { mutableStateOf(false) }
    val flip by animateFloatAsState(if (opened) 360f else 0f, tween(600), label = "seal")

    Column(
        Modifier
            .width(260.dp)
            .shadow(4.dp, RoundedCornerShape(8.dp), spotColor = Color.Black.copy(alpha = 0.12f))
            .clip(RoundedCornerShape(8.dp))
            .background(Brush.verticalGradient(listOf(WeChatColors.RedDot, Color(0xFFC72E29))))
            .clickable { opened = true }
            .padding(vertical = 22.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(10.dp),
    ) {
        Box(
            Modifier.size(44.dp).clip(CircleShape).background(WeChatColors.Gold)
                .graphicsLayer { rotationY = flip },
            contentAlignment = Alignment.Center,
        ) {
            Icon(if (opened) Icons.Filled.MailOutline else Icons.Filled.Paid,
                contentDescription = "Red packet from $sender, open",
                tint = Color(0xFFC72E29), modifier = Modifier.size(22.dp))
        }
        Text("Best wishes!", style = WeChatText.Button, color = Color.White)
        Text("From $sender", style = WeChatText.Caption, color = Color.White.copy(alpha = 0.85f))
    }
}

Composer

import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.GraphicEq
import androidx.compose.material.icons.outlined.EmojiEmotions
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback

@Composable
fun ComposerBar() {
    var text by remember { mutableStateOf("") }
    val hasText = text.trim().isNotEmpty()
    val haptics = LocalHapticFeedback.current

    Row(
        Modifier.fillMaxWidth().background(WeChatColors.Backdrop).padding(8.dp),
        verticalAlignment = Alignment.Bottom,
        horizontalArrangement = Arrangement.spacedBy(10.dp),
    ) {
        Icon(Icons.Filled.GraphicEq, contentDescription = "Voice",
            tint = WeChatColors.TextPrimary, modifier = Modifier.size(26.dp))
        BasicTextField(
            value = text, onValueChange = { text = it },
            textStyle = WeChatText.MessageBody.copy(color = WeChatColors.TextPrimary),
            modifier = Modifier
                .weight(1f)
                .heightIn(min = 36.dp)
                .clip(RoundedCornerShape(6.dp))
                .background(WeChatColors.Canvas)
                .padding(horizontal = 10.dp, vertical = 8.dp),
        )
        Icon(Icons.Outlined.EmojiEmotions, contentDescription = "Stickers",
            tint = WeChatColors.TextPrimary, modifier = Modifier.size(26.dp))
        if (hasText) {
            Box(
                Modifier
                    .clip(RoundedCornerShape(4.dp))
                    .background(WeChatColors.Green)
                    .clickable {
                        haptics.performHapticFeedback(HapticFeedbackType.TextHandleMove)
                        text = ""
                    }
                    .padding(horizontal = 14.dp, vertical = 8.dp),
            ) { Text("Send", style = WeChatText.ButtonSec, color = Color.White) }
        } else {
            Icon(Icons.Filled.Add, contentDescription = "More",
                tint = WeChatColors.TextPrimary, modifier = Modifier.size(26.dp))
        }
    }
}

4. Time Separator

@Composable
fun TimeSeparator(label: String) {
    Box(Modifier.fillMaxWidth().padding(vertical = 8.dp), contentAlignment = Alignment.Center) {
        Text(label, style = WeChatText.TimeSep, color = WeChatColors.TextSecondary)
    }
}

5. Bottom Navigation

Material 3 NavigationBar (Chats / Contacts / Discover / Me). Android has no live blur; use a very-light #F7F7F7 surface with a hairline. Active tint is WeChat Green.

import androidx.compose.material3.*
import androidx.compose.material.icons.filled.*

@Composable
fun WeChatBottomBar(selected: Int, onSelect: (Int) -> Unit) {
    NavigationBar(containerColor = WeChatColors.Surface, tonalElevation = 0.dp) {
        val items = listOf(
            "Chats"    to Icons.Filled.Chat,
            "Contacts" to Icons.Filled.Contacts,
            "Discover" to Icons.Filled.Explore,
            "Me"       to Icons.Filled.Person,
        )
        items.forEachIndexed { i, (label, icon) ->
            NavigationBarItem(
                selected = selected == i,
                onClick = { onSelect(i) },
                icon = { Icon(icon, contentDescription = label, modifier = Modifier.size(26.dp)) },
                label = { Text(label, style = WeChatText.Tab) },
                colors = NavigationBarItemDefaults.colors(
                    selectedIconColor   = WeChatColors.Green,
                    selectedTextColor   = WeChatColors.Green,
                    unselectedIconColor = Color(0xFF999999),
                    unselectedTextColor = Color(0xFF999999),
                    indicatorColor      = Color.Transparent, // WeChat has no Material pill
                ),
            )
        }
    }
}

6. Motion

Moment Compose recipe
Red-packet open (the one flourish) animateFloatAsState rotationY 0 → 360 over tween(600)
Send (text) AnimatedVisibility + fadeIn() + scaleIn(initialScale = 0.98f), 150ms
Press states collectIsPressedAsState toggling row/button background, ~120ms
Attachment grid present AnimatedVisibility + slideInVertically { it }, 260ms; “+” rotate to 45°
Tab switch none (instant)
Red dot render/unrender with no animation — authoritative, not playful
// Attachment "+" → "×" rotation
val rot by animateFloatAsState(if (gridOpen) 45f else 0f, tween(260), label = "plusRot")
Icon(Icons.Filled.Add, contentDescription = "More",
    modifier = Modifier.size(26.dp).graphicsLayer { rotationZ = rot },
    tint = WeChatColors.TextPrimary)

Haptics: prefer LocalHapticFeedback. For the send tap use LocalView.current.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) (API 30+) to approximate iOS’s light impact. Keep haptics sparse — WeChat’s restraint extends to feedback.

7. Icons

The closest first-party set is androidx.compose.material:material-icons-extended. Export WeChat’s exact Discover glyphs as vector drawables for parity if needed.

Purpose SF Symbol (iOS) Material Icon (Compose)
Voice waveform.circle Icons.Filled.GraphicEq
Sticker face.smiling Icons.Outlined.EmojiEmotions
Plus / attachment plus.circle Icons.Filled.Add
Moments (Discover) photo.on.rectangle Icons.Filled.PhotoLibrary
Scan (Discover) qrcode.viewfinder Icons.Filled.QrCodeScanner
Channels (Discover) play.rectangle.fill Icons.Filled.PlayCircle
Mini Programs (Discover) square.grid.2x2.fill Icons.Filled.GridView
Search (Discover) magnifyingglass Icons.Filled.Search
Nearby (Discover) location.fill Icons.Filled.LocationOn
Chevron (rows) chevron.right Icons.Filled.ChevronRight
Red packet yensign.circle.fill / envelope.open.fill Icons.Filled.Paid / Icons.Filled.MailOutline
Chats (tab) bubble.left.fill Icons.Filled.Chat
Contacts (tab) person.crop.square.fill Icons.Filled.Contacts
Discover (tab) safari.fill Icons.Filled.Explore
Me (tab) person.fill Icons.Filled.Person

8. Minimum SDK & Accessibility Notes

  • minSdk 24 (Compose floor is 21; comfortable at 24). compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+.
  • Edge-to-edge: call enableEdgeToEdge(); the gray/white system wants dark system-bar icons. Apply Scaffold insets so the composer clears gesture nav and rises with the IME (Modifier.imePadding()).
  • Discover is a grouped list, not a dashboard — white rows on the #EDEDED base, 12dp gray gutters between clusters, hairline dividers within. This screen is the identity.
  • Rounded-square avatars: every avatar is RoundedCornerShape(8.dp) on a square — never CircleShape. A constant brand tell.
  • CJK: PingFang SC is iOS-only; on Android bundle Noto Sans CJK in res/font/ and keep lineHeight generous (≈1.4 on 17sp body) so dense CJK isn’t cramped.
  • Font scaling: sp honors the user’s font scale — keep it on names, rows, and bodies (they share the 17sp base). Pin layout-sensitive text (tab labels, badge counts, time separators) via a fixed-density wrapper.
  • TalkBack: announce the Discover row’s red dot as “has new updates”; each row is one button. Label the red-packet seal “Red packet from <sender>, double-tap to open”. Merge a chat row’s name + preview with Modifier.semantics(mergeDescendants = true).
  • Touch targets: Material guidance is 48.dp minimum. Give the compact “Send” button and composer glyphs a 48.dp effective hit via padding; Discover rows are a full 56.dp and chat rows 64.dp.
  • Contrast: #181818 on #95EC69, #FFFFFF, and #EDEDED passes WCAG AAA; #888888 on white passes AA at 14sp+ — validate #888888 time separators on the #EDEDED backdrop.
  • Reduce motion: gate the red-packet rotationY behind the system animator scale and crossfade to the opened state; the red dot must never animate.
  • Dynamic color: do not enable Material You dynamicColorScheme() — WeChat Green and the pale-green bubble are fixed brand 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