← Back to Blog
Design··38 min read

DDS — The dani design system

One monochrome foundation. One red accent. A single rectangular, monospace-data design language that scales across every dani app on every platform.

DDS (dani design system) is the visual language shared by every dani product — from dani.subs to dani.zone, dani.bot to dani.pet. It is deliberately narrow: one typographic scale, one spacing grid, one control ladder, one monochrome base. Every product uses the same Dot Red accent for UI. Product-specific colors exist only for app icons and brand assets — never in-app.

This document is the complete specification. If a number is missing here, it has not been decided, and it should not ship. Every value below is expressed in logical pixels — Flutter dp, CSS px, SwiftUI pt — which are equivalent for layout purposes across the platforms dani targets.

01 · Philosophy

DDS draws from four schools — Dieter Rams' functional discipline, the Swiss International Typographic style, Nothing OS's industrial monochrome, and Apple's platform polish. From each we took one thing:

  • 01
    Less, but better.Every element earns its place. No decoration, no gradients, no shadows. Only functional surface, text, and border.
  • 02
    Typography is the interface.Clear hierarchy beats ornament. A grid, a monospace, weight, and whitespace do the work of a hundred icons.
  • 03
    Rectangular, not round.Sharp 4px radii and hairline 1px borders. Circles and pills are out; rectangles are honest about what they are.
  • 04
    Monochrome speaks, accent shouts.Black, white, and one saturated red. Accent is reserved for action — never decoration.
  • 05
    One accent, every app.All dani products share the same Dot Red UI accent. Product identity comes from the app icon and brand assets, not from in-app colors.
  • 06
    Status is shape, not hue.There is no green-for-success, amber-for-warning palette. Meaning is carried by label, weight, border width, and position — which survives colorblindness, monochrome printing, and glare.

02 · Color

The DDS color system has two layers. The monochrome base is shared across every product, dark mode and light mode. The accent is Dot Red — the same across all products — and appears only on action surfaces.

Monochrome base

Nine tokens per mode. Memorize them. Never deviate.

Darkon #000000
bg#000000
surface#0B0B0B
surface_2#141414
border#1F1F1F
text#FFFFFF
text_2#777777
text_3#444444
Lighton #F7F7F7
bg#F7F7F7
surface#FFFFFF
surface_2#F0F0F0
border#E0E0E0
text#111111
text_2#666666
text_3#AAAAAA
TokenDarkLightRole
bg#000000#F7F7F7app background, the deepest layer
surface#0B0B0B#FFFFFFcards, list rows, inputs, sheets
surface_2#141414#F0F0F0avatars, nested surfaces, hover step
border#1F1F1F#E0E0E0card boundaries, dividers, control outlines
border_soft#141414#EBEBEBlow-contrast dividers inside a card
text#FFFFFF#111111primary text, active icons
text_2#777777#666666secondary text, labels, inactive icons
text_3#444444#AAAAAAtertiary text, disabled content
on_accent#FFFFFF#FFFFFFtext and icons on an accent fill

Dark mode is matte black (#000000), not charcoal. This gives DDS its electronic/industrial character — closer to a piece of consumer hardware than a typical software UI. Light mode uses warm off-white (#F7F7F7) with pure-white surfaces stacked on top.

UI accent — Dot Red

Every dani product uses the same accent for UI: Dot Red. It ships as a pair — #FF2D2D tuned for #000 surfaces (dark mode) and #E80000 tuned for #F7F7F7 (light mode). Never cross-use.

TokenDarkLightUsed for
accent#FF2D2D#E80000rest state of every accent surface
accent_hover#FF4747#CC0000pointer hover only — never on touch
accent_pressed#E52626#B80000active/pressed, 100ms
accent_subtle12% accent8% accentselected-row wash, chart fills
on_accent#FFFFFF#FFFFFFlabel on an accent fill
focus_ring#FF2D2D#E800002px outline, 2px offset

accent_subtle is the only place DDS mixes a color with a surface. It is a wash, never a gradient.

Dot Red is pre-verified for ≥4.5:1 contrast against both mode surfaces (WCAG AA). White text on a Dot Red button hits the same floor.

Brand colors (icons & assets only)

Each dani product has a unique hue for its app icon and marketing assets. These are never used as the in-app accent — all UI uses Dot Red above.

Dark brand colors20 hues · icons & assets only
subs
news
penny
park
do
tos
money
snap
go
plant
budget
clean
zone
track
web
ko
bot
photo
design
pet
Light brand colorssame hues · tuned deeper for white
subs
news
penny
park
do
tos
money
snap
go
plant
budget
clean
zone
track
web
ko
bot
photo
design
pet
ProductNameDarkLight
dani.subsAlert Red#FF2D2D#E80000
dani.newsScarlet#FF4518#D11500
dani.pennyCopper#FF6B1A#C4470A
dani.parkSignal Orange#FF8A00#B26500
dani.doSignal Orange#FF8A00#B26500
dani.tosAmber#FFB300#8F5C00
dani.moneyGold#FFD400#8C7000
dani.snapFlash Yellow#E8FF1A#5C7A00
dani.goAlert Red#FF2D2D#E80000
dani.plantGreen#3AFF7D#0A8F3D
dani.budgetMint#00FFB0#008F5E
dani.cleanTeal#00E0C4#00806F
dani.zoneElectric Cyan#00F0FF#008A94
dani.trackSky#00B8FF#0070B2
dani.webBlue#3D7AFF#0047D6
dani.koreaFlag Blue#3D7BD9#0047A0
dani.botViolet#8F5CFF#4B1FBF
dani.photoPurple#C14DFF#7A14B5
dani.designMagenta#FF4DD4#B51495
dani.petPink#FF4D8F#C4144F
dani.ko is the exception
Dot Navy is one value in both modes #000080, not a dark/light pair. It scores 16.0:1 on white and 1.31:1 on black, which is below the 3:1 floor for even non-text UI. The trade is deliberate: in dark mode the dani.ko accent can never be the only thing carrying a meaning. Anything that must stay legible there needs a second cue — a border, a light fill behind it, or a weight change. This is also why the dani.ko logo tile works where bare navy text would not: the white mark sits on the fill rather than beside it.

Every dani product shares one logo. It is a two-part lockup: a filled tile carrying the d. mark, and a wordmark set beside it. Only two things ever change between products — the string and the tile color. Nothing else: not the proportions, not the mark, not the spacing.

dani.go is the reference artwork. Every ratio below was measured off it, and dani.ko is drawn from those same numbers.

dani.go · dark
ddani.go
dani.korea · dark
ddani.korea
dani.go · light
ddani.go
dani.korea · light
ddani.korea

Construction

The lockup is driven by a single input: H, the nominal height a caller asks for. Every other dimension is a ratio of H or of the tile edge, so the lockup keeps its proportions at any size. Nothing in the logo is an absolute pixel value.

dtile = 1.96 H0.66r 0.220.28dani.gowordmark = 2.02 H = 1.031 tiledot ⌀ 0.115 · at (+0.275, +0.175)"d" at (−0.125, +0.025) from tile centre

H is the nominal height every caller passes. Offsets are fractions of the tile edge, measured from the tile centre.

PartRatioOfH = 14H = 24H = 40
tile edge1.96H27.4447.0478.40
tile radius0.22tile6.0410.3517.25
letter “d”0.66tile18.1131.0551.74
letter offset x−0.125tile, from centre−3.43−5.88−9.80
letter offset y+0.025tile, from centre+0.69+1.18+1.96
dot diameter0.115tile3.155.419.02
dot offset x+0.275tile, from centre+7.55+12.94+21.56
dot offset y+0.175tile, from centre+4.80+8.23+13.72
gap tile → wordmark0.28tile7.6813.1721.95
wordmark size2.02H (= 1.031 tile)28.2848.4880.80
wordmark tracking−0.03wordmark size−0.76−1.31−2.18

Offsets are measured from the tile centre, as fractions of the tile edge. In Flutter these are Alignment(−0.25, 0.05) for the letter and Alignment(0.55, 0.35) for the dot — Alignment units are twice the fraction.

Why 2.02 and not 1.82
The token file said 1.82 × H0.928 × the tile edge — until August 2026, and no shipping lockup actually drew it. Measured off the rendered dani.go artwork the wordmark is 1.031 × its tile. dani.ko, drawing the token value faithfully, read small and timid beside its own tile where dani.go reads level with its own. That 10% was the whole of the difference. The token file was corrected to the artwork rather than the other way round, and dot.css, dani.go's widget and dani.ko now all carry 2.02 / 1.031.

Color

PartTokenDarkLightInverts?
tile fillthe product accent#FF2D2D (go)#E80000 (go)yes — except dani.ko
letter “d”on_accent#FFFFFF#FFFFFFno
doton_accent#FFFFFF#FFFFFFno
wordmarktext#FFFFFF#111111yes, always

The tile is the accent and the wordmark is text. Never the reverse: a navy or red wordmark on the page background fails contrast, which is the whole reason the mark lives on a filled tile.

Typeface

ElementFamilyWeightLine heightTracking
letter “d”Space Grotesk7001.00
wordmarkSpace Grotesk7001.0−0.03em

Line height 1.0 on both — the lockup is centred geometrically, so any leading would push the parts out of alignment.

Platform notes

The same drawing needs two different vertical corrections, because Flutter and CSS/SVG do not put a baseline in the same place.

PlatformBaseline atCorrection
Flutter (height: 1.0)0.771 × fontSize below the line-box toptranslate the wordmark down by 0.075 × wordmark size
CSS / SVG (half-leading)0.846 × fontSize below the line-box topnone — this is the reference

Centre-aligned in a Row against the same tile, Flutter otherwise hangs the wordmark about 8% of a tile-edge too high. Use Transform.translate, not Padding — padding adds to the laid-out height and a surrounding FittedBox would scale the whole lockup down to pay for it.

The lockup's intrinsic width is driven by H, so a caller asking for a large height is really asking for a very wide row — at H = 34 that is roughly 550px. Always wrap it in a scale-down box (FittedBox(fit: BoxFit.scaleDown) in Flutter, max-width plus height: auto on the web) so it shrinks to the space it is handed instead of overflowing.

Sizes and clear space

HTileUse
1427in-app header, nav bar, footer — the default
2039auth screens, dialog headers
3467marketing hero, onboarding
anytile alone: app icon, favicon, avatar, splash

Clear space on all four sides is 0.28 × tile — the same value as the gap. Below H = 14 the wordmark stops being legible: drop it and ship the tile alone.

Logo — always
  • · Draw the mark; never typeset the name by hand
  • · Keep the tile letter as “d” for every product
  • · Change only the string and the tile color
  • · Let the wordmark follow the text token, so it inverts
  • · Wrap it in a scale-down box
  • · Below H = 14, ship the tile alone
× Logo — never
  • · Re-letter the tile with the product initial
  • · Color the wordmark with the accent
  • · Invert the dani.ko tile between modes
  • · Stretch, rotate, outline, or shadow the lockup
  • · Substitute a picture of the word for the mark
  • · Re-space the parts by eye instead of by ratio
A logo has a mark
dani.ko shipped twice without one — first as three hand-set text runs, then as a picture of the same word. The first silently ignored two accent changes because nothing tied it to the artwork; the second was indistinguishable from the first on screen. Neither was a logo. If it can be retyped, it is not a mark.

04 · Typography

Two families. Space Grotesk is the voice of the UI. Space Mono is the voice of data — every number, date, code, label, and timestamp uses mono. This split is the system's signature and it is not a stylistic preference: a monospace column of figures aligns without tabular-figure hacks, and it makes numbers instantly distinguishable from prose.

display28/32 · 600
Netflix Premium
title20/26 · 600
Subscriptions
heading18/24 · 600
Payment history
subhead15/20 · 600
Renewing this week
body13/18 · 500
Netflix · renews 04 apr · monthly
body_sm12/16 · 500
secondary copy here
caption11/14 · 500
Card ending 4242
amount_xl44/44 · 700
$2,571.84
amount36/36 · 700
$214.32
amount_sm20/24 · 700
$15.99
data12/16 · 700
04.APR.2026
data_sm10/14 · 700
12 ACTIVE
label9/12 · 700
MONTHLY TOTAL
micro8/10 · 700
08 NEW · X3

The full scale

Fourteen steps. Sizes below are the phone baseline — multiply by the viewport factor in the next table for tablet and desktop.

TokenSizeLine heightWeightTrackingFamilyUse
display2832 · 1.14600-0.01emsansdetail-screen hero title
title2026 · 1.30600-0.01emsansscreen titles
heading1824 · 1.33600-0.01emsanscard titles
subhead1520 · 1.336000sansrow-group headers, dialog titles
body1318 · 1.385000sanslist item names, primary copy
body_sm1216 · 1.335000sanssecondary copy
caption1114 · 1.275000sanshelper text, inline errors
amount_xl4444 · 1.00700-0.02emmonohero totals, one per screen
amount3636 · 1.00700-0.02emmonocard totals
amount_sm2024 · 1.20700-0.01emmonoinline totals, row amounts
data1216 · 1.337000monodates, counts, inline numbers
data_sm1014 · 1.407000monodense table cells
label912 · 1.337000.10emmonosection labels, tab bar — UPPERCASE
micro810 · 1.257000.08emmonobadges, row meta — UPPERCASE

Line height is absolute, not a ratio. Ratios drift between Flutter, CSS and SwiftUI; absolute leading does not.

Responsive scaling

The scale above is tuned for a phone. Wider viewports multiply every content size by a single factor, so the whole system grows together instead of drifting apart.

ViewportWidthFactorbodytitleamountdisplay
phone≤ 5991.00×13203628
tablet600 – 10231.15×15234132
desktop≥ 10241.30×17264736

Rounded to the nearest whole pixel. Round once, at the token layer — never per component.

Chrome does not scale
label, micro and data_sm are system chrome, not content. They stay at their phone size on every viewport, with a hard floor of 8px and a ceiling of 12px. A tab-bar label that grows with the window looks broken; a body paragraph that does not looks cramped.

Numerals

RuleValue
Figure styleTabular, lining — Space Mono is monospaced so this is automatic
Currency symboltext_2, one step smaller than the amount, top-aligned
Thousands separatorLocale-aware; never hand-inserted commas
Negative valuesLeading minus in the accent, never parentheses, never a second color
Alignment in tablesRight-aligned; decimal points line up because the face is monospaced
DatesDD.MMM.YYYY uppercase in mono — 04.APR.2026
Times24h in mono — 18:30. 12h only where the locale demands it

Text color roles

RoleTokenApplies to
primarytexttitles, list item names, amounts, active nav
secondarytext_2meta rows, labels, helper text, inactive nav
tertiarytext_3disabled text, empty-state hints, placeholders
accentaccentkey values, active state, destructive labels
on-accenton_accentany text sitting on an accent fill

Wrapping and truncation

  • List item names truncate with an ellipsis at one line. Never wrap a row title.
  • Amounts never truncate. If space is tight, the title yields, not the number.
  • Body copy wraps freely; reading columns cap at 720px.
  • Labels never wrap. If a label does not fit, the label is too long — shorten the words, not the type.
  • Two-line clamp is the maximum anywhere inside a card.

05 · Spacing & layout

DDS is built on a 4px grid with a 2px half-step. Every padding, margin, gap and dimension is a multiple of 2, and the overwhelming majority are multiples of 4. The half-step exists for optical corrections only — compensating a hairline border, centering a glyph — the same role Carbon gives its 2px mini-unit and Atlassian gives space.025.

2xs · 2
xs · 4
sm · 8
md · 12
lg · 16
xl · 20
2xl · 24
3xl · 32
4xl · 40
5xl · 48
6xl · 64

Semantic spacing

Primitives are for building components. Screens use the semantic tokens below, so a change to card rhythm is one edit rather than four hundred.

TokenPhoneTabletDesktopApplies to
screen_padding_x162432left/right gutter of every screen
screen_padding_top162424below the app bar
card_padding181820inside a bordered card
card_padding_compact121212dense cards, table cells
card_gap141416between sibling cards
section_gap243240between logical groupings
row_gap666between list rows inside one card
inline_gap888icon → label, chip → chip
stack_tight444label → value
stack_normal888field → field
stack_loose121212group → group inside a card

card_padding 18 = 16 + 2. The half-step compensates the 1px border on each side so text inside a card sits on the same optical rhythm as text on the bare background.

Breakpoints and the layout grid

NameMin widthColumnsGutterMarginNavigation
phone041216bottom tab bar, 56 + safe area
tablet60081624left rail, 72 wide
desktop1024122432sidebar, 240 wide
wide14401224centered at 1200sidebar, 240 wide
Phone · < 600
Bottom tab bar · 56 + safe area
Tablet · 600–1023
Left rail · 72 wide, icon + label
Desktop · ≥ 1024
Sidebar · 240 wide, always expanded

Content never stretches past 1200px. Reading-heavy views cap earlier, at 720px. Beyond 1440px the shell centers and the background extends — the layout does not keep growing.

Density

Two densities. Default everywhere touch is possible; compact on pointer-only desktop surfaces and inside data tables. Compact drops every control one step on the ladder and swaps card_padding for card_padding_compact. It never changes type sizes.

06 · Radii

DDS is rectangular. Three radii exist. There are no pills and no circles — with one sanctioned exception: the small decorative dot indicators, which are the system's single break from its own rule.

xs · 2px
checkboxes, swatches, inner hairline shapes
sm · 4px
every control, card, button, avatar, chip
md · 8px
sheets, device frames, full-bleed overlays
TokenValueApplies toNever
radius_xs2checkboxes, swatches, badges, inner shapescards, buttons
radius_sm4every control, card, button, avatar, chip, input
radius_md8bottom sheets (top corners), device framesanything inline
radius_fulldot indicators only, max 6px diameterbuttons, chips, avatars, FABs

On iOS, use plain corner radii — not continuous/squircle corners. The squircle is Apple's shape language, not ours.

07 · Borders & depth

DDS uses borders instead of shadows. Two widths exist: 1px hairline for standard dividers and card boundaries, and 2px strong for emphasis and selected states. No drop shadows. Ever. Depth comes from surface stacking and borders, never from blur.

LayerDarkLightBorderExample
L0 · background#000000#F7F7F7the screen
L1 · surface#0B0B0B#FFFFFF1px bordercards, list rows, inputs
L2 · surface_2#141414#F0F0F01px borderavatars, nested blocks
L3 · overlay#0B0B0B#FFFFFF1px border + scrimsheets, dialogs, menus
scrim60% #00040% #000behind any L3 overlay

Three layers is the ceiling. A fourth nested surface means the hierarchy is wrong, not that a new token is needed.

Border compensation
A control that gains a 2px border on focus must lose 1px of padding on each side, so the text does not shift. Every DDS input ships with this compensation baked in — rest is 1px border + 12px padding, focus is 2px border + 11px padding.

08 · Sizing & controls

Every interactive element in DDS is height-driven, not padding-driven. You set a height from the ladder and center the content; you never derive a height by adding vertical padding to a font size. This is the single change that keeps a button, an input and a select on the same baseline when their contents differ.

Dark
XS · 24
SM · 32
MD · 40
LG · 48
XL · 56
Light
XS · 24
SM · 32
MD · 40
LG · 48
XL · 56
SizeHeightPadding XGapTypeIconUsed for
xs2464micro 812table row actions, dismiss, inline tags
sm32106label 916filter chips, toolbar actions, segmented controls
md40148body_sm 1216default — inputs, selects, secondary buttons
lg48168body 1320primary CTA, full-width mobile buttons
xl562010body 1324onboarding and empty-state CTAs

Padding X is the inline padding at rest, with a 1px border. Add the border compensation rule when a 2px border applies.

Component anatomy

The ladder above sets heights. This table sets everything else. Values are phone defaults; desktop drops one ladder step under compact density.

ComponentHeightPaddingRadiusBorderType
Button · primary480 / 1641px accentbody 13 / 600
Button · secondary480 / 1641px borderbody 13 / 600
Button · ghost480 / 1641px borderbody 13 / 600
Button · destructive480 / 1642px accentbody 13 / 600
Icon button40 × 4041px bordericon 20
Text input400 / 1241px → 2px focusbody 13 / 500
Select400 / 1241px → 2px focusbody 13 / 500
Textareamin 961241px → 2px focusbody 13 / 500
Search field400 / 1241px borderbody 13 / 500
Chip · filter280 / 841px borderlabel 9
Chip · tag240 / 841px accentmicro 8
Badge · count160 / 62none, accent fillmicro 8
List row · one line481041px border_softbody 13
List row · two line561041px border_softbody 13 + micro 8
List row · with amount561041px border_softbody 13 + data 12
Avatar24 / 30 / 40 / 5641px bordermono 700
Checkbox18 × 1822px
Radio18 × 1822px8px inner square
Switch44 × 24241px18px knob, radius 2
Slidertrack 20nonethumb 12 × 20, radius 2
Progress bar42noneaccent fill on border track
Segmented control320 / 1041px, 2px on selectedlabel 9
Tab bar · bottom560 / 801px top bordericon 20 + label 9
App bar56 / 64 desktop0 / 1601px bottom bordertitle 20
Nav railw 72801px right bordericon 20 + micro 8
Sidebarw 2401201px right borderbody 13
Bottom sheetauto188 top1px bordergrabber 32 × 3
Dialogmax-w 4002441px bordersubhead 15
Toast480 / 1641px borderbody_sm 12
Table row40 / 32 compact0 / 1201px bottom borderdata 12
Table header320 / 1201px bottom borderlabel 9
Divider10border token
Hit area is not visual size
A 24px chip is a 24px chip. Its hit area is still 44 × 44 on iOS and 48 × 48 on Android — expanded transparently around the visual box. Never grow the control to satisfy the target; grow the target.

09 · Icons

Icons are outline, uniform stroke, square-cornered, and drawn on the same 4px grid as everything else. There is no filled variant and no duotone — an active icon changes color, not weight.

Dark
12
16
20
24
32
Light
12
16
20
24
32
SizeStrokeOptical boxUsed in
131.516inline with micro/label text, table affordances
171.520default — inside buttons, chips, list rows
20224controls at lg and above, tab bar, nav rail
24228app bar actions, empty states
32240empty-state illustration anchor only

Optical box is the layout footprint. Align the box, not the glyph — otherwise a wide icon and a narrow icon in the same row will not line up.

10 · Motion

Motion is functional, not expressive. If a user cannot explain what an animation is for, it should not be there. There are no physics-based animations, no parallax, no particle effects, and no easter-egg flourishes.

TokenDurationEasingApplies to
instant100msease-outpress feedback, tab switch, checkbox toggle
normal200msease-outreveal, dismiss, expand, route change
slow300msease-outbottom sheet and dialog entry only
none0mseverything, when reduced-motion is on
TransitionPropertyDuration
Button pressbackground → accent_pressed100ms
Hover (pointer only)background → accent_hover / surface_2100ms
Focus ringoutline opacity 0 → 1100ms
Route pushopacity 0 → 1, translateY 8 → 0200ms
Sheet entrytranslateY 100% → 0300ms
Scrimopacity 0 → 1200ms
List item insertopacity 0 → 1200ms, no height animation
Skeleton loadingopacity 0.4 ⇄ 0.81200ms, linear, infinite

No transition animates layout size. Animating height causes reflow jank on every platform we ship to.

Haptics are light on tap and selection on tab or segment change. Nothing else vibrates.

11 · States & interaction

Eight states, defined once, applied identically to every control. States are expressed as explicit token swaps — never as a blanket opacity multiplier, which stacks unpredictably when two states coincide.

Dark
Save
rest
Save
hover
Save
pressed
Save
focus
Save
disabled
Light
Save
rest
Save
hover
Save
pressed
Save
focus
Save
disabled
StateSurfaceBorderTextNotes
restper component1px bordertextthe baseline
hoversurface → surface_2unchangedunchangedpointer devices only
pressedaccent → accent_pressedunchangedunchanged100ms, no scale transform
focus-visibleunchangedunchangedunchanged2px accent outline, 2px offset
selectedunchanged1px → 2px accenttextborder carries selection, not fill
disabledunchangedborder → border_softtext → text_3no opacity multiplier, not focusable
loadingunchangedunchangedreplaced3-dot mono ellipsis in accent, size unchanged
errorunchanged1px → 2px accenttextleading ! marker + caption below
No semantic color palette
DDS has no green, amber or blue status colors. Success is a in mono at text. Warning and error are a ! marker plus a 2px accent border. This is a deliberate constraint: it survives colorblindness, greyscale printing and direct sunlight, and it keeps the accent meaning exactly one thing — attention here.

Selection controls

Dark
Checkbox · checked
Checkbox · rest
Radio · selected (square, not round)
Switch · on
Switch · off
Light
Checkbox · checked
Checkbox · rest
Radio · selected (square, not round)
Switch · on
Switch · off

12 · Components

Every DDS component is built from the same primitives: a height from the ladder, a 4px radius, a 1px border, and type from the scale. The system is combinatorial — there are no special-case components.

Buttons

Dark
Light
VariantFillBorderLabelUsed for
primaryaccent1px accenton_accentthe one action a screen exists for
secondarysurface1px bordertextsupporting actions
ghosttransparent1px bordertext_2tertiary, dismissive actions
destructivetransparent2px accentaccentdelete, cancel subscription, sign out
disabledsurface1px border_softtext_3blocked actions — never hidden

One primary button per screen. Two primaries means the screen has two purposes and should be two screens.

List items

Dark
N
Netflix
RENEWS 04 APR · MONTHLY
$15.99
S
Spotify Family
RENEWS 11 APR · MONTHLY
$16.99
A
Adobe Creative
RENEWS 07 APR · MONTHLY
$54.99
Light
N
Netflix
RENEWS 04 APR · MONTHLY
$15.99
S
Spotify Family
RENEWS 11 APR · MONTHLY
$16.99
A
Adobe Creative
RENEWS 07 APR · MONTHLY
$54.99

Data card / totals

Dark
MONTHLY SPEND
$214.32
ANNUAL$2,571.84
Light
MONTHLY SPEND
$214.32
ANNUAL$2,571.84

Chips & tags

Dark
12 ACTIVE08 NEWTAG3
Light
12 ACTIVE08 NEWTAG3

Gauge / progress

Dark
PROGRESS · 5 OF 5
PROGRESS · 3 OF 5
PROGRESS · 1 OF 5
Light
PROGRESS · 5 OF 5
PROGRESS · 3 OF 5
PROGRESS · 1 OF 5

Inputs

Dark
$15.99
Netflix Premium
!Netflix
! This name is already in use
Light
$15.99
Netflix Premium
!Netflix
! This name is already in use
PartTypeSpacingNotes
Field labellabel 9 uppercase6 belowalways above the field, never a floating placeholder
Fieldbody 13 / mono for numbersheight 401px border, 2px accent on focus
Prefix / suffixmono 700, text_28 inline gapcurrency symbols, units
Helper textcaption 11, text_26 aboveone line, always reserved so layout does not jump
Error textcaption 11, accent6 abovereplaces helper text, prefixed with !

13 · Usage rules

These are the enforcement rules. Every design and code review applies them.

Accent applies to
  • · Primary button fill
  • · Key numeric values (amounts, counts)
  • · Active tab / selected state
  • · Alert badges and status pills
  • · Tag indicators (small dots)
  • · Section bullets
  • · Focus rings
× Accent never applies to
  • · Body text
  • · Borders or dividers at rest
  • · Page or card backgrounds
  • · Disabled / muted states
  • · Standard icons
  • · Section headers
  • · Decorative fills or gradients
Always
  • · Pick heights from the control ladder
  • · Round once, at the token layer
  • · Reserve space for helper and error text
  • · Expand hit areas rather than growing controls
  • · Give every state an explicit token swap
× Never
  • · Derive a control height from font size + padding
  • · Use opacity to express a disabled state
  • · Animate layout height
  • · Introduce a second accent, ever
  • · Add a shadow, a gradient, or a pill

14 · Accessibility

RequirementFloorSource
Text contrast4.5:1WCAG 2.2 AA · 1.4.3
Large text (≥ 24px)3:1WCAG 2.2 AA · 1.4.3
UI component contrast3:1WCAG 2.2 AA · 1.4.11
Target size · web24 × 24 pxWCAG 2.2 AA · 2.5.8
Target size · iOS44 × 44 ptApple HIG
Target size · Android48 × 48 dpMaterial 3
Type scalingup to 130%DDS cap on Dynamic Type / fontScale
Focus indicator2px, 2px offsetWCAG 2.2 AA · 2.4.11
  • Every interactive element has a semantic label — icon-only controls always carry one.
  • Focus order follows visual order. Overlays trap focus and return it on dismiss.
  • Reduced-motion users get motion disabled, not shortened — zero animation.
  • Nothing is communicated by color alone. Every accent signal has a label, marker, or position that repeats it.
  • Text scales to 130% without clipping. Rows grow; type does not shrink to fit.
  • Layout mirrors fully in RTL. Icons that encode direction mirror; logos and numerals do not.
  • Both modes are first-class. Neither is a filter applied to the other.

15 · Platforms

The tokens are identical everywhere. What differs is what each platform's conventions force on us — hit-target floors, safe areas, input model, and the native behaviours we deliberately switch off so DDS still looks like DDS.

ConcerniOS / iPadOSAndroidWeb / desktop
Min hit target44 × 44 pt48 × 48 dp24 × 24 px (44 on touch)
Default button sizelg · 48lg · 48md · 40 at ≥ 1024
Type scalingDynamic Type, capped 130%fontScale, capped 130%browser zoom, uncapped
Densitydefaultdefaultcompact at ≥ 1024
Bottom nav56 + safe area inset56 + gesture insetsidebar 240 / rail 72
Header56, custom — no large titles56, no elevation on scroll64
Press feedback100ms token swap + light haptic100ms token swap, ripple OFFhover + :focus-visible
Overscrollbounce, no custom glowclamp, stretch disablednative
Corner styleplain radii, not continuousplain radiiplain radii
System chromestatus bar follows modeedge-to-edge, transparent barstheme-color meta follows mode
Backedge swipe stays enabledpredictive back supportedbrowser history, Esc closes overlays

iOS / iPadOS

  • Never hardcode the home-indicator inset. Tab bar height is 56 + safeAreaInsets.bottom.
  • Large titles are off. DDS uses its own 56pt bar with a title 20 heading.
  • Amount fields use the decimal pad and the mono face, so the keyboard and the value agree.
  • Haptics: .light impact on tap, .selection on tab and segment change.
  • Continuous corners are disabled — DDS radii are plain, on every platform.

Android

  • The Material ripple is replaced by a 100ms surface-step press state. Splash factory is set to none.
  • Minimum hit target rises to 48 × 48 dp — the visual control size does not change.
  • Edge-to-edge with transparent system bars; icon brightness follows the mode.
  • Stretch overscroll is disabled in favour of a clamped scroll physics.
  • Elevation is zero on every surface. Depth is borders, as everywhere else.

Web & desktop

  • Hover states apply only under @media (hover: hover) and (pointer: fine).
  • Focus rings use :focus-visible. Outlines are never removed, only restyled.
  • Full keyboard model: Tab order follows layout, Esc closes overlays, Enter submits, arrows move within tabs and segmented controls.
  • Scrollbars are 8px, thumb text_3, no arrows, no track.
  • Selection background is the accent with on_accent text.
  • Print forces light mode, keeps hairlines, and drops navigation chrome.
  • Desktop windows have a custom 44px title bar and a 720 × 560 minimum size.

Token implementations

DDS tokens live in shared/design/dot-tokens.yaml as the single source of truth. Each platform mirrors them into native code. There is no auto-generation — the yaml is the contract, each project owns its translation.

Flutter · lib/core/theme/dds_tokens.dart
class DdsBase {
  // Dark
  static const darkBg        = Color(0xFF000000);
  static const darkSurface   = Color(0xFF0B0B0B);
  static const darkSurface2  = Color(0xFF141414);
  static const darkBorder    = Color(0xFF1F1F1F);
  static const darkText      = Color(0xFFFFFFFF);
  static const darkText2     = Color(0xFF777777);

  // Light
  static const lightBg       = Color(0xFFF7F7F7);
  static const lightSurface  = Color(0xFFFFFFFF);
  static const lightBorder   = Color(0xFFE0E0E0);
  static const lightText     = Color(0xFF111111);
  static const lightText2    = Color(0xFF666666);
}

class DdsUiAccent {
  // Dot Red — same for all products
  static const dark    = Color(0xFFFF2D2D);
  static const light   = Color(0xFFE80000);
  static const pressed = Color(0xFFE52626);
}

class DdsSize {
  static const xs = 24.0, sm = 32.0, md = 40.0, lg = 48.0, xl = 56.0;
}

class DdsRadii {
  static const xs = 2.0;   // checkboxes, badges
  static const sm = 4.0;   // all controls
  static const md = 8.0;   // sheets, frames
}

// Ripple off, elevation zero — depth is borders.
final ddsTheme = ThemeData(
  splashFactory: NoSplash.splashFactory,
  highlightColor: Colors.transparent,
);
Web · globals.css
:root {
  /* Light mode (default) */
  --bg: #F7F7F7;
  --surface: #FFFFFF;
  --border: #E0E0E0;
  --text: #111111;
  --text-2: #666666;
  --accent: #E80000;
  --accent-pressed: #B80000;

  /* Control ladder */
  --control-xs: 24px;
  --control-sm: 32px;
  --control-md: 40px;
  --control-lg: 48px;
  --control-xl: 56px;
}

[data-theme="dark"] {
  --bg: #000000;
  --surface: #0B0B0B;
  --border: #1F1F1F;
  --text: #FFFFFF;
  --text-2: #777777;
  --accent: #FF2D2D;
  --accent-pressed: #E52626;
}

.btn-primary {
  height: var(--control-lg);
  padding-inline: 16px;
  background: var(--accent);
  color: #FFFFFF;
  border: 1px solid var(--accent);
  border-radius: 4px;
  font: 600 13px 'Space Grotesk';
}

@media (hover: hover) and (pointer: fine) {
  .btn-primary:hover { background: color-mix(in srgb, var(--accent) 88%, #FFF); }
}
.btn-primary:active { background: var(--accent-pressed); }
.btn-primary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
SwiftUI · DdsTokens.swift
enum DdsBase {
  static let darkBg       = Color(hex: 0x000000)
  static let darkSurface  = Color(hex: 0x0B0B0B)
  static let darkBorder   = Color(hex: 0x1F1F1F)
  static let darkText     = Color(hex: 0xFFFFFF)

  static let lightBg      = Color(hex: 0xF7F7F7)
  static let lightSurface = Color(hex: 0xFFFFFF)
  static let lightBorder  = Color(hex: 0xE0E0E0)
  static let lightText    = Color(hex: 0x111111)
}

enum DdsUiAccent {
  // Dot Red — same for all products
  static let dark  = Color(hex: 0xFF2D2D)
  static let light = Color(hex: 0xE80000)
}

enum DdsSize {
  static let xs: CGFloat = 24, sm = 32, md = 40, lg = 48, xl = 56
  static let minTarget: CGFloat = 44
}

// Plain corners, not continuous — and a 44pt target around a smaller control.
extension View {
  func ddsControl(_ height: CGFloat) -> some View {
    self.frame(height: height)
      .clipShape(RoundedRectangle(cornerRadius: 4, style: .circular))
      .contentShape(Rectangle())
      .frame(minWidth: DdsSize.minTarget, minHeight: DdsSize.minTarget)
  }
}

16 · What we measured against

DDS is opinionated, but it is not invented from nothing. Before fixing the control ladder and the spacing half-step we read what the large public systems actually publish, and where a number already had a good answer we took it rather than inventing a worse one.

SystemBase gridControl heightsMin targetRadiusColor model
Material 34dpXS 32 · S 40 · M 56 · L 96 · XL 136 dp48 × 48 dpShape scale, fully rounded by defaultTonal palettes generated from a seed color
Apple HIGPoints, no published gridPlatform-native, set by the control44 × 44 ptContinuous (squircle) cornersSystem tint + semantic system colors
IBM Carbon2px mini-unit, 8px macrosm 32 · md 40 · lg 48 pxNot specified separately0px — fully squareFixed IBM palette behind semantic token layers
GitHub Primer8px baseXS 24 · S 28 · M 32 · L 40 · XL 48 pxNot specified separatelySmall, non-zeroFunctional token layers over a fixed scale
Atlassian8px base with 2 / 4 / 6 half-stepsNot published as a single ladderNot specified separatelySmall, non-zeroFixed palette behind semantic tokens
WCAG 2.224 × 24 CSS px (AA) · 44 × 44 (AAA)4.5:1 text contrast (AA)
DDS4px base with a 2px half-stepxs 24 · sm 32 · md 40 · lg 48 · xl 56 px44 pt iOS · 48 dp Android · 24 px web4px, everywhere, alwaysMonochrome base + exactly one accent
Adopted
  • · Carbon's 2px mini-unit, as our half-step
  • · Primer's five-step control ladder and its condensed / normal / spacious padding triple
  • · Atlassian's semantic-over-primitive token naming
  • · Material 3's 48dp Android target floor
  • · Apple's 44pt iOS target floor and Dynamic Type respect
  • · WCAG 2.2's 24px web target floor
× Rejected
  • · Material 3 tonal palettes — DDS has exactly one accent
  • · Material elevation levels and the ripple
  • · Apple's continuous (squircle) corners
  • · Carbon's 0px radius — 4px keeps a control reading as a control
  • · Semantic status palettes — status is shape and label, not hue
  • · Pills, circles, and fully-rounded shapes

The pattern is consistent: we take accessibility floors from the platform owners, because those are constraints rather than opinions, and we reject everything that would add a second visual language on top of ours.

17 · Governance & versioning

DDS is versioned semantically. The current version is 1.3.0.

  • Patch (1.x.y) — accent contrast tweaks, doc corrections.
  • Minor (1.y.0) — new component, new token, new size step. Backwards compatible.
  • Major (x.0.0) — breaking: removed tokens, changed monochrome base, new type family.

Any change to the yaml must be mirrored in every consuming project's local token file before that project's next release. The yaml is the contract.

Changelog

VersionDateChange
1.3.02026-08-04Logo & wordmark construction spec; wordmark ratio corrected 1.82 → 2.02 to match the artwork; dani.ko (Dot Navy) and dani.do added
1.2.02026-08-04Control ladder, component anatomy, state matrix, icon scale, per-platform rules, comparative survey
1.1.02026-04-08One shared Dot Red UI accent; brand hues demoted to icons and assets
1.0.02026-04-05Initial specification — color, type, spacing, radii, motion, components

DDS is not a design system you customize. It's a design system you live inside. The constraints are the feature.

Version 1.3.0 · Updated August 4, 2026 · Tokens at shared/design/dot-tokens.yaml