coderef

Trello

Trello's iOS app is a tactile, board-first workspace where the content is the layout. Unlike most apps, Trello has no single canvas color — the board background sets the mood.
DownloadRaw
Trello

Build with the Trello 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
8
Length
3,604 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · Trello

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

Trello (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports Trello’s visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, paste-ready @Composables, Modifiers, 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 (Trello’s board-as-backdrop, pale lists of white cards, the navy-tinted card shadow, the drag lift, the functional label palette) while making everything idiomatic Android — LazyRow/LazyColumn instead of UIScrollView, sp/dp instead of pt, and no bottom NavigationBar (Trello has none).

Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, and Coil 2.6+ for board photo backgrounds.

1. Color Tokens

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

object TrelloColors {
    // Action & board
    val Action        = Color(0xFF0C66E4)
    val ActionPressed = Color(0xFF0055CC)
    val ActionTint    = Color(0xFFE9F2FF)
    val BoardBlue     = Color(0xFF0079BF) // default backdrop

    // Surfaces & text
    val Card          = Color(0xFFFFFFFF)
    val ListSurface   = Color(0xFFF1F2F4)
    val Sunken        = Color(0xFFEBECF0)
    val Divider       = Color(0xFFDFE1E6)
    val Border        = Color(0xFFC1C7D0)
    val TextPrimary   = Color(0xFF172B4D)
    val TextSecondary = Color(0xFF5E6C84)
    val TextTertiary  = Color(0xFF8993A4)

    // Card label palette (functional)
    val LabelGreen  = Color(0xFF4BCE97)
    val LabelYellow = Color(0xFFF5CD47)
    val LabelOrange = Color(0xFFFEA362)
    val LabelRed    = Color(0xFFF87168)
    val LabelPurple = Color(0xFF9F8FEF)
    val LabelBlue   = Color(0xFF579DFF)

    // Semantic
    val Success = Color(0xFF1F845A)
    val DueSoon = Color(0xFFB65C02)
    val Overdue = Color(0xFFC9372C)

    // Signature navy shadow base
    val ShadowNavy = Color(0xFF091E42)
}

Wire it into a Material 3 lightColorScheme. Trello supports a real dark mode for UI chrome; the board backdrop still drives the board area regardless.

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

private val TrelloScheme = lightColorScheme(
    primary          = TrelloColors.Action,
    onPrimary        = Color.White,
    primaryContainer = TrelloColors.ActionTint,
    background        = TrelloColors.ListSurface,
    onBackground      = TrelloColors.TextPrimary,
    surface           = TrelloColors.Card,
    onSurface         = TrelloColors.TextPrimary,
    surfaceVariant    = TrelloColors.ListSurface,
    outline           = TrelloColors.Border,
    outlineVariant    = TrelloColors.Divider,
    error             = TrelloColors.Overdue,
)

@Composable
fun TrelloTheme(content: @Composable () -> Unit) =
    MaterialTheme(colorScheme = TrelloScheme, typography = TrelloTypography, content = content)

2. Typography

Trello uses the system font deliberately (legibility on any backdrop). Compose’s default FontFamily.Default is Roboto on Android; Inter is the closest substitute if bundled.

// ui/theme/TrelloType.kt
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp

private val Sys = FontFamily.Default

// Named ramp — mirrors DESIGN.md §3 exactly (pt → sp 1:1)
object TrelloText {
    val SheetTitle = TextStyle(Sys, fontWeight = FontWeight.Bold,     fontSize = 24.sp, lineHeight = 29.sp, letterSpacing = (-0.3).sp)
    val BoardName  = TextStyle(Sys, fontWeight = FontWeight.Bold,     fontSize = 20.sp, lineHeight = 24.sp, letterSpacing = (-0.2).sp)
    val Section    = TextStyle(Sys, fontWeight = FontWeight.Bold,     fontSize = 17.sp, lineHeight = 22.sp, letterSpacing = (-0.1).sp)
    val ListTitle  = TextStyle(Sys, fontWeight = FontWeight.SemiBold, fontSize = 15.sp, lineHeight = 20.sp)
    val CardTitle  = TextStyle(Sys, fontWeight = FontWeight.SemiBold, fontSize = 15.sp, lineHeight = 20.sp)
    val Body       = TextStyle(Sys, fontWeight = FontWeight.Normal,   fontSize = 15.sp, lineHeight = 22.sp)
    val Button     = TextStyle(Sys, fontWeight = FontWeight.SemiBold, fontSize = 16.sp, lineHeight = 20.sp)
    val Badge      = TextStyle(Sys, fontWeight = FontWeight.SemiBold, fontSize = 12.sp, lineHeight = 14.sp)
    val Subtitle   = TextStyle(Sys, fontWeight = FontWeight.Normal,   fontSize = 13.sp, lineHeight = 17.sp)
    val LabelUpper = TextStyle(Sys, fontWeight = FontWeight.Bold,     fontSize = 11.sp, lineHeight = 13.sp, letterSpacing = 0.4.sp)
    val Composer   = TextStyle(Sys, fontWeight = FontWeight.Normal,   fontSize = 15.sp, lineHeight = 21.sp)
    val Caption    = TextStyle(Sys, fontWeight = FontWeight.Normal,   fontSize = 11.sp, lineHeight = 14.sp)
}

// Map onto Material 3 slots so stock components inherit the brand
val TrelloTypography = Typography(
    headlineLarge = TrelloText.SheetTitle,
    headlineSmall = TrelloText.Section,
    titleMedium   = TrelloText.CardTitle,
    bodyMedium    = TrelloText.Body,
    labelSmall    = TrelloText.Badge,
)

3. Signature Components

Card (with drag lift)

import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.ui.draw.scale
import androidx.compose.ui.draw.shadow

@Composable
fun TrelloCard(
    title: String,
    labels: List<Color>,
    due: String? = null,
    checklist: String? = null,
    isDragging: Boolean = false,
    modifier: Modifier = Modifier,
) {
    val scale by animateFloatAsState(if (isDragging) 1.03f else 1f, spring(), label = "cardScale")
    val elevation by animateDpAsState(if (isDragging) 24.dp else 1.dp, label = "cardElev")

    Column(
        modifier
            .scale(scale)
            .shadow(
                elevation = elevation,
                shape = RoundedCornerShape(8.dp),
                ambientColor = TrelloColors.ShadowNavy,
                spotColor = TrelloColors.ShadowNavy,
            )
            .clip(RoundedCornerShape(8.dp))
            .background(TrelloColors.Card)
            .padding(horizontal = 12.dp, vertical = 8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
    ) {
        if (labels.isNotEmpty()) {
            Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
                labels.forEach { c ->
                    Box(Modifier.size(width = 36.dp, height = 8.dp).clip(RoundedCornerShape(4.dp)).background(c))
                }
            }
        }
        Text(title, style = TrelloText.CardTitle, color = TrelloColors.TextPrimary, maxLines = 4, overflow = TextOverflow.Ellipsis)
        if (due != null || checklist != null) {
            Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
                if (due != null) {
                    Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
                        Icon(Icons.Filled.Schedule, null, tint = TrelloColors.DueSoon, modifier = Modifier.size(12.dp))
                        Text(due, style = TrelloText.Badge, color = TrelloColors.DueSoon)
                    }
                }
                if (checklist != null) {
                    Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
                        Icon(Icons.Filled.CheckBox, null, tint = TrelloColors.TextSecondary, modifier = Modifier.size(12.dp))
                        Text(checklist, style = TrelloText.Badge, color = TrelloColors.TextSecondary)
                    }
                }
            }
        }
    }
}

List / Column

@Composable
fun TrelloListColumn(
    title: String,
    cards: List<CardModel>,
    modifier: Modifier = Modifier,
) {
    var composing by remember { mutableStateOf(false) }
    var draft by remember { mutableStateOf("") }

    Column(
        modifier
            .width(272.dp)
            .shadow(2.dp, RoundedCornerShape(12.dp), ambientColor = TrelloColors.ShadowNavy, spotColor = TrelloColors.ShadowNavy)
            .clip(RoundedCornerShape(12.dp))
            .background(TrelloColors.ListSurface),
    ) {
        Row(
            Modifier.fillMaxWidth().padding(start = 8.dp, end = 8.dp, top = 8.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(6.dp),
        ) {
            Text(title, style = TrelloText.ListTitle, color = TrelloColors.TextPrimary)
            Text("${cards.size}", style = TrelloText.Badge, color = TrelloColors.TextSecondary)
            Spacer(Modifier.weight(1f))
            Icon(Icons.Filled.MoreHoriz, "More", tint = TrelloColors.TextSecondary, modifier = Modifier.size(16.dp))
        }

        LazyColumn(
            Modifier.heightIn(max = 520.dp).padding(8.dp),
            verticalArrangement = Arrangement.spacedBy(8.dp),
        ) {
            items(cards) { c -> TrelloCard(c.title, c.labels, c.due, c.checklist) }
        }

        if (composing) {
            Column(Modifier.padding(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
                BasicTextField(
                    value = draft, onValueChange = { draft = it },
                    textStyle = TrelloText.Composer.copy(color = TrelloColors.TextPrimary),
                    modifier = Modifier
                        .fillMaxWidth()
                        .clip(RoundedCornerShape(8.dp))
                        .background(TrelloColors.Card)
                        .border(2.dp, TrelloColors.Action, RoundedCornerShape(8.dp))
                        .padding(8.dp),
                )
                Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
                    Box(
                        Modifier.clip(RoundedCornerShape(8.dp)).background(TrelloColors.Action)
                            .height(36.dp).padding(horizontal = 16.dp).clickable { },
                        contentAlignment = Alignment.Center,
                    ) { Text("Add card", style = TrelloText.Button, color = Color.White) }
                    Icon(Icons.Filled.Close, "Cancel", tint = TrelloColors.TextSecondary,
                        modifier = Modifier.size(20.dp).clickable { composing = false })
                }
            }
        } else {
            Row(
                Modifier.fillMaxWidth().clickable { composing = true }.padding(12.dp),
                verticalAlignment = Alignment.CenterVertically,
                horizontalArrangement = Arrangement.spacedBy(8.dp),
            ) {
                Icon(Icons.Filled.Add, null, tint = TrelloColors.TextSecondary, modifier = Modifier.size(16.dp))
                Text("Add a card", style = TrelloText.Body, color = TrelloColors.TextSecondary)
            }
        }
    }
}

Board (horizontal scroll, backdrop)

@Composable
fun TrelloBoard(
    boardName: String,
    lists: List<ListModel>,
    backdrop: Color = TrelloColors.BoardBlue, // or AsyncImage for a photo board + scrim
) {
    Box(Modifier.fillMaxSize().background(backdrop)) {
        // For a photo backdrop: AsyncImage(...) then Box(Modifier.matchParentSize().background(Color.Black.copy(alpha = 0.16f)))
        Column {
            Row(
                Modifier.fillMaxWidth().height(44.dp).padding(horizontal = 12.dp),
                verticalAlignment = Alignment.CenterVertically,
                horizontalArrangement = Arrangement.spacedBy(12.dp),
            ) {
                Icon(Icons.Filled.ArrowBack, "Back", tint = Color.White)
                Text(boardName, style = TrelloText.BoardName, color = Color.White)
                Spacer(Modifier.weight(1f))
                Row(
                    Modifier.clip(RoundedCornerShape(8.dp)).background(Color.White.copy(alpha = 0.16f))
                        .height(32.dp).padding(horizontal = 12.dp),
                    verticalAlignment = Alignment.CenterVertically,
                    horizontalArrangement = Arrangement.spacedBy(6.dp),
                ) {
                    Icon(Icons.Filled.FilterList, null, tint = Color.White, modifier = Modifier.size(14.dp))
                    Text("Filter", style = TrelloText.Badge, color = Color.White)
                }
                Icon(Icons.Filled.MoreHoriz, "Menu", tint = Color.White)
            }

            LazyRow(
                contentPadding = PaddingValues(8.dp),
                horizontalArrangement = Arrangement.spacedBy(8.dp),
                verticalAlignment = Alignment.Top,
            ) {
                items(lists) { l -> TrelloListColumn(l.title, l.cards) }
                item {
                    Box(
                        Modifier.width(272.dp).height(44.dp)
                            .clip(RoundedCornerShape(12.dp)).background(Color.White.copy(alpha = 0.16f))
                            .padding(horizontal = 12.dp),
                        contentAlignment = Alignment.CenterStart,
                    ) { Text("+ Add list", style = TrelloText.Body, color = Color.White) }
                }
            }
        }
    }
}

Card Detail Sheet

import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState

@Composable
fun CardDetailSheet(title: String, progress: Float, onDismiss: () -> Unit) {
    ModalBottomSheet(
        onDismissRequest = onDismiss,
        sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
        containerColor = TrelloColors.Card,
        shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
    ) {
        Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(20.dp)) {
            Text(title, style = TrelloText.SheetTitle, color = TrelloColors.TextPrimary)
            Text("CHECKLIST", style = TrelloText.LabelUpper, color = TrelloColors.TextSecondary)
            LinearProgressIndicator(
                progress = { progress },
                color = TrelloColors.Success,
                trackColor = TrelloColors.Sunken,
                modifier = Modifier.fillMaxWidth().height(6.dp).clip(RoundedCornerShape(3.dp)),
            )
            Text("ACTIVITY", style = TrelloText.LabelUpper, color = TrelloColors.TextSecondary)
            // comment rows…
        }
    }
}

4. Drag-and-Drop Lift

// Use Modifier.pointerInput(detectDragGesturesAfterLongPress) to set isDragging.
// Visual contract: scale 1.03 + navy-tinted Level-3 shadow + a placeholder gap.
@Composable
fun DropPlaceholder() {
    Box(
        Modifier
            .fillMaxWidth()
            .height(48.dp)
            .clip(RoundedCornerShape(8.dp))
            .background(TrelloColors.ShadowNavy.copy(alpha = 0.14f)),
    )
}

5. Navigation (no bottom bar)

// Trello has NO bottom NavigationBar. The board is the primary surface.
// Use a Navigation graph: a boards list -> a board route that owns the screen
// and scrolls horizontally. Do NOT add Material 3 NavigationBar.

@Composable
fun TrelloNavHost(nav: NavHostController) {
    NavHost(nav, startDestination = "boards") {
        composable("boards") { BoardsListScreen(onOpen = { nav.navigate("board/$it") }) }
        composable("board/{id}") { TrelloBoard(boardName = "Sprint 12", lists = demoLists()) }
    }
}

6. Motion

Moment Compose recipe
Card lift on drag animateFloatAsState scale 1→1.03 spring() + animateDpAsState elevation 1→24.dp (navy ambient/spot color)
Card drop spring settle back to scale 1 / 1.dp elevation
Placeholder gap AnimatedVisibility expand of the DropPlaceholder over ~160ms
Card detail present ModalBottomSheet default slide (~300ms) over a scrim
Composer expand animateContentSize() on the list footer, ~180ms
Checklist progress animateFloatAsState feeding LinearProgressIndicator, ~200ms

Haptics: LocalHapticFeedback.current.performHapticFeedback(HapticFeedbackType.LongPress) when a card is picked up; HapticFeedbackType.TextHandleMove (or a short Vibrator one-shot) when it snaps into a new slot.

7. Icons

Closest first-party set is androidx.compose.material:material-icons-extended. For exact parity, export Trello glyphs as vector drawables and load via ImageVector.vectorResource(...).

Purpose SF Symbol (iOS) Material Icon (Compose)
Back chevron.left Icons.Filled.ArrowBack
Board overflow ellipsis Icons.Filled.MoreHoriz
Filter (on board) line.3.horizontal.decrease Icons.Filled.FilterList
Search magnifyingglass Icons.Filled.Search
Add card / list plus Icons.Filled.Add
Close composer xmark Icons.Filled.Close
Due date badge clock Icons.Filled.Schedule
Checklist badge checklist Icons.Filled.CheckBox
Comment badge text.bubble Icons.Filled.ChatBubbleOutline
Member person.crop.circle.fill Icons.Filled.AccountCircle
Attachment paperclip Icons.Filled.AttachFile
Move card arrow.right.square Icons.Filled.DriveFileMove
Checklist item checkmark.square / square Icons.Filled.CheckBox / Icons.Outlined.CheckBoxOutlineBlank

8. Minimum SDK & Accessibility Notes

  • minSdk 24 (Compose floor is 21; ModalBottomSheet + drag gestures are comfortable at 24). compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+.
  • Edge-to-edge: call enableEdgeToEdge(); on a colored board use light-content system bars (WindowCompat), on white screens use dark-content. No bottom bar to inset — the board scrolls beneath the on-board header.
  • Font scaling: sp honors the user’s scale — cards grow taller to fit titles. Pin badges and label chips with a fixed Density wrapper where layout is rigid.
  • TalkBack: a card’s contentDescription should read “Card, <title>, 2 labels, due tomorrow, checklist 3 of 8”; expose “Move card” via Modifier.semantics { customActions = ... } since drag is hard for screen readers; mark the board name with Modifier.semantics { heading() }.
  • Touch targets: Material minimum is 48.dp. The whole card is the drag/tap target; give 32.dp on-board chips and 16–24.dp icons a 48.dp hit area via padding; checklist boxes get 48.dp.
  • Contrast: navy #172B4D on white cards passes WCAG AAA; on-board white text requires the scrim — never white on a light photo without Color.Black.copy(alpha = 0.16f+) behind it. #5E6C84 on white passes AA at 12sp+ (validate badges).
  • Color independence: label colors must not be the only signal — show label text in expanded mode so color-blind users can distinguish them.
  • Reduced motion: honor Settings.Global.ANIMATOR_DURATION_SCALE == 0 by replacing the drag scale with an alpha change and instant placeholder.
  • Dynamic color: do not enable Material You dynamicLightColorScheme() — Trello’s action blue #0C66E4 and the functional label palette must stay fixed; the board background is user-chosen, not wallpaper-derived.
All mobile designs →

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