Skip to content

Token reference

Every token, its value and where it comes from. The rules for when to use each one are in README.md. Code lives in lib/presentation/theme/app_tokens.dart and app_text.dart, and test/presentation/theme/app_tokens_test.dart pins every property below that can be checked (contrast, ordering, scale).

AppColors.* roles point at appTheme.* primitives in theme_helper.dart.

Role Primitive Hex
background black #000000
surface blueGray #18181B
surfaceRaised popUp #27272A
border stroke #3F3F46
textPrimary white #FFFFFF
textSecondary gray300 #D4D4D8
textTertiary gray100 #A1A1AA
textDisabled gray #7E7E83
accent primary #E11D48
onAccent white #FFFFFF
accentText rose400 #FB7185
selection white #FFFFFF
onSelection black #000000
danger errorBorder #EF4444
onDanger white #FFFFFF
premium premium #F97316
award award #EAB308
pillIdle gray700 #2D2D32
pillSelected primary (= accent) #E11D48

Text on each ground. AA is 4.5:1 for body text and 3:1 for large or bold text. The glass column uses the glass tint colour (navBar #202023) opaque, which is the lightest a glass surface gets over dark content.

Text role background surface surfaceRaised glass
textPrimary 21.0 17.7 14.9 16.3
textSecondary 14.2 12.0 10.1 11.0
textTertiary 8.2 6.9 5.8 6.3
accentText 7.8 6.6 5.5 6.0
textDisabled (exempt) 5.2 4.4 3.7 4.0
accent #E11D48 as text ✗ 4.47 3.8 3.2 3.5
legacy gray600 hint ✗ 3.6 3.1 2.6 2.8
Pair Contrast
onAccent on accent 4.7
onSelection on selection 21.0
onDanger on danger 3.8 (bold labels only)

Why textTertiary is #A1A1AA and accentText is #FB7185: the first candidates (#7E7E83 and #F43F5E) passed on black but failed on raised surfaces (3.7 and 4.1). The contrast test caught this before any screen used them.

AppText.* uses Inter at fixed sizes, and textPrimary by default.

Role Size Weight
display 24 700
titleLarge 20 600
title 18 600
section 16 500
body 14 400
label 13 500
caption 12 400
micro 11 500

Migrate a style by picking its role and changing the tone with copyWith(color:).

Legacy style(s) Size / weight Role
text2Xl, text3Xl and up 24–60 display (hero sizes above 24 are one-offs; keep them local)
textXl, textXl700 20 / 600–700 titleLarge
textLg, textLg600 18 / 600–700 title
textBase, bodyMedium, bodyMediumW500, bodyMediumW600 16 / 400–600 section
bodySmall, inputText, textPopUpGray, textTitleCardGray 14 / 400 body
textTitleCard, bodySmallw500 14 / 500 body + w500
bodySmallHint 14, gray600 body + textTertiary
bodySmallBlue, bodyMediumBlue, textTitleCardBlue 14–16, primaryButton body / section + accentText
textXs, textXsLight, textXsMedium, textSmallContent, bodySmallExtra 12 caption
errorText 12 / 500 caption + w500 + danger
textXsContent, bodyMediumBrownW600 11 micro
textTn, textTn400, textSmall 10 micro (10 is below the floor)

Design points; the getters apply s (see README.md).

Token Value
xxs 2
xs 4
sm 8
md 12
lg 16
xl 24
xxl 32
gutter = lg
Token Value
xs 4
sm 8
md 12
lg 16
xl 20
pill GlassTokens.stadium (999)
Token Value
quick 140 ms (= GlassTokens.liftDuration)
standard 250 ms
curve Curves.easeOutCubic

The travelling spring is GlassTokens.spring; see glass.md.

Design points at the kDesignWidth-wide (370) phone. The getters already apply s, so never add .s to a token.

Token Value
minTouch 44
controlCompact 28
control 44
controlLarge 48
iconSm 16
icon 20
iconLg 24
headerBar 44
bottomNav 56
sheetMaxWidth 480
dialogMaxWidth 480

PosterShape in widgets/content_widget/poster/poster_metrics.dart. Widths are design points: phone values go through s, tablet values do not.

Shape Ratio Grid min tile (phone / tablet) Columns Rail visible Rail card cap
portrait 2:3 96 / 140 3–8 2.4 180
landscape 16:9 150 / 220 2–5 1.7 260
square 1:1 96 / 140 3–8 2.7 170
rounded 1:1 96 / 130 3–8 3.3 150

Grid (PosterGridLayout), for a row W wide with gap g:

columns = clamp(floor((W + g) / (minTile + g)), min, max)
tileWidth = (W − g·(columns − 1)) / columns // tiles add up to W exactly
height = tileWidth / ratio + metaHeight

Rail (PosterRailLayout), for a viewport W, leading gutter G, gap g and visible count v:

cardWidth = (W − G − g·floor(v)) / v // the last card peeks by v − floor(v)

When that is wider than the cap (tablets), the rail shows the fewest whole cards that bring it under the cap, keeping the same peek. The rail’s height is cardWidth / ratio + metaHeight, so it cannot drift from the card.

Meta line (PosterMeta.height): the gap, two title lines of 11 × 1.3, and for artists a type line of 10 × 1.1, all at the viewer’s text scale.

Metric Where What
FloatingNavBarMetrics.accessoryBottomOffset(context, p) floating_nav_bar_metrics.dart Bottom of an accessory above the nav bar at hide progress p
FloatingNavBarMetrics.accessoryGap same AppSpace.sm.v between the accessory and the bar
SearchCapsule.height search_capsule.dart AppSize.controlLarge (48)
SearchFacetBar.height search_facet_bar.dart AppSize.minTouch (44), the chip row
GlassChip.height glass_chip.dart AppSize.controlCompact (28) drawn, 44 hit; the shared flat pill
FloatingNavBarMetrics.pillHeight floating_nav_bar_metrics.dart AppSize.bottomNav (56)
NavStripMetrics.headerRowHeight / chipHeight nav_strip_metrics.dart AppSize.headerBar (44) / AppSize.controlCompact (28)