coderef

BeReal

BeReal's iOS app is a deliberate rejection of the polished social-media aesthetic. The interface is a true-black canvas (#000000) with almost no chrome, no gradients, no decorative color — a stark, photographic darkroom where the only…
DownloadRaw
BeReal

Build with the BeReal look.

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

Palette

At a glance

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

Add it to your project

Then prompt your agent

Design System · BeReal

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

BeReal (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports BeReal’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 (BeReal’s pure-black darkroom, the monochrome no-brand-hue stance, the dual-lens composite, RealMoji reactions) while making everything idiomatic Android — NavigationBar instead of a UITabBar, CameraX instead of AVFoundation, sp/dp instead of pt.

Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, Coil 2.6+ for images, and CameraX for the dual capture.

1. Color Tokens

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

object BeRealColors {
    // Canvas & Surfaces
    val Canvas   = Color(0xFF000000)
    val Surface1 = Color(0xFF1C1C1E)
    val Surface2 = Color(0xFF2C2C2E)
    val Divider  = Color(0xFF2C2C2E)

    // Text
    val TextPrimary   = Color(0xFFFFFFFF)
    val TextSecondary = Color(0xFF8E8E93)
    val TextTertiary  = Color(0xFF636366)

    // Accent — intentionally monochrome, no brand hue
    val Accent   = Color(0xFFFFFFFF)
    val OnAccent = Color(0xFF000000)

    // Semantic
    val Late   = Color(0xFFFFD60A)
    val Error  = Color(0xFFFF453A)
    val Online = Color(0xFF30D158)
}

Wire it into a Material 3 darkColorScheme. BeReal is dark-only by design — the photographic darkroom metaphor depends on true black; do not provide a light scheme.

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

private val BeRealScheme = darkColorScheme(
    primary        = BeRealColors.Accent,
    onPrimary      = BeRealColors.OnAccent,    // black on white — intentional inversion
    background     = BeRealColors.Canvas,
    onBackground   = BeRealColors.TextPrimary,
    surface        = BeRealColors.Surface1,
    onSurface      = BeRealColors.TextPrimary,
    surfaceVariant = BeRealColors.Surface2,
    outline        = BeRealColors.Divider,
    error          = BeRealColors.Error,
)

@Composable
fun BeRealTheme(content: @Composable () -> Unit) =
    MaterialTheme(colorScheme = BeRealScheme, typography = BeRealTypography, content = content)

2. Typography

BeReal ships no custom typeface — it uses the system font. On Android that is Roboto; reference FontFamily.Default so there is nothing to drop in res/font/.

// ui/theme/BeRealType.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.unit.sp

private val Sys = FontFamily.Default   // BeReal uses the system font, no custom face

// Named ramp — mirrors DESIGN.md §3 exactly (pt → sp 1:1)
object BeRealText {
    val Timer        = TextStyle(Sys, fontWeight = FontWeight.Bold,     fontSize = 24.sp, lineHeight = 26.sp) // use tabular figures via FontFeatureSettings("tnum")
    val ScreenTitle  = TextStyle(Sys, fontWeight = FontWeight.Bold,     fontSize = 22.sp, lineHeight = 26.sp, letterSpacing = (-0.3).sp)
    val Username     = TextStyle(Sys, fontWeight = FontWeight.SemiBold, fontSize = 17.sp, lineHeight = 20.sp, letterSpacing = (-0.2).sp)
    val Caption      = TextStyle(Sys, fontWeight = FontWeight.Normal,   fontSize = 16.sp, lineHeight = 22.sp)
    val Body         = TextStyle(Sys, fontWeight = FontWeight.Normal,   fontSize = 15.sp, lineHeight = 21.sp)
    val CommentAuthor= TextStyle(Sys, fontWeight = FontWeight.SemiBold, fontSize = 14.sp, lineHeight = 18.sp)
    val Meta         = TextStyle(Sys, fontWeight = FontWeight.Normal,   fontSize = 13.sp, lineHeight = 17.sp)
    val LateBadge    = TextStyle(Sys, fontWeight = FontWeight.Bold,     fontSize = 12.sp, lineHeight = 14.sp, letterSpacing = 0.2.sp)
    val Button       = TextStyle(Sys, fontWeight = FontWeight.SemiBold, fontSize = 17.sp, lineHeight = 20.sp, letterSpacing = (-0.2).sp)
    val Tab          = TextStyle(Sys, fontWeight = FontWeight.SemiBold, fontSize = 10.sp, lineHeight = 12.sp)
    val Retake       = TextStyle(Sys, fontWeight = FontWeight.SemiBold, fontSize = 11.sp, lineHeight = 13.sp)
}

// Map onto Material 3 slots so stock components inherit the system-font, dark identity
val BeRealTypography = Typography(
    headlineMedium = BeRealText.ScreenTitle,
    titleMedium    = BeRealText.Username,
    bodyMedium     = BeRealText.Body,
    labelSmall     = BeRealText.Tab,
)

3. Signature Components

Dual-Lens Composite Card (the signature)

import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.*
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.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.shape.RoundedCornerShape
import coil.compose.AsyncImage

enum class SelfieCorner { TopStart, TopEnd, BottomStart, BottomEnd }

@Composable
fun DualLensCard(backUrl: String, frontUrl: String, modifier: Modifier = Modifier) {
    var corner by remember { mutableStateOf(SelfieCorner.TopStart) }
    var lifted by remember { mutableStateOf(false) }
    val scale by androidx.compose.animation.core.animateFloatAsState(
        if (lifted) 1.05f else 1f,
        androidx.compose.animation.core.spring(dampingRatio = 0.75f, stiffness = 300f),
        label = "selfieLift",
    )

    Box(
        modifier
            .fillMaxWidth()
            .aspectRatio(3f / 4f)
            .shadow(24.dp, RoundedCornerShape(18.dp), spotColor = androidx.compose.ui.graphics.Color.Black.copy(0.6f))
            .clip(RoundedCornerShape(18.dp)),
    ) {
        AsyncImage(model = backUrl, contentDescription = "BeReal back photo",
            modifier = Modifier.fillMaxSize())

        AsyncImage(
            model = frontUrl,
            contentDescription = "BeReal selfie",
            modifier = Modifier
                .align(corner.toAlignment())
                .padding(12.dp)
                .size(100.dp, 134.dp)
                .scale(scale)
                .shadow(if (lifted) 18.dp else 0.dp, RoundedCornerShape(14.dp))
                .clip(RoundedCornerShape(14.dp))
                .pointerInput(Unit) {
                    detectDragGestures(
                        onDragStart = { lifted = true },
                        onDragEnd = {
                            lifted = false
                            corner = SelfieCorner.entries.random()
                        },
                    ) { _, _ -> }
                },
        )
    }
}

private fun SelfieCorner.toAlignment() = when (this) {
    SelfieCorner.TopStart    -> Alignment.TopStart
    SelfieCorner.TopEnd      -> Alignment.TopEnd
    SelfieCorner.BottomStart -> Alignment.BottomStart
    SelfieCorner.BottomEnd   -> Alignment.BottomEnd
}

Capture Button (Dual Shutter)

import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

@Composable
fun CaptureButton(onCapture: () -> Unit, modifier: Modifier = Modifier) {
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    val haptics = LocalHapticFeedback.current
    val scope = rememberCoroutineScope()
    val scale by androidx.compose.animation.core.animateFloatAsState(if (pressed) 0.92f else 1f, label = "capScale")
    val disc  by androidx.compose.animation.core.animateDpAsState(if (pressed) 64.dp else 0.dp, label = "capDisc")

    Box(
        modifier
            .size(76.dp)
            .scale(scale)
            .clip(androidx.compose.foundation.shape.CircleShape)
            .border(4.dp, BeRealColors.Accent, androidx.compose.foundation.shape.CircleShape)
            .clickable(interaction, indication = null) {
                haptics.performHapticFeedback(HapticFeedbackType.LongPress)   // heavy ≈ back camera
                onCapture()
                scope.launch {
                    delay(500)
                    haptics.performHapticFeedback(HapticFeedbackType.TextHandleMove) // soft ≈ front
                }
            },
        contentAlignment = Alignment.Center,
    ) {
        Box(Modifier.size(disc).clip(androidx.compose.foundation.shape.CircleShape).background(BeRealColors.Accent))
    }
}

Primary / Secondary Button

enum class BeRealBtn { Filled, Outline }

@Composable
fun BeRealButton(text: String, onClick: () -> Unit, style: BeRealBtn = BeRealBtn.Filled, modifier: Modifier = Modifier) {
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    val scale by androidx.compose.animation.core.animateFloatAsState(if (pressed) 0.97f else 1f, label = "btn")
    val filled = style == BeRealBtn.Filled

    Box(
        modifier
            .scale(scale)
            .clip(androidx.compose.foundation.shape.CircleShape)
            .then(
                if (filled) Modifier.background(BeRealColors.Accent)
                else Modifier.border(1.dp, BeRealColors.TextPrimary.copy(alpha = 0.4f), androidx.compose.foundation.shape.CircleShape)
            )
            .clickable(interaction, indication = null, onClick = onClick)
            .padding(horizontal = if (filled) 32.dp else 24.dp, vertical = if (filled) 14.dp else 12.dp),
        contentAlignment = Alignment.Center,
    ) {
        Text(text, style = BeRealText.Button,
            color = if (filled) BeRealColors.OnAccent else BeRealColors.TextPrimary)
    }
}

Countdown Banner & Late Badge

import androidx.compose.ui.text.font.FontFeature
import androidx.compose.ui.text.style.TextAlign

@Composable
fun CountdownBanner(remaining: String, modifier: Modifier = Modifier) {
    // Android has no live blur — use a 60%-opaque black pill with the amber border
    Box(
        modifier
            .fillMaxWidth()
            .clip(androidx.compose.foundation.shape.CircleShape)
            .background(androidx.compose.ui.graphics.Color.Black.copy(alpha = 0.6f))
            .border(1.dp, BeRealColors.Late.copy(alpha = 0.4f), androidx.compose.foundation.shape.CircleShape)
            .padding(horizontal = 20.dp, vertical = 12.dp),
        contentAlignment = Alignment.Center,
    ) {
        Text(
            "⏰ $remaining left to capture a BeReal",
            style = BeRealText.Timer.copy(fontFeatureSettings = "tnum"),  // tabular digits
            color = BeRealColors.Late,
            textAlign = TextAlign.Center,
        )
    }
}

@Composable
fun LateBadge(text: String) =
    Text("⚠ $text", style = BeRealText.LateBadge, color = BeRealColors.Late)

RealMoji Reaction Cluster

@Composable
fun RealMojiCluster(selfieUrls: List<String>, onAdd: () -> Unit, modifier: Modifier = Modifier) {
    Row(modifier, verticalAlignment = Alignment.CenterVertically) {
        selfieUrls.forEachIndexed { i, url ->
            AsyncImage(
                model = url,
                contentDescription = "Reaction",          // never a count — reactions are faces
                modifier = Modifier
                    .offset(x = if (i == 0) 0.dp else (-8 * i).dp)  // overlap by 8dp
                    .size(30.dp)
                    .clip(androidx.compose.foundation.shape.CircleShape)
                    .border(2.dp, androidx.compose.ui.graphics.Color.Black, androidx.compose.foundation.shape.CircleShape),
            )
        }
        Box(
            Modifier
                .padding(start = 12.dp)
                .size(30.dp)
                .clip(androidx.compose.foundation.shape.CircleShape)
                .border(1.5.dp, BeRealColors.TextPrimary.copy(alpha = 0.6f), androidx.compose.foundation.shape.CircleShape)
                .clickable(onClick = onAdd),
            contentAlignment = Alignment.Center,
        ) { Text("+", style = BeRealText.Button, color = BeRealColors.TextPrimary) }
    }
}

4. Distinctive System — Selfie Corner-Snap in Compose

The front selfie inset is the soul of BeReal. DualLensCard (above) wires detectDragGestures: drag start sets lifted = true (scale animates to 1.05 via spring(dampingRatio = 0.75f)), drag end snaps corner to a SelfieCorner and the Alignment recomposes. A non-drag tap on the selfie swaps which camera is full-bleed — animate a Crossfade between the two AsyncImages. Keep the snap spring at dampingRatio = 0.75f, stiffness = 300f to match the iOS feel.

5. Bottom Navigation (Tab Bar)

Use Material 3 NavigationBar. BeReal’s iOS tab bar is opaque true-black with no blur — Android has no first-class live blur anyway, so a solid #000000 surface is an exact match. Active tint is white (BeReal has no brand hue); kill the Material indicator pill.

@Composable
fun BeRealBottomBar(selected: Int, onSelect: (Int) -> Unit) {
    NavigationBar(containerColor = BeRealColors.Canvas, tonalElevation = 0.dp) {
        val items = listOf(
            "Friends"  to Icons.Filled.People,
            "Official" to Icons.Filled.Star,
            "Search"   to Icons.Filled.Search,
            "Profile"  to Icons.Filled.AccountCircle,
        )
        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 = BeRealText.Tab) },
                colors = NavigationBarItemDefaults.colors(
                    selectedIconColor   = BeRealColors.TextPrimary,         // white, no brand hue
                    selectedTextColor   = BeRealColors.TextPrimary,
                    unselectedIconColor = BeRealColors.TextPrimary.copy(alpha = 0.45f),
                    unselectedTextColor = BeRealColors.TextPrimary.copy(alpha = 0.45f),
                    indicatorColor = androidx.compose.ui.graphics.Color.Transparent, // BeReal has no pill
                ),
            )
        }
    }
}

6. Motion

Moment Compose recipe
Dual-shutter capture white-flash overlay (AnimatedVisibility 120ms) + HapticFeedbackType.LongPress, then delay(500) + soft haptic for the front lens
Camera flip graphicsLayer { rotationY = … } animated 0 → 180 over 350ms tween, light haptic
Selfie drag-snap animateFloatAsState/alignment recompose with spring(dampingRatio = 0.75f, stiffness = 300f)
RealMoji pop Animatable keyframes 0 → 1.2 → 1 over 280ms
Feed un-blur friends’ cards Modifier.blur(30.dp) → 0.dp once you post, animateDpAsState 400ms
// Camera flip
val rot by animateFloatAsState(if (flipped) 180f else 0f, tween(350), label = "flip")
Box(Modifier.graphicsLayer { rotationY = rot; cameraDistance = 12f * density }) { /* viewfinder */ }

// RealMoji pop
val pop = remember { Animatable(0f) }
LaunchedEffect(Unit) {
    pop.animateTo(1.2f, tween(160)); pop.animateTo(1f, spring())
}

Haptics: prefer LocalHapticFeedback. For the heavy capture impact use LocalView.current.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) or a Vibrator VibrationEffect.createOneShot(20, 255); follow with a lighter CONTEXT_CLICK ~500ms later for the front camera.

7. Icons

BeReal uses simple glyphs; the closest first-party set is androidx.compose.material:material-icons-extended.

Purpose SF Symbol (iOS) Material Icon (Compose)
Camera flip arrow.triangle.2.circlepath.camera Icons.Filled.FlipCameraIos
Flash bolt.fill / bolt.slash.fill Icons.Filled.FlashOn / Icons.Filled.FlashOff
Late badge exclamationmark.triangle.fill Icons.Filled.Warning
Add RealMoji plus Icons.Filled.Add
Comments bubble.left Icons.Outlined.ChatBubbleOutline
Share square.and.arrow.up Icons.Filled.IosShare
More ellipsis Icons.Filled.MoreHoriz
Friends (tab) person.2.fill Icons.Filled.People
Official (tab) star.circle.fill Icons.Filled.Star
Search (tab) magnifyingglass Icons.Filled.Search
Profile (tab) person.crop.circle Icons.Filled.AccountCircle
BTS video play.rectangle.on.rectangle Icons.Filled.Collections

8. Minimum SDK & Accessibility Notes

  • minSdk 24 (Compose floor is 21; CameraX dual capture + modern motion are comfortable at 24). compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+.
  • Edge-to-edge: call enableEdgeToEdge(); the pure-black canvas wants light-content system bars via WindowCompat. Let the dual-lens card draw edge-to-edge horizontally (Modifier.fillMaxWidth(), no inset) while keeping the tab bar inside systemBars insets.
  • Font scaling: sp honors the user’s font scale — keep it on usernames, captions, comments. Pin the countdown (fontFeatureSettings = "tnum" and a fixed size), late badge, retake count, and tab labels by deriving from dp or a CompositionLocalProvider(LocalDensity …, fontScale = 1f).
  • TalkBack: set contentDescription on the dual-lens card (“jordan’s BeReal, 2 hours late”); mark the selfie as a draggable element with a custom stateDescription; announce RealMojis as “Reaction from <name>” and never a numeric count — there is none.
  • Touch targets: Material guidance is 48.dp minimum. The 76.dp capture button is well clear; give the 30.dp RealMoji + a 48.dp hit area via padding.
  • Contrast: #8E8E93 on #000000 passes WCAG AA at 13sp+. The amber #FFD60A on black is high-contrast — reserve it for lateness/urgency only.
  • Dynamic color: do not enable Material You dynamicDarkColorScheme() — BeReal’s identity is the fixed pure-black canvas and the deliberate absence of a brand hue, 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