coderef

Tumblr

Tumblr's iOS app is a deep-navy canvas built for an endlessly scrolling dashboard of expressive, multi-format posts. The signature background is a rich midnight blue (#001935) — not black, not gray, a saturated navy that feels like the…
DownloadRaw
Tumblr

Build with the Tumblr look.

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

Palette

At a glance

Theme
dark
Typefaces
SF Pro Text, SF Pro Display, Georgia
Sections
8
Length
3,293 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · Tumblr

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

Tumblr (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports Tumblr’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 (Tumblr’s deep-navy dashboard, the blue/green/pink accent trio, the heart-burst, the indented reblog chain, the tag bar) while making everything idiomatic Android — NavigationBar with a docked center FAB via Scaffold, Canvas/Animatable particles instead of UIKit, sp/dp instead of pt. Tumblr also ships on Android, so this maps cleanly.

Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, and Coil 2.6+ (with coil-gif for animated GIFs).

1. Color Tokens

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

object TumblrColors {
    // Canvas & Surfaces — deep navy, the default
    val Canvas   = Color(0xFF001935)
    val Surface1 = Color(0xFF001020)
    val Surface2 = Color(0xFF0B2A45)
    val Surface3 = Color(0xFF13395B)
    val Divider  = Color(0xFF36465D)

    // Text
    val TextPrimary   = Color(0xFFFFFFFF)
    val TextSecondary = Color(0xFF8A9AAE)
    val TextTertiary  = Color(0xFF5C6B80)

    // Brand trio
    val Blue        = Color(0xFF00B8FF)
    val BluePressed = Color(0xFF0090CC)
    val Green       = Color(0xFF00CF35)
    val PinkRed     = Color(0xFFFF4930)

    // Semantic
    val Warning = Color(0xFFFFB300)
}

Wire it into a Material 3 darkColorScheme. Tumblr’s iOS default and brand-defining experience is the deep navy; a light theme exists but is opt-in. Provide the dark scheme by default.

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

private val TumblrScheme = darkColorScheme(
    primary        = TumblrColors.Blue,
    onPrimary      = TumblrColors.Canvas,        // dark navy text on bright blue
    secondary      = TumblrColors.Green,
    background      = TumblrColors.Canvas,
    onBackground    = TumblrColors.TextPrimary,
    surface         = TumblrColors.Surface2,
    onSurface       = TumblrColors.TextPrimary,
    surfaceVariant  = TumblrColors.Surface3,
    outline         = TumblrColors.Divider,
    error           = TumblrColors.PinkRed,
)

@Composable
fun TumblrTheme(content: @Composable () -> Unit) =
    MaterialTheme(colorScheme = TumblrScheme, typography = TumblrTypography, content = content)

2. Typography

Tumblr uses the system font; the web client renders Inter/Helvetica. Reference FontFamily.Default (Roboto) — or drop Inter in res/font/ for parity. Use a serif (FontFamily.Serif) for quote-type posts.

// ui/theme/TumblrType.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, 16sp body at 1.5)
object TumblrText {
    val TitleLarge  = TextStyle(Sys, fontWeight = FontWeight.Bold,   fontSize = 28.sp, lineHeight = 34.sp, letterSpacing = (-0.4).sp)
    val Quote       = TextStyle(FontFamily.Serif, fontWeight = FontWeight.Bold, fontSize = 26.sp, lineHeight = 34.sp, letterSpacing = (-0.3).sp)
    val Section     = TextStyle(Sys, fontWeight = FontWeight.Bold,   fontSize = 20.sp, lineHeight = 25.sp, letterSpacing = (-0.2).sp)
    val BlogName    = TextStyle(Sys, fontWeight = FontWeight.Bold,   fontSize = 16.sp, lineHeight = 20.sp)
    val Body        = TextStyle(Sys, fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp)   // 1.5
    val Reblog      = TextStyle(Sys, fontWeight = FontWeight.Normal, fontSize = 15.sp, lineHeight = 22.sp)
    val BodySettings= TextStyle(Sys, fontWeight = FontWeight.Normal, fontSize = 15.sp, lineHeight = 21.sp)
    val Tag         = TextStyle(Sys, fontWeight = FontWeight.Normal, fontSize = 14.sp, lineHeight = 18.sp)
    val Notes       = TextStyle(Sys, fontWeight = FontWeight.SemiBold, fontSize = 13.sp, lineHeight = 17.sp)
    val Button      = TextStyle(Sys, fontWeight = FontWeight.Bold,   fontSize = 16.sp, lineHeight = 20.sp)
    val Tab         = TextStyle(Sys, fontWeight = FontWeight.SemiBold, fontSize = 10.sp, lineHeight = 12.sp)
    val ReblogSrc   = TextStyle(Sys, fontWeight = FontWeight.Bold,   fontSize = 14.sp, lineHeight = 18.sp)
}

val TumblrTypography = Typography(
    headlineLarge = TumblrText.TitleLarge,
    headlineSmall = TumblrText.Section,
    titleMedium   = TumblrText.BlogName,
    bodyMedium    = TumblrText.Body,
    labelSmall    = TumblrText.Tab,
)

3. Signature Components

Dashboard Post Card (the core unit)

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
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 PostCard(
    blogName: String,
    body: String?,
    tags: List<String>,
    notes: Int,
    media: (@Composable () -> Unit)? = null,
    modifier: Modifier = Modifier,
) {
    Column(
        modifier
            .fillMaxWidth()
            .padding(horizontal = 8.dp)
            .padding(bottom = 8.dp)
            .clip(RoundedCornerShape(8.dp))
            .background(TumblrColors.Surface2),
    ) {
        Row(
            Modifier.padding(12.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(10.dp),
        ) {
            Box(Modifier.size(36.dp).clip(RoundedCornerShape(6.dp)).background(TumblrColors.Surface3))
            Text(blogName, style = TumblrText.BlogName, color = TumblrColors.TextPrimary)
            Spacer(Modifier.weight(1f))
            FollowButton()
            Icon(Icons.Filled.MoreHoriz, null, tint = TumblrColors.TextSecondary, modifier = Modifier.size(22.dp))
        }

        media?.invoke()   // full-bleed inside the card, no letterboxing

        if (!body.isNullOrEmpty()) {
            Text(body, style = TumblrText.Body, color = TumblrColors.TextPrimary,
                modifier = Modifier.padding(12.dp))
        }

        TagBar(tags)

        Row(
            Modifier.fillMaxWidth().padding(horizontal = 12.dp).padding(bottom = 12.dp),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Text("${"%,d".format(notes)} notes", style = TumblrText.Notes, color = TumblrColors.TextSecondary)
            Spacer(Modifier.weight(1f))
            Icon(Icons.Outlined.ChatBubbleOutline, "Reply", tint = TumblrColors.TextSecondary,
                modifier = Modifier.size(22.dp).padding(end = 0.dp))
            Spacer(Modifier.width(16.dp))
            ReblogButton()
            Spacer(Modifier.width(16.dp))
            LikeButton(notes)
            Spacer(Modifier.width(16.dp))
            Icon(Icons.Filled.Share, "Share", tint = TumblrColors.TextSecondary, modifier = Modifier.size(22.dp))
        }
    }
}

Like Button with Heart-Burst (the signature)

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import kotlin.math.cos
import kotlin.math.sin

@Composable
fun LikeButton(notes: Int, modifier: Modifier = Modifier) {
    var liked by remember { mutableStateOf(false) }
    val t = remember { Animatable(0f) }
    val haptics = LocalHapticFeedback.current
    val scope = rememberCoroutineScope()
    val particles = 8

    Box(modifier.size(44.dp), contentAlignment = Alignment.Center) {
        // Confetti hearts
        Canvas(Modifier.matchParentSize()) {
            if (t.value > 0f && t.value < 1f) {
                val cx = size.width / 2; val cy = size.height / 2
                repeat(particles) { i ->
                    val a = i.toFloat() / particles * 2f * Math.PI.toFloat()
                    val r = 26.dp.toPx() * t.value
                    val px = cx + cos(a) * r
                    val py = cy + sin(a) * r - 8.dp.toPx() * t.value
                    val s = (1f - 0.7f * t.value) * 6.dp.toPx()
                    val heart = Path().apply {
                        moveTo(px, py + s * 0.3f)
                        cubicTo(px, py - s * 0.3f, px - s, py - s * 0.3f, px - s, py + s * 0.2f)
                        cubicTo(px - s, py + s * 0.6f, px, py + s, px, py + s * 1.1f)
                        cubicTo(px, py + s, px + s, py + s * 0.6f, px + s, py + s * 0.2f)
                        cubicTo(px + s, py - s * 0.3f, px, py - s * 0.3f, px, py + s * 0.3f)
                    }
                    drawPath(heart, color = TumblrColors.PinkRed, alpha = 1f - t.value)
                }
            }
        }
        IconButton(onClick = {
            liked = !liked
            if (liked) {
                haptics.performHapticFeedback(HapticFeedbackType.TextHandleMove)
                scope.launch { t.snapTo(0f); t.animateTo(1f, tween(600)) }
            }
        }) {
            Icon(
                if (liked) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
                contentDescription = if (liked) "Liked" else "Like",
                tint = if (liked) TumblrColors.PinkRed else TumblrColors.TextSecondary,
                modifier = Modifier.size(22.dp),
            )
        }
    }
}

Reblog Button

import androidx.compose.animation.core.spring
import androidx.compose.ui.draw.scale

@Composable
fun ReblogButton(modifier: Modifier = Modifier) {
    var reblogged by remember { mutableStateOf(false) }
    val scale = remember { Animatable(1f) }
    val haptics = LocalHapticFeedback.current
    val scope = rememberCoroutineScope()

    IconButton(
        onClick = {
            reblogged = !reblogged
            haptics.performHapticFeedback(HapticFeedbackType.LongPress)
            scope.launch { scale.animateTo(1.15f, spring()); scale.animateTo(1f, spring()) }
        },
        modifier = modifier.size(44.dp),
    ) {
        Icon(
            Icons.Filled.Repeat,
            contentDescription = if (reblogged) "Reblogged" else "Reblog",
            tint = if (reblogged) TumblrColors.Green else TumblrColors.TextSecondary,
            modifier = Modifier.size(22.dp).scale(scale.value),
        )
    }
}

Reblog Chain (the signature structure)

data class ReblogSegment(val sourceBlog: String, val comment: String, val depth: Int) // 0 = original

@Composable
fun ReblogChain(segments: List<ReblogSegment>, maxDepth: Int, modifier: Modifier = Modifier) {
    Column(modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
        segments.forEach { seg ->
            val indent = ((maxDepth - seg.depth) * 12).dp
            Row(Modifier.padding(start = indent)) {
                if (indent > 0.dp) {
                    Box(Modifier.width(2.dp).fillMaxHeight().background(TumblrColors.Divider))
                    Spacer(Modifier.width(12.dp))
                }
                Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
                    Text(seg.sourceBlog, style = TumblrText.ReblogSrc, color = TumblrColors.Blue)
                    Text(seg.comment, style = TumblrText.Reblog, color = TumblrColors.TextPrimary)
                }
            }
        }
    }
}

Buttons (Follow / Primary / FAB)

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.shape.CircleShape

@Composable
fun FollowButton(modifier: Modifier = Modifier) {
    var following by remember { mutableStateOf(false) }
    if (following) {
        Button(
            onClick = { following = false }, modifier = modifier,
            shape = RoundedCornerShape(6.dp),
            colors = ButtonDefaults.buttonColors(containerColor = TumblrColors.Green, contentColor = TumblrColors.Canvas),
            contentPadding = PaddingValues(horizontal = 18.dp, vertical = 8.dp),
        ) { Text("Following", style = TumblrText.Button) }
    } else {
        OutlinedButton(
            onClick = { following = true }, modifier = modifier,
            shape = RoundedCornerShape(6.dp),
            border = BorderStroke(1.5.dp, TumblrColors.Green),
            colors = ButtonDefaults.outlinedButtonColors(contentColor = TumblrColors.Green),
            contentPadding = PaddingValues(horizontal = 18.dp, vertical = 8.dp),
        ) { Text("Follow", style = TumblrText.Button) }
    }
}

@Composable
fun TmblrPrimaryButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
    Button(
        onClick = onClick, modifier = modifier,
        shape = RoundedCornerShape(6.dp),
        colors = ButtonDefaults.buttonColors(containerColor = TumblrColors.Blue, contentColor = TumblrColors.Canvas),
        contentPadding = PaddingValues(horizontal = 24.dp, vertical = 12.dp),
    ) { Text(text, style = TumblrText.Button) }
}

@Composable
fun NewPostFab(onClick: () -> Unit) {
    val haptics = LocalHapticFeedback.current
    FloatingActionButton(
        onClick = { haptics.performHapticFeedback(HapticFeedbackType.LongPress); onClick() },
        containerColor = TumblrColors.Blue,
        contentColor = TumblrColors.Canvas,
        shape = CircleShape,
        modifier = Modifier.size(52.dp),
    ) { Icon(Icons.Filled.Add, contentDescription = "New post", modifier = Modifier.size(24.dp)) }
}

Tag Bar

import androidx.compose.foundation.layout.FlowRow

@Composable
fun TagBar(tags: List<String>, modifier: Modifier = Modifier) {
    FlowRow(
        modifier
            .fillMaxWidth()
            .padding(horizontal = 12.dp)
            .padding(bottom = 8.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
    ) {
        tags.forEach { tag ->
            Text("#$tag", style = TumblrText.Tag, color = TumblrColors.TextSecondary)
        }
    }
}

4. Distinctive System — Heart-Burst & Reblog Chain in Compose

The two souls of Tumblr are coded above. LikeButton uses a single Animatable t driving a Canvas that draws 8 heart Paths on radial vectors, fading and shrinking over 600ms — for double-tap-on-media, overlay the same Canvas/t centered on the image and reuse the animate call. ReblogChain renders progressively-indented attributed segments with a 2.dp #36465D left rule, blue source names, newest comment unindented at the bottom; keep the per-level indent at 12.dp.

5. Bottom Navigation (Tab Bar with Center FAB)

Material 3 has no built-in center-docked FAB in NavigationBar. Compose it via Scaffold — a NavigationBar with a gap, plus a FloatingActionButton placed FabPosition.Center so the blue New-post button rises out of the navy bar.

@Composable
fun TumblrScaffold(selected: Int, onSelect: (Int) -> Unit, onNewPost: () -> Unit,
                   content: @Composable (PaddingValues) -> Unit) {
    Scaffold(
        containerColor = TumblrColors.Canvas,
        floatingActionButton = { NewPostFab(onNewPost) },
        floatingActionButtonPosition = FabPosition.Center,
        bottomBar = {
            NavigationBar(containerColor = TumblrColors.Surface1, tonalElevation = 0.dp) {
                val items = listOf(
                    "Home"   to Icons.Filled.Home,
                    "Search" to Icons.Filled.Search,
                    null     to null,                          // gap for the docked FAB
                    "Activity" to Icons.Filled.Notifications,
                    "Account"  to Icons.Filled.AccountBox,
                )
                items.forEachIndexed { i, pair ->
                    if (pair.first == null) {
                        Spacer(Modifier.weight(1f))
                    } else {
                        NavigationBarItem(
                            selected = selected == i,
                            onClick = { onSelect(i) },
                            icon = { Icon(pair.second!!, contentDescription = pair.first, modifier = Modifier.size(26.dp)) },
                            label = { Text(pair.first!!, style = TumblrText.Tab) },
                            colors = NavigationBarItemDefaults.colors(
                                selectedIconColor   = TumblrColors.Blue,
                                selectedTextColor   = TumblrColors.Blue,
                                unselectedIconColor = TumblrColors.TextSecondary,
                                unselectedTextColor = TumblrColors.TextSecondary,
                                indicatorColor      = TumblrColors.Blue.copy(alpha = 0.12f),
                            ),
                        )
                    }
                }
            }
        },
        content = content,
    )
}

A 0.5.dp HorizontalDivider(color = TumblrColors.Divider) above the bar gives the hairline; the #001020 bar sits on the navy canvas.

6. Motion

Moment Compose recipe
Heart-burst one Animatable t 0 → 1 over tween(600) drives a Canvas of 8 heart paths on radial vectors, fading + shrinking; HapticFeedbackType.TextHandleMove
Reblog Animatable 1 → 1.15 → 1 spring(), tint → Green, HapticFeedbackType.LongPress
Double-tap media overlay the same burst Canvas centered on the image, reuse the animate call
FAB press FAB ripple + soft haptic; post-type picker via ModalBottomSheet rising ~300ms
Tag press brief tint flash to Blue then navigate to the tag route
// Heart-burst (see LikeButton): t.snapTo(0f); t.animateTo(1f, tween(600))
// Reblog (see ReblogButton): scale.animateTo(1.15f, spring()); scale.animateTo(1f, spring())

Haptics: prefer LocalHapticFeedback. For the light like-tap use LocalView.current.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) (API 30+); for reblog use CONFIRM.

7. Icons

The closest first-party set is androidx.compose.material:material-icons-extended. For the Tumblr “t” wordmark, ship a vector drawable and load via ImageVector.vectorResource(R.drawable.tumblr_t).

Purpose SF Symbol (iOS) Material Icon (Compose)
Reply bubble.left Icons.Outlined.ChatBubbleOutline
Reblog arrow.2.squarepath Icons.Filled.Repeat
Like heart / heart.fill Icons.Outlined.FavoriteBorder / Icons.Filled.Favorite
Share square.and.arrow.up Icons.Filled.Share
More ellipsis Icons.Filled.MoreHoriz
New post (center FAB) plus Icons.Filled.Add
Home (tab) house.fill Icons.Filled.Home
Search (tab) magnifyingglass Icons.Filled.Search
Activity (tab) bell.fill Icons.Filled.Notifications
Account (tab) person.crop.square Icons.Filled.AccountBox
Messaging paperplane.fill Icons.AutoMirrored.Filled.Send
Mature label exclamationmark.triangle.fill Icons.Filled.Warning

8. Minimum SDK & Accessibility Notes

  • minSdk 24 (Compose floor is 21; Canvas particles, coil-gif, modern motion comfortable at 24). compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+. FlowRow is in androidx.compose.foundation.layout (stable in recent BOMs).
  • Edge-to-edge: call enableEdgeToEdge(); the deep-navy canvas wants light-content system bars via WindowCompat. Use Scaffold insets so the docked FAB and bar clear the gesture nav.
  • Dark default: provide the navy darkColorScheme by default; a light theme exists but should be a user-opt-in setting — the brand experience is the midnight dashboard.
  • GIFs: add coil-gif and use AsyncImage with an ImageLoader that has GifDecoder/ImageDecoderDecoder — Tumblr is GIF-heavy; render media full-bleed inside the clipped card (no letterboxing).
  • Font scaling: sp honors the user’s scale — keep it generous on post body and quote posts (long-form). Pin only tab labels and the reblog-source minimum.
  • TalkBack: mark the burst Canvas Modifier.clearAndSetSemantics {} (decorative); expose like stateDescription (“Liked”); announce reblog-chain segments as “<source> reblogged: <comment>”; keep the bright-blue primary button’s dark navy text for contrast.
  • Touch targets: Material guidance is 48.dp minimum. Post action icons are 22.dp glyphs inside 44.dp IconButtons — ensure each tag has ≥32.dp effective touch height if tappable.
  • Contrast: #8A9AAE on #001935 passes WCAG AA at 13sp+; the bright accent trio on navy is high-contrast. Validate 10sp tab labels and bump if targeting strict compliance.
  • Dynamic color: do not enable Material You — Tumblr’s identity is the fixed deep-navy canvas and the blue/green/pink accent trio regardless of wallpaper.
All mobile designs →

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