coderef

Vercel

Vercel's iOS app is the most uncompromisingly minimal developer tool on the App Store. It opens on pure black (#000000) — not the charcoal #191919 that most "dark mode" apps settle on, but absolute, OLED-true black — and almost everything…
DownloadRaw
Vercel

Build with the Vercel look.

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

Palette

At a glance

Theme
light
Typefaces
Geist Sans, Geist Mono, SF Mono
Sections
7
Length
3,990 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · Vercel

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

Vercel (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports Vercel’s visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, paste-ready @Composables, the deployments list + build-log viewer, 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 (Vercel’s true-black canvas, the deployments list, the status traffic-light, Geist Sans/Mono, hairline borders instead of shadows) while making everything idiomatic Android — NavigationBar instead of a UITabBar, ModalBottomSheet instead of a SwiftUI sheet, sp/dp instead of pt.

Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, and Coil 2.6+ for avatars. No color extraction — Vercel’s palette is fixed and monochrome, so Palette is not needed. Vercel is dark-mode-native; a light scheme is provided as a clean inversion.

1. Color Tokens

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

object VercelColors {
    // Canvas & Surfaces (Dark — default & brand)
    val Canvas   = Color(0xFF000000) // TRUE black — NOT #191919
    val Surface1 = Color(0xFF0A0A0A)
    val Surface2 = Color(0xFF111111)
    val Surface3 = Color(0xFF1A1A1A)
    val Divider  = Color(0xFF2E2E2E)
    val Border   = Color(0xFF333333)

    // Canvas & Surfaces (Light)
    val CanvasLight   = Color(0xFFFFFFFF)
    val SurfaceLight  = Color(0xFFFAFAFA)
    val SurfaceLight2 = Color(0xFFF2F2F2)
    val DividerLight  = Color(0xFFEAEAEA)
    val BorderLight   = Color(0xFFE1E1E1)

    // Text
    val TextPrimary   = Color(0xFFEDEDED)
    val TextSecondary = Color(0xFFA1A1A1)
    val TextTertiary  = Color(0xFF707070)
    val TextOnLight   = Color(0xFF000000)

    // Interactive
    val WhiteFill  = Color(0xFFEDEDED) // primary button fill (dark)
    val WhitePress = Color(0xFFCFCFCF)
    val Blue       = Color(0xFF0070F3)
    val BlueHover  = Color(0xFF3291FF)

    // Status (deployment state — the systemic accent)
    val Ready    = Color(0xFF0CCE6B)
    val Building = Color(0xFFF5A623)
    val ErrorRed = Color(0xFFE5484D)
    val ReadyDim = Color(0xFF0F3D2E)
    val ErrorDim = Color(0xFF3A1416)

    // Highlight (decorative only — avatar gradient / rare marketing)
    val HlPurple = Color(0xFF7928CA)
    val HlCyan   = Color(0xFF50E3C2)
    val HlPink   = Color(0xFFFF0080)
}

Wire it into both schemes. Vercel is dark-first (true black); the light scheme is a clean inversion.

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

private val VercelDark = darkColorScheme(
    primary        = VercelColors.WhiteFill,    // white fill — no brand accent
    onPrimary      = VercelColors.Canvas,
    background     = VercelColors.Canvas,        // TRUE black
    onBackground   = VercelColors.TextPrimary,
    surface        = VercelColors.Surface1,
    onSurface      = VercelColors.TextPrimary,
    surfaceVariant = VercelColors.Surface2,
    outline        = VercelColors.Divider,
    error          = VercelColors.ErrorRed,
)

private val VercelLight = lightColorScheme(
    primary        = VercelColors.TextOnLight,
    onPrimary      = VercelColors.CanvasLight,
    background      = VercelColors.CanvasLight,
    onBackground   = VercelColors.TextOnLight,
    surface        = VercelColors.SurfaceLight,
    onSurface      = VercelColors.TextOnLight,
    surfaceVariant = VercelColors.SurfaceLight2,
    outline        = VercelColors.DividerLight,
    error          = VercelColors.ErrorRed,
)

@Composable
fun VercelTheme(
    dark: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit,
) = MaterialTheme(
    colorScheme = if (dark) VercelDark else VercelLight,
    typography = VercelTypography,
    content = content,
)

2. Typography

Vercel’s type system is Geist Sans (UI prose) + Geist Mono (every URL/branch/commit/env var/log). Both SIL OFL — drop the TTFs in res/font/. The split is by content type, never a user setting. Tight negative tracking on headings.

// ui/theme/VercelType.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 Geist = FontFamily(
    Font(R.font.geist_regular,   FontWeight.Normal),
    Font(R.font.geist_medium,    FontWeight.Medium),
    Font(R.font.geist_semibold,  FontWeight.SemiBold),
    Font(R.font.geist_bold,      FontWeight.Bold),
    Font(R.font.geist_extrabold, FontWeight.ExtraBold),
)
val GeistMono = FontFamily(
    Font(R.font.geist_mono_regular, FontWeight.Normal),
    Font(R.font.geist_mono_medium,  FontWeight.Medium),
)

// Named ramp — mirrors DESIGN.md §3 (pt → sp 1:1)
object VercelText {
    val Display    = TextStyle(Geist, fontWeight = FontWeight.ExtraBold, fontSize = 32.sp, lineHeight = 35.sp, letterSpacing = (-0.6).sp)
    val Title      = TextStyle(Geist, fontWeight = FontWeight.Bold,      fontSize = 26.sp, lineHeight = 30.sp, letterSpacing = (-0.4).sp)
    val Section    = TextStyle(Geist, fontWeight = FontWeight.Bold,      fontSize = 22.sp, lineHeight = 26.sp, letterSpacing = (-0.3).sp)
    val Subsection = TextStyle(Geist, fontWeight = FontWeight.SemiBold,  fontSize = 18.sp, lineHeight = 23.sp, letterSpacing = (-0.2).sp)
    val Body       = TextStyle(Geist, fontWeight = FontWeight.Normal,    fontSize = 16.sp, lineHeight = 24.sp)
    val Label      = TextStyle(Geist, fontWeight = FontWeight.Medium,    fontSize = 15.sp, lineHeight = 20.sp, letterSpacing = (-0.1).sp)
    val BodyStrong = TextStyle(Geist, fontWeight = FontWeight.SemiBold,  fontSize = 14.sp, lineHeight = 19.sp, letterSpacing = (-0.1).sp)
    val Meta       = TextStyle(Geist, fontWeight = FontWeight.Normal,    fontSize = 14.sp, lineHeight = 19.sp)
    val Caption    = TextStyle(Geist, fontWeight = FontWeight.Medium,    fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.2.sp)
    val Tab        = TextStyle(Geist, fontWeight = FontWeight.Medium,    fontSize = 10.sp, lineHeight = 10.sp, letterSpacing = 0.1.sp)
    val Button     = TextStyle(Geist, fontWeight = FontWeight.SemiBold,  fontSize = 14.sp, lineHeight = 14.sp, letterSpacing = (-0.1).sp)
    // Mono
    val MonoUrl    = TextStyle(GeistMono, fontWeight = FontWeight.Normal, fontSize = 13.sp, lineHeight = 18.sp)
    val MonoLog    = TextStyle(GeistMono, fontWeight = FontWeight.Normal, fontSize = 12.sp, lineHeight = 18.sp)
    val MonoTag    = TextStyle(GeistMono, fontWeight = FontWeight.Medium, fontSize = 10.sp, lineHeight = 12.sp)
    val MonoBadge  = TextStyle(GeistMono, fontWeight = FontWeight.Medium, fontSize = 12.sp, lineHeight = 14.sp)
}

// Map onto Material 3 slots so stock components inherit the brand
val VercelTypography = Typography(
    headlineLarge  = VercelText.Display,
    headlineMedium = VercelText.Title,
    titleMedium    = VercelText.Subsection,
    bodyMedium     = VercelText.Body,
    labelSmall     = VercelText.Tab,
)

3. Signature Components

Deployment Card (the spine of the app)

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CallSplit
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
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.unit.dp

enum class DeployState(val color: Color, val label: String) {
    Ready(VercelColors.Ready, "Ready"),
    Building(VercelColors.Building, "Building"),
    Error(VercelColors.ErrorRed, "Error"),
}

@Composable
fun DeploymentCard(
    state: DeployState,
    env: String,           // "Production" / "Preview"
    url: String,
    branch: String,
    relativeTime: String,
    modifier: Modifier = Modifier,
) {
    Column(
        modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(12.dp))
            .background(VercelColors.Surface1)
            .border(1.dp, VercelColors.Divider, RoundedCornerShape(12.dp)) // border, NOT shadow
            .padding(14.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
    ) {
        Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            // 9dp dot + 3dp halo at 15% of the status color
            Box(
                Modifier.size(15.dp)
                    .clip(androidx.compose.foundation.shape.CircleShape)
                    .background(state.color.copy(alpha = 0.15f)),
                contentAlignment = Alignment.Center,
            ) { Box(Modifier.size(9.dp).clip(androidx.compose.foundation.shape.CircleShape).background(state.color)) }
            Text(
                state.label,
                style = VercelText.BodyStrong,
                color = if (state == DeployState.Ready) VercelColors.TextPrimary else state.color,
            )
            Spacer(Modifier.weight(1f))
            EnvTag(env)
        }
        Text(url, style = VercelText.MonoUrl, color = VercelColors.TextPrimary, maxLines = 1)
        Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
            Icon(Icons.Outlined.CallSplit, null, tint = VercelColors.TextTertiary, modifier = Modifier.size(12.dp))
            Text(branch, style = VercelText.MonoUrl, color = VercelColors.TextSecondary)
            Spacer(Modifier.weight(1f))
            Text(relativeTime, style = VercelText.Meta, color = VercelColors.TextTertiary)
        }
    }
}

@Composable
fun EnvTag(label: String) {
    Text(
        label,
        style = VercelText.MonoTag,
        color = VercelColors.TextSecondary,
        modifier = Modifier
            .clip(RoundedCornerShape(5.dp))
            .background(VercelColors.Surface3)
            .border(1.dp, VercelColors.Border, RoundedCornerShape(5.dp))
            .padding(horizontal = 7.dp, vertical = 3.dp),
    )
}

Status Badge (pill)

import androidx.compose.foundation.shape.CircleShape

@Composable
fun StatusBadge(state: DeployState) {
    Row(
        Modifier
            .clip(CircleShape)
            .background(state.color.copy(alpha = 0.08f))
            .border(1.dp, state.color.copy(alpha = 0.4f), CircleShape)
            .padding(horizontal = 10.dp, vertical = 5.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(6.dp),
    ) {
        Box(Modifier.size(7.dp).clip(CircleShape).background(state.color))
        Text(state.label, style = VercelText.MonoBadge, color = state.color)
    }
}

Primary / Secondary Buttons

import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.OutlinedButton
import androidx.compose.runtime.remember

@Composable
fun VercelPrimaryButton(title: String, onClick: () -> Unit) {
    val src = remember { MutableInteractionSource() }
    val pressed by src.collectIsPressedAsState()
    Button(
        onClick = onClick,
        interactionSource = src,
        shape = RoundedCornerShape(8.dp),
        colors = ButtonDefaults.buttonColors(
            containerColor = if (pressed) VercelColors.WhitePress else VercelColors.WhiteFill,
            contentColor = Color.Black,
        ),
        contentPadding = PaddingValues(horizontal = 20.dp, vertical = 11.dp),
    ) { Text(title, style = VercelText.Button) }
}

@Composable
fun VercelSecondaryButton(title: String, onClick: () -> Unit) {
    OutlinedButton(
        onClick = onClick,
        shape = RoundedCornerShape(8.dp),
        border = androidx.compose.foundation.BorderStroke(1.dp, VercelColors.Border),
        contentPadding = PaddingValues(horizontal = 18.dp, vertical = 10.dp),
    ) { Text(title, style = VercelText.Button.copy(fontWeight = FontWeight.Medium), color = VercelColors.TextPrimary) }
}

Build-Log Viewer

import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.LaunchedEffect

data class LogLine(val n: Int, val text: String, val kind: Kind) {
    enum class Kind { Normal, Error, Success, Dim }
}

@Composable
fun BuildLogViewer(lines: List<LogLine>) {
    val listState = rememberLazyListState()
    LaunchedEffect(lines.size) { if (lines.isNotEmpty()) listState.animateScrollToItem(lines.lastIndex) }
    fun color(k: LogLine.Kind) = when (k) {
        LogLine.Kind.Normal -> VercelColors.TextPrimary
        LogLine.Kind.Error -> VercelColors.ErrorRed
        LogLine.Kind.Success -> VercelColors.Ready
        LogLine.Kind.Dim -> VercelColors.TextTertiary
    }
    LazyColumn(
        state = listState,
        modifier = Modifier.fillMaxSize().background(VercelColors.Canvas),
        contentPadding = PaddingValues(horizontal = 12.dp, vertical = 10.dp),
    ) {
        items(lines, key = { it.n }) { l ->
            Row(Modifier.padding(vertical = 1.dp), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
                Text("${l.n}", style = VercelText.MonoLog, color = VercelColors.TextTertiary, modifier = Modifier.width(32.dp))
                Text(l.text, style = VercelText.MonoLog, color = color(l.kind))
            }
        }
    }
}

Triangle Logomark

import androidx.compose.foundation.Canvas
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Path

@Composable
fun VercelTriangle(size: androidx.compose.ui.unit.Dp = 22.dp, fill: Color = VercelColors.TextPrimary) {
    Canvas(Modifier.size(size, size * 0.855f)) {
        val p = Path().apply {
            moveTo(this@Canvas.size.width / 2f, 0f)
            lineTo(this@Canvas.size.width, this@Canvas.size.height)
            lineTo(0f, this@Canvas.size.height)
            close()
        }
        drawPath(p, fill)
    }
}

4. Navigation

Vercel has minimal chrome: a compact top bar and a 4-tab bottom strip. On Android, model the strip as a NavigationBar (no tint pill — active is pure white). The project switcher is a ModalBottomSheet.

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

@Composable
fun VercelBottomBar(selected: Int, onSelect: (Int) -> Unit) {
    NavigationBar(containerColor = VercelColors.Canvas, tonalElevation = 0.dp) {
        val items = listOf(
            "Overview"    to Icons.Filled.Home,
            "Deployments" to Icons.Filled.List,
            "Analytics"   to Icons.Filled.ShowChart,
            "Settings"    to Icons.Filled.Settings,
        )
        items.forEachIndexed { i, (label, icon) ->
            NavigationBarItem(
                selected = selected == i,
                onClick = { onSelect(i) },
                icon = { Icon(icon, label, Modifier.size(21.dp)) },
                label = { Text(label, style = VercelText.Tab) },
                colors = NavigationBarItemDefaults.colors(
                    selectedIconColor = Color.White,            // pure white, no accent
                    selectedTextColor = Color.White,
                    unselectedIconColor = VercelColors.TextTertiary,
                    unselectedTextColor = VercelColors.TextTertiary,
                    indicatorColor = Color.Transparent,         // NO Material pill — Vercel has none
                ),
            )
        }
    }
}

The project switcher (ModalBottomSheet, Surface1 container, 16.dp top radius) lists projects: name (Label) + production domain (MonoUrl TextSecondary) + trailing chevron, with a pinned search field. sheetState gives drag-dismiss for free.

5. Motion

Vercel motion is minimal and immediate. Borders + surface lightness signal elevation; there are essentially no shadows.

Moment Compose recipe
Status transition (Building → Ready) animateColorAsState(targetValue = state.color, tween(240)); soft success haptic
Building pulse halo scale via rememberInfiniteTransition tween(1200, easing = LinearEasing) reverse
Button press collectIsPressedAsState → swap container color + scale 0.98 tween(120)
Sheet present ModalBottomSheet default slide-up; scrim scrimColor = Black.copy(alpha = 0.7f)
Tab switch instant content swap — NO slide (Vercel favors immediacy)
Pull-to-refresh PullToRefreshBox; indicator tint VercelColors.Blue
Log auto-scroll LazyListState.animateScrollToItem(lastIndex) on append
// Status cross-fade — the canonical Vercel motion
val dotColor by animateColorAsState(targetValue = state.color, animationSpec = tween(240), label = "deployStatus")

Haptics: LocalHapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) for the soft success on a Ready transition and on destructive confirm. Auto-refresh is silent; only show a snackbar on error.

6. Icons

The closest first-party set is androidx.compose.material:material-icons-extended. The Vercel triangle is a custom Canvas path, not an icon glyph.

Purpose SF Symbol (iOS) Material Icon (Compose)
Overview (tab) house Icons.Filled.Home
Deployments (tab) list.bullet Icons.Filled.List
Analytics (tab) chart.line.uptrend.xyaxis Icons.Filled.ShowChart
Settings (tab) gearshape Icons.Filled.Settings
Git branch arrow.triangle.branch Icons.Outlined.CallSplit
Commit circle.dotted Icons.Filled.Commit
Domain globe Icons.Filled.Language
Redeploy arrow.clockwise Icons.Filled.Refresh
Visit / open arrow.up.right Icons.Filled.OpenInNew
Logs terminal Icons.Filled.Terminal
Copy doc.on.doc Icons.Filled.ContentCopy
Search magnifyingglass Icons.Filled.Search
Reveal env value eye / eye.slash Icons.Filled.Visibility / VisibilityOff
Back chevron.left Icons.Filled.ArrowBack
More ellipsis Icons.Filled.MoreHoriz
Success checkmark.circle.fill Icons.Filled.CheckCircle
Error exclamationmark.triangle.fill Icons.Filled.Warning

7. Minimum SDK & Accessibility Notes

  • minSdk 24 (Compose floor is 21; ModalBottomSheet + modern motion are comfortable at 24). compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+.
  • Edge-to-edge: call enableEdgeToEdge(); the true-black canvas wants light-content system bars. The compact top bar respects the camera cutout; the build-log and forms must respect the IME — use Modifier.imePadding().
  • True black: the dark background is #000000, NOT #191919. Do not enable Material You dynamicColorScheme() — Vercel’s monochrome identity must hold regardless of wallpaper (there is no brand accent to harmonize with one).
  • No shadows: elevation is a lighter surface (Surface1) + a 1dp Divider border. Set tonalElevation = 0.dp everywhere; never add shadow() to cards (invisible on black, off-brand).
  • Font scaling: sp honors the user’s font scale — keep it on Display/Title/Section/Body/Meta and the build-log Mono (stays monospace as it scales). Pin layout-sensitive text (10sp tab labels, env tags, mono badges, ≤12sp captions) via CompositionLocalProvider(LocalDensity provides Density(density, fontScale = 1f)).
  • TalkBack: compose the deployment card into one announcement — “Deployment: {state}, {env}, {branch}, {relativeTime}” via Modifier.semantics(mergeDescendants = true) { contentDescription = ... }; the status dot is decorative.
  • Color is not the only signal: every status dot is paired with its text label (“Ready”/“Building”/“Error”), so color-blind users never rely on hue.
  • Touch targets: Material guidance is 48.dp. The deployment card is full-row tappable (≥64.dp); give icon buttons a 48.dp hit area; primary buttons ≥ 40.dp tall.
  • Contrast: #EDEDED on #000000 ≈ 18:1 (AAA). #A1A1A1 on #000000 passes AA for secondary. Status-on-dim badge pairs (#0CCE6B on rgba(12,206,107,0.08)) are validated.
  • Reduce motion: when Settings.Global.ANIMATOR_DURATION_SCALE == 0, disable the Building-pulse loop and snap the status color (no cross-fade); keep the success haptic and the selection highlight.
  • Logs performance: LazyColumn (never a plain Column) for build logs — they can be thousands of lines; animateScrollToItem(lastIndex) to follow the tail.
  • Dark mode: dark IS the brand default. The light scheme is a clean inversion — #FFFFFF canvas, #000000 text, #FAFAFA cards, #EAEAEA borders; status colors stay identical.
All mobile designs →

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