coderef

Dropbox

Dropbox's iOS app is a calm, paper-white workspace built around one idea: your files should feel as legible and stress-free as a tidy desk.
DownloadRaw
Dropbox

Build with the Dropbox look.

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

Palette

At a glance

Theme
light
Typefaces
Sharp Grotesk
Sections
8
Length
3,521 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · Dropbox

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

Dropbox (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports Dropbox’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 (Dropbox’s paper-white canvas, single Dropbox-Blue accent, colored file-type rows, determinate upload bar) while making everything idiomatic Android — NavigationBar instead of a UITabBar, FloatingActionButton instead of a custom iOS FAB, sp/dp instead of pt.

Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, and Coil 2.6+ for image thumbnails.

1. Color Tokens

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

object DropboxColors {
    // Canvas & Surfaces (Light)
    val Canvas   = Color(0xFFFFFFFF)
    val Surface  = Color(0xFFF7F5F2)
    val Divider  = Color(0xFFE6E1DA)

    // Text
    val TextPrimary   = Color(0xFF1E1919)
    val TextSecondary = Color(0xFF6F6A65)
    val TextTertiary  = Color(0xFFA39E98)

    // Brand
    val Blue        = Color(0xFF0061FF)
    val BluePressed = Color(0xFF0050D0)
    val BlueTint    = Color(0xFFE6F0FF)

    // Dark
    val DarkCanvas  = Color(0xFF1E1919)
    val DarkSurface = Color(0xFF2A2424)
    val DarkDivider = Color(0xFF3A3331)
    val DarkBlue    = Color(0xFF3D8BFF)

    // File-type icons
    val PdfRed     = Color(0xFFFA551E)
    val DocBlue    = Color(0xFF0061FF)
    val SheetGreen = Color(0xFF1A8754)
    val ImageTeal  = Color(0xFF00B2A9)
    val FolderSlate = Color(0xFF8C97A8)

    // Semantic
    val Success = Color(0xFF1A8754)
    val Warning = Color(0xFFFFAF00)
    val Error   = Color(0xFFD1180B)
}

Wire it into Material 3 light and dark schemes so ripples, dividers, and default component colors inherit the brand.

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

private val DropboxLight = lightColorScheme(
    primary        = DropboxColors.Blue,
    onPrimary      = Color.White,
    background     = DropboxColors.Canvas,
    onBackground   = DropboxColors.TextPrimary,
    surface        = DropboxColors.Canvas,
    onSurface      = DropboxColors.TextPrimary,
    surfaceVariant = DropboxColors.Surface,
    outline        = DropboxColors.Divider,
    error          = DropboxColors.Error,
)

private val DropboxDark = darkColorScheme(
    primary        = DropboxColors.DarkBlue,
    onPrimary      = Color.White,
    background     = DropboxColors.DarkCanvas,
    onBackground   = Color.White,
    surface        = DropboxColors.DarkSurface,
    onSurface      = Color.White,
    surfaceVariant = DropboxColors.DarkSurface,
    outline        = DropboxColors.DarkDivider,
    error          = DropboxColors.Error,
)

@Composable
fun DropboxTheme(
    dark: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit,
) = MaterialTheme(
    colorScheme = if (dark) DropboxDark else DropboxLight,
    typography  = DropboxTypography,
    content     = content,
)

2. Typography

Sharp Grotesk is Dropbox’s licensed brand typeface. Drop the TTFs in res/font/ (lowercase, snake_case) and build a FontFamily. The closest free substitute is Inter — load that if Sharp Grotesk is unavailable; otherwise fall back to the system grotesque (Roboto).

// ui/theme/DropboxType.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 Sharp = FontFamily(
    Font(R.font.sharp_grotesk_regular,  FontWeight.Normal),   // 400
    Font(R.font.sharp_grotesk_semibold, FontWeight.SemiBold), // 600
    Font(R.font.sharp_grotesk_bold,     FontWeight.Bold),     // 700
)

// Named ramp — mirrors DESIGN.md §3 exactly (pt → sp 1:1)
object DropboxText {
    val TitleLarge = TextStyle(Sharp, fontWeight = FontWeight.Bold,     fontSize = 28.sp, lineHeight = 32.sp, letterSpacing = (-0.4).sp)
    val Section    = TextStyle(Sharp, fontWeight = FontWeight.Bold,     fontSize = 22.sp, lineHeight = 26.sp, letterSpacing = (-0.3).sp)
    val SheetTitle = TextStyle(Sharp, fontWeight = FontWeight.Bold,     fontSize = 20.sp, lineHeight = 24.sp, letterSpacing = (-0.2).sp)
    val CardTitle  = TextStyle(Sharp, fontWeight = FontWeight.SemiBold, fontSize = 17.sp, lineHeight = 22.sp, letterSpacing = (-0.1).sp)
    val RowName    = TextStyle(Sharp, fontWeight = FontWeight.SemiBold, fontSize = 16.sp, lineHeight = 20.sp, letterSpacing = (-0.1).sp)
    val Body       = TextStyle(Sharp, fontWeight = FontWeight.Normal,   fontSize = 15.sp, lineHeight = 22.sp)
    val Meta       = TextStyle(Sharp, fontWeight = FontWeight.Normal,   fontSize = 14.sp, lineHeight = 18.sp)
    val Caption    = TextStyle(Sharp, fontWeight = FontWeight.Normal,   fontSize = 12.sp, lineHeight = 16.sp)
    val LabelUpper = TextStyle(Sharp, fontWeight = FontWeight.Bold,     fontSize = 11.sp, lineHeight = 13.sp, letterSpacing = 0.6.sp)
    val Button     = TextStyle(Sharp, fontWeight = FontWeight.SemiBold, fontSize = 16.sp, lineHeight = 20.sp)
    val Tab        = TextStyle(Sharp, fontWeight = FontWeight.SemiBold, fontSize = 11.sp, lineHeight = 13.sp, letterSpacing = 0.2.sp)
}

// Map onto Material 3 slots so stock components inherit the brand
val DropboxTypography = Typography(
    headlineLarge = DropboxText.TitleLarge,
    headlineSmall = DropboxText.Section,
    titleMedium   = DropboxText.RowName,
    bodyMedium    = DropboxText.Body,
    labelSmall    = DropboxText.Tab,
)

For tabular figures on file sizes/dates, apply TextStyle(fontFeatureSettings = "tnum") to Meta/Caption usages.

3. Signature Components

Primary Button

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp

@Composable
fun DropboxPrimaryButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
) {
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    val scale by animateFloatAsState(if (pressed) 0.98f else 1f, label = "btnScale")

    Box(
        modifier = modifier
            .fillMaxWidth()
            .scale(scale)
            .graphicsLayer { alpha = if (enabled) 1f else 0.4f }
            .clip(RoundedCornerShape(8.dp))
            .background(if (pressed) DropboxColors.BluePressed else DropboxColors.Blue)
            .clickable(interaction, indication = null, enabled = enabled, onClick = onClick)
            .padding(vertical = 14.dp, horizontal = 24.dp),
        contentAlignment = Alignment.Center,
    ) {
        Text(text, style = DropboxText.Button, color = Color.White)
    }
}

Upload / Create FAB

import androidx.compose.material3.FloatingActionButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Icon
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback

@Composable
fun DropboxUploadFab(onClick: () -> Unit, modifier: Modifier = Modifier) {
    val haptics = LocalHapticFeedback.current
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    val scale by animateFloatAsState(if (pressed) 0.92f else 1f, label = "fabScale")

    FloatingActionButton(
        onClick = {
            haptics.performHapticFeedback(HapticFeedbackType.LongPress) // ~iOS soft impact
            onClick()
        },
        modifier = modifier.size(56.dp).scale(scale),
        shape = CircleShape,
        containerColor = if (pressed) DropboxColors.BluePressed else DropboxColors.Blue,
        contentColor = Color.White,
        interactionSource = interaction,
    ) {
        Icon(Icons.Filled.Add, contentDescription = "Upload or create", modifier = Modifier.size(24.dp))
    }
}

File / Folder Row

import androidx.compose.material.icons.filled.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import coil.compose.AsyncImage

enum class FileKind { Pdf, Doc, Sheet, Image, Folder }

@Composable
fun DropboxFileRow(
    name: String,
    meta: String,
    kind: FileKind,
    selected: Boolean = false,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()

    val (icon, tint) = when (kind) {
        FileKind.Pdf    -> Icons.Filled.PictureAsPdf to DropboxColors.PdfRed
        FileKind.Doc    -> Icons.Filled.Description   to DropboxColors.DocBlue
        FileKind.Sheet  -> Icons.Filled.GridOn        to DropboxColors.SheetGreen
        FileKind.Image  -> Icons.Filled.Image         to DropboxColors.ImageTeal
        FileKind.Folder -> Icons.Filled.Folder        to DropboxColors.FolderSlate
    }
    val bg = when {
        selected -> DropboxColors.BlueTint
        pressed  -> DropboxColors.Surface
        else     -> DropboxColors.Canvas
    }

    Row(
        modifier = modifier
            .fillMaxWidth()
            .height(60.dp)
            .background(bg)
            .clickable(interaction, indication = null, onClick = onClick)
            .padding(horizontal = 16.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(12.dp),
    ) {
        if (selected) {
            Icon(Icons.Filled.CheckCircle, contentDescription = "Selected",
                tint = DropboxColors.Blue, modifier = Modifier.size(40.dp))
        } else {
            Box(
                Modifier.size(40.dp).clip(RoundedCornerShape(8.dp))
                    .background(tint.copy(alpha = 0.14f)),
                contentAlignment = Alignment.Center,
            ) {
                Icon(icon, contentDescription = null, tint = tint, modifier = Modifier.size(20.dp))
            }
        }
        Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
            Text(name, style = DropboxText.RowName, color = DropboxColors.TextPrimary,
                maxLines = 1, overflow = TextOverflow.Ellipsis)
            Text(meta, style = DropboxText.Meta.copy(fontFeatureSettings = "tnum"),
                color = DropboxColors.TextSecondary, maxLines = 1, overflow = TextOverflow.Ellipsis)
        }
        Icon(
            if (kind == FileKind.Folder) Icons.Filled.ChevronRight else Icons.Filled.MoreHoriz,
            contentDescription = if (kind == FileKind.Folder) "Open" else "More",
            tint = DropboxColors.TextSecondary, modifier = Modifier.size(18.dp),
        )
    }
}

Upload Progress Bar

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.progressSemantics
import androidx.compose.ui.draw.drawBehind

@Composable
fun DropboxUploadBar(
    label: String,
    progress: Float, // 0f..1f real byte ratio
    done: Boolean = false,
    modifier: Modifier = Modifier,
) {
    val animated by animateFloatAsState(progress, tween(200), label = "uploadProgress")
    Column(
        modifier
            .fillMaxWidth()
            .background(DropboxColors.Canvas)
            .padding(horizontal = 16.dp, vertical = 12.dp),
        verticalArrangement = Arrangement.spacedBy(6.dp),
    ) {
        Row(verticalAlignment = Alignment.CenterVertically) {
            Text(label, style = DropboxText.Meta.copy(fontWeight = FontWeight.SemiBold),
                color = DropboxColors.TextPrimary, modifier = Modifier.weight(1f))
            if (done) Icon(Icons.Filled.CheckCircle, "Done", tint = DropboxColors.Success,
                modifier = Modifier.size(18.dp))
        }
        Box(
            Modifier
                .fillMaxWidth().height(3.dp)
                .clip(RoundedCornerShape(2.dp))
                .background(DropboxColors.Divider)
                .progressSemantics(animated)
                .drawBehind {
                    drawRect(
                        color = DropboxColors.Blue,
                        size = size.copy(width = size.width * animated),
                    )
                }
        )
    }
}

Recent File Card

@Composable
fun DropboxRecentCard(
    name: String,
    meta: String,
    thumbUrl: String?,
    modifier: Modifier = Modifier,
) {
    Column(
        modifier
            .size(width = 140.dp, height = 160.dp)
            .clip(RoundedCornerShape(12.dp))
            .border(1.dp, DropboxColors.Divider, RoundedCornerShape(12.dp))
            .background(DropboxColors.Canvas)
            .padding(12.dp),
    ) {
        Box(
            Modifier.fillMaxWidth().height(96.dp).clip(RoundedCornerShape(4.dp))
                .background(DropboxColors.Surface),
            contentAlignment = Alignment.Center,
        ) {
            if (thumbUrl != null) {
                AsyncImage(thumbUrl, null, Modifier.fillMaxSize())
            } else {
                Icon(Icons.Filled.Description, null, tint = DropboxColors.Blue,
                    modifier = Modifier.size(28.dp))
            }
        }
        Spacer(Modifier.height(8.dp))
        Text(name, style = DropboxText.CardTitle, color = DropboxColors.TextPrimary,
            maxLines = 2, overflow = TextOverflow.Ellipsis)
        Text(meta, style = DropboxText.Caption.copy(fontFeatureSettings = "tnum"),
            color = DropboxColors.TextSecondary)
    }
}

4. Photo Grid (contact-sheet)

import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items

@Composable
fun DropboxPhotoGrid(urls: List<String>) {
    var selected by remember { mutableStateOf(setOf<Int>()) }
    LazyVerticalGrid(
        columns = GridCells.Fixed(3),
        horizontalArrangement = Arrangement.spacedBy(2.dp),
        verticalArrangement = Arrangement.spacedBy(2.dp),
    ) {
        itemsIndexed(urls) { i, url ->
            Box(
                Modifier
                    .aspectRatio(1f)
                    .combinedClickable(
                        onClick = {},
                        onLongClick = {
                            selected = if (i in selected) selected - i else selected + i
                        },
                    ),
            ) {
                AsyncImage(url, null, Modifier.fillMaxSize(), contentScale = ContentScale.Crop)
                if (i in selected) {
                    Box(Modifier.matchParentSize().border(4.dp, DropboxColors.Blue))
                    Icon(Icons.Filled.CheckCircle, "Selected", tint = DropboxColors.Blue,
                        modifier = Modifier.align(Alignment.TopEnd).padding(6.dp).size(20.dp))
                }
            }
        }
    }
}

5. Bottom Navigation (Tab Bar)

Use Material 3 NavigationBar. Dropbox’s iOS tab bar is .regularMaterial blur; Android has no first-class live blur, so use a 94%-opaque canvas surface. Active tint is Dropbox Blue.

@Composable
fun DropboxBottomBar(selected: Int, onSelect: (Int) -> Unit) {
    NavigationBar(
        containerColor = DropboxColors.Canvas.copy(alpha = 0.94f),
        tonalElevation = 0.dp,
    ) {
        val items = listOf(
            "Home"    to Icons.Filled.Home,
            "Files"   to Icons.Filled.Folder,
            "Photos"  to Icons.Filled.PhotoLibrary,
            "Offline" to Icons.Filled.CloudDownload,
            "Account" to Icons.Filled.AccountCircle,
        )
        items.forEachIndexed { i, (label, icon) ->
            NavigationBarItem(
                selected = selected == i,
                onClick = { onSelect(i) },
                icon = { Icon(icon, contentDescription = label, modifier = Modifier.size(24.dp)) },
                label = { Text(label, style = DropboxText.Tab) },
                colors = NavigationBarItemDefaults.colors(
                    selectedIconColor   = DropboxColors.Blue,
                    selectedTextColor   = DropboxColors.Blue,
                    unselectedIconColor = DropboxColors.TextSecondary,
                    unselectedTextColor = DropboxColors.TextSecondary,
                    indicatorColor      = DropboxColors.BlueTint,
                ),
            )
        }
    }
}

Anchor DropboxUploadFab in the Scaffold floatingActionButton slot with FabPosition.End so it floats bottom-right above the bar.

6. Motion

Moment Compose recipe
FAB tap animateFloatAsState 1 → 0.92 spring; HapticFeedbackType.LongPress (~iOS soft impact)
Primary button press animateFloatAsState 1 → 0.98
Upload progress animateFloatAsState(progress, tween(200)) — driven by real byte ratio, no shimmer
Star toggle Animatable keyframes 1 → 1.12 → 1 over 240ms; HapticFeedbackType.LongPress; tint flips to Blue
Thumbnail decode Coil crossfade(200) fade-in as tiles load
Sheet present ModalBottomSheet default spring; scrim Color(0xFF1E1919).copy(alpha = 0.40f)
// Star toggle bounce
@Composable
fun StarToggle(starred: Boolean, onToggle: () -> Unit) {
    val scale = remember { Animatable(1f) }
    LaunchedEffect(starred) {
        if (starred) {
            scale.animateTo(1.12f, tween(120)); scale.animateTo(1f, tween(120))
        }
    }
    val haptics = LocalHapticFeedback.current
    Icon(
        if (starred) Icons.Filled.Star else Icons.Outlined.StarBorder,
        contentDescription = "Star",
        tint = if (starred) DropboxColors.Blue else DropboxColors.TextSecondary,
        modifier = Modifier.size(22.dp).scale(scale.value).clickable {
            haptics.performHapticFeedback(HapticFeedbackType.LongPress); onToggle()
        },
    )
}

Haptics: prefer LocalHapticFeedback. For richer control use LocalView.current.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) (API 30+) or a Vibrator VibrationEffect.createOneShot(10, ...) to approximate iOS’s .soft impact.

7. Icons

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

Purpose SF Symbol (iOS) Material Icon (Compose)
Upload / Create plus Icons.Filled.Add
PDF file doc.richtext.fill Icons.Filled.PictureAsPdf
Doc file doc.text.fill Icons.Filled.Description
Sheet file tablecells.fill Icons.Filled.GridOn
Image file photo.fill Icons.Filled.Image
Folder folder.fill Icons.Filled.Folder
Selected checkmark.circle.fill Icons.Filled.CheckCircle
More ellipsis Icons.Filled.MoreHoriz
Folder chevron chevron.right Icons.Filled.ChevronRight
Star star / star.fill Icons.Outlined.StarBorder / Icons.Filled.Star
Share square.and.arrow.up Icons.Filled.IosShare
Download / Offline arrow.down.circle Icons.Filled.CloudDownload
Search magnifyingglass Icons.Filled.Search
Home (tab) house Icons.Filled.Home
Files (tab) folder Icons.Filled.Folder
Photos (tab) photo.on.rectangle Icons.Filled.PhotoLibrary
Account (tab) person.crop.circle Icons.Filled.AccountCircle

8. Minimum SDK & Accessibility Notes

  • minSdk 24 (Compose floor is 21; modern motion + Coil are comfortable at 24). compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+.
  • Edge-to-edge: call enableEdgeToEdge() in the Activity; the light canvas wants dark system-bar icons via WindowCompat. Use Scaffold insets so the FAB clears the gesture nav and the upload bar clears the bottom bar.
  • Font scaling: sp honors the user’s font scale automatically — keep it on file names, metadata, body. Pin layout-sensitive text (tab labels, the 3.dp progress bar, the 56.dp FAB glyph) via dp or a fixed Density override.
  • Tabular numerals: apply fontFeatureSettings = "tnum" to Meta/Caption so file sizes and dates align in columns.
  • TalkBack: set contentDescription on the FAB ("Upload or create"); merge file-row name + metadata with Modifier.semantics(mergeDescendants = true) and mark the trailing overflow/chevron as a separate button. Add progressSemantics() to the upload bar.
  • Touch targets: Material guidance is 48.dp minimum. The 56.dp FAB and 60.dp rows are clear; give the 18.dp ellipsis a 48.dp hit area via padding.
  • Contrast: #6F6A65 on #FFFFFF passes WCAG AA at 14sp+. On the dark canvas use DarkBlue #3D8BFF for blue text/icons over #1E1919. Validate 11sp tab/label text and bump toward #5A554F if targeting strict compliance.
  • Dynamic color: do not enable Material You dynamicLightColorScheme() — Dropbox’s brand requires the fixed paper-white canvas and single Dropbox-Blue accent 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