coderef

GitHub

GitHub's iOS app is the Primer design system rendered for mobile — a developer's tool that is dense, information-first, and engineered to make code, diffs, and project state legible at a glance.
DownloadRaw
GitHub

Build with the GitHub look.

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

Palette

At a glance

Theme
dark
Typefaces
Mona Sans, Monaspace
Sections
7
Length
4,157 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · GitHub

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

GitHub (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports GitHub’s visual language (the Primer design system) to Android with Jetpack Compose (Material 3): a color token object, a Typography set, paste-ready @Composables, the repo home + code browser, the contributions heatmap, the diff viewer, motion, and haptics.

Why a Compose guide for an iOS-referenced app? The DESIGN.md tokens are platform-neutral (Primer is cross-platform by design). This file keeps the visual identity — Primer dark default #0D1117, the one accent blue, the reserved green, semantic state colors, the iconic heatmap ramp, monospace for all code/SHAs/paths — while making everything idiomatic Android: a NavigationBar instead of a UITabBar, LazyColumn diffs, sp/dp instead of pt.

Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, Coil 2.6+ for avatars. No Material You / dynamic color — GitHub’s Primer palette and its semantic state colors must hold regardless of wallpaper. Dark is the default; light is a first-class peer.

1. Color Tokens

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

object GH {
    // ---- Dark (default) ----
    val Canvas         = Color(0xFF0D1117) // blue-tinted near-black
    val CanvasSubtle   = Color(0xFF161B22) // raised surfaces, card headers, heatmap L0
    val CanvasInset    = Color(0xFF21262D) // default button, code-block bg, count pills
    val SurfacePressed = Color(0xFF30363D)
    val BorderDefault  = Color(0xFF30363D)
    val BorderMuted    = Color(0xFF21262D)

    val Fg          = Color(0xFFE6EDF3)
    val FgMuted     = Color(0xFF7D8590)
    val FgSubtle    = Color(0xFF6E7681)
    val FgOnEmphasis = Color(0xFFFFFFFF)

    val AccentFg       = Color(0xFF2F81F7) // links, owner/name, active
    val AccentEmphasis = Color(0xFF1F6FEB)
    val BtnPrimary     = Color(0xFF238636) // RESERVED — Code / Merge only
    val BtnPrimaryHover = Color(0xFF2EA043)
    val BtnDanger      = Color(0xFFDA3633)

    // Semantic state (fixed meaning)
    val Open      = Color(0xFF3FB950) // open issue/PR, passing
    val Merged    = Color(0xFFA371F7) // merged PR (purple = done)
    val Closed    = Color(0xFFF85149) // closed, failing, delete
    val Draft     = Color(0xFF6E7681) // draft/neutral
    val Attention = Color(0xFFD29922) // pending/warning
    val Sponsor   = Color(0xFFDB61A2)

    val TabUnderline = Color(0xFFF78166) // orange-coral active underline

    val DiffAddBg = Color(0xFF3FB950).copy(alpha = 0.15f)
    val DiffDelBg = Color(0xFFF85149).copy(alpha = 0.15f)

    // ---- Light (first-class peer) ----
    val LCanvas        = Color(0xFFFFFFFF)
    val LCanvasSubtle  = Color(0xFFF6F8FA)
    val LBorderDefault = Color(0xFFD0D7DE)
    val LFg            = Color(0xFF1F2328)
    val LFgMuted       = Color(0xFF656D76)
    val LAccentFg      = Color(0xFF0969DA)
    val LBtnPrimary    = Color(0xFF1F883D)
    val LOpen          = Color(0xFF1A7F37)
    val LMerged        = Color(0xFF8250DF)
    val LClosed        = Color(0xFFCF222E)
    val LTabUnderline  = Color(0xFFFD8C73)

    // Contributions heatmap ramp — the iconic GitHub visual (never alter)
    val HeatDark  = listOf(Color(0xFF161B22), Color(0xFF0E4429), Color(0xFF006D32), Color(0xFF26A641), Color(0xFF39D353))
    val HeatLight = listOf(Color(0xFFEBEDF0), Color(0xFF9BE9A8), Color(0xFF40C463), Color(0xFF30A14E), Color(0xFF216E39))
}
// ui/theme/Theme.kt
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable

private val GHDark = darkColorScheme(
    primary        = GH.BtnPrimary,   // reserved green (Code / Merge)
    onPrimary      = GH.FgOnEmphasis,
    background     = GH.Canvas,
    onBackground   = GH.Fg,
    surface        = GH.CanvasSubtle,
    onSurface      = GH.Fg,
    surfaceVariant = GH.CanvasInset,
    outline        = GH.BorderDefault,
    error          = GH.Closed,
)

private val GHLight = lightColorScheme(
    primary        = GH.LBtnPrimary,
    onPrimary      = GH.FgOnEmphasis,
    background     = GH.LCanvas,
    onBackground   = GH.LFg,
    surface        = GH.LCanvasSubtle,
    onSurface      = GH.LFg,
    surfaceVariant = GH.LCanvasSubtle,
    outline        = GH.LBorderDefault,
    error          = GH.LClosed,
)

@Composable
fun GitHubTheme(dark: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) =
    MaterialTheme(colorScheme = if (dark) GHDark else GHLight, typography = GHTypography, content = content)

2. Typography (Material 3)

Mona Sans (GitHub’s variable grotesque, SIL OFL) for UI; a monospace for all code, SHAs, file/branch names, diffs. Drop both in res/font/. Weight + size establish hierarchy (one sans family); the monospace contract is functional, not stylistic.

// ui/theme/GHType.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 MonaSans = FontFamily(
    Font(R.font.mona_sans_regular,   FontWeight.Normal),
    Font(R.font.mona_sans_medium,    FontWeight.Medium),
    Font(R.font.mona_sans_semibold,  FontWeight.SemiBold),
    Font(R.font.mona_sans_bold,      FontWeight.Bold),
    Font(R.font.mona_sans_extrabold, FontWeight.ExtraBold),
)
val Mono = FontFamily(
    Font(R.font.jetbrains_mono_regular,  FontWeight.Normal),
    Font(R.font.jetbrains_mono_semibold, FontWeight.SemiBold),
)

// Named ramp — mirrors DESIGN.md §3 exactly (pt → sp 1:1)
object GHText {
    val Display    = TextStyle(MonaSans, fontWeight = FontWeight.ExtraBold, fontSize = 32.sp, lineHeight = 37.sp, letterSpacing = (-0.4).sp)
    val RepoTitle  = TextStyle(MonaSans, fontWeight = FontWeight.Bold,      fontSize = 24.sp, lineHeight = 29.sp, letterSpacing = (-0.3).sp)
    val Section    = TextStyle(MonaSans, fontWeight = FontWeight.SemiBold,  fontSize = 20.sp, lineHeight = 26.sp, letterSpacing = (-0.2).sp)
    val Subtitle   = TextStyle(MonaSans, fontWeight = FontWeight.SemiBold,  fontSize = 16.sp, lineHeight = 22.sp)
    val Body       = TextStyle(MonaSans, fontWeight = FontWeight.Normal,    fontSize = 14.sp, lineHeight = 21.sp)
    val BodyStrong = TextStyle(MonaSans, fontWeight = FontWeight.SemiBold,  fontSize = 14.sp, lineHeight = 21.sp)
    val Code       = TextStyle(Mono,     fontWeight = FontWeight.Normal,    fontSize = 13.sp, lineHeight = 19.sp)
    val CodeInline = TextStyle(Mono,     fontWeight = FontWeight.Normal,    fontSize = 13.sp, lineHeight = 18.sp)
    val Sha        = TextStyle(Mono,     fontWeight = FontWeight.SemiBold,  fontSize = 12.sp, lineHeight = 12.sp)
    val Metadata   = TextStyle(MonaSans, fontWeight = FontWeight.Normal,    fontSize = 12.sp, lineHeight = 17.sp)
    val Label      = TextStyle(MonaSans, fontWeight = FontWeight.Medium,    fontSize = 12.sp, lineHeight = 12.sp)
    val CountPill  = TextStyle(MonaSans, fontWeight = FontWeight.SemiBold,  fontSize = 11.sp, lineHeight = 11.sp)
    val Tab        = TextStyle(MonaSans, fontWeight = FontWeight.Medium,    fontSize = 10.sp, lineHeight = 10.sp, letterSpacing = 0.1.sp)
    val Caption    = TextStyle(MonaSans, fontWeight = FontWeight.Normal,    fontSize = 11.sp, lineHeight = 14.sp)
}

val GHTypography = Typography(
    headlineLarge = GHText.Display,
    headlineMedium = GHText.RepoTitle,
    titleLarge    = GHText.Section,
    titleMedium   = GHText.Subtitle,
    bodyMedium    = GHText.Body,
    labelSmall    = GHText.Tab,
)

3. Signature Components

Repo Header (owner / name + stats + tab strip)

import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp

@Composable
fun RepoHeader(
    owner: String, name: String, description: String,
    language: String, languageColor: Color, stars: String, forks: String,
    tabs: List<String> = listOf("Code", "Issues", "Pull requests", "Actions"),
    activeTab: Int, onTab: (Int) -> Unit,
) {
    Column(Modifier.fillMaxWidth().background(GH.Canvas).padding(top = 12.dp)) {
        Row(Modifier.padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
            Icon(GitHubIcons.Repo, null, tint = GH.FgMuted, modifier = Modifier.size(17.dp))
            Text(owner, style = GHText.Subtitle, color = GH.AccentFg)
            Text("/", style = GHText.Subtitle, color = GH.FgMuted)
            Text(name, style = GHText.Subtitle.copy(fontWeight = FontWeight.Bold), color = GH.AccentFg)
        }
        Text(description, style = GHText.Body, color = GH.FgMuted, modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp))
        Row(Modifier.padding(horizontal = 16.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp)) {
            Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) {
                Box(Modifier.size(11.dp).clip(CircleShape).background(languageColor))
                Text(language, style = GHText.Metadata, color = GH.FgMuted)
            }
            StatGlyph(GitHubIcons.Star, stars)
            StatGlyph(GitHubIcons.Fork, forks)
        }
        ScrollableTabRow(
            selectedTabIndex = activeTab,
            containerColor = GH.Canvas,
            edgePadding = 16.dp,
            divider = { HorizontalDivider(thickness = 1.dp, color = GH.BorderDefault) },
            indicator = { pos ->
                TabRowDefaults.SecondaryIndicator(
                    Modifier.tabIndicatorOffset(pos[activeTab]),
                    height = 2.dp, color = GH.TabUnderline, // orange-coral
                )
            },
        ) {
            tabs.forEachIndexed { i, t ->
                Tab(selected = i == activeTab, onClick = { onTab(i) }) {
                    Text(
                        t, style = GHText.Body.copy(fontWeight = if (i == activeTab) FontWeight.SemiBold else FontWeight.Normal),
                        color = if (i == activeTab) GH.Fg else GH.FgMuted,
                        modifier = Modifier.padding(vertical = 12.dp),
                    )
                }
            }
        }
    }
}

@Composable
private fun StatGlyph(icon: ImageVector, value: String) {
    Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
        Icon(icon, null, tint = GH.FgMuted, modifier = Modifier.size(13.dp))
        Text(value, style = GHText.Metadata, color = GH.FgMuted)
    }
}

Branch Pill + Latest-Commit Strip

import androidx.compose.foundation.shape.RoundedCornerShape
import coil.compose.AsyncImage

@Composable
fun BranchPill(branch: String) {
    Row(
        Modifier
            .clip(RoundedCornerShape(6.dp))
            .background(GH.CanvasSubtle)
            .border(1.dp, GH.BorderDefault, RoundedCornerShape(6.dp))
            .padding(horizontal = 12.dp, vertical = 7.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(6.dp),
    ) {
        Icon(GitHubIcons.Branch, null, tint = GH.FgMuted, modifier = Modifier.size(13.dp))
        Text(branch, style = GHText.Sha, color = GH.Fg) // monospace
    }
}

@Composable
fun LatestCommit(avatarUrl: String, message: String, sha: String, whenText: String) {
    Row(
        Modifier
            .fillMaxWidth()
            .background(GH.CanvasSubtle)
            .drawBottomBorder(GH.BorderDefault)
            .padding(12.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(8.dp),
    ) {
        AsyncImage(model = avatarUrl, contentDescription = null, modifier = Modifier.size(18.dp).clip(CircleShape))
        Text(message, style = GHText.Body, color = GH.Fg, maxLines = 1, modifier = Modifier.weight(1f))
        Text(sha, style = GHText.Caption.copy(fontFamily = Mono), color = GH.FgMuted) // monospace SHA
        Text(whenText, style = GHText.Caption, color = GH.FgMuted)
    }
}

Code Browser (sticky path breadcrumb + monospace blob)

import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.rememberScrollState

@Composable
fun CodeBrowser(path: String, lines: List<String>) {
    Column(Modifier.fillMaxSize().background(GH.Canvas)) {
        // Sticky path breadcrumb — monospace
        Box(
            Modifier.fillMaxWidth().background(GH.CanvasSubtle).drawBottomBorder(GH.BorderDefault).padding(horizontal = 16.dp, vertical = 10.dp)
        ) { Text(path, style = GHText.Code, color = GH.FgMuted) }

        LazyColumn(Modifier.fillMaxWidth().background(GH.CanvasSubtle).horizontalScroll(rememberScrollState())) {
            itemsIndexed(lines) { i, ln ->
                Row(Modifier.padding(horizontal = 12.dp, vertical = 1.dp)) {
                    Text("${i + 1}", style = GHText.Code, color = GH.FgSubtle, modifier = Modifier.widthIn(min = 36.dp))
                    Spacer(Modifier.width(16.dp))
                    Text(ln, style = GHText.Code, color = GH.Fg)
                }
            }
        }
    }
}

State Pill (open / merged / closed / draft) — color carries meaning

enum class IssueState(val color: Color, val icon: ImageVector, val label: String) {
    Open(GH.Open, GitHubIcons.IssueOpened, "Open"),
    Merged(GH.Merged, GitHubIcons.Merge, "Merged"),
    Closed(GH.Closed, GitHubIcons.IssueClosed, "Closed"),
    Draft(GH.Draft, GitHubIcons.Draft, "Draft"),
}

@Composable
fun StatePill(state: IssueState) {
    Row(
        Modifier.clip(RoundedCornerShape(999.dp)).background(state.color).padding(horizontal = 12.dp, vertical = 5.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(5.dp),
    ) {
        Icon(state.icon, null, tint = Color.White, modifier = Modifier.size(12.dp))
        Text(state.label, style = GHText.Label, color = Color.White) // glyph + label so it's not color-only
    }
}

Contributions Heatmap (signature)

@Composable
fun ContributionsHeatmap(weeks: List<List<Int>> /* [week][day] -> 0..4 */) {
    Column {
        Row(horizontalArrangement = Arrangement.spacedBy(3.dp)) {
            weeks.forEach { week ->
                Column(verticalArrangement = Arrangement.spacedBy(3.dp)) {
                    week.forEach { level ->
                        Box(
                            Modifier
                                .size(11.dp)
                                .clip(RoundedCornerShape(2.dp))
                                .background(GH.HeatDark[level])
                                .then(if (level == 0) Modifier.border(1.dp, Color(0xFF6E7681).copy(alpha = 0.15f), RoundedCornerShape(2.dp)) else Modifier)
                        )
                    }
                }
            }
        }
        Row(Modifier.padding(top = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
            Text("Less", style = GHText.Caption, color = GH.FgMuted)
            GH.HeatDark.forEach { c -> Box(Modifier.size(11.dp).clip(RoundedCornerShape(2.dp)).background(c)) }
            Text("More", style = GHText.Caption, color = GH.FgMuted)
        }
    }
}

Diff Viewer (additions green wash / deletions red wash)

data class DiffLine(val kind: String /* add | del | ctx */, val text: String)

@Composable
fun DiffViewer(file: String, adds: Int, dels: Int, lines: List<DiffLine>) {
    Column(Modifier.clip(RoundedCornerShape(6.dp)).border(1.dp, GH.BorderDefault, RoundedCornerShape(6.dp))) {
        Row(
            Modifier.fillMaxWidth().background(GH.CanvasSubtle).drawBottomBorder(GH.BorderDefault).padding(10.dp),
            verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp),
        ) {
            Text(file, style = GHText.Code, color = GH.Fg, modifier = Modifier.weight(1f)) // monospace filename
            Text("+$adds", style = GHText.Code, color = GH.Open)
            Text("-$dels", style = GHText.Code, color = GH.Closed)
        }
        Column(Modifier.fillMaxWidth().background(GH.CanvasSubtle).horizontalScroll(rememberScrollState())) {
            lines.forEach { l ->
                val bg = when (l.kind) { "add" -> GH.DiffAddBg; "del" -> GH.DiffDelBg; else -> Color.Transparent }
                Row(Modifier.fillMaxWidth().background(bg)) {
                    Text(if (l.kind == "add") "+" else if (l.kind == "del") "-" else " ", style = GHText.Code, color = GH.FgSubtle, modifier = Modifier.width(28.dp))
                    Text(l.text, style = GHText.Code, color = GH.Fg, modifier = Modifier.padding(end = 16.dp))
                }
            }
        }
    }
}

Buttons (reserved green + default)

// RESERVED — only for "Code" download and "Merge pull request"
@Composable
fun GreenButton(title: String, icon: ImageVector, onClick: () -> Unit) {
    Button(
        onClick = onClick,
        shape = RoundedCornerShape(6.dp),
        colors = ButtonDefaults.buttonColors(containerColor = GH.BtnPrimary, contentColor = Color.White),
        contentPadding = PaddingValues(horizontal = 16.dp, vertical = 10.dp),
    ) {
        Icon(icon, null, modifier = Modifier.size(16.dp))
        Spacer(Modifier.width(8.dp))
        Text(title, style = GHText.BodyStrong, color = Color.White)
    }
}

@Composable
fun DefaultButton(title: String, onClick: () -> Unit) {
    OutlinedButton(
        onClick = onClick,
        shape = RoundedCornerShape(6.dp),
        border = BorderStroke(1.dp, GH.BorderDefault),
        colors = ButtonDefaults.outlinedButtonColors(containerColor = GH.CanvasInset, contentColor = GH.Fg),
        contentPadding = PaddingValues(horizontal = 16.dp, vertical = 10.dp),
    ) { Text(title, style = GHText.BodyStrong) }
}

drawBottomBorder is a tiny helper: fun Modifier.drawBottomBorder(c: Color) = drawBehind { drawLine(c, Offset(0f, size.height), Offset(size.width, size.height), 1.dp.toPx()) }.

4. Navigation (Bottom Bar)

4 destinations, Canvas container, 1dp top BorderMuted, no tint pill; active Fg, inactive FgMuted. (Android has no live blur for nav bars — use the solid #0D1117; the iOS translucency is cosmetic.)

@Composable
fun GitHubBottomBar(selected: Int, onSelect: (Int) -> Unit) {
    Column {
        HorizontalDivider(thickness = 1.dp, color = GH.BorderMuted)
        NavigationBar(containerColor = GH.Canvas, tonalElevation = 0.dp) {
            val items = listOf(
                "Home"          to GitHubIcons.Home,
                "Notifications" to GitHubIcons.Bell,
                "Explore"       to GitHubIcons.Telescope,
                "Profile"       to GitHubIcons.Person,
            )
            items.forEachIndexed { i, (label, icon) ->
                NavigationBarItem(
                    selected = selected == i,
                    onClick = { onSelect(i) },
                    icon = { Icon(icon, label, modifier = Modifier.size(22.dp)) },
                    label = { Text(label, style = GHText.Tab) },
                    colors = NavigationBarItemDefaults.colors(
                        selectedIconColor = GH.Fg,
                        selectedTextColor = GH.Fg,
                        unselectedIconColor = GH.FgMuted,
                        unselectedTextColor = GH.FgMuted,
                        indicatorColor = Color.Transparent, // no Material pill — GitHub has none
                    ),
                )
            }
        }
    }
}

5. Motion

GitHub motion is utilitarian — no gratuitous animation; state changes are crisp.

Moment Compose recipe
Tab-strip underline ScrollableTabRow indicator tabIndicatorOffset slides tween(200)
Merge button → “Merged” Crossfade(targetState = merged, tween(200)) swapping label + container #238636 → #A371F7
Star count-up animateIntAsState(stars, tween(250))
Diff “Expand hidden” AnimatedVisibility(expandVertically(tween(180))) to reveal collapsed context
List/tree expand animateContentSize(tween(180)) on the file-tree folder
Pull-to-refresh PullToRefreshBox tinted FgMuted
// Merge confirm — the one expressive moment
Crossfade(targetState = isMerged, animationSpec = tween(200), label = "merge") { merged ->
    if (merged) StatePill(IssueState.Merged) else GreenButton("Merge pull request", GitHubIcons.Merge) { /* merge */ }
}

Haptics: LocalHapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) on Merge confirm and Star toggle only — GitHub is not a haptic-heavy app. No haptics on scroll, tab switch, or navigation.

6. Icons

GitHub ships Octicons. Compose has no bundled Octicons, so either vector-drawable the official Octicons SVGs into res/drawable and expose them, or map to the closest material-icons-extended. Define a GitHubIcons object so call sites stay stable:

Purpose Octicon (iOS) Compose source
Repo repo Octicon repo vector, or Icons.Filled.Book
Code / <> code Octicon code, or Icons.Filled.Code
Issue opened issue-opened Octicon, or Icons.Outlined.Adjust
Issue closed issue-closed Octicon, or Icons.Filled.CheckCircle
Pull request git-pull-request Octicon, or Icons.Filled.CallMerge
Merge git-merge Octicon, or Icons.Filled.Merge
Draft PR git-pull-request-draft Octicon, or Icons.Outlined.Circle
Branch git-branch Octicon, or Icons.Filled.AltRoute
Commit git-commit Octicon, or Icons.Filled.Commit
Star star / star-fill Octicon, or Icons.Filled.Star
Fork repo-forked Octicon, or Icons.Filled.ForkRight
Folder (tree) file-directory-fill Octicon, or Icons.Filled.Folder
File (tree) file Octicon, or Icons.Filled.InsertDriveFile
Home (tab) home Octicon, or Icons.Filled.Home
Notifications (tab) bell Octicon, or Icons.Filled.Notifications
Explore (tab) telescope Octicon, or Icons.Filled.Explore
Profile (tab) person Octicon, or Icons.Filled.Person

7. Minimum SDK & Accessibility Notes

  • minSdk 24 (compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+). Coil for avatars; vector-drawable the Octicons for a faithful glyph set.
  • Edge-to-edge: enableEdgeToEdge(); dark canvas wants light-content system bars (dark-content in light theme). The code browser must scroll horizontally — wrap the blob in horizontalScroll.
  • No dynamic color: do not call dynamicColorScheme() — Primer’s palette and the semantic state colors (open/merged/closed/draft) must be exact regardless of wallpaper; Material You would corrupt the “green = merge” learned affordance.
  • Reserved green: GH.BtnPrimary (#238636) is ONLY the “Code” download and “Merge pull request” buttons — every other action uses DefaultButton. Never tint another control green.
  • Monospace contract: code, SHAs, file/branch names, and diffs MUST use Mono — never substitute MonaSans; glyph distinction (0/O, 1/l) is an accessibility and correctness requirement for developers.
  • Heatmap ramp: never alter the 5 stops (#161B22 → #0E4429 → #006D32 → #26A641 → #39D353 dark) — it’s the single most recognizable GitHub visual.
  • Font scaling: sp honors the user’s scale — keep it on titles, body, and code (code stays monospace as it scales). Pin layout-sensitive text (10sp tab labels, count pills, SHAs in tables/trees) via dp or a fixed-fontScale CompositionLocalProvider.
  • TalkBack: state pills carry a leading glyph + text label so the meaning isn’t color-only; diff lines expose +/- markers in the content description, not just the wash; label the repo header “Repository {owner} slash {name}”; the heatmap squares as “{n} contributions on {date}”.
  • Touch targets: Material guidance is 48dp — give the 13–17dp inline glyphs (star, fork, branch) a 48dp hit via padding when tappable; file-tree rows are full-row tappable; primary buttons ≥ 36dp.
  • Contrast: #E6EDF3 on #0D1117 passes WCAG AA; #7D8590 muted text passes AA for ≥12sp; the reserved green #238636 with #FFFFFF text passes AA. Validate any custom issue-label tint (text = label color on a 10% fill).
  • Reduce motion: when Settings.Global.ANIMATOR_DURATION_SCALE == 0, make the tab-underline an instant jump, the merge an instant state swap, the count an instant set, and diff-expand an instant insert.
All mobile designs →

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