coderef

Apple Fitness

Apple Fitness is the purest expression of "the data is the brand" in Apple's app catalog. The entire identity rests on three concentric Activity rings — Move (#FA114F, a hot pink-red), Exercise (#92E82A, electric lime-green), and Stand…
DownloadRaw
Apple Fitness

Build with the Apple Fitness 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
7
Length
4,301 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · Apple Fitness

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

Apple Fitness (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports Apple Fitness’s rings-on-black visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, paste-ready @Composables for the Activity rings + Summary + Fitness+ shelf, navigation, motion, and haptics.

Why a Compose guide for an Apple app? The DESIGN.md tokens are platform-neutral. This file keeps the visual identity (true-black canvas, the three immutable ring colors on 22%-opacity tracks, grouped rounded cards, the label-opacity text ramp, cinematic Fitness+ artwork) while making everything idiomatic Android — NavigationBar instead of a UITabBar, Canvas for the rings, a Material Surface instead of a grouped UITableView cell, sp/dp instead of pt.

Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, and Coil 2.6+ for Fitness+ artwork. Apple Fitness is dark-first. Do not enable Material You dynamic color — the three ring colors are the brand and must never be re-themed to the wallpaper.

1. Color Tokens

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

object AFColors {
    // The three Activity rings (the brand — immutable, theme-invariant)
    val Move       = Color(0xFFFA114F)
    val MoveLabel  = Color(0xFFFF375F)   // brightened on dark for text
    val Exercise   = Color(0xFF92E82A)
    val ExerciseHi = Color(0xFF66FF00)
    val Stand      = Color(0xFF1EE4E1)
    val StandHi    = Color(0xFF00F0FF)

    // Ring tracks = ring color @ 22% opacity
    val MoveTrack     = Move.copy(alpha = 0.22f)
    val ExerciseTrack = Exercise.copy(alpha = 0.22f)
    val StandTrack    = Stand.copy(alpha = 0.22f)

    // Chrome accent (single)
    val Accent        = Color(0xFFFF375F)
    val AccentPressed = Color(0xFFD80E45)
    val FitnessPlus   = Color(0xFFC969E0)

    // Surfaces (dark — primary)
    val Canvas    = Color(0xFF000000)
    val Grouped1  = Color(0xFF1C1C1E)
    val Grouped2  = Color(0xFF2C2C2E)
    val Grouped3  = Color(0xFF3A3A3C)
    val Separator = Color(0xFF38383A)
    val Fill      = Color(0xFF767680).copy(alpha = 0.24f)

    // Surfaces (light — tablet / system light)
    val LightCanvas    = Color(0xFFF2F2F7)
    val LightSurface   = Color(0xFFFFFFFF)
    val LightSeparator = Color(0xFFC6C6C8)

    // Text — Apple label-opacity ramp (white-on-dark)
    val LabelPrimary   = Color(0xFFFFFFFF)
    val LabelSecondary = Color(0xFFEBEBF5).copy(alpha = 0.60f)
    val LabelTertiary  = Color(0xFFEBEBF5).copy(alpha = 0.30f)

    // Semantic
    val Success   = Color(0xFF30D158)
    val Error     = Color(0xFFFF453A)
    val AwardGold = Color(0xFFFFD60A)
}

Wire it into a dark-first scheme. The ring colors live outside the M3 scheme on purpose — they are brand constants, not theme roles.

// 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 AFDark = darkColorScheme(
    primary        = AFColors.Accent,        // Move-pink chrome accent
    onPrimary      = AFColors.LabelPrimary,
    background     = AFColors.Canvas,
    onBackground   = AFColors.LabelPrimary,
    surface        = AFColors.Grouped1,
    onSurface      = AFColors.LabelPrimary,
    surfaceVariant = AFColors.Grouped2,
    outline        = AFColors.Separator,
    error          = AFColors.Error,
)

private val AFLight = lightColorScheme(
    primary        = AFColors.Accent,
    onPrimary      = AFColors.LabelPrimary,
    background     = AFColors.LightCanvas,
    onBackground   = Color(0xFF000000),
    surface        = AFColors.LightSurface,
    onSurface      = Color(0xFF000000),
    surfaceVariant = Color(0xFFE5E5EA),
    outline        = AFColors.LightSeparator,
    error          = AFColors.Error,
)

@Composable
fun AFTheme(
    dark: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit,
) = MaterialTheme(
    colorScheme = if (dark) AFDark else AFLight,   // dark is the intended default (phone)
    typography  = AFTypography,
    content     = content,
)

2. Typography

Apple Fitness uses SF Pro (iOS system). On Android the closest substitute is Inter (SIL OFL) — drop the TTFs in res/font/. Follow Apple’s text-style ramp; numerals tabular; secondary/tertiary text via the label-opacity colors above.

// ui/theme/AFType.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 Inter = FontFamily(
    Font(R.font.inter_regular,   FontWeight.Normal),
    Font(R.font.inter_medium,    FontWeight.Medium),
    Font(R.font.inter_semibold,  FontWeight.SemiBold),
    Font(R.font.inter_bold,      FontWeight.Bold),
    Font(R.font.inter_extrabold, FontWeight.ExtraBold),
)

private const val TNUM = "tnum"   // tabular figures

object AFText {
    val LargeTitle = TextStyle(Inter, fontWeight = FontWeight.ExtraBold, fontSize = 40.sp, lineHeight = 44.sp, letterSpacing = (-1.0).sp)
    val Date       = TextStyle(Inter, fontWeight = FontWeight.ExtraBold, fontSize = 32.sp, lineHeight = 35.sp, letterSpacing = (-0.6).sp)
    val Header     = TextStyle(Inter, fontWeight = FontWeight.ExtraBold, fontSize = 26.sp, lineHeight = 30.sp, letterSpacing = (-0.5).sp)
    val Section    = TextStyle(Inter, fontWeight = FontWeight.ExtraBold, fontSize = 22.sp, lineHeight = 26.sp, letterSpacing = (-0.4).sp)
    val Title3     = TextStyle(Inter, fontWeight = FontWeight.Bold,      fontSize = 20.sp, lineHeight = 25.sp, letterSpacing = (-0.3).sp)
    val Body       = TextStyle(Inter, fontWeight = FontWeight.SemiBold,  fontSize = 17.sp, lineHeight = 24.sp, letterSpacing = (-0.2).sp)
    val BodyReg    = TextStyle(Inter, fontWeight = FontWeight.Normal,    fontSize = 17.sp, lineHeight = 26.sp, letterSpacing = (-0.2).sp)
    val RingValue  = TextStyle(Inter, fontWeight = FontWeight.ExtraBold, fontSize = 19.sp, lineHeight = 21.sp, fontFeatureSettings = TNUM)
    val TileValue  = TextStyle(Inter, fontWeight = FontWeight.ExtraBold, fontSize = 22.sp, lineHeight = 24.sp, fontFeatureSettings = TNUM)
    val CardTitle  = TextStyle(Inter, fontWeight = FontWeight.SemiBold,  fontSize = 15.sp, lineHeight = 20.sp, letterSpacing = (-0.2).sp)
    val Footnote   = TextStyle(Inter, fontWeight = FontWeight.Normal,    fontSize = 13.sp, lineHeight = 18.sp)
    val Eyebrow    = TextStyle(Inter, fontWeight = FontWeight.Bold,      fontSize = 12.sp, lineHeight = 15.sp, letterSpacing = 0.4.sp)
    val Button     = TextStyle(Inter, fontWeight = FontWeight.SemiBold,  fontSize = 17.sp, lineHeight = 17.sp, letterSpacing = (-0.2).sp)
    val Tab        = TextStyle(Inter, fontWeight = FontWeight.Medium,    fontSize = 10.sp, lineHeight = 10.sp)
    val Badge      = TextStyle(Inter, fontWeight = FontWeight.Bold,      fontSize = 10.sp, lineHeight = 10.sp, letterSpacing = 0.5.sp)
}

val AFTypography = Typography(
    headlineLarge = AFText.LargeTitle,
    titleLarge    = AFText.Section,
    bodyLarge     = AFText.Body,
    labelSmall    = AFText.Tab,
)

3. Signature Components

Activity Rings (Canvas)

import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

@Composable
fun ActivityRings(
    move: Float, exercise: Float, stand: Float,
    size: Dp = 130.dp, lineWidth: Dp = 14.dp,
) {
    val m = remember { Animatable(0f) }
    val e = remember { Animatable(0f) }
    val s = remember { Animatable(0f) }
    LaunchedEffect(Unit) {
        val spec = tween<Float>(1000, easing = LinearOutSlowInEasing)
        launch { m.animateTo(move.coerceAtMost(1f), spec) }
        launch { delay(80);  e.animateTo(exercise.coerceAtMost(1f), spec) }
        launch { delay(160); s.animateTo(stand.coerceAtMost(1f), spec) }
    }

    Canvas(Modifier.size(size)) {
        val sw = lineWidth.toPx()
        val gap = sw + 3.dp.toPx()
        fun ring(progress: Float, color: Color, track: Color, inset: Float) {
            val d = this.size.minDimension - sw - inset * 2
            val topLeft = Offset(sw / 2 + inset, sw / 2 + inset)
            val arc = Size(d, d)
            drawArc(track, 0f, 360f, false, topLeft, arc, style = Stroke(sw))
            drawArc(color, -90f, 360f * progress, false, topLeft, arc,
                style = Stroke(sw, cap = StrokeCap.Round))
        }
        ring(m.value, AFColors.Move,     AFColors.MoveTrack,     0f)
        ring(e.value, AFColors.Exercise, AFColors.ExerciseTrack, gap)
        ring(s.value, AFColors.Stand,    AFColors.StandTrack,    gap * 2)
    }
}

Ring Legend + Hero Card

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.ui.draw.clip
import androidx.compose.ui.Alignment

private data class Leg(val name: String, val value: String, val goal: String, val color: Color)

@Composable
fun RingHeroCard() {
    val legs = listOf(
        Leg("Move", "486", "/620 KCAL", AFColors.MoveLabel),
        Leg("Exercise", "38", "/30 MIN", AFColors.Exercise),
        Leg("Stand", "9", "/12 HR", AFColors.Stand),
    )
    Row(
        Modifier
            .padding(horizontal = 16.dp)
            .clip(RoundedCornerShape(18.dp))
            .background(AFColors.Grouped1)
            .padding(22.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(20.dp),
    ) {
        ActivityRings(move = 0.80f, exercise = 1.27f, stand = 0.75f, size = 130.dp)
        Column(verticalArrangement = Arrangement.spacedBy(14.dp), modifier = Modifier.weight(1f)) {
            legs.forEach { l ->
                Column {
                    Text(l.name.uppercase(), style = AFText.Eyebrow, color = l.color)
                    Row(verticalAlignment = Alignment.Bottom) {
                        Text(l.value, style = AFText.RingValue, color = AFColors.LabelPrimary)
                        Text(" ${l.goal}", style = AFText.Footnote.copy(fontSize = 12.sp),
                            color = AFColors.LabelSecondary)
                    }
                }
            }
        }
    }
}

Metric Tile

import androidx.compose.material3.Icon
import androidx.compose.ui.graphics.vector.ImageVector

@Composable
fun MetricTile(
    icon: ImageVector, tint: Color, name: String,
    value: String, unit: String = "", sub: String,
    modifier: Modifier = Modifier,
) {
    Column(
        modifier
            .clip(RoundedCornerShape(14.dp))
            .background(AFColors.Grouped1)
            .padding(14.dp),
    ) {
        Row(verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(6.dp)) {
            Icon(icon, null, tint = tint, modifier = Modifier.size(16.dp))
            Text(name, style = AFText.Footnote.copy(fontSize = 12.sp), color = AFColors.LabelSecondary)
        }
        Row(verticalAlignment = Alignment.Bottom, modifier = Modifier.padding(top = 8.dp)) {
            Text(value, style = AFText.TileValue, color = AFColors.LabelPrimary)
            if (unit.isNotEmpty())
                Text(" $unit", style = AFText.Footnote, color = AFColors.LabelSecondary)
        }
        Text(sub, style = AFText.Footnote, color = AFColors.LabelTertiary,
            modifier = Modifier.padding(top = 2.dp))
    }
}

Fitness+ Card

import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.ui.graphics.Brush

@Composable
fun FitnessPlusCard(
    badge: String, type: String, title: String, meta: String,
    gradient: List<Color>,
) {
    Column(Modifier.width(168.dp)) {
        Box(
            Modifier
                .width(168.dp).height(200.dp)
                .clip(RoundedCornerShape(16.dp))
                .background(Brush.linearGradient(gradient)),
        ) {
            // Android has no live blur — translucent dark scrim approximates the frosted badge
            Text(
                badge.uppercase(), style = AFText.Badge, color = Color.White,
                modifier = Modifier
                    .align(Alignment.TopStart).padding(12.dp)
                    .clip(RoundedCornerShape(6.dp))
                    .background(Color.Black.copy(alpha = 0.40f))
                    .padding(horizontal = 8.dp, vertical = 4.dp),
            )
            Box(
                Modifier
                    .align(Alignment.BottomStart).padding(14.dp)
                    .size(34.dp).clip(CircleShape)
                    .background(Color.White.copy(alpha = 0.22f)),
                contentAlignment = Alignment.Center,
            ) {
                Icon(Icons.Filled.PlayArrow, null, tint = Color.White, modifier = Modifier.size(18.dp))
            }
        }
        Text(type.uppercase(), style = AFText.Eyebrow, color = AFColors.MoveLabel,
            modifier = Modifier.padding(top = 10.dp))
        Text(title, style = AFText.CardTitle, color = AFColors.LabelPrimary,
            modifier = Modifier.padding(top = 3.dp))
        Text(meta, style = AFText.Footnote, color = AFColors.LabelSecondary,
            modifier = Modifier.padding(top = 2.dp))
    }
}

Buttons

import androidx.compose.foundation.clickable

@Composable
fun AFPrimaryButton(title: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
    Box(
        modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(14.dp))
            .background(AFColors.Move)
            .clickable(onClick = onClick)
            .padding(vertical = 15.dp),
        contentAlignment = Alignment.Center,
    ) { Text(title, style = AFText.Button, color = Color.White) }
}

@Composable
fun AFTintedButton(title: String, onClick: () -> Unit) {
    Box(
        Modifier
            .clip(RoundedCornerShape(12.dp))
            .background(AFColors.Move.copy(alpha = 0.18f))
            .clickable(onClick = onClick)
            .padding(horizontal = 22.dp, vertical = 12.dp),
    ) { Text(title, style = AFText.Button.copy(fontSize = 15.sp), color = AFColors.Accent) }
}

4. Navigation

Apple Fitness has a 3-tab bottom strip with no tint pill — active is the Move-pink accent only. The header is a day eyebrow + large date + avatar.

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

@Composable
fun AFBottomBar(selected: Int, onSelect: (Int) -> Unit) {
    NavigationBar(containerColor = AFColors.Grouped1, tonalElevation = 0.dp) {
        val items = listOf(
            "Summary"  to Icons.Filled.RadioButtonChecked,
            "Fitness+" to Icons.Filled.PlayCircleFilled,
            "Sharing"  to Icons.Filled.Group,
        )
        items.forEachIndexed { i, (label, icon) ->
            NavigationBarItem(
                selected = selected == i,
                onClick = { onSelect(i) },
                icon = { Icon(icon, contentDescription = label, modifier = Modifier.size(24.dp)) },
                label = { Text(label, style = AFText.Tab) },
                colors = NavigationBarItemDefaults.colors(
                    selectedIconColor   = AFColors.Accent,    // Move-pink, no pill
                    selectedTextColor   = AFColors.Accent,
                    unselectedIconColor = AFColors.LabelTertiary,
                    unselectedTextColor = AFColors.LabelTertiary,
                    indicatorColor      = Color.Transparent,  // no Material pill — Apple uses tint only
                ),
            )
        }
    }
}

The Summary header is a Column with a day eyebrow (AFText.Eyebrow in MoveLabel), the date (AFText.Date), and a trailing 32.dp avatar; the “Summary” large title collapses with a TopAppBar + nested scroll. The Fitness+ shelf is a LazyRow of FitnessPlusCards with snapFlingBehavior.

5. Motion

Apple Fitness motion: the rings carry the energy; chrome stays calm. 200–1000ms ease-out.

Moment Compose recipe
Rings sweep from 0 three Animatable 0→value tween(1000, LinearOutSlowInEasing), staggered 80ms (Move→Exercise→Stand)
Ring close particle burst (a Canvas/InfiniteTransition emitter in the ring color) + success haptic at progress ≥ 1
All rings closed larger celebration + stronger haptic
Card press Modifier.scale via animateFloatAsState(spring(dampingRatio = 0.7f)) 1→0.97 on press
Tab switch Crossfade(tween(200)); glyph tint tween 150ms
Fitness+ shelf LazyRow + rememberSnapFlingBehavior; parallax via lazyListState item offset
Sheet present ModalBottomSheet slide-up (Material default ~300ms) + scrim
Live HUD (in-workout) rings animateFloatAsState(tween(300)) as metrics change
Award flip graphicsLayer { rotationY } animateFloatAsState(tween(500))
// Canonical Apple Fitness motion — staggered ring sweep
LaunchedEffect(Unit) {
    val spec = tween<Float>(1000, easing = LinearOutSlowInEasing)
    launch { move.animateTo(targetMove, spec) }
    launch { delay(80);  exercise.animateTo(targetExercise, spec) }
    launch { delay(160); stand.animateTo(targetStand, spec) }
}

Haptics: LocalHapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove) (selection analog) on tab/segment change; a success pattern on ring close / all-rings-closed; HapticFeedbackType.LongPress (medium impact analog) on Start Workout. Reduce motion replaces the sweep with an instant final value via Crossfade.

6. Icons

Apple Fitness uses SF Symbols; the closest first-party set is androidx.compose.material:material-icons-extended.

Purpose SF Symbol (iOS) Material Icon (Compose)
Summary (tab) circle.circle.fill Icons.Filled.RadioButtonChecked
Fitness+ (tab) play.rectangle.fill Icons.Filled.PlayCircleFilled
Sharing (tab) person.2.fill Icons.Filled.Group
Steps figure.walk Icons.Filled.DirectionsWalk
Distance (curved path) Icons.Filled.Straighten
Heart Rate heart.fill Icons.Filled.Favorite
Workouts figure.run Icons.Filled.DirectionsRun
Play (card) play.fill Icons.Filled.PlayArrow
Move detail flame.fill Icons.Filled.LocalFireDepartment
Exercise detail figure.run Icons.Filled.DirectionsRun
Stand detail figure.stand Icons.Filled.Accessibility
Award medal.fill Icons.Filled.EmojiEvents
Share square.and.arrow.up Icons.Filled.IosShare
See All chevron.right Icons.Filled.ChevronRight
Trends up / down arrow.up.right / arrow.down.right Icons.Filled.TrendingUp / Icons.Filled.TrendingDown

7. Minimum SDK & Accessibility Notes

  • minSdk 24 (Compose floor is 21; Canvas rings + modern motion comfortable at 24). compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+.
  • Edge-to-edge: call enableEdgeToEdge(); true-black canvas wants light-content system bars in dark mode. The bottom bar respects the navigation inset; the in-workout HUD must respect all insets over video.
  • Tabular numerals: every numeric TextStyle sets fontFeatureSettings = "tnum" so the ring legend, metric tiles, and live HUD don’t jitter.
  • No live blur on Android: Apple’s frosted badges/play buttons and blurred tab bar are approximated with translucent scrims (Color.Black.copy(alpha = 0.40f) / Color.White.copy(alpha = 0.22f)) or a RenderEffect.createBlurEffect on API 31+; provide the scrim fallback below 31 and when reduce-transparency is requested.
  • Font scaling: sp honors the user’s font scale — keep it on titles/body/footnotes. Pin layout-sensitive text (ring legend numerals, in-workout HUD, 10sp tab labels, frosted badges) via dp or a fixed-density CompositionLocalProvider; ring size scales with container width, not font scale.
  • TalkBack: announce rings via Modifier.semantics(mergeDescendants = true) { contentDescription = "Move ring, 486 of 620 kilocalories, 78 percent" }; metric tiles “Steps, 8214, average 7540”; Fitness+ cards “20 minute HIIT with Bakari, Pop Anthems playlist”.
  • Color is never the only signal: ring legend rows carry the text label + value beside the colored ring; rings are also distinguishable by radius/position; trend arrows pair color with an up/down glyph.
  • Touch targets: Material guidance is 48.dp — metric tiles full-tile tappable (≥ 72.dp), Fitness+ cards full-card (≥ 280.dp), buttons ≥ 48.dp, tab icons 24.dp glyph in the 56.dp bar.
  • Contrast: white on #000000 is maximal; #FF375F on #000000 passes WCAG AA for the accent; keep ring labels at the brightened variants for text contrast (raw #FA114F text on black is borderline).
  • Reduce motion: when Settings.Global.ANIMATOR_DURATION_SCALE == 0, set the rings to their final value via Crossfade, drop the particle burst (static “closed” check), no card scale, no shelf parallax, awards crossfade instead of 3D flip.
  • Dark mode: dark is the default (phone); surfaces step #000000 → #1C1C1E → #2C2C2E; light is allowed on tablet via the M3 light scheme. The three ring colors and their 22%-opacity tracks NEVER change between modes — they are the brand. Do not enable Material You dynamicColorScheme() — the rings must never re-theme to the wallpaper.
All mobile designs →

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