← Back to Blog
Design··9 min read

DDS 2 — one accent per product

The rule that every dani product shares one red accent is gone. Each product now carries its own brand colour as its UI accent. Two smaller corrections come with it: the neutrals we published were never the neutrals we shipped, and the logo lockup finally has written ratios.

The system itself
Open DDS 2 →

Every token, component and specimen in one place: 20 accent pairs across four platform mirrors, 13 components with usage rules, 21 foundation specimens and four product UI kits. This article is the release note. That is the reference.

Browse the system

DDS 1.1.0 was emphatic on this point. Product colours existed for app icons and marketing assets; in-app accent was Dot Red, the same in every product, and §09 listed the six places it was allowed to appear. That rule is now retired.

Why
The rule had already stopped being true. Products shipped their own accents, the spec said they did not, and a specification that is wrong where you can check it stops being trusted where you cannot. Ratifying what shipped costs less than enforcing a rule nobody followed.

01 · One accent per product

A product's brand colour is now its UI accent. Nothing else changes: the neutral base, the type scale, the 4px grid, the radii, the no-shadow rule and every component are identical across all twenty products. The accent is the only variable.

Each accent ships as a pair — one tuned for light surfaces, one for dark — following the pattern Dot Red already used. Never cross-use them.

ProductNameLightDark
dani.subsAlert Red#E80000#FF2D2D
dani.newsScarlet#D11500#FF4518
dani.pennyCopper#C4470A#FF6B1A
dani.parkSignal Orange#B26500#FF8A00
dani.tosAmber#8F5C00#FFB300
dani.moneyGold#8C7000#FFD400
dani.snapFlash Yellow#5C7A00#E8FF1A
dani.goAlert Red#E80000#FF2D2D
dani.plantGreen#0A8F3D#3AFF7D
dani.budgetMint#008F5E#00FFB0
dani.cleanTeal#00806F#00E0C4
dani.trackSky#0070B2#00B8FF
dani.webBlue#0047D6#3D7AFF
dani.botViolet#4B1FBF#8F5CFF
dani.photoPurple#7A14B5#C14DFF
dani.designMagenta#B51495#FF4DD4
dani.petPink#C4144F#FF4D8F
dani.zoneGraphite#333333#666666
dani.koreaFlag Blue#0047A0#3D7BD9
dani.do— not assigned#000080#4D7CFF

dani.subs and dani.go share Alert Red; dani.ko and dani.do currently share navy. See “Still open”.

All twenty pairs as CSS, YAML, Dart and Swift: products.css · accent specimen

dddddddd

The mark carries the accent. One geometry, twenty hues.

What this costs

Under 1.1.0 a screenshot from any dani product was recognisably dani, because the red was the through-line. That is gone. What remains shared is the monochrome base, the Grotesk / Mono split, the rectangles and the hairlines — which is most of the system, but it is a quieter kind of family resemblance. This was the trade, and it was made deliberately.

Accent still applies to
  • · Primary button fill
  • · Key numeric values
  • · Active tab / selected state
  • · Alert badges and status pills
  • · Section bullets and tag dots
  • · Focus rings
× Accent still never applies to
  • · Body text
  • · Borders or dividers
  • · Page or card backgrounds
  • · Disabled or muted states
  • · Standard icons
  • · Section headers

§09 is unchanged. Only which colour fills those slots has changed.

In code

The Flutter package reads a per-product lookup instead of two constants. On the web, scope a product with a data attribute and --accent follows.

css
[data-product="plant"] { --accent: #0A8F3D; --on-accent: #FFFFFF; }
[data-theme="dark"] [data-product="plant"] { --accent: #3AFF7D; }
dart
// lib/src/skin/skins/dds_accents.dart
const DdsAccents.of('plant');   // → (light: 0xFF0A8F3D, dark: 0xFF3AFF7D)

02 · The published neutrals never shipped

§02 of the 1.1.0 spec gave nine tokens per mode and said to memorise them and never deviate. Those are not the values in dds_skin.dart, which is what every product actually renders. Eleven of them differ.

TokenPublishedShipped
light bg#F7F7F7#F9FAFB
light surface_2#F0F0F0#F3F4F6
light border#E0E0E0#E5E7EB
light text#111111#111827
light text_2#666666#6B7280
light text_3#AAAAAA#9CA3AF
dark bg#000000#111827
dark surface#0B0B0B#1F2937
dark surface_2#141414#374151
dark border#1F1F1F#374151
dark text#FFFFFF#F9FAFB

The shipped ramp is Tailwind's slate. The published one was never in the code.

Both ramps side by side: code vs. published spec

Correction
Dark mode is charcoal, not matte black. The 1.1.0 post said matte black (#000000), not charcoal, and called it the source of the system's electronic character. The code has always been #111827. The shipped values are canonical; the published ones are withdrawn.

This is a documentation fix, not a design change — nothing on screen moves. But it means the most-quoted sentence in the original post was describing an intention rather than a behaviour, which is worth saying plainly.

03 · The lockup has ratios

The mark had artwork and two implementations but no written specification, so consumers re-derived its proportions and drifted. Every ratio below is normalised to the tile edge S, taken from dani_logo_dds_v3.svg — the 560×160 master.

PropertyValueNote
tile edgeSthe unit everything else derives from
tile shapesuperellipse|x/r|^5 + |y/r|^5 = 1
tile r0.42 · S≈ 0.20 effective circular roundness
glyph size0.658 · SSpace Grotesk 700, live text
dot diameter0.125 · Sr 7.5 at S 120
gap, tile → wordmark0.25 · Sx 170 against a tile ending at 140
wordmark size0.867 · S104 at S 120
wordmark weight700not 600
wordmark tracking−0.029 em−3px at 104px
clear space0.25 · Sall four sides — same as the gap
Flutter baseline nudge+0.075 · wordmarkFlutter only; CSS/SVG is 0

Derive everything from S. Never hardcode pixel values at one size.

The last row is the only number not in the artwork, and it is a platform correction rather than a proportion. Flutter puts the alphabetic baseline 0.771 of the font size below the line box at height: 1.0; CSS and SVG half-leading put it at 0.846. Centre a wordmark against the tile in a Flutter Row and it hangs about 8% of a tile edge too high, so Flutter consumers translate it back down. Use a Transform.translate, never padding — padding adds to the laid-out height and a surrounding FittedBox scales the whole lockup down to pay for it.

Construction diagram and the twenty marks: lockup · app icons

ddani.zone

Tile 56, gap 14, wordmark 48.5 — all derived from S = 56.

The tile is the system's second curve

§05 says DDS is rectangular, with three radii and one stated exception: the small dot indicator. There is a second, and it is not on the radius scale at all. The tile is a superellipse — |x/r|^5 + |y/r|^5 = 1 at r 0.42 of its edge — rather than a rounded rectangle. A circular arc of the same apparent roundness pinches visibly at the sizes the mark actually ships at, 34px in a list row and 24px in a nav bar; continuous curvature does not. Pre-DDS-2 artwork carries rx 26 (0.217 of the edge) and needs regenerating. The rule now reads: two curves, the tile and the dot, both in the mark.

04 · What did not change

Space Grotesk for UI and Space Mono for data. The 4px grid, with 18px card padding, 14px card gap and 24px section gap. The three radii. Hairline 1px and strong 2px borders, and no drop shadows anywhere. 100ms and 200ms, ease-out, reduced motion disabled rather than shortened. The 4.5:1 contrast floor, the 44pt hit area, the 2px focus ring at 2px offset. §09's two lists. All of it stands.

05 · Migrating

01Replace the shared Dot Red constant with the per-product accent for your product.
02Delete any local brand-colour constant used outside an icon — it is the accent now.
03Re-check contrast on your accent pair against both mode surfaces before shipping.
04Correct any local copy of the §02 neutrals to the shipped slate values.

06 · Still open

Four things this release does not settle, listed so nobody has to rediscover them.

dani.do has no accent

It ships, and it was absent from the 1.1.0 colour table. It currently borrows dani.ko's navy, which means two unrelated products share a hue.

Two accent pairs are duplicated

subs and go both use Alert Red; ko and do both use navy. If the accent is what distinguishes a product, four products are not distinguished.

White on the brightest accents fails contrast

on_accent is #FFFFFF for all twenty pairs, but Flash Yellow #E8FF1A and Gold #FFD400 need dark text, not white. Those two pairs need an on_accent of their own.

The canonical token file has two names

This post's predecessor cites shared/design/dot-tokens.yaml; dds_skin.dart cites shared/design/dds-tokens.yaml. Resolved for the logo block on 2026-08-30 — shared/design/dot-tokens.yaml and dot.css now carry §03 — but two files still claim the same name.

Nothing draws the superellipse

§03 specifies the tile as a superellipse at r 0.42 S. Every shipping consumer — this site's own AppIcon, dani.ko's icon builder, both Flutter widgets — still draws the pre-DDS-2 rounded rect at 0.217. The tile shape and every generated icon in every product have to move in one pass or the marks disagree.

Where this lives
The mark ratios in §03 are documented here and in the design system, but not in the shared Flutter package — there is no logo widget in it. The two that exist live inside dani.go and dani.korea, and on 2026-08-30 both were brought onto §03: dani.go had been drawing the wordmark at 1.031 S, and dani.korea had copied dani.go's widget rather than this spec, which is how it surfaced. That is exactly the arrangement that produced the drift §03 corrects, and it should still move.