coderef

Fidelity

Fidelity's iOS app is institutional calm rendered as software. It is the home screen of one of the largest brokerages in the world, and the design carries that weight: the interface is dense with numbers, but the chrome around those…
DownloadRaw
Fidelity

Build with the Fidelity look.

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

Palette

At a glance

Theme
dark
Typefaces
Inter
Sections
7
Length
4,158 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · Fidelity

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

Fidelity (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports Fidelity’s visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, paste-ready @Composables, the portfolio→quote spine, 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 (Fidelity’s calm institutional surfaces, the green brand action, sacred gain/loss color, tabular money, the balance-hero + sparkline + sticky Trade button) while making everything idiomatic Android — NavigationBar instead of a UITabBar, Modifier.drawBehind/Canvas for sparklines, dp/sp instead of pt.

Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, and Coil 2.6+ for any imagery. No color extraction — Fidelity’s palette is a fixed institutional set. Fidelity is light-first but ships a full evergreen dark scheme.

1. Color Tokens

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

object FidelityColors {
    // Brand
    val Green        = Color(0xFF368727)
    val GreenPressed = Color(0xFF2A6B1F)
    val Heritage     = Color(0xFF00754A)
    val Leaf         = Color(0xFF4FB23B)

    // Canvas & Surfaces (Light)
    val Canvas         = Color(0xFFFFFFFF)
    val Surface        = Color(0xFFF4F6F4)
    val SurfacePressed = Color(0xFFE8ECE8)
    val Divider        = Color(0xFFE1E5E1)

    // Canvas & Surfaces (Dark)
    val DarkCanvas   = Color(0xFF0E1411) // evergreen — NOT pure black
    val DarkSurface1 = Color(0xFF16201B)
    val DarkSurface2 = Color(0xFF1F2C24)
    val DarkDivider  = Color(0xFF26342B)

    // Text
    val TextPrimary       = Color(0xFF1A1F1B)
    val TextSecondary     = Color(0xFF5A655C)
    val TextTertiary      = Color(0xFF8A938C)
    val DarkTextPrimary   = Color(0xFFE8EFE9)
    val DarkTextSecondary = Color(0xFF9DB0A2)
    val DarkTextTertiary  = Color(0xFF6B7E72)

    // Financial semantics (sacred — gain/loss only)
    val GainLight = Color(0xFF15833E)
    val GainDark  = Color(0xFF15B374)
    val LossLight = Color(0xFFD32F2F)
    val LossDark  = Color(0xFFE5544B)

    // System / accent
    val Warning = Color(0xFFC8862B)
    val Info    = Color(0xFF1E6FB8)
    val Gold    = Color(0xFFC8A24B)
}

/** Resolve a change value to its semantic color (flat -> secondary text). */
fun changeColor(change: Double, dark: Boolean): Color = when {
    change > 0 -> if (dark) FidelityColors.GainDark else FidelityColors.GainLight
    change < 0 -> if (dark) FidelityColors.LossDark else FidelityColors.LossLight
    else       -> if (dark) FidelityColors.DarkTextSecondary else FidelityColors.TextSecondary
}

Wire it into both schemes. Fidelity is light-first (paper-calm); the dark scheme uses the signature #0E1411 evergreen, never true black. Do not enable Material You dynamicColorScheme() — the institutional green identity must hold regardless of wallpaper.

// ui/theme/Theme.kt
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.*
import androidx.compose.runtime.Composable

private val FidLight = lightColorScheme(
    primary        = FidelityColors.Green,
    onPrimary      = FidelityColors.Canvas,
    secondary      = FidelityColors.Heritage,
    background     = FidelityColors.Canvas,
    onBackground   = FidelityColors.TextPrimary,
    surface        = FidelityColors.Surface,
    onSurface      = FidelityColors.TextPrimary,
    surfaceVariant = FidelityColors.SurfacePressed,
    outline        = FidelityColors.Divider,
    error          = FidelityColors.LossLight,
)

private val FidDark = darkColorScheme(
    primary        = FidelityColors.Green,
    onPrimary      = FidelityColors.Canvas,
    secondary      = FidelityColors.Leaf,
    background     = FidelityColors.DarkCanvas,
    onBackground   = FidelityColors.DarkTextPrimary,
    surface        = FidelityColors.DarkSurface1,
    onSurface      = FidelityColors.DarkTextPrimary,
    surfaceVariant = FidelityColors.DarkSurface2,
    outline        = FidelityColors.DarkDivider,
    error          = FidelityColors.LossDark,
)

@Composable
fun FidelityTheme(
    dark: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit,
) = MaterialTheme(
    colorScheme = if (dark) FidDark else FidLight,
    typography = FidelityTypography,
    content = content,
)

2. Typography

Fidelity’s brand face is “Average Sans”; bundle Inter (SIL OFL) in res/font/ as the closest free match. The defining rule: every monetary figure carries tabular figures via TextStyle(fontFeatureSettings = "tnum").

// ui/theme/FidelityType.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),
)

// Apply to ANY currency / price / % / quantity
private const val TNUM = "tnum"

object FidText {
    val BalanceHero = TextStyle(Inter, fontWeight = FontWeight.ExtraBold, fontSize = 34.sp, lineHeight = 38.sp, letterSpacing = (-0.6).sp, fontFeatureSettings = TNUM)
    val ScreenTitle = TextStyle(Inter, fontWeight = FontWeight.Bold,      fontSize = 26.sp, lineHeight = 31.sp, letterSpacing = (-0.3).sp)
    val Section     = TextStyle(Inter, fontWeight = FontWeight.Bold,      fontSize = 22.sp, lineHeight = 28.sp, letterSpacing = (-0.2).sp)
    val CardTitle   = TextStyle(Inter, fontWeight = FontWeight.Bold,      fontSize = 18.sp, lineHeight = 23.sp)
    val Body        = TextStyle(Inter, fontWeight = FontWeight.Normal,    fontSize = 16.sp, lineHeight = 24.sp)
    val RowValue    = TextStyle(Inter, fontWeight = FontWeight.Bold,      fontSize = 16.sp, lineHeight = 21.sp, fontFeatureSettings = TNUM)
    val RowSymbol   = TextStyle(Inter, fontWeight = FontWeight.SemiBold,  fontSize = 15.sp, lineHeight = 20.sp)
    val Meta        = TextStyle(Inter, fontWeight = FontWeight.Normal,    fontSize = 14.sp, lineHeight = 19.sp)
    val NumericMono = TextStyle(Inter, fontWeight = FontWeight.SemiBold,  fontSize = 13.sp, lineHeight = 17.sp, fontFeatureSettings = TNUM)
    val Caption     = TextStyle(Inter, fontWeight = FontWeight.Medium,    fontSize = 12.sp, lineHeight = 16.sp)
    val Button      = TextStyle(Inter, fontWeight = FontWeight.Bold,      fontSize = 16.sp, lineHeight = 16.sp)
    val Tab         = TextStyle(Inter, fontWeight = FontWeight.SemiBold,  fontSize = 10.sp, lineHeight = 10.sp, letterSpacing = 0.1.sp)
    val RangeTab    = TextStyle(Inter, fontWeight = FontWeight.SemiBold,  fontSize = 11.sp, lineHeight = 11.sp, letterSpacing = 0.2.sp)
}

val FidelityTypography = Typography(
    headlineLarge  = FidText.BalanceHero,
    headlineMedium = FidText.ScreenTitle,
    titleMedium    = FidText.CardTitle,
    bodyMedium     = FidText.Body,
    labelSmall     = FidText.Tab,
)

3. Signature Components

Balance Header (Compose) — the screen hero

import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun BalanceHeader(
    greeting: String, title: String, value: String,
    changeAmount: String, changePercent: String, change: Double,
    dark: Boolean,
) {
    val primary = if (dark) FidelityColors.DarkTextPrimary else FidelityColors.TextPrimary
    val secondary = if (dark) FidelityColors.DarkTextSecondary else FidelityColors.TextSecondary
    val c = changeColor(change, dark)

    Column(Modifier.fillMaxWidth().padding(horizontal = 20.dp).padding(bottom = 16.dp)) {
        Text(greeting, style = FidText.Caption, color = secondary)
        Text(title, style = FidText.ScreenTitle, color = primary, modifier = Modifier.padding(top = 2.dp))
        Text("Total account value", style = FidText.Caption, color = secondary, modifier = Modifier.padding(top = 18.dp))
        Text(value, style = FidText.BalanceHero, color = primary, modifier = Modifier.padding(top = 4.dp))
        Row(Modifier.padding(top = 8.dp)) {
            Text(if (change >= 0) "▲ " else "▼ ", color = c, style = FidText.NumericMono)
            Text("$changeAmount ", style = FidText.NumericMono, color = c)
            Text("($changePercent) today", style = FidText.NumericMono, color = secondary)
        }
    }
}

Holding / Watchlist Row

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow

@Composable
fun HoldingRow(
    symbol: String, name: String, value: String,
    changePct: String, change: Double, isBrandFund: Boolean = false, dark: Boolean,
) {
    val primary = if (dark) FidelityColors.DarkTextPrimary else FidelityColors.TextPrimary
    val secondary = if (dark) FidelityColors.DarkTextSecondary else FidelityColors.TextSecondary
    val chipBg = if (isBrandFund) FidelityColors.Green.copy(alpha = 0.16f)
                 else if (dark) FidelityColors.DarkSurface2 else FidelityColors.Surface

    Column {
        Row(
            Modifier.fillMaxWidth().height(64.dp).clickable {}
                .padding(horizontal = 20.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(12.dp),
        ) {
            Box(
                Modifier.size(38.dp).clip(RoundedCornerShape(10.dp)).background(chipBg),
                contentAlignment = Alignment.Center,
            ) {
                Text(symbol, style = FidText.Caption.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.ExtraBold),
                     color = if (isBrandFund) FidelityColors.Leaf else primary)
            }
            Column(Modifier.weight(1f)) {
                Text(symbol, style = FidText.RowSymbol, color = primary)
                Text(name, style = FidText.Caption, color = secondary, maxLines = 1, overflow = TextOverflow.Ellipsis)
            }
            Column(horizontalAlignment = Alignment.End) {
                Text(value, style = FidText.RowValue, color = primary)
                Text(changePct, style = FidText.NumericMono, color = changeColor(change, dark))
            }
        }
        Box(Modifier.fillMaxWidth().height(0.5.dp)
            .background(if (dark) FidelityColors.DarkDivider else FidelityColors.Divider))
    }
}

Sparkline (Canvas) + Range Tabs

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.*
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.drawscope.Stroke

@Composable
fun Sparkline(points: List<Float>, isUp: Boolean, modifier: Modifier = Modifier) {
    val stroke = if (isUp) FidelityColors.Leaf else FidelityColors.LossDark
    Canvas(modifier.fillMaxWidth().height(64.dp)) {
        val step = size.width / (points.size - 1).coerceAtLeast(1)
        val line = Path().apply {
            points.forEachIndexed { i, v ->
                val x = i * step; val y = size.height - v * size.height
                if (i == 0) moveTo(x, y) else lineTo(x, y)
            }
        }
        val area = Path().apply {
            addPath(line); lineTo(size.width, size.height); lineTo(0f, size.height); close()
        }
        drawPath(area, Brush.verticalGradient(listOf(stroke.copy(alpha = 0.32f), stroke.copy(alpha = 0f))))
        drawPath(line, stroke, style = Stroke(width = 2.dp.toPx(), join = StrokeJoin.Round))
    }
}

@Composable
fun RangeTabs() {
    var sel by remember { mutableStateOf(0) }
    val haptics = LocalHapticFeedback.current
    val ranges = listOf("1D", "1W", "1M", "1Y", "5Y", "All")
    Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
        ranges.forEachIndexed { i, r ->
            Box(
                Modifier.clip(RoundedCornerShape(50))
                    .background(if (i == sel) FidelityColors.Green else Color.Transparent)
                    .clickable { sel = i; haptics.performHapticFeedback(HapticFeedbackType.TextHandleMove) }
                    .padding(horizontal = 9.dp, vertical = 3.dp),
            ) {
                Text(r, style = FidText.RangeTab, color = if (i == sel) Color.White else FidelityColors.DarkTextSecondary)
            }
        }
    }
}

Sticky Trade Button

import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text

@Composable
fun StickyTradeBar(onTrade: () -> Unit) {
    val haptics = LocalHapticFeedback.current
    Column(
        Modifier.fillMaxWidth()
            .background(FidelityColors.DarkCanvas.copy(alpha = 0.94f))
            .navigationBarsPadding(),
    ) {
        Box(Modifier.fillMaxWidth().height(0.5.dp).background(FidelityColors.DarkDivider))
        Button(
            onClick = { haptics.performHapticFeedback(HapticFeedbackType.LongPress); onTrade() },
            shape = RoundedCornerShape(8.dp),
            colors = ButtonDefaults.buttonColors(containerColor = FidelityColors.Green, contentColor = Color.White),
            modifier = Modifier.fillMaxWidth().height(52.dp).padding(horizontal = 16.dp).padding(top = 12.dp),
        ) { Text("Trade", style = FidText.Button) }
    }
}

Performance Pill

@Composable
fun PerformancePill(kind: String /* gain | loss | flat */, text: String) {
    val (fg, bg, pre) = when (kind) {
        "gain" -> Triple(FidelityColors.GainDark, FidelityColors.GainDark.copy(alpha = 0.16f), "▲ ")
        "loss" -> Triple(FidelityColors.LossDark, FidelityColors.LossDark.copy(alpha = 0.16f), "▼ ")
        else   -> Triple(FidelityColors.DarkTextSecondary, FidelityColors.DarkSurface2, "— ")
    }
    Box(
        Modifier.clip(RoundedCornerShape(8.dp)).background(bg).padding(horizontal = 14.dp, vertical = 8.dp),
    ) { Text("$pre$text", style = FidText.NumericMono, color = fg) }
}

4. Navigation

Fidelity has minimal chrome: a five-tab bottom strip. On Android model it as a NavigationBar; there is no Material tint pill — active is just the green icon/label.

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

@Composable
fun FidelityBottomBar(selected: Int, onSelect: (Int) -> Unit) {
    NavigationBar(containerColor = FidelityColors.DarkCanvas, tonalElevation = 0.dp) {
        val items = listOf(
            "Summary"  to Icons.Filled.Home,
            "Planning" to Icons.Filled.BarChart,
            "Invest"   to Icons.Filled.TrendingUp,
            "News"     to Icons.Filled.Article,
            "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(22.dp)) },
                label = { Text(label, style = FidText.Tab) },
                colors = NavigationBarItemDefaults.colors(
                    selectedIconColor = FidelityColors.Leaf,
                    selectedTextColor = FidelityColors.Leaf,
                    unselectedIconColor = FidelityColors.DarkTextTertiary,
                    unselectedTextColor = FidelityColors.DarkTextTertiary,
                    indicatorColor = Color.Transparent, // no Material pill
                ),
            )
        }
    }
}

The portfolio→quote spine is a NavHost slide push: Summary → Account (Positions/Activity/Balances tabs via TabRow underline) → Position → Quote (chart + range tabs + sticky StickyTradeBar) → Trade ticket (ModalBottomSheet).

5. Motion

Fidelity motion is calm — 120–300ms ease-out, never aggressive on money.

Moment Compose recipe
Price tick animate text color only via animateColorAsState(tween(600)), then settle to primary — digits never roll
Chart range morph re-path Canvas; animate progress with animateFloatAsState(tween(280)) interpolating point arrays
Chart scrub pointerInput drag → vertical crosshair + tabular value bubble, 1:1, no easing
Push navigation NavHost slide tween(300)
Sticky Trade button always present (not animated); top divider visible as content scrolls beneath
Tab switch instant content swap; icon color cross-fade tween(120)
Pull-to-refresh PullToRefreshContainer tinted FidelityColors.Green; light haptic on trigger

Haptics: LocalHapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove) for tab/range change; HapticFeedbackType.LongPress for tap-Trade / Place-order; on order filled use view.performHapticFeedback(HapticFeedbackConstants.CONFIRM).

6. Icons

Closest first-party set is androidx.compose.material:material-icons-extended. For directional gain/loss prefer Unicode triangles ▲ ▼ so the sign survives monochrome rendering.

Purpose SF Symbol (iOS) Material Icon (Compose)
Summary (tab) house / house.fill Icons.Filled.Home
Planning (tab) chart.bar.fill Icons.Filled.BarChart
Invest (tab) chart.line.uptrend.xyaxis Icons.Filled.TrendingUp
News (tab) newspaper.fill Icons.Filled.Article
Profile (tab) person.crop.circle Icons.Filled.AccountCircle
Day change up triangle.fill ▲ (text) / Icons.Filled.ArrowDropUp
Day change down arrowtriangle.down.fill ▼ (text) / Icons.Filled.ArrowDropDown
Watchlist toggle star / star.fill Icons.Outlined.StarBorder / Icons.Filled.Star
Search magnifyingglass Icons.Filled.Search
Back chevron.left Icons.Filled.ArrowBack
Overflow ellipsis Icons.Filled.MoreHoriz
Trade arrow.left.arrow.right Icons.Filled.SwapHoriz
Transfer arrow.down.circle Icons.Filled.ArrowCircleDown
Alerts bell.fill Icons.Filled.Notifications
Disclosure chevron.right Icons.Filled.ChevronRight
Allocation ring chart.pie.fill Icons.Filled.PieChart
Documents doc.text Icons.Filled.Description

7. Minimum SDK & Accessibility Notes

  • minSdk 24, compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+.
  • Edge-to-edge: call enableEdgeToEdge(); light canvas wants dark-content system bars (light-content in dark mode). The sticky Trade bar uses Modifier.navigationBarsPadding(); trade-ticket fields use Modifier.imePadding().
  • Font scaling: sp honors the user’s font scale — keep it on balance hero, titles, body, meta. Pin layout-sensitive text (10sp tab labels, range tabs, chart-axis labels, table headers) via dp-derived sizes or a fixed-fontScale LocalDensity. Tabular figures must stay tabular at every scale (fontFeatureSettings = "tnum").
  • Do not rely on color alone: gain/loss must always carry a +/− sign and ▲/▼ glyph (the components above do). This is a hard accessibility requirement for a finance app.
  • TalkBack: announce a holding row as one node — “Apple, A A P L, 229 dollars 87 cents, up 1.42 percent today”. Use Modifier.semantics(mergeDescendants = true) and a composed contentDescription; never expose color as the only state.
  • Touch targets: Material guidance is 48.dp — list rows are 64.dp (fine); give the range-tab pills and the watchlist star a 48.dp hit area via padding. Primary buttons ≥ 48.dp.
  • Contrast: #15B374/#E5544B on #0E1411 pass WCAG AA at the bold numeric weights used; #368727 with white button text passes AA at 16sp bold. Validate any custom pair.
  • Reduce motion: when Settings.Global.ANIMATOR_DURATION_SCALE == 0, skip the price-tick color animation and the chart morph (set final state instantly); keep the static sign/arrow which conveys direction.
  • Dark mode: invert via the Dark* palette — #0E1411, NOT true black; #1A1F1B text becomes #E8EFE9. Shadows are nearly invisible on the evergreen canvas, so add a 1dp DarkDivider border to cards, the trade-ticket sheet, and the quote panel as the elevation cue. Do not enable dynamicColorScheme() — the institutional green identity must not harmonize with wallpaper.
All mobile designs →

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