coderef

Calm

Calm's iOS app is a night sky you can fall asleep inside. The defining surface is a full-screen vertical gradient that runs from a deep twilight blue (#2A6FD6) at the top down to a near-black navy (#0B1E3F) at the bottom — the color of…
DownloadRaw
Calm

Build with the Calm look.

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

Palette

At a glance

Theme
light
Typefaces
Inter, Lora
Sections
7
Length
3,527 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · Calm

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

Calm (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports Calm’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 (Calm’s night-sky gradient, glass surfaces, serif/sans pairing, the breathe bubble) while making everything idiomatic Android — NavigationBar instead of a UITabBar, Brush.verticalGradient instead of CAGradientLayer, sp/dp instead of pt.

Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, Coil 2.6+ for imagery, and Lora + Inter FontFamilys in res/font/.

1. Color Tokens

// ui/theme/CalmColors.kt
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color

object CalmColors {
    // Night-sky gradient
    val GradientTop    = Color(0xFF2A6FD6)
    val GradientMid    = Color(0xFF1A4A8F)
    val GradientBottom = Color(0xFF0B1E3F)
    val ScrimNavy      = Color(0xFF091830)

    // Glass surfaces
    val Glass        = Color.White.copy(alpha = 0.08f)
    val GlassRaised  = Color.White.copy(alpha = 0.12f)
    val GlassDivider = Color.White.copy(alpha = 0.12f)
    val GlassHover   = Color.White.copy(alpha = 0.16f)

    // Text
    val TextPrimary   = Color(0xFFFFFFFF)
    val TextSecondary = Color(0xFFB8C4D8)
    val TextTertiary  = Color(0xFF8794A8)

    // Accent
    val CalmBlue        = Color(0xFF2A6FD6)
    val CalmBluePressed = Color(0xFF1F57AB)
    val CalmBlueSoft    = Color(0xFF4A85E0)
    val StreakGold      = Color(0xFFF2C94C)
    val GentleError     = Color(0xFFE08A8A)
    val Success         = Color(0xFF7FB8A0)
}

val CalmSky = Brush.verticalGradient(
    0.0f  to CalmColors.GradientTop,
    0.45f to CalmColors.GradientMid,
    1.0f  to CalmColors.GradientBottom,
)

Wire it into a Material 3 darkColorScheme. Calm is dark-only; 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 CalmScheme = darkColorScheme(
    primary        = CalmColors.CalmBlue,
    onPrimary      = CalmColors.TextPrimary,
    background     = CalmColors.GradientBottom,
    onBackground   = CalmColors.TextPrimary,
    surface        = CalmColors.GradientMid,
    onSurface      = CalmColors.TextPrimary,
    surfaceVariant = CalmColors.GlassRaised,
    outline        = CalmColors.GlassDivider,
    error          = CalmColors.GentleError,
)

@Composable
fun CalmTheme(content: @Composable () -> Unit) =
    MaterialTheme(colorScheme = CalmScheme, typography = CalmTypography, content = content)

2. Typography

Lora (serif headlines) and Inter (sans body) are both open-source (SIL OFL). Drop the TTFs in res/font/ (lowercase, snake_case) and build two FontFamilys.

// ui/theme/CalmType.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 Lora = FontFamily(
    Font(R.font.lora_regular,  FontWeight.Normal),   // 400
    Font(R.font.lora_semibold, FontWeight.SemiBold), // 600
)
val Inter = FontFamily(
    Font(R.font.inter_light,    FontWeight.Light),    // 300
    Font(R.font.inter_regular,  FontWeight.Normal),   // 400
    Font(R.font.inter_medium,   FontWeight.Medium),   // 500
    Font(R.font.inter_semibold, FontWeight.SemiBold), // 600
    Font(R.font.inter_bold,     FontWeight.Bold),     // 700
)

// Named ramp — mirrors DESIGN.md §3 exactly (pt → sp 1:1)
object CalmText {
    // Lora — serif, the brand voice
    val Greeting    = TextStyle(Lora, fontWeight = FontWeight.SemiBold, fontSize = 30.sp, lineHeight = 38.sp, letterSpacing = (-0.2).sp)
    val ScreenTitle = TextStyle(Lora, fontWeight = FontWeight.SemiBold, fontSize = 26.sp, lineHeight = 34.sp, letterSpacing = (-0.2).sp)
    val DailyTitle  = TextStyle(Lora, fontWeight = FontWeight.SemiBold, fontSize = 24.sp, lineHeight = 31.sp, letterSpacing = (-0.1).sp)
    val CardTitle   = TextStyle(Lora, fontWeight = FontWeight.SemiBold, fontSize = 19.sp, lineHeight = 26.sp)
    val BreatheCue  = TextStyle(Lora, fontWeight = FontWeight.Normal,   fontSize = 22.sp, letterSpacing = 0.5.sp)

    // Inter — sans, supporting
    val Section   = TextStyle(Inter, fontWeight = FontWeight.Bold,     fontSize = 16.sp, letterSpacing = 0.2.sp)
    val RowTitle  = TextStyle(Inter, fontWeight = FontWeight.SemiBold, fontSize = 17.sp, lineHeight = 23.sp)
    val Body      = TextStyle(Inter, fontWeight = FontWeight.Normal,   fontSize = 17.sp, lineHeight = 26.sp)
    val Subtitle  = TextStyle(Inter, fontWeight = FontWeight.Normal,   fontSize = 15.sp, lineHeight = 21.sp)
    val Button    = TextStyle(Inter, fontWeight = FontWeight.SemiBold, fontSize = 17.sp, letterSpacing = 0.1.sp)
    val Meta      = TextStyle(Inter, fontWeight = FontWeight.Medium,   fontSize = 13.sp, letterSpacing = 0.2.sp)
    val Tab       = TextStyle(Inter, fontWeight = FontWeight.SemiBold, fontSize = 11.sp, letterSpacing = 0.2.sp)
    val Timer     = TextStyle(Inter, fontWeight = FontWeight.Light,    fontSize = 44.sp)
    val LabelUpper = TextStyle(Inter, fontWeight = FontWeight.Bold,    fontSize = 12.sp, letterSpacing = 1.0.sp)
}

// Map onto Material 3 slots so stock components inherit the brand
val CalmTypography = Typography(
    headlineLarge = CalmText.Greeting,
    headlineSmall = CalmText.ScreenTitle,
    titleMedium   = CalmText.RowTitle,
    bodyLarge     = CalmText.Body,
    labelSmall    = CalmText.Tab,
)

3. Signature Components

Night-Sky Background

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

@Composable
fun CalmBackground(content: @Composable BoxScope.() -> Unit) {
    Box(Modifier.fillMaxSize().background(CalmSky), content = content)
}

Breathe Bubble (signature)

import androidx.compose.animation.core.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay

@Composable
fun BreatheBubble(modifier: Modifier = Modifier) {
    val anim = remember { Animatable(120f) }
    var cue by remember { mutableStateOf("Breathe in") }

    LaunchedEffect(Unit) {
        while (true) {
            cue = "Breathe in"
            anim.animateTo(220f, tween(4000, easing = EaseIn))   // inhale
            cue = "Hold"
            delay(7000)                                          // hold
            cue = "Breathe out"
            anim.animateTo(120f, tween(8000, easing = EaseOut))  // exhale
        }
    }

    Box(modifier, contentAlignment = Alignment.Center) {
        Box(
            Modifier
                .size(anim.value.dp)
                .shadow(40.dp, CircleShape, spotColor = CalmColors.CalmBlueSoft)
                .background(Color.White.copy(alpha = 0.9f), CircleShape),
            contentAlignment = Alignment.Center,
        ) {
            Text(cue, style = CalmText.BreatheCue, color = CalmColors.GradientBottom)
        }
    }
}

Primary Play Pill

import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Icon
import androidx.compose.ui.draw.scale
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback

@Composable
fun CalmPlayPill(title: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    val scale by animateFloatAsState(if (pressed) 0.97f else 1f,
        tween(250, easing = EaseOut), label = "pillScale")
    val haptics = LocalHapticFeedback.current

    Row(
        modifier
            .scale(scale)
            .clip(CircleShape)
            .background(if (pressed) CalmColors.CalmBluePressed else CalmColors.CalmBlue)
            .clickable(interaction, indication = null) {
                haptics.performHapticFeedback(HapticFeedbackType.LongPress) // ~iOS soft
                onClick()
            }
            .padding(horizontal = 40.dp, vertical = 16.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(10.dp),
    ) {
        Icon(Icons.Filled.PlayArrow, null, tint = Color.White, modifier = Modifier.size(16.dp))
        Text(title, style = CalmText.Button, color = CalmColors.TextPrimary)
    }
}

Daily Calm Hero Card (signature)

import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.graphics.Brush

@Composable
fun DailyCalmHero(
    title: String, subtitle: String, photoUrl: String, onPlay: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Box(
        modifier
            .fillMaxWidth()
            .height(220.dp)
            .clip(RoundedCornerShape(20.dp)),
    ) {
        AsyncImage(model = photoUrl, contentDescription = null,
            contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize())
        Box(
            Modifier.fillMaxSize().background(
                Brush.verticalGradient(
                    0.4f to Color.Transparent,
                    1f   to CalmColors.ScrimNavy.copy(alpha = 0.85f),
                )
            )
        )
        Row(
            Modifier.align(Alignment.BottomStart).padding(20.dp).fillMaxWidth(),
            verticalAlignment = Alignment.Bottom,
        ) {
            Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(6.dp)) {
                Text("DAILY CALM", style = CalmText.LabelUpper, color = CalmColors.TextPrimary)
                Text(title, style = CalmText.DailyTitle, color = CalmColors.TextPrimary)
                Text(subtitle, style = CalmText.Subtitle, color = CalmColors.TextSecondary)
            }
            Box(
                Modifier.size(44.dp).clip(CircleShape)
                    .background(CalmColors.GlassHover)
                    .clickable(onClick = onPlay),
                contentAlignment = Alignment.Center,
            ) {
                Icon(Icons.Filled.PlayArrow, "Play",
                    tint = Color.White, modifier = Modifier.size(18.dp))
            }
        }
    }
}

Sleep Story List Row

import androidx.compose.material.icons.outlined.Download

@Composable
fun SleepStoryRow(
    title: String, narrator: String, duration: String, thumbUrl: String,
    onClick: () -> Unit, modifier: Modifier = Modifier,
) {
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    Column {
        Row(
            modifier
                .fillMaxWidth()
                .height(76.dp)
                .background(if (pressed) CalmColors.GlassRaised else Color.Transparent)
                .clickable(interaction, indication = null, onClick = onClick)
                .padding(horizontal = 20.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(14.dp),
        ) {
            AsyncImage(model = thumbUrl, contentDescription = null,
                contentScale = ContentScale.Crop,
                modifier = Modifier.size(56.dp).clip(RoundedCornerShape(12.dp)))
            Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
                Text(title, style = CalmText.RowTitle, color = CalmColors.TextPrimary, maxLines = 1)
                Text("Narrated by $narrator · $duration",
                    style = CalmText.Subtitle, color = CalmColors.TextSecondary, maxLines = 1)
            }
            Icon(Icons.Outlined.Download, "Download",
                tint = Color.White.copy(alpha = 0.8f), modifier = Modifier.size(22.dp))
        }
        HorizontalDivider(color = CalmColors.GlassDivider, thickness = 1.dp)
    }
}

Glass Card

@Composable
fun CalmGlassCard(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) {
    Column(
        modifier
            .clip(RoundedCornerShape(20.dp))
            .background(CalmColors.Glass) // Android has no live blur; value-only glass
            .border(1.dp, CalmColors.GlassDivider, RoundedCornerShape(20.dp))
            .padding(20.dp),
        content = content,
    )
}

4. Bottom Navigation (Tab Bar)

Material 3 NavigationBar. Calm’s iOS tab bar is .regularMaterial glass over the deep navy; Android has no first-class live blur, so use an 88%-opaque navy surface. Active tint is Calm Blue.

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

@Composable
fun CalmBottomBar(selected: Int, onSelect: (Int) -> Unit) {
    NavigationBar(
        containerColor = CalmColors.GradientBottom.copy(alpha = 0.88f),
        tonalElevation = 0.dp,
    ) {
        val items = listOf(
            "Home"     to Icons.Filled.Home,
            "Sleep"    to Icons.Filled.Bedtime,
            "Meditate" to Icons.Filled.SelfImprovement,
            "Music"    to Icons.Filled.MusicNote,
            "More"     to Icons.Filled.MoreHoriz,
        )
        items.forEachIndexed { i, (label, icon) ->
            NavigationBarItem(
                selected = selected == i,
                onClick = { onSelect(i) },
                icon = { Icon(icon, label, modifier = Modifier.size(24.dp)) },
                label = { Text(label, style = CalmText.Tab) },
                colors = NavigationBarItemDefaults.colors(
                    selectedIconColor   = CalmColors.CalmBlue,
                    selectedTextColor   = CalmColors.CalmBlue,
                    unselectedIconColor = Color.White.copy(alpha = 0.6f),
                    unselectedTextColor = Color.White.copy(alpha = 0.6f),
                    indicatorColor      = Color.Transparent, // Calm has no Material pill
                ),
            )
        }
    }
}

5. Motion

Moment Compose recipe
Breathe bubble Animatable 120 → 220 tween(4000, EaseIn), delay(7000), 220 → 120 tween(8000, EaseOut), looped in LaunchedEffect
Card / pill tap animateFloatAsState 1 → 0.97 tween(250, EaseOut), HapticFeedbackType.LongPress
Screen transition AnimatedContent with fadeIn(tween(550)) togetherWith fadeOut(tween(550)) — Calm fades, never hard-slides
Session timer ring Animatable 0 → 1 tween(sessionMs, LinearEasing) driving a drawArc sweep
Session complete soft sage check fadeIn(tween(600)) + single HapticFeedbackType.LongPress
Photo parallax offset hero ~8.dp from scroll via Modifier.graphicsLayer { translationY = scroll * 0.1f }
@Composable
fun SessionTimerRing(progress: Float) { // progress 0f..1f animated linearly
    Canvas(Modifier.size(220.dp)) {
        drawArc(CalmColors.GlassHover, -90f, 360f, false,
            style = Stroke(3.dp.toPx(), cap = StrokeCap.Round))
        drawArc(CalmColors.CalmBlue, -90f, 360f * progress, false,
            style = Stroke(3.dp.toPx(), cap = StrokeCap.Round))
    }
}

Haptics: prefer LocalHapticFeedback. Keep them gentle and rare — Calm is serene. For a softer pulse use Vibrator VibrationEffect.createOneShot(8, 60) rather than the default click.

6. Icons

The closest first-party set is androidx.compose.material:material-icons-extended.

Purpose SF Symbol (iOS) Material Icon (Compose)
Play play.fill Icons.Filled.PlayArrow
Pause pause.fill Icons.Filled.Pause
Favorite heart / heart.fill Icons.Outlined.FavoriteBorder / Icons.Filled.Favorite
Download arrow.down.circle Icons.Outlined.Download
Share square.and.arrow.up Icons.Filled.IosShare
Background sound slider.horizontal.3 Icons.Filled.Tune
Timer timer Icons.Filled.Timer
Search magnifyingglass Icons.Filled.Search
Streak flame.fill Icons.Filled.LocalFireDepartment
Settings gearshape Icons.Filled.Settings
Home (tab) house.fill Icons.Filled.Home
Sleep (tab) moon.stars.fill Icons.Filled.Bedtime
Meditate (tab) figure.mind.and.body Icons.Filled.SelfImprovement
Music (tab) music.note Icons.Filled.MusicNote
More (tab) ellipsis Icons.Filled.MoreHoriz

7. 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(); the night-sky gradient runs full-bleed behind the system bars (light-content). Apply Modifier.windowInsetsPadding(WindowInsets.systemBars) to content so the tab bar clears the gesture nav, but let the gradient ignore insets.
  • No live blur: Android lacks a first-class backdrop blur — glass surfaces are value-only (Color.White.copy(alpha = 0.08f) over the gradient). Bump alpha slightly vs iOS so cards register without the blur depth cue.
  • Font scaling: sp honors the user’s font scale — keep it on greetings, titles, body. Pin layout-sensitive text (tab labels, duration meta, the 44sp timer) by wrapping in CompositionLocalProvider(LocalDensity provides Density(density, fontScale = 1f)).
  • TalkBack: mark the breathe bubble Modifier.clearAndSetSemantics { } (decorative) and announce phase changes via a live region; label the play pill (“Play Daily Calm”); merge story-row text with Modifier.semantics(mergeDescendants = true).
  • Reduce Motion: gate the breathe-bubble animation on Settings.Global.ANIMATOR_DURATION_SCALE — render a static circle with a text-only phase cue when animations are off; replace fades with instant swaps.
  • Touch targets: Material guidance is 48.dp minimum. The 44.dp play buttons are close — give the 22.dp download glyph a Modifier.size(48.dp) hit area via padding; rows are already 76.dp.
  • Contrast: #B8C4D8 on the mid-gradient passes AA at 15sp+; validate the 13sp meta over the lighter top band and darken the scrim if your build targets accessibility compliance.
  • Dynamic color: do not enable Material You dynamicDarkColorScheme() — Calm’s identity requires the fixed night-sky gradient and single Calm Blue accent regardless of wallpaper.

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