coderef

The League

The League's iOS app is a black-tie members' club rendered as software. Open it and the entire screen is a near-true-black canvas (#0A0A0A) — the color of a private club after dark, a velvet rope, a leather-bound member directory.
DownloadRaw
The League

Build with the The League look.

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

Palette

At a glance

Theme
dark
Typefaces
Sans, Cormorant Garamond
Sections
7
Length
4,340 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · The League

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

The League (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports The League’s visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, paste-ready @Composables, the embossed prospect card + concierge note, 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 (The League’s near-true-black canvas, the single rationed gold, the no-shadow hairline system, the serif/sans split, the calling-card prospect) while making everything idiomatic Android — NavigationBar instead of a UITabBar, BorderStroke instead of a shadow, sp/dp instead of pt.

Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, and Coil 2.6+ for prospect photos. The League’s palette is fixed black + one gold — no Palette extraction. The League is dark-native: there is no light scheme — force the dark aesthetic regardless of system setting.

1. Color Tokens

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

object LeagueColors {
    // Canvas & Surfaces (dark-native — the only theme)
    val Canvas   = Color(0xFF0A0A0A) // near-true black — the club after dark
    val Surface1 = Color(0xFF141414)
    val Surface2 = Color(0xFF1C1A17)
    val CardBase = Color(0xFF100F0C)
    val Hairline = Color(0xFF2A2620) // the 1px gold-tinted border — does ALL depth work

    // Text
    val TextPrimary   = Color(0xFFEDE9E2) // warm off-white — never pure white
    val TextSecondary = Color(0xFF9E988C)
    val TextTertiary  = Color(0xFF6B665C)
    val OnGold        = Color(0xFF1A1408)

    // Accent — the SINGLE accent is gold
    val Gold       = Color(0xFFC8A35A)
    val GoldBright = Color(0xFFDBBA71)
    val GoldDeep   = Color(0xFFA8863F)
    val Champagne  = Color(0xFFE8DCC0) // premium / celebration only

    // Semantic
    val Success = Color(0xFF6FAE8A) // muted sage
    val Error   = Color(0xFFC77A7A) // dusty rose
}

Wire it into a dark-only scheme. The League has no light mode in spirit — force dark.

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

private val LeagueDark = darkColorScheme(
    primary        = LeagueColors.Gold,
    onPrimary      = LeagueColors.OnGold,
    background     = LeagueColors.Canvas,
    onBackground   = LeagueColors.TextPrimary,
    surface        = LeagueColors.Surface1,
    onSurface      = LeagueColors.TextPrimary,
    surfaceVariant = LeagueColors.Surface2,
    outline        = LeagueColors.Hairline,
    error          = LeagueColors.Error,
)

@Composable
fun LeagueTheme(content: @Composable () -> Unit) =
    MaterialTheme(
        colorScheme = LeagueDark, // always dark — never a light variant
        typography  = LeagueTypography,
        content     = content,
    )

2. Typography

The League pairs a high-contrast display serif (Cormorant Garamond) with a clean geometric sans (Jost) — both SIL OFL, drop the TTFs in res/font/. The serif/sans split IS the hierarchy: serif = the person & institution, sans = the machine & metadata. Never collapse to one family.

// ui/theme/LeagueType.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.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp

val Cormorant = FontFamily(
    Font(R.font.cormorant_regular,  FontWeight.Normal),
    Font(R.font.cormorant_medium,   FontWeight.Medium),
    Font(R.font.cormorant_semibold, FontWeight.SemiBold),
    Font(R.font.cormorant_bold,     FontWeight.Bold),
    Font(R.font.cormorant_medium_italic, FontWeight.Medium, FontStyle.Italic),
)
val Jost = FontFamily(
    Font(R.font.jost_regular,  FontWeight.Normal),
    Font(R.font.jost_medium,   FontWeight.Medium),
    Font(R.font.jost_semibold, FontWeight.SemiBold),
)

// Named ramp — mirrors DESIGN.md §3 exactly (pt → sp 1:1)
object LeagueText {
    val Wordmark    = TextStyle(Cormorant, fontWeight = FontWeight.Bold,     fontSize = 34.sp, letterSpacing = 0.2.sp)
    val Name        = TextStyle(Cormorant, fontWeight = FontWeight.Bold,     fontSize = 26.sp, letterSpacing = 0.2.sp)
    val AgeInline   = TextStyle(Cormorant, fontWeight = FontWeight.Normal,   fontSize = 26.sp)
    val ScreenTitle = TextStyle(Cormorant, fontWeight = FontWeight.SemiBold, fontSize = 21.sp, letterSpacing = 0.2.sp)
    val Concierge   = TextStyle(Cormorant, fontWeight = FontWeight.Medium,   fontSize = 16.sp, fontStyle = FontStyle.Italic, lineHeight = 26.sp)
    val Stat        = TextStyle(Cormorant, fontWeight = FontWeight.SemiBold, fontSize = 21.sp)
    val Subtitle    = TextStyle(Jost, fontWeight = FontWeight.Medium,   fontSize = 16.sp, letterSpacing = 0.3.sp)
    val Body        = TextStyle(Jost, fontWeight = FontWeight.Normal,   fontSize = 15.sp, lineHeight = 24.sp)
    val Credential  = TextStyle(Jost, fontWeight = FontWeight.Normal,   fontSize = 13.sp, lineHeight = 22.sp)
    val Eyebrow     = TextStyle(Jost, fontWeight = FontWeight.SemiBold, fontSize = 11.sp, letterSpacing = 2.sp)
    val Button      = TextStyle(Jost, fontWeight = FontWeight.SemiBold, fontSize = 13.sp, letterSpacing = 1.6.sp)
    val Caption     = TextStyle(Jost, fontWeight = FontWeight.Normal,   fontSize = 12.sp)
    val Tab         = TextStyle(Jost, fontWeight = FontWeight.Medium,   fontSize = 9.sp,  letterSpacing = 1.sp)
}

// Map onto Material 3 slots so stock components inherit the brand
val LeagueTypography = Typography(
    headlineLarge = LeagueText.Name,
    headlineMedium = LeagueText.ScreenTitle,
    titleMedium   = LeagueText.Subtitle,
    bodyMedium    = LeagueText.Body,
    labelSmall    = LeagueText.Tab,
)

Compose has no automatic text-transform. For uppercase eyebrows / buttons / tabs, call .uppercase() on the string — never bake casing into the font.

3. Signature Components

Prospect Card (the embossed calling card)

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star
import androidx.compose.runtime.Composable
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.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage

@Composable
fun ProspectCard(
    photoUrl: String,
    name: String,
    age: Int,
    title: String,     // "VP Strategy · Goldman Sachs"
    school: String,    // "Harvard Business School"
    location: String,  // "2 mi · Tribeca, New York"
    verified: Boolean,
    modifier: Modifier = Modifier,
) {
    Card(
        modifier = modifier.fillMaxWidth(),
        shape = RoundedCornerShape(6.dp),
        colors = CardDefaults.cardColors(containerColor = LeagueColors.CardBase),
        border = BorderStroke(1.dp, LeagueColors.Hairline), // NO elevation — depth is the hairline
        elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
    ) {
        Box {
            AsyncImage(
                model = photoUrl,
                contentDescription = "$name, $age",
                modifier = Modifier.fillMaxWidth().height(280.dp),
                contentScale = ContentScale.Crop,
            )
            if (verified) {
                Box(
                    Modifier
                        .align(Alignment.TopEnd).padding(14.dp)
                        .size(30.dp).clip(RoundedCornerShape(50))
                        .background(Color.Black.copy(alpha = 0.6f))
                        .border(1.dp, LeagueColors.Gold, RoundedCornerShape(50)),
                    contentAlignment = Alignment.Center,
                ) { Icon(Icons.Filled.Star, null, tint = LeagueColors.Gold, modifier = Modifier.size(15.dp)) }
            }
        }
        Column(Modifier.padding(horizontal = 18.dp, vertical = 14.dp)) {
            Row(verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
                Text(name, style = LeagueText.Name, color = LeagueColors.TextPrimary)
                Text("$age", style = LeagueText.AgeInline, color = LeagueColors.TextSecondary)
            }
            Box(Modifier.padding(vertical = 8.dp).width(28.dp).height(1.dp).background(LeagueColors.Gold))
            Credential(title)
            Credential(school)
            Credential(location)
        }
    }
}

@Composable
private fun Credential(text: String) {
    Row(Modifier.padding(vertical = 2.dp), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
        Text("◦", style = LeagueText.Credential, color = LeagueColors.Gold)
        Text(text, style = LeagueText.Credential, color = LeagueColors.TextSecondary, modifier = Modifier.weight(1f))
    }
}

Card Action Row

import androidx.compose.foundation.clickable
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.ChatBubbleOutline
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback

@Composable
fun CardActions(onPass: () -> Unit, onHeart: () -> Unit, onMessage: () -> Unit) {
    Row(
        Modifier.fillMaxWidth().padding(horizontal = 18.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically,
    ) {
        CircleAction(Icons.Filled.Close, 48.dp, LeagueColors.TextSecondary, LeagueColors.Hairline, onPass)
        // Heart is the focal action — gold OUTLINE, not a fill
        CircleAction(Icons.Outlined.FavoriteBorder, 56.dp, LeagueColors.Gold, LeagueColors.Gold, onHeart)
        CircleAction(Icons.Outlined.ChatBubbleOutline, 48.dp, LeagueColors.Gold, LeagueColors.Hairline, onMessage)
    }
}

@Composable
private fun CircleAction(icon: ImageVector, size: Dp, tint: Color, border: Color, onClick: () -> Unit) {
    val haptics = LocalHapticFeedback.current
    Box(
        Modifier
            .size(size).clip(RoundedCornerShape(50))
            .background(LeagueColors.Surface1)
            .border(1.dp, border, RoundedCornerShape(50))
            .clickable {
                haptics.performHapticFeedback(HapticFeedbackType.LongPress) // soft analog
                onClick()
            },
        contentAlignment = Alignment.Center,
    ) { Icon(icon, contentDescription = null, tint = tint, modifier = Modifier.size(size * 0.4f)) }
}

Concierge Note

import androidx.compose.animation.core.*
import androidx.compose.runtime.*
import androidx.compose.ui.draw.scale

@Composable
fun ConciergeNote(message: String, onIntroduce: (() -> Unit)? = null) {
    val ruleScale = remember { Animatable(0f) }
    val textAlpha = remember { Animatable(0f) }
    LaunchedEffect(Unit) {
        ruleScale.animateTo(1f, tween(200))
        textAlpha.animateTo(1f, tween(180))
    }

    Row(
        Modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(4.dp))
            .background(LeagueColors.Surface2)
            .border(1.dp, LeagueColors.Hairline, RoundedCornerShape(4.dp)),
    ) {
        Box(
            Modifier
                .width(2.dp).fillMaxHeight()
                .graphicsLayer { scaleY = ruleScale.value; transformOrigin = TransformOrigin(0f, 0f) }
                .background(LeagueColors.Gold)
        )
        Column(Modifier.padding(18.dp).graphicsLayer { alpha = textAlpha.value }) {
            Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
                Icon(Icons.Filled.Star, null, tint = LeagueColors.Gold, modifier = Modifier.size(12.dp))
                Text("YOUR CONCIERGE", style = LeagueText.Eyebrow, color = LeagueColors.Gold)
            }
            Spacer(Modifier.height(8.dp))
            Text(message, style = LeagueText.Concierge, color = LeagueColors.TextPrimary)
            if (onIntroduce != null) {
                Spacer(Modifier.height(12.dp))
                Text(
                    "MAKE THE INTRODUCTION →",
                    style = LeagueText.Button, color = LeagueColors.Gold,
                    modifier = Modifier.clickable { onIntroduce() },
                )
            }
        }
    }
}

Batch Banner

@Composable
fun BatchBanner(count: Int) {
    Row(
        Modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(4.dp))
            .background(LeagueColors.Surface1)
            .border(1.dp, LeagueColors.Hairline, RoundedCornerShape(4.dp))
            .padding(horizontal = 14.dp, vertical = 10.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Text("TODAY'S BATCH", style = LeagueText.Eyebrow, color = LeagueColors.TextSecondary)
        Text("$count Prospects", style = LeagueText.Stat, color = LeagueColors.Gold)
    }
}

Buttons

import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.OutlinedButton

@Composable
fun LeaguePrimaryButton(title: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
    Button(
        onClick = onClick,
        modifier = modifier.fillMaxWidth(),
        shape = RoundedCornerShape(3.dp), // sharp engraved-plate geometry
        colors = ButtonDefaults.buttonColors(
            containerColor = LeagueColors.Gold,
            contentColor = LeagueColors.OnGold,
        ),
        contentPadding = PaddingValues(vertical = 15.dp),
    ) { Text(title.uppercase(), style = LeagueText.Button) }
}

@Composable
fun LeagueOutlineButton(title: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
    OutlinedButton(
        onClick = onClick,
        modifier = modifier.fillMaxWidth(),
        shape = RoundedCornerShape(3.dp),
        border = BorderStroke(1.dp, LeagueColors.Gold),
    ) { Text(title.uppercase(), style = LeagueText.Button, color = LeagueColors.Gold) }
}

4. Navigation

The League has minimal chrome: a 4-tab bottom strip and a centered serif wordmark top bar. On Android, model the strip as a NavigationBar. There is no Material tint pill — the gold tint alone signals selection, and the top “border” is a 1dp hairline, never a shadow.

@Composable
fun LeagueBottomBar(selected: Int, onSelect: (Int) -> Unit) {
    Column {
        // 1dp hairline instead of a NavigationBar shadow
        Box(Modifier.fillMaxWidth().height(1.dp).background(LeagueColors.Hairline))
        NavigationBar(
            containerColor = LeagueColors.Canvas, // opaque black — no blur
            tonalElevation = 0.dp,
        ) {
            val items = listOf(
                "Prospects" to Icons.Outlined.ViewAgenda,
                "Matches"   to Icons.Outlined.FavoriteBorder,
                "Concierge" to Icons.Outlined.ChatBubbleOutline,
                "Profile"   to Icons.Outlined.PersonOutline,
            )
            items.forEachIndexed { i, (label, icon) ->
                NavigationBarItem(
                    selected = selected == i,
                    onClick = { onSelect(i) },
                    icon = { Icon(icon, contentDescription = label, modifier = Modifier.size(21.dp)) },
                    label = { Text(label.uppercase(), style = LeagueText.Tab) },
                    colors = NavigationBarItemDefaults.colors(
                        selectedIconColor = LeagueColors.Gold,
                        selectedTextColor = LeagueColors.Gold,
                        unselectedIconColor = LeagueColors.TextTertiary,
                        unselectedTextColor = LeagueColors.TextTertiary,
                        indicatorColor = Color.Transparent, // The League has no Material pill
                    ),
                )
            }
        }
    }
}

5. Motion

The League’s motion is restrained — fades and draws, never a thrown swipe or confetti. Celebrations are a drawn wax seal.

Moment Compose recipe
Card advance current card animateFloatAsState alpha 1f → 0f + offset y 0 → -12.dp, tween(280, easing = EaseOut); next Crossfade in
Heart send outline Icon swaps to filled + scale 1f → 1.12f → 1f via animateFloatAsState tween(120) legs; single soft haptic
Concierge arrival gold rule graphicsLayer { scaleY } 0f → 1f tween(200), then text alpha 0f → 1f tween(180)
Batch count AnimatedContent(targetState = count, transitionSpec = { fadeIn() togetherWith fadeOut() }) (restrained — no odometer)
Match celebration wax-seal Canvas drawArc/Path with animated PathEffect/trim 0f → 1f tween(600); NO confetti
Tab switch instant; tint cross-fades via NavigationBarItemDefaults (no shared-axis)
Field focus bottom border color animateColorAsState Transparent → Gold tween(160)
Page push Nav3/NavHost slide push tween(300); backgrounds stay true-black for seamlessness
// Concierge rule-draw — the canonical League motion
val ruleScale = remember { Animatable(0f) }
LaunchedEffect(Unit) { ruleScale.animateTo(1f, tween(200, easing = EaseOut)) }
Box(
    Modifier
        .width(2.dp).fillMaxHeight()
        .graphicsLayer { scaleY = ruleScale.value; transformOrigin = TransformOrigin(0f, 0f) }
        .background(LeagueColors.Gold)
)

Haptics: prefer LocalHapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) for the single soft impact on heart-send and introduction-accepted. For the lighter tab tick use LocalView.current.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK). Nothing buzzy — restraint extends to touch; never use a long/heavy vibration.

6. Icons

The League’s iconography is fine-line, never filled (except the gold star seal). The closest first-party set is androidx.compose.material:material-icons-extended — prefer the Outlined variants.

Purpose SF Symbol (iOS) Material Icon (Compose)
Prospects (tab) rectangle.portrait.on.rectangle.portrait Icons.Outlined.ViewAgenda
Matches (tab) heart Icons.Outlined.FavoriteBorder
Concierge (tab) bubble.left Icons.Outlined.ChatBubbleOutline
Profile (tab) person Icons.Outlined.PersonOutline
Heart (card action) heart Icons.Outlined.FavoriteBorder
Pass (card action) xmark Icons.Filled.Close
Message (card action) bubble.left Icons.Outlined.ChatBubbleOutline
Verification seal star.fill Icons.Filled.Star (gold)
Concierge mark star.fill Icons.Filled.Star (gold, 12dp)
Search magnifyingglass Icons.Outlined.Search
Settings gearshape Icons.Outlined.Settings
Membership / premium crown Icons.Outlined.WorkspacePremium
Back chevron.left Icons.AutoMirrored.Filled.ArrowBack
Credential mark circle text ◦ (not an icon)

7. Minimum SDK & Accessibility Notes

  • minSdk 24 (Compose floor is 21; nothing here needs a higher API — no blur, no shadow elevation). compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+.
  • Dark-native: The League has no light mode. Always pass LeagueDark; never branch on isSystemInDarkTheme(). enableEdgeToEdge() with light system-bar content over the near-true-black canvas.
  • No shadows: never set elevation/shadow — separation is the 1dp Hairline BorderStroke plus the tonal surface lift (#141414 vs #0A0A0A). A drop shadow breaks the engraved, black-tie identity.
  • No gradients, no blur: surfaces are flat; never reach for Brush gradients or Modifier.blur — depth is line, not light.
  • Font scaling: sp honors the user’s font scale — keep it on serif names, screen titles, body, credentials, concierge notes. Pin layout-sensitive text (11sp eyebrows, 13sp button labels, 9sp tab labels, the batch count) by deriving from dp or fixing fontScale = 1f via a local Density.
  • Uppercase: call String.uppercase() for eyebrows / buttons / tabs — never bake casing into the font; TalkBack should still read the natural-case word.
  • Serif/sans split: the split is the hierarchy — never substitute Jost for a name or Cormorant for a tab label, even under font-loading failure (provide a serif and a sans system fallback respectively).
  • TalkBack: the prospect card is one focusable node announcing “{name}, {age}, {title}, {school}, {location}, verified member”; card actions get contentDescriptions (“Pass”, “Send heart”, “Message”); the concierge note announces “Concierge: {message}”; the batch banner announces “{count} prospects in today’s batch”. Ensure the gold hairline is never the sole carrier of meaning — pair structural borders with text.
  • Touch targets: Material guidance is 48.dp — the Heart action is 56.dp (good); Pass / Message are 48.dp (good); pad the 21.dp tab icons to a 48.dp hit; the concierge inline CTA ≥ 48.dp row.
  • Contrast: #EDE9E2 on #0A0A0A far exceeds WCAG AA; #C8A35A on #0A0A0A passes AA for gold UI text; the CTA (#1A1408 on #C8A35A) passes AA at 13sp bold — re-check if you tweak the gold.
  • Reduce motion: when Settings.Global.ANIMATOR_DURATION_SCALE == 0, replace the card drift with an instant Crossfade, show the concierge rule + text immediately, and render the completed wax seal without the draw — keep the success haptic.
  • No dynamic color: do not enable Material You dynamicColorScheme() — The League’s black-and-single-gold identity must hold regardless of wallpaper; there is no second accent to harmonize, and a tinted scheme would destroy the engraved members-club aesthetic.
All mobile designs →

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