coderef

Quora

Quora's iOS app is a bright, paper-white reading environment built around a single tension: a question deserves the gravity of a printed headline, but an answer should feel like a person talking to you.
DownloadRaw
Quora

Build with the Quora look.

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

Palette

At a glance

Theme
light
Typefaces
Inter, Georgia
Sections
8
Length
3,535 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · Quora

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

Quora (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports Quora’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 (Quora’s paper-white reading surface, the serif-question / sans-answer split, the single red accent, the vote pill) while making everything idiomatic Android — NavigationBar instead of a UITabBar, ScrollableTabRow for the top segments, sp/dp instead of pt.

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

1. Color Tokens

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

object QuoraColors {
    // Canvas & Surface
    val Canvas  = Color(0xFFFFFFFF)
    val Surface = Color(0xFFF7F7F8)
    val Divider = Color(0xFFE0E0E0)

    // Text
    val TextPrimary   = Color(0xFF282829)
    val TextSecondary = Color(0xFF636466)
    val TextTertiary  = Color(0xFF9A9A9C)

    // Brand
    val Red        = Color(0xFFB92B27)
    val RedPressed = Color(0xFF9E1F1B)
    val RedWash    = Color(0xFFFBEAEA)
    val Upvote     = Color(0xFF2E69FF)
    val UpvoteWash = Color(0xFFEAF0FF)
    val Success    = Color(0xFF1FA463)

    // Dark (system-dark users)
    val DarkCanvas        = Color(0xFF1A1A1B)
    val DarkSurface       = Color(0xFF262627)
    val DarkDivider       = Color(0xFF383839)
    val DarkTextPrimary   = Color(0xFFE8E8E8)
    val DarkTextSecondary = Color(0xFFA0A0A2)
}

Wire it into Material 3 schemes so ripples, dividers, and stock component colors inherit the brand. Quora is light-first; provide a dark scheme for system-dark users.

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

private val QuoraLight = lightColorScheme(
    primary        = QuoraColors.Red,
    onPrimary      = Color.White,
    background      = QuoraColors.Canvas,
    onBackground   = QuoraColors.TextPrimary,
    surface         = QuoraColors.Canvas,
    onSurface      = QuoraColors.TextPrimary,
    surfaceVariant = QuoraColors.Surface,
    outline        = QuoraColors.Divider,
    secondary      = QuoraColors.Upvote,
    error          = Color(0xFFD93025),
)

private val QuoraDark = darkColorScheme(
    primary        = QuoraColors.Red,
    onPrimary      = Color.White,
    background      = QuoraColors.DarkCanvas,
    onBackground   = QuoraColors.DarkTextPrimary,
    surface         = QuoraColors.DarkSurface,
    onSurface      = QuoraColors.DarkTextPrimary,
    surfaceVariant = QuoraColors.DarkSurface,
    outline        = QuoraColors.DarkDivider,
    secondary      = QuoraColors.Upvote,
)

@Composable
fun QuoraTheme(content: @Composable () -> Unit) =
    MaterialTheme(
        colorScheme = if (isSystemInDarkTheme()) QuoraDark else QuoraLight,
        typography  = QuoraTypography,
        content     = content,
    )

2. Typography

The split is the brand: Georgia for questions (ship the TTFs in res/font/ — georgia.ttf, georgia_bold.ttf — since Georgia is not a system font on Android), Inter for answers and chrome.

// ui/theme/QuoraType.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

// Georgia — the serif (question) voice
val Georgia = FontFamily(
    Font(R.font.georgia,      FontWeight.Normal),
    Font(R.font.georgia_bold, FontWeight.Bold),
)

// Inter — the sans (answer / UI) voice
val Inter = FontFamily(
    Font(R.font.inter_regular,  FontWeight.Normal),   // 400
    Font(R.font.inter_semibold, FontWeight.SemiBold), // 600
    Font(R.font.inter_bold,     FontWeight.Bold),     // 700
)

// Named ramp — mirrors DESIGN.md §3 exactly (pt → sp 1:1)
object QuoraText {
    // Questions — Georgia
    val QuestionDetail  = TextStyle(fontFamily = Georgia, fontWeight = FontWeight.Bold,     fontSize = 26.sp, lineHeight = 34.sp, letterSpacing = (-0.2).sp)
    val QuestionCard    = TextStyle(fontFamily = Georgia, fontWeight = FontWeight.Bold,     fontSize = 19.sp, lineHeight = 26.sp, letterSpacing = (-0.1).sp)
    val QuestionRelated = TextStyle(fontFamily = Georgia, fontWeight = FontWeight.Normal,   fontSize = 16.sp, lineHeight = 22.sp)
    val QuestionPrompt  = TextStyle(fontFamily = Georgia, fontWeight = FontWeight.Bold,     fontSize = 17.sp, lineHeight = 23.sp)

    // Answers / UI — Inter
    val AnswerBody  = TextStyle(fontFamily = Inter, fontWeight = FontWeight.Normal,   fontSize = 16.sp, lineHeight = 26.sp)  // 1.6
    val AnswerLead  = TextStyle(fontFamily = Inter, fontWeight = FontWeight.Normal,   fontSize = 17.sp, lineHeight = 27.sp)
    val Credential  = TextStyle(fontFamily = Inter, fontWeight = FontWeight.SemiBold, fontSize = 13.sp, lineHeight = 18.sp)
    val AuthorName  = TextStyle(fontFamily = Inter, fontWeight = FontWeight.Bold,     fontSize = 15.sp, lineHeight = 20.sp)
    val Section     = TextStyle(fontFamily = Inter, fontWeight = FontWeight.Bold,     fontSize = 18.sp, lineHeight = 23.sp, letterSpacing = (-0.1).sp)
    val Meta        = TextStyle(fontFamily = Inter, fontWeight = FontWeight.Normal,   fontSize = 13.sp, lineHeight = 18.sp)
    val VoteCount   = TextStyle(fontFamily = Inter, fontWeight = FontWeight.SemiBold, fontSize = 14.sp, lineHeight = 14.sp)
    val Button      = TextStyle(fontFamily = Inter, fontWeight = FontWeight.Bold,     fontSize = 15.sp, lineHeight = 15.sp, letterSpacing = 0.1.sp)
    val ButtonSec   = TextStyle(fontFamily = Inter, fontWeight = FontWeight.SemiBold, fontSize = 14.sp, lineHeight = 14.sp)
    val Tab         = TextStyle(fontFamily = Inter, fontWeight = FontWeight.SemiBold, fontSize = 10.sp, lineHeight = 12.sp, letterSpacing = 0.1.sp)
    val Segment     = TextStyle(fontFamily = Inter, fontWeight = FontWeight.Bold,     fontSize = 14.sp, lineHeight = 14.sp)
    val Caption     = TextStyle(fontFamily = Inter, fontWeight = FontWeight.Normal,   fontSize = 12.sp, lineHeight = 16.sp)
}

// Map onto Material 3 slots so stock components inherit the brand
val QuoraTypography = Typography(
    headlineMedium = QuoraText.QuestionDetail,
    titleLarge     = QuoraText.QuestionCard,
    bodyLarge      = QuoraText.AnswerBody,
    titleMedium    = QuoraText.AuthorName,
    labelSmall     = QuoraText.Tab,
)

3. Signature Components

The Serif/Sans Split — Question Feed Card

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp

@Composable
fun QuestionFeedCard(
    authorName: String,
    credential: String,
    question: String,
    answerPreview: String,
    modifier: Modifier = Modifier,
) {
    Column(
        modifier
            .fillMaxWidth()
            .background(QuoraColors.Canvas)
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(12.dp),
    ) {
        Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            Box(Modifier.size(24.dp).clip(CircleShape).background(QuoraColors.Surface))
            Column {
                Text(authorName, style = QuoraText.AuthorName, color = QuoraColors.TextPrimary)
                Text(credential, style = QuoraText.Credential, color = QuoraColors.TextSecondary)
            }
        }

        // QUESTION — Georgia, always
        Text(question, style = QuoraText.QuestionCard, color = QuoraColors.TextPrimary)

        // ANSWER — Inter, always, 3-line clamp + (more)
        val preview = buildAnnotatedString {
            append(answerPreview)
            withStyle(SpanStyle(color = QuoraColors.Upvote)) { append(" (more)") }
        }
        Text(preview, style = QuoraText.AnswerBody, color = QuoraColors.TextPrimary,
            maxLines = 3, overflow = TextOverflow.Ellipsis)

        Row(horizontalArrangement = Arrangement.spacedBy(24.dp), verticalAlignment = Alignment.CenterVertically) {
            VotePill(count = "1.2K")
            Text("💬 84", style = QuoraText.ButtonSec, color = QuoraColors.TextSecondary)
            Text("Share", style = QuoraText.ButtonSec, color = QuoraColors.TextSecondary)
        }
    }
}

Upvote / Downvote Pill (Signature)

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material3.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropUp
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback

private enum class Vote { None, Up, Down }

@Composable
fun VotePill(count: String, modifier: Modifier = Modifier) {
    var vote by remember { mutableStateOf(Vote.None) }
    val haptics = LocalHapticFeedback.current
    fun tap(next: Vote) {
        haptics.performHapticFeedback(HapticFeedbackType.TextHandleMove) // ≈ iOS light impact
        vote = if (vote == next) Vote.None else next
    }
    val upActive = vote == Vote.Up

    Row(
        modifier
            .height(32.dp)
            .clip(CircleShape)
            .border(BorderStroke(1.dp, QuoraColors.Divider), CircleShape),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Row(
            Modifier
                .background(if (upActive) QuoraColors.UpvoteWash else Color.Transparent)
                .clickable(MutableInteractionSource(), indication = null) { tap(Vote.Up) }
                .padding(horizontal = 12.dp).height(32.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(6.dp),
        ) {
            Icon(Icons.Filled.ArrowDropUp, contentDescription = "Upvote",
                tint = if (upActive) QuoraColors.Upvote else QuoraColors.TextSecondary)
            Text(count, style = QuoraText.VoteCount,
                color = if (upActive) QuoraColors.Upvote else QuoraColors.TextSecondary)
        }
        Box(Modifier.width(1.dp).height(20.dp).background(QuoraColors.Divider))
        Box(
            Modifier
                .clickable(MutableInteractionSource(), indication = null) { tap(Vote.Down) }
                .padding(horizontal = 12.dp).height(32.dp),
            contentAlignment = Alignment.Center,
        ) {
            // downvote stays neutral — never red
            Icon(Icons.Filled.ArrowDropDown, contentDescription = "Downvote",
                tint = QuoraColors.TextSecondary)
        }
    }
}

Primary “Answer” CTA

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.filled.Edit
import androidx.compose.ui.draw.scale

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

    Row(
        modifier
            .scale(scale)
            .clip(RoundedCornerShape(6.dp))
            .background(if (pressed) QuoraColors.RedPressed else QuoraColors.Red)
            .clickable(interaction, indication = null, onClick = onClick)
            .padding(horizontal = 24.dp, vertical = 11.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(6.dp),
    ) {
        Icon(Icons.Filled.Edit, contentDescription = null, tint = Color.White,
            modifier = Modifier.size(15.dp))
        Text(text, style = QuoraText.Button, color = Color.White)
    }
}

Answer Detail Block (Credential Byline)

import androidx.compose.material3.HorizontalDivider

@Composable
fun AnswerDetailBlock(
    authorName: String,
    credential: String,
    timestamp: String,
    body: String,
) {
    Column(
        Modifier
            .fillMaxWidth()
            .background(QuoraColors.Canvas)
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(14.dp),
    ) {
        Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
            Box(Modifier.size(40.dp).clip(CircleShape).background(QuoraColors.Surface))
            Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
                Text(authorName, style = QuoraText.AuthorName, color = QuoraColors.TextPrimary)
                Text(credential, style = QuoraText.Credential, color = QuoraColors.TextSecondary)
                Text(timestamp,  style = QuoraText.Meta, color = QuoraColors.TextSecondary)
            }
        }
        Text(body, style = QuoraText.AnswerBody, color = QuoraColors.TextPrimary)
        Row(horizontalArrangement = Arrangement.spacedBy(24.dp), verticalAlignment = Alignment.CenterVertically) {
            VotePill(count = "3.4K")
            Text("💬 212", style = QuoraText.ButtonSec, color = QuoraColors.TextSecondary)
            Text("Bookmark", style = QuoraText.ButtonSec, color = QuoraColors.TextSecondary)
        }
        HorizontalDivider(color = QuoraColors.Divider)
    }
}

The only place per-community color enters the otherwise-monochrome feed. Each tile carries its Space banner color, driven by data.

import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items

data class SpaceTile(val name: String, val members: String, val banner: Color)

@Composable
fun SpacesRow(spaces: List<SpaceTile>) {
    Column {
        Row(
            Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Text("Spaces for you", style = QuoraText.Section, color = QuoraColors.TextPrimary)
            Text("See all", style = QuoraText.ButtonSec, color = QuoraColors.Upvote)
        }
        LazyRow(
            contentPadding = PaddingValues(horizontal = 16.dp),
            horizontalArrangement = Arrangement.spacedBy(12.dp),
        ) {
            items(spaces) { s ->
                Column(
                    Modifier
                        .width(140.dp)
                        .clip(RoundedCornerShape(10.dp))
                        .border(BorderStroke(1.dp, QuoraColors.Divider), RoundedCornerShape(10.dp))
                        .background(QuoraColors.Canvas),
                ) {
                    Box(Modifier.fillMaxWidth().height(64.dp).background(s.banner))
                    Column(Modifier.padding(10.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
                        Text(s.name, style = QuoraText.AuthorName, color = QuoraColors.TextPrimary)
                        Text(s.members, style = QuoraText.Credential, color = QuoraColors.TextSecondary)
                    }
                }
            }
        }
    }
}

5. Navigation

Bottom: Material 3 NavigationBar (Home / Following / Answer / Spaces / Notifications) — the center Answer glyph is always red. Top: a ScrollableTabRow with a red underline indicator. Android has no live blur, so both bars use an opaque white surface with a hairline.

import androidx.compose.material3.*
import androidx.compose.material.icons.filled.*

@Composable
fun QuoraBottomBar(selected: Int, onSelect: (Int) -> Unit) {
    NavigationBar(containerColor = QuoraColors.Canvas, tonalElevation = 0.dp) {
        val items = listOf(
            Triple("Home", Icons.Filled.Home, false),
            Triple("Following", Icons.Filled.People, false),
            Triple("Answer", Icons.Filled.Edit, true),  // red anchor
            Triple("Spaces", Icons.Filled.ViewAgenda, false),
            Triple("Notifications", Icons.Filled.Notifications, false),
        )
        items.forEachIndexed { i, (label, icon, isAnswer) ->
            NavigationBarItem(
                selected = selected == i,
                onClick = { onSelect(i) },
                icon = {
                    Icon(icon, contentDescription = label,
                        tint = if (isAnswer) QuoraColors.Red
                               else if (selected == i) QuoraColors.TextPrimary
                               else QuoraColors.TextSecondary,
                        modifier = Modifier.size(24.dp))
                },
                label = { Text(label, style = QuoraText.Tab) },
                colors = NavigationBarItemDefaults.colors(
                    selectedIconColor   = QuoraColors.TextPrimary,
                    selectedTextColor   = QuoraColors.TextPrimary,
                    unselectedIconColor = QuoraColors.TextSecondary,
                    unselectedTextColor = QuoraColors.TextSecondary,
                    indicatorColor      = Color.Transparent, // Quora has no Material pill
                ),
            )
        }
    }
}

@Composable
fun QuoraTopTabs(selected: Int, onSelect: (Int) -> Unit) {
    val tabs = listOf("For You", "Following", "Answer", "Spaces")
    ScrollableTabRow(
        selectedTabIndex = selected,
        containerColor = QuoraColors.Canvas,
        edgePadding = 16.dp,
        indicator = { pos ->
            TabRowDefaults.SecondaryIndicator(
                Modifier.tabIndicatorOffset(pos[selected]),
                height = 2.dp,
                color = QuoraColors.Red,
            )
        },
        divider = { HorizontalDivider(color = QuoraColors.Divider) },
    ) {
        tabs.forEachIndexed { i, t ->
            Tab(selected = selected == i, onClick = { onSelect(i) }, text = {
                Text(t, style = QuoraText.Segment,
                    color = if (selected == i) QuoraColors.TextPrimary else QuoraColors.TextSecondary)
            })
        }
    }
}

6. Motion

Moment Compose recipe
Upvote tap animateColorAsState wash + arrow over 200ms tween; count via AnimatedContent; HapticFeedbackType.TextHandleMove
Pill press animateFloatAsState 1 → 0.96 → 1, 150ms
Card → detail AnimatedVisibility crossfade + slideInVertically { 8 }, 280ms
Composer present ModalBottomSheet / full-screen Dialog with slideInVertically, spring (dampingRatio 0.85)
Follow toggle animateColorAsState border + AnimatedContent label crossfade, 180ms
// Upvote count roll
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.tween

@Composable
fun VoteCountRoll(count: String, active: Boolean) {
    AnimatedContent(targetState = count, label = "vote",
        transitionSpec = { fadeIn(tween(200)) togetherWith fadeOut(tween(120)) }) { c ->
        Text(c, style = QuoraText.VoteCount,
            color = if (active) QuoraColors.Upvote else QuoraColors.TextSecondary)
    }
}

Haptics: prefer LocalHapticFeedback. For a crisper tick use LocalView.current.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK) (API 30+) to approximate iOS’s .light impact on upvote.

7. Icons

The closest first-party set is androidx.compose.material:material-icons-extended. Export Quora’s exact vote chevrons as vector drawables for pixel parity if needed.

Purpose SF Symbol (iOS) Material Icon (Compose)
Upvote arrowtriangle.up / .fill Icons.Filled.ArrowDropUp
Downvote arrowtriangle.down / .fill Icons.Filled.ArrowDropDown
Comment bubble.left Icons.Outlined.ChatBubbleOutline
Share square.and.arrow.up Icons.Filled.Share
Bookmark bookmark / bookmark.fill Icons.Outlined.BookmarkBorder / Icons.Filled.Bookmark
More ellipsis Icons.Filled.MoreHoriz
Answer / Compose square.and.pencil Icons.Filled.Edit
Search magnifyingglass Icons.Filled.Search
Home (tab) house / house.fill Icons.Filled.Home
Following (tab) person.2 Icons.Filled.People
Spaces (tab) rectangle.3.group Icons.Filled.ViewAgenda
Notifications (tab) bell / bell.fill Icons.Filled.Notifications

8. Minimum SDK & Accessibility Notes

  • minSdk 24 (Compose floor is 21; comfortable at 24). compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+.
  • Edge-to-edge: call enableEdgeToEdge(); the white canvas wants dark system-bar icons (isAppearanceLightStatusBars = true). Apply Scaffold insets so the bottom nav clears gesture nav.
  • Serif must survive: Georgia is not a system font on Android — you must bundle it in res/font/. Do not let the question fall back to the same family as the answer; the serif/sans split is the entire identity.
  • Font scaling: sp honors the user’s font scale — keep it on questions and answer bodies. Pin layout-sensitive text (vote counts, tab labels, segment labels) with a fixed-density wrapper.
  • TalkBack: announce the credential byline after the author name and before the timestamp — it is the trust signal. Label the vote pill segments separately ("Upvote, 1,200", "Downvote") and never expose a downvote count (Quora hides it). Merge each card’s question + answer with Modifier.semantics(mergeDescendants = true) but keep the vote pill an independent focus group.
  • Touch targets: Material guidance is 48.dp minimum. Give each vote-pill segment ≥48.dp effective hit via padding even though the pill is 32.dp tall; the 18.dp footer icons need a 48.dp hit area.
  • Contrast: #636466 on #FFFFFF passes WCAG AA at 13sp+. #9A9A9C is placeholder-only — never essential text. Validate the 10sp tab labels and bump toward #4A4B4D if targeting strict compliance.
  • Dynamic color: do not enable Material You dynamicLightColorScheme() — Quora’s identity requires the fixed white canvas and single red 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