monday.com (iOS) — Jetpack Compose Implementation Guide
Companion to DESIGN.md — the framework-neutral spec. This file ports monday.com’s visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, paste-ready @Composables, the colorful board + full-bleed status cells + battery rollup, 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 (monday.com’s deep-indigo canvas, full-bleed saturated status cells, the leading group-color stripe, bold Figtree headers) while making everything idiomatic Android —
NavigationBarinstead of a UITabBar, aFloatingActionButton, aModalBottomSheetlabel picker,dp/spinstead ofpt.
Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, and Coil 2.6+ for avatars. No color extraction — monday.com’s palette is a fixed multicolor set, so Palette is not needed. monday.com is light-first; a full deep-indigo night scheme is provided.
1. Color Tokens
// ui/theme/MondayColors.kt
import androidx.compose.ui.graphics.Color
object MondayColors {
// Canvas & Surfaces (Light)
val Canvas = Color(0xFFFFFFFF)
val SurfaceGray = Color(0xFFF6F7FB)
val SurfacePressed = Color(0xFFEDEFF5)
val Divider = Color(0xFFE6E9F0)
val CellBorder = Color(0xFFD0D4E4)
// Canvas & Surfaces (Night)
val NightCanvas = Color(0xFF181B34) // deep indigo — NOT pure black
val NightSurface1 = Color(0xFF20243F)
val NightSurface2 = Color(0xFF292F4C)
val NightDivider = Color(0xFF353B5C)
val NightCellBorder = Color(0xFF3D436A)
// Text
val TextPrimary = Color(0xFF323338)
val TextSecondary = Color(0xFF676879)
val TextTertiary = Color(0xFF9699A6)
val DarkTextPrimary = Color(0xFFF5F6F8)
val DarkTextSecondary = Color(0xFFA7AFC6)
val DarkTextTertiary = Color(0xFF6B7394)
// Action (the only "UI" color amid the data colors)
val Blue = Color(0xFF0073EA)
val BluePressed = Color(0xFF0060C2)
// Multicolor data palette (full-bleed fills)
val Red = Color(0xFFE2445C)
val Orange = Color(0xFFFDAB3D)
val Green = Color(0xFF00C875)
val BrightBlue = Color(0xFF579BFC)
val Purple = Color(0xFFA25DDC)
val DarkPurple = Color(0xFF401694)
val Indigo = Color(0xFF5559DF)
val Yellow = Color(0xFFFFCB00)
val Teal = Color(0xFF00D2D2)
val Pink = Color(0xFFFF158A)
val BrightGreen = Color(0xFF9CD326)
val Navy = Color(0xFF333D6E)
// Semantic
val Success = Color(0xFF00C875)
val Error = Color(0xFFE2445C)
val Warning = Color(0xFFFDAB3D)
}
// On-color text: yellow / teal / bright-green flip to dark
private val DarkOn = setOf(MondayColors.Yellow, MondayColors.Teal, MondayColors.BrightGreen)
fun onColor(bg: Color): Color = if (bg in DarkOn) MondayColors.TextPrimary else Color.White
data class StatusOption(val label: String, val color: Color)
val defaultStatuses = listOf(
StatusOption("Done", MondayColors.Green),
StatusOption("Working on it", MondayColors.Orange),
StatusOption("Stuck", MondayColors.Red),
StatusOption("Not started", MondayColors.Navy),
)
Wire it into both schemes. monday.com is light-first; the night scheme uses the signature #181B34 deep indigo, never true black.
// 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 MondayLight = lightColorScheme(
primary = MondayColors.Blue,
onPrimary = Color.White,
background = MondayColors.Canvas,
onBackground = MondayColors.TextPrimary,
surface = MondayColors.SurfaceGray,
onSurface = MondayColors.TextPrimary,
surfaceVariant = MondayColors.SurfacePressed,
outline = MondayColors.Divider,
error = MondayColors.Error,
)
private val MondayNight = darkColorScheme(
primary = MondayColors.Blue, // action blue identical in night
onPrimary = Color.White,
background = MondayColors.NightCanvas,
onBackground = MondayColors.DarkTextPrimary,
surface = MondayColors.NightSurface1,
onSurface = MondayColors.DarkTextPrimary,
surfaceVariant = MondayColors.NightSurface2,
outline = MondayColors.NightDivider,
error = MondayColors.Error,
)
@Composable
fun MondayTheme(
dark: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) = MaterialTheme(
colorScheme = if (dark) MondayNight else MondayLight,
typography = MondayTypography,
content = content,
)
2. Typography
monday.com uses Figtree (Poppins-class brand sans) — drop the TTFs in res/font/. Bold and confident: item names 500, group headers 800.
// ui/theme/MondayType.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 Figtree = FontFamily(
Font(R.font.figtree_regular, FontWeight.Normal),
Font(R.font.figtree_medium, FontWeight.Medium),
Font(R.font.figtree_semibold, FontWeight.SemiBold),
Font(R.font.figtree_bold, FontWeight.Bold),
Font(R.font.figtree_extrabold, FontWeight.ExtraBold),
)
object MondayText {
val ScreenTitle = TextStyle(Figtree, fontWeight = FontWeight.ExtraBold, fontSize = 32.sp, lineHeight = 37.sp, letterSpacing = (-0.5).sp)
val BoardTitle = TextStyle(Figtree, fontWeight = FontWeight.Bold, fontSize = 26.sp, lineHeight = 31.sp, letterSpacing = (-0.3).sp)
val GroupHeader = TextStyle(Figtree, fontWeight = FontWeight.ExtraBold, fontSize = 22.sp, lineHeight = 26.sp, letterSpacing = (-0.2).sp) // color per group
val Section = TextStyle(Figtree, fontWeight = FontWeight.Bold, fontSize = 18.sp, lineHeight = 23.sp)
val Body = TextStyle(Figtree, fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp)
val ItemName = TextStyle(Figtree, fontWeight = FontWeight.Medium, fontSize = 14.sp, lineHeight = 19.sp)
val ColumnHead = TextStyle(Figtree, fontWeight = FontWeight.SemiBold, fontSize = 13.sp, lineHeight = 16.sp, letterSpacing = 0.2.sp)
val Meta = TextStyle(Figtree, fontWeight = FontWeight.Normal, fontSize = 14.sp, lineHeight = 19.sp)
val StatusLabel = TextStyle(Figtree, fontWeight = FontWeight.Bold, fontSize = 11.sp, lineHeight = 12.sp, letterSpacing = 0.2.sp)
val Button = TextStyle(Figtree, fontWeight = FontWeight.SemiBold, fontSize = 15.sp, lineHeight = 15.sp)
val TextAction = TextStyle(Figtree, fontWeight = FontWeight.SemiBold, fontSize = 14.sp, lineHeight = 14.sp)
val PersonInit = TextStyle(Figtree, fontWeight = FontWeight.Bold, fontSize = 9.sp, lineHeight = 9.sp, letterSpacing = 0.2.sp)
val Tab = TextStyle(Figtree, fontWeight = FontWeight.SemiBold, fontSize = 10.sp, lineHeight = 10.sp, letterSpacing = 0.1.sp)
}
// Map onto Material 3 slots so stock components inherit the brand
val MondayTypography = Typography(
headlineLarge = MondayText.ScreenTitle,
headlineMedium = MondayText.BoardTitle,
titleMedium = MondayText.Section,
bodyMedium = MondayText.Body,
labelSmall = MondayText.Tab,
)
3. Signature Components
Board Item Row (stripe + cells)
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
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.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@Composable
fun StatusCell(label: String, color: Color, modifier: Modifier = Modifier) {
Box(
modifier
.widthIn(min = 88.dp)
.clip(RoundedCornerShape(4.dp))
.background(color)
.padding(horizontal = 10.dp, vertical = 5.dp),
contentAlignment = Alignment.Center,
) {
Text(label.uppercase(), style = MondayText.StatusLabel.copy(color = onColor(color)))
}
}
@Composable
fun BoardItemRow(
groupColor: Color,
name: String,
personInitials: String,
personColor: Color,
status: StatusOption,
modifier: Modifier = Modifier,
) {
Column(modifier.fillMaxWidth()) {
Row(Modifier.fillMaxWidth().height(IntrinsicSize.Min)) {
Box(Modifier.width(6.dp).fillMaxHeight().background(groupColor)) // group stripe, flush leading
Row(
Modifier.weight(1f).padding(horizontal = 14.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
Text(name, style = MondayText.ItemName.copy(color = MondayColors.TextPrimary),
maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f))
Box(
Modifier.size(24.dp).clip(CircleShape).background(personColor),
contentAlignment = Alignment.Center,
) { Text(personInitials, style = MondayText.PersonInit.copy(color = Color.White)) }
StatusCell(status.label, status.color)
}
}
HorizontalDivider(color = MondayColors.Divider, thickness = 1.dp)
}
}
Group Header (colored, collapsible)
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.ui.draw.rotate
@Composable
fun GroupHeader(name: String, groupColor: Color, count: Int, expanded: Boolean, onToggle: () -> Unit) {
val rot by animateFloatAsState(if (expanded) 90f else 0f, tween(150), label = "groupCaret")
Row(
Modifier
.fillMaxWidth()
.clickable { onToggle() }
.padding(horizontal = 18.dp).padding(top = 12.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Icon(Icons.Filled.ChevronRight, null, tint = groupColor, modifier = Modifier.size(14.dp).rotate(rot))
Text(name, style = MondayText.GroupHeader.copy(color = groupColor))
Text("$count items", style = MondayText.ColumnHead.copy(color = MondayColors.TextTertiary))
}
}
Status Label Picker (ModalBottomSheet grid)
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.material3.ModalBottomSheet
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun StatusPickerSheet(
options: List<StatusOption> = defaultStatuses,
onPick: (StatusOption) -> Unit,
onDismiss: () -> Unit,
) {
ModalBottomSheet(onDismissRequest = onDismiss, containerColor = MondayColors.Canvas) {
FlowRow(
Modifier.fillMaxWidth().padding(20.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
options.forEach { opt ->
Box(
Modifier
.weight(1f, fill = false)
.defaultMinSize(minWidth = 140.dp)
.clip(RoundedCornerShape(4.dp))
.background(opt.color)
.clickable { onPick(opt) }
.padding(vertical = 14.dp),
contentAlignment = Alignment.Center,
) { Text(opt.label.uppercase(), style = MondayText.StatusLabel.copy(color = onColor(opt.color))) }
}
}
}
}
Battery Rollup (group health)
@Composable
fun BatteryRollup(segments: List<Pair<Color, Float>>, modifier: Modifier = Modifier) {
Row(
modifier
.fillMaxWidth()
.height(24.dp)
.clip(RoundedCornerShape(4.dp))
.background(MondayColors.NightSurface2),
) {
segments.forEach { (color, fraction) ->
val animated by animateFloatAsState(fraction, tween(250), label = "battery")
Box(Modifier.weight(animated.coerceAtLeast(0.0001f)).fillMaxHeight().background(color))
}
}
}
Floating Action Button
import androidx.compose.material.icons.filled.Add
@Composable
fun CreateFab(onClick: () -> Unit) {
FloatingActionButton(
onClick = onClick,
containerColor = MondayColors.Blue,
contentColor = Color.White,
shape = CircleShape,
modifier = Modifier.size(56.dp),
) { Icon(Icons.Filled.Add, contentDescription = "Create", modifier = Modifier.size(24.dp)) }
}
4. Navigation
monday.com has minimal chrome: a 4-tab bottom strip with the action-blue accent (no Material tint pill) and per-board view tabs.
import androidx.compose.material.icons.filled.*
@Composable
fun MondayBottomBar(selected: Int, onSelect: (Int) -> Unit) {
NavigationBar(containerColor = MondayColors.Canvas, tonalElevation = 0.dp) {
val items = listOf(
"Home" to Icons.Filled.Home,
"Boards" to Icons.Filled.GridView,
"Inbox" to Icons.Filled.Notifications,
"Search" to Icons.Filled.Search,
)
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 = MondayText.Tab) },
colors = NavigationBarItemDefaults.colors(
selectedIconColor = MondayColors.Blue,
selectedTextColor = MondayColors.Blue,
unselectedIconColor = MondayColors.TextTertiary,
unselectedTextColor = MondayColors.TextTertiary,
indicatorColor = Color.Transparent, // no Material pill — monday.com has none
),
)
}
}
}
The per-board view switcher (Main Table / Kanban / Timeline / Calendar / Chart) is a Row of clickable labels; the active one gets TextPrimary plus a 2.5.dp blue underline via Modifier.drawBehind. The Kanban view is a horizontally scrolling LazyRow of ~260.dp columns containing draggable cards; dropping a card into a column changes its status and recolors the stripe.
5. Motion
monday.com motion is energetic but purposeful — color transitions carry feedback.
| Moment | Compose recipe |
|---|---|
| Status change | ModalBottomSheet picker (Material slide-up); on pick, animate cell color via animateColorAsState(tween(200)); soft haptic |
| Group collapse/expand | caret animateFloatAsState 0 → 90° tween(150); items AnimatedVisibility(expandVertically(tween(200)) + fadeIn()) |
| FAB → new item | navigate to a full-screen / bottom-sheet item editor; FAB container scale on press |
| Drag item / Kanban card | LazyColumn/LazyRow + Modifier.draggable; neighbors animateItemPlacement(tween(200)); soft haptic on drop; stripe + status recolor |
| Battery rollup | each segment animateFloatAsState(tween(250)) on a status change |
| View tab switch | 2.5.dp blue underline animateDpAsState translateX 200ms; board Crossfade(tween(150)) |
| Confetti | board milestone → a brief Canvas-drawn confetti burst (~1s) |
| Page navigation | Nav3/NavHost slide push tween(300) |
// The canonical monday.com feedback — status cell color crossfade
val cellColor by animateColorAsState(status.color, tween(200), label = "statusCell")
Box(Modifier.background(cellColor)) { /* label */ }
Haptics: LocalHapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) on status change, drag start, and group toggle. For a softer tick use LocalView.current.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK). Auto-save is silent; only show a “Saved” snackbar on manual save or error.
6. Icons
monday.com’s iconography maps cleanly to androidx.compose.material:material-icons-extended. The group stripe and status cell are drawn shapes, not icons.
| Purpose | SF Symbol (iOS) | Material Icon (Compose) |
|---|---|---|
| Home (tab) | house / house.fill |
Icons.Filled.Home |
| Boards (tab) | tablecells |
Icons.Filled.GridView |
| Inbox (tab) | bell / bell.fill |
Icons.Filled.Notifications |
| Search (tab) | magnifyingglass |
Icons.Filled.Search |
| Add / FAB | plus |
Icons.Filled.Add |
| Group caret | chevron.right |
Icons.Filled.ChevronRight (rotate 90°) |
| Person (empty) | person.crop.circle |
Icons.Filled.AccountCircle |
| Date column | calendar |
Icons.Filled.CalendarToday |
| Timeline column | chart.bar.xaxis |
Icons.Filled.Timeline |
| Numbers column | number |
Icons.Filled.Tag |
| More | ellipsis |
Icons.Filled.MoreHoriz |
| Back | chevron.left |
Icons.Filled.ArrowBack |
| Filter | line.3.horizontal.decrease |
Icons.Filled.FilterList |
| Sort | arrow.up.arrow.down |
Icons.Filled.SwapVert |
| Updates | bubble.left |
Icons.Filled.ChatBubbleOutline |
| Automation | bolt |
Icons.Filled.Bolt |
| Main Table view | tablecells |
Icons.Filled.GridOn |
| Kanban view | rectangle.split.3x1 |
Icons.Filled.ViewKanban |
| Timeline view | calendar.day.timeline.left |
Icons.Filled.AccountTree |
| Chart view | chart.pie |
Icons.Filled.PieChart |
7. Minimum SDK & Accessibility Notes
- minSdk 24 (Compose floor is 21; modern motion comfortable at 24).
compileSdk 34+, Compose BOM2024.09+, Kotlin2.0+. - Edge-to-edge: call
enableEdgeToEdge(); light canvas wants dark-content system bars (light-content in night). The FAB sits above theNavigationBarand respects the gesture inset; inline cell edit usesModifier.imePadding(). - Font scaling:
sphonors the user’s font scale — keep it on screen/board/group titles, body, item names. Pin layout-sensitive text (status labels, 10sp tab labels, person initials, column headers) by deriving fromdporCompositionLocalProvider(LocalDensity provides Density(density, fontScale = 1f)). AddfontFeatureSettings = "tnum"on Numbers/date/percentage text. - TalkBack: never rely on cell color alone — always render the label text and set
contentDescription = "Status: {label}, double tap to change"; announce the group stripe via group context (“Group {name}, item {name}”); the battery rollup as “Group progress: {x}% done, {y}% working, {z}% stuck”. Status cell isRole.Button. - Touch targets: Material guidance is 48.dp — the status cell row is ≥ 48.dp tall and fully tappable; give the 14.dp group caret a 48.dp hit area; the FAB is 56.dp; primary buttons ≥ 36.dp.
- Contrast: white on
#00C875/#FDAB3D/#E2445C/#0073EApasses WCAG AA at 11sp/700. Yellow#FFCB00, teal#00D2D2, and bright-green#9CD326MUST use#323338text (white fails) — theonColor()helper enforces this; validate any user-remapped color. - Reduce motion: when
Settings.Global.ANIMATOR_DURATION_SCALE == 0, disable the cell-color crossfade and battery-segment animation (snap instead) and skip confetti; keep the soft haptic and selection state. - Night mode: invert chrome via the
Night*palette —#181B34deep indigo, NOT true black; text →#F5F6F8. Keep the multicolor data palette fully saturated and identical — meaning must never wash out. Shadows vanish on night, so the 1dpNightCellBorderon Kanban cards is the elevation cue; the FAB keeps its blue surface. Do not enable Material YoudynamicColorScheme()— monday.com’s multicolor identity must hold regardless of wallpaper.