coderef

Shazam

Shazam's iOS app is a single-purpose instrument, and its design is ruthlessly devoted to that one purpose: press the button, name the song.
DownloadRaw
Shazam

Build with the Shazam look.

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

Palette

At a glance

Theme
light
Typefaces
Montserrat
Sections
8
Length
3,449 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · Shazam

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

Shazam (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports Shazam’s visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, the radial gradient, the signature pulsing button + concentric rings, 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 (Shazam’s radial blue gradient, the single dominant button, concentric pulse rings, glass surfaces, no tab bar) while making everything idiomatic Android — Brush.radialGradient instead of SwiftUI’s RadialGradient, a ModalBottomSheet instead of a UIKit sheet, sp/dp instead of pt.

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

1. Color Tokens

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

object ShazamColors {
    // Gradient stops
    val Core        = Color(0xFF0050FF)
    val Blue        = Color(0xFF0088FF)
    val Space       = Color(0xFF08090E)
    val BluePressed = Color(0xFF006FE0)

    // Text
    val TextPrimary   = Color(0xFFFFFFFF)
    val TextSecondary = Color(0xFFB8C4FF)               // periwinkle, tuned to the gradient
    val TextTertiary  = Color(0xFFB8C4FF).copy(alpha = 0.55f)

    // Glass
    val Glass       = Color.White.copy(alpha = 0.08f)
    val GlassStrong = Color.White.copy(alpha = 0.14f)
    val Divider     = Color.White.copy(alpha = 0.12f)

    // Semantic
    val AppleMusicPink = Color(0xFFFA243C)
    val ErrorRed       = Color(0xFFFF453A)
}

Wire it into a Material 3 darkColorScheme. Shazam’s hero is dark by design; do not provide a light scheme.

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

private val ShazamScheme = darkColorScheme(
    primary        = ShazamColors.Blue,
    onPrimary      = Color.White,
    background      = ShazamColors.Space,
    onBackground    = ShazamColors.TextPrimary,
    surface         = ShazamColors.Space,
    onSurface       = ShazamColors.TextPrimary,
    surfaceVariant  = ShazamColors.Glass,
    outline         = ShazamColors.Divider,
    error           = ShazamColors.ErrorRed,
)

@Composable
fun ShazamTheme(content: @Composable () -> Unit) =
    MaterialTheme(colorScheme = ShazamScheme, typography = ShazamTypography, content = content)

The signature hero gradient:

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush

@Composable
fun heroBrush(size: androidx.compose.ui.geometry.Size) = Brush.radialGradient(
    colorStops = arrayOf(
        0.0f  to ShazamColors.Blue,
        0.30f to ShazamColors.Core,
        1.0f  to ShazamColors.Space,
    ),
    center = Offset(size.width * 0.5f, size.height * 0.42f),
    radius = size.maxDimension * 0.75f,
)

2. Typography

Montserrat is on Google Fonts. Drop the TTFs in res/font/ (lowercase, snake_case) and build a FontFamily. Fall back to the system font (Roboto).

// ui/theme/ShazamType.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 Montserrat = FontFamily(
    Font(R.font.montserrat_medium, FontWeight.Medium), // 500
    Font(R.font.montserrat_bold,   FontWeight.Bold),   // 700
)

// Named ramp — mirrors DESIGN.md §3 exactly (pt → sp 1:1)
object ShazamText {
    val ResultLarge = TextStyle(Montserrat, fontWeight = FontWeight.Bold,   fontSize = 28.sp, lineHeight = 32.sp, letterSpacing = (-0.3).sp)
    val Result      = TextStyle(Montserrat, fontWeight = FontWeight.Bold,   fontSize = 22.sp, lineHeight = 26.sp, letterSpacing = (-0.2).sp)
    val Section     = TextStyle(Montserrat, fontWeight = FontWeight.Bold,   fontSize = 20.sp, lineHeight = 24.sp, letterSpacing = (-0.2).sp)
    val Prompt      = TextStyle(Montserrat, fontWeight = FontWeight.Bold,   fontSize = 18.sp, lineHeight = 22.sp, letterSpacing = 0.2.sp)
    val CardTitle   = TextStyle(Montserrat, fontWeight = FontWeight.Bold,   fontSize = 16.sp, lineHeight = 21.sp)
    val Subtitle    = TextStyle(Montserrat, fontWeight = FontWeight.Medium, fontSize = 14.sp, lineHeight = 18.sp)
    val Body        = TextStyle(Montserrat, fontWeight = FontWeight.Medium, fontSize = 15.sp, lineHeight = 22.sp)
    val Meta        = TextStyle(Montserrat, fontWeight = FontWeight.Medium, fontSize = 13.sp, lineHeight = 17.sp)
    val LabelUpper  = TextStyle(Montserrat, fontWeight = FontWeight.Bold,   fontSize = 11.sp, lineHeight = 13.sp, letterSpacing = 1.0.sp)
    val Button      = TextStyle(Montserrat, fontWeight = FontWeight.Bold,   fontSize = 16.sp, lineHeight = 20.sp, letterSpacing = 0.3.sp)
    val ButtonSec   = TextStyle(Montserrat, fontWeight = FontWeight.Bold,   fontSize = 14.sp, lineHeight = 18.sp, letterSpacing = 0.2.sp)
}

// Map onto Material 3 slots so stock components inherit the brand
val ShazamTypography = Typography(
    headlineLarge = ShazamText.ResultLarge,
    headlineSmall = ShazamText.Section,
    titleMedium   = ShazamText.CardTitle,
    bodyMedium    = ShazamText.Body,
    labelSmall    = ShazamText.LabelUpper,
)

3. Signature Components

The Pulsing Shazam Button + Concentric Rings

import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp

@Composable
fun ShazamButton(
    isListening: Boolean,
    onTap: () -> Unit,
    modifier: Modifier = Modifier,
) {
    val size = 132.dp
    val haptics = LocalHapticFeedback.current
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()

    // Idle breathing
    val breatheT = rememberInfiniteTransition(label = "breathe")
    val breathe by breatheT.animateFloat(
        1f, 1.04f, infiniteRepeatable(tween(2400, easing = EaseInOut), RepeatMode.Reverse), label = "b",
    )
    val pressScale by animateFloatAsState(if (pressed) 0.94f else 1f, spring(dampingRatio = 0.8f), label = "press")
    val glowAlpha by animateFloatAsState(if (isListening) 0.5f else 0f, tween(500), label = "glow")

    Box(modifier.size(size * 2.6f), contentAlignment = Alignment.Center) {
        // Concentric emitting rings
        if (isListening) {
            repeat(3) { i -> PulseRing(delayMs = i * 600, base = size) }
        }

        // Listening glow
        Box(
            Modifier
                .size(size * 1.4f)
                .clip(CircleShape)
                .background(ShazamColors.Blue.copy(alpha = glowAlpha)),
        )

        // The button
        Box(
            Modifier
                .size(size)
                .scale(pressScale * if (isListening) 1f else breathe)
                .clip(CircleShape)
                .background(
                    Brush.radialGradient(
                        listOf(Color.White, Color(0xFFE8F0FF), Color(0xFFCFE0FF)),
                        center = Offset(size.value * 0.38f, size.value * 0.32f),
                        radius = size.value * 0.9f,
                    )
                )
                .border(1.dp, Color.White.copy(alpha = 0.4f), CircleShape)
                .clickable(interaction, indication = null) {
                    haptics.performHapticFeedback(HapticFeedbackType.LongPress) // ~iOS heavy impact
                    onTap()
                },
            contentAlignment = Alignment.Center,
        ) {
            // Stylized Shazam mark — replace with the licensed vector asset in production
            Canvas(Modifier.size(size * 0.4f)) {
                val w = this.size.width
                val path = androidx.compose.ui.graphics.Path().apply {
                    moveTo(w * 0.62f, w * 0.18f)
                    cubicTo(w * 0.40f, w * 0.18f, w * 0.30f, w * 0.30f, w * 0.30f, w * 0.46f)
                    cubicTo(w * 0.30f, w * 0.62f, w * 0.70f, w * 0.50f, w * 0.70f, w * 0.62f)
                    cubicTo(w * 0.70f, w * 0.78f, w * 0.58f, w * 0.86f, w * 0.38f, w * 0.86f)
                }
                drawPath(path, ShazamColors.Blue, style = Stroke(width = w * 0.13f, cap = androidx.compose.ui.graphics.StrokeCap.Round))
            }
        }
    }
}

@Composable
private fun PulseRing(delayMs: Int, base: androidx.compose.ui.unit.Dp) {
    val t = rememberInfiniteTransition(label = "ring")
    val p by t.animateFloat(
        0f, 1f,
        infiniteRepeatable(tween(1800, delayMillis = delayMs, easing = EaseOut), RepeatMode.Restart),
        label = "p",
    )
    Box(
        Modifier
            .size(base)
            .scale(1f + p * 1.6f)
            .border((2).dp, Color.White.copy(alpha = 0.22f * (1f - p)), CircleShape),
    )
}

Hero Screen (no bottom bar)

@Composable
fun ShazamHome() {
    var listening by remember { mutableStateOf(false) }
    BoxWithConstraints(Modifier.fillMaxSize()) {
        val brush = Brush.radialGradient(
            colorStops = arrayOf(0f to ShazamColors.Blue, 0.30f to ShazamColors.Core, 1f to ShazamColors.Space),
            center = Offset(constraints.maxWidth * 0.5f, constraints.maxHeight * 0.42f),
            radius = maxOf(constraints.maxWidth, constraints.maxHeight) * 0.75f,
        )
        Box(Modifier.fillMaxSize().background(brush)) {
            Row(
                Modifier.fillMaxWidth().statusBarsPadding().padding(horizontal = 20.dp, vertical = 8.dp),
                horizontalArrangement = Arrangement.SpaceBetween,
            ) {
                Icon(Icons.Outlined.AccountCircle, "Profile", tint = Color.White, modifier = Modifier.size(26.dp))
                Icon(Icons.Filled.QueueMusic, "History", tint = Color.White, modifier = Modifier.size(26.dp))
            }
            Column(
                Modifier.fillMaxSize(),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center,
            ) {
                ShazamButton(isListening = listening, onTap = { listening = !listening })
                Spacer(Modifier.height(24.dp))
                Text(
                    if (listening) "Listening for music…" else "Tap to Shazam",
                    style = if (listening) ShazamText.Button.copy(color = Color.White) else ShazamText.Prompt,
                    color = Color.White,
                )
            }
        }
    }
}

Result Card

@Composable
fun ShazamResultCard(title: String, artist: String, artworkUrl: String) {
    Column(
        Modifier
            .padding(horizontal = 20.dp)
            .clip(RoundedCornerShape(20.dp))
            .background(ShazamColors.Glass)
            .border(1.dp, ShazamColors.GlassStrong, RoundedCornerShape(20.dp))
            .padding(20.dp),
        verticalArrangement = Arrangement.spacedBy(20.dp),
    ) {
        Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
            AsyncImage(
                model = artworkUrl, contentDescription = null,
                modifier = Modifier.size(88.dp).clip(RoundedCornerShape(12.dp)),
                contentScale = ContentScale.Crop,
            )
            Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(6.dp)) {
                Text(title, style = ShazamText.Result, color = Color.White, maxLines = 2)
                Text(artist, style = ShazamText.Subtitle, color = ShazamColors.TextSecondary, maxLines = 1)
            }
        }

        val ams = remember { MutableInteractionSource() }
        val amsPressed by ams.collectIsPressedAsState()
        Row(
            Modifier
                .fillMaxWidth()
                .clip(CircleShape)
                .background(if (amsPressed) Color(0xFFE8F0FF) else Color.White)
                .clickable(ams, indication = null) { }
                .padding(vertical = 14.dp),
            horizontalArrangement = Arrangement.Center,
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Icon(Icons.Filled.MusicNote, null, tint = ShazamColors.AppleMusicPink, modifier = Modifier.size(18.dp))
            Spacer(Modifier.width(8.dp))
            Text("Open in Apple Music", style = ShazamText.Button, color = ShazamColors.Space)
        }

        Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
            listOf(
                Icons.Filled.IosShare to "Share",
                Icons.Filled.Add to "Add",
                Icons.Filled.FormatQuote to "Lyrics",
            ).forEach { (icon, label) ->
                Column(
                    Modifier
                        .weight(1f)
                        .clip(CircleShape)
                        .background(ShazamColors.Glass)
                        .border(1.dp, Color.White.copy(alpha = 0.16f), CircleShape)
                        .padding(vertical = 12.dp),
                    horizontalAlignment = Alignment.CenterHorizontally,
                    verticalArrangement = Arrangement.spacedBy(6.dp),
                ) {
                    Icon(icon, label, tint = Color.White, modifier = Modifier.size(18.dp))
                    Text(label, style = ShazamText.ButtonSec, color = Color.White)
                }
            }
        }
    }
}

4. Listening State Machine

sealed interface ShazamState {
    data object Idle : ShazamState
    data object Listening : ShazamState
    data class Matched(val title: String, val artist: String) : ShazamState
    data object NoMatch : ShazamState
}
// tap → Listening; recognizer result → Matched (reveal card);
// failure → NoMatch (soft shake + HapticFeedbackType.LongPress / error vibration).

5. Navigation — No Bottom Bar

Shazam deliberately has no NavigationBar. The hero is the root; the library/history is a ModalBottomSheet:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ShazamRoot() {
    var showLibrary by remember { mutableStateOf(false) }
    val sheet = rememberModalBottomSheetState(skipPartiallyExpanded = false)

    ShazamHome()

    if (showLibrary) {
        ModalBottomSheet(
            onDismissRequest = { showLibrary = false },
            sheetState = sheet,
            containerColor = ShazamColors.GlassStrong,
            shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp),
            dragHandle = {
                Box(
                    Modifier.fillMaxWidth().padding(vertical = 12.dp),
                    contentAlignment = Alignment.Center,
                ) { Box(Modifier.size(36.dp, 4.dp).clip(CircleShape).background(Color.White.copy(alpha = 0.4f))) }
            },
        ) {
            // "Your Shazams" list — 68.dp rows, 52.dp art (10.dp radius), 1.dp Divider
        }
    }
}

A true backdrop blur behind the sheet needs HazeState/Modifier.haze (the dev.chrisbanes.haze library) or a RenderEffect.createBlurEffect on API 31+. Material 3’s ModalBottomSheet alone gives a scrim, not a blur.

6. Motion

Moment Compose recipe
Idle breathing rememberInfiniteTransition animateFloat 1 → 1.04, tween(2400, EaseInOut), RepeatMode.Reverse
Concentric rings animateFloat 0 → 1 over tween(1800, EaseOut), RepeatMode.Restart, staggered delayMillis = i*600; scale 1 + p*1.6, alpha 0.22*(1-p)
Tap animateFloatAsState 1 → 0.94 spring(dampingRatio = 0.8f), HapticFeedbackType.LongPress
Result reveal AnimatedVisibility + scaleIn(initialScale = 0.85f) + fadeIn() over spring(dampingRatio = 0.8f)
No match Animatable keyframes offset 0 → -8 → 8 → 0; error vibration
Sheet ModalBottomSheet detents (rememberModalBottomSheetState)
// Result reveal
AnimatedVisibility(
    visible = state is ShazamState.Matched,
    enter = scaleIn(initialScale = 0.85f, animationSpec = spring(dampingRatio = 0.8f)) + fadeIn(),
) { ShazamResultCard(/* … */) }

Haptics: prefer LocalHapticFeedback. For the heavy tap, LocalView.current.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); for no-match use a Vibrator VibrationEffect.createWaveform(...).

7. Icons

The closest first-party set is androidx.compose.material:material-icons-extended. The Shazam mark itself ships as a licensed vector drawable — load via ImageVector.vectorResource(R.drawable.shazam_glyph).

Purpose SF Symbol (iOS) Material Icon (Compose)
Profile / Settings person.crop.circle Icons.Outlined.AccountCircle
Library / History music.note.list Icons.Filled.QueueMusic
Open in Apple Music music.note Icons.Filled.MusicNote
Share square.and.arrow.up Icons.Filled.IosShare
Add plus Icons.Filled.Add
Lyrics text.quote Icons.Filled.FormatQuote
Close result xmark Icons.Filled.Close
Sheet chevron chevron.right Icons.Filled.ChevronRight
Search (library) magnifyingglass Icons.Filled.Search
Play / Pause (mini) play.fill / pause.fill Icons.Filled.PlayArrow / Icons.Filled.Pause
No-match waveform.slash Icons.Filled.MusicOff

8. Minimum SDK & Accessibility Notes

  • minSdk 24 (Compose floor is 21; Brush.radialGradient + modern motion are comfortable at 24). compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+. For a true sheet blur, RenderEffect is API 31+.
  • Edge-to-edge: call enableEdgeToEdge(); the dark gradient wants light-content system bars. The radial gradient draws full-bleed under the status bar — apply Modifier.statusBarsPadding() only to the top icon row.
  • Font scaling: sp honors the user’s font scale — keep it on result title, artist, sheet rows, body. Pin the centered “Tap to Shazam” prompt and listening label by deriving size from dp or CompositionLocalProvider(LocalDensity provides Density(density, fontScale = 1f)).
  • Reduce Motion: check Settings.Global.ANIMATOR_DURATION_SCALE (or wrap in an app setting) — when motion is reduced, replace ring emission with a static glow + a subtle alpha pulse.
  • TalkBack: the button’s contentDescription = “Shazam. Tap to identify the music playing around you.”; announce state changes (“Listening”, “Match found: <title> by <artist>”, “No match found”) via LiveRegionMode.Assertive on a status Text.
  • Touch targets: the 132.dp button vastly exceeds the 48.dp minimum. Give the 26.dp top-bar icons a 48.dp hit area via padding.
  • Contrast: periwinkle #B8C4FF on the dark gradient edge passes WCAG AA at 14sp+; validate over the brighter #0088FF core and darken toward #A6B4FF if targeting strict compliance.
  • No bottom bar: do not add a Material NavigationBar — the single-purpose hero is the defining choice. Library is a ModalBottomSheet.
  • Dynamic color: do not enable Material You dynamicDarkColorScheme() — Shazam’s brand requires the fixed radial blue gradient regardless of wallpaper. There is no light theme.
All mobile designs →

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