Content first
Numbers and useful information lead the eye. Decoration appears only when it carries meaning.
01 / PRINCIPLES
A page shows the content people need to read and the actions they need to take. Color, spacing, and motion have fixed roles and do not change from page to page.
Numbers and useful information lead the eye. Decoration appears only when it carries meaning.
Use the same spacing, alignment, and grouping throughout. The same meaning always gets the same visual treatment.
Soft sage green, generous corners, and plain language keep a precise data interface approachable.
Build hierarchy with weight, size, and space.
Neon colors, page gradients, glows, and decoration without a purpose.
02 / FOUNDATIONS
A shared visual grammar keeps the experience consistent across platforms and screen sizes. Every foundation value comes from a semantic token.
A black night theme and a warm paper theme share the same semantics. Changing the theme changes surfaces and contrast, not layout.
Canvas #000000
Canvas #F4EFE5
A preset replaces the accent only. Text, surfaces, and status colors stay the same. Select one to preview it across the site.
Select a swatch to copy its value. Never communicate status with color alone; pair it with text, shape, or an icon.
A calm text face paired with compact numerals. Regular 400 and semibold 600 cover most hierarchy.
| Role | Size / line height | Weight | Tracking |
|---|---|---|---|
| Large title | 38 / 1.04 | 600 | −1.3 |
| Page title | 32 / 1.08 | 600 | −0.9 |
| Section title | 21 / 1.20 | 600 | −0.4 |
| Body | 16 / 1.50 | 400 | 0 |
| Footnote | 13 / 1.50 | 400 | 0 |
Align numbersMeasurements and dates use tabular figures. Units are smaller and share the baseline.
Limit line lengthBody text does not stretch indefinitely. When text is enlarged, let it wrap instead of clipping.
A 4px rhythm. Space follows content relationships instead of being divided evenly.
Narrow screens stay single column. Wide screens cap the reading width and center the content. Reflow multi-column controls before reducing type. Fixed actions must not cover inputs, and scrollable content leaves about 144px behind a floating navigation bar.
Rounded corners are soft without becoming playful. Thin borders define surfaces; shadows only suggest a slight spatial relationship.
Use a 1px translucent card border and 0.7px dividers.
Outlining every nested element or stacking heavy shadows.
Motion confirms input and preserves spatial continuity. It is not a demonstration of technique. Nothing loops, and entrances do not queue.
Respect the system “Reduce motion” setting. Replace spatial movement with an immediate state change or a quick fade.
03 / COMPONENTS
A finite set of composable variants covers most product needs. Every specimen below is a real control and can be used directly.
For submitting, confirming, and navigating. One primary action per area; everything else is secondary, outline, quiet, or glass.
Press scales to 0.97 with no ripple. Loading keeps the original width. Disabled buttons do not respond. Use the glass variant only over an existing surface or divider.
<button class="dd-button primary">Save changes</button>
<button class="dd-button secondary">Add item</button>
<button class="dd-button outline">Outline</button>
<button class="dd-button glass">Glass</button>
<button class="dd-button secondary" disabled>Unavailable</button>
<style>
.dd-button { min-height: 48px; padding: 0 20px; border-radius: 999px;
border: 1px solid transparent; font: 600 14px/1 sans-serif; }
.primary { background: #f5f5f7; color: #000; }
.secondary { background: rgba(255,255,255,.07); color: #f5f5f7;
border-color: rgba(255,255,255,.12); }
.outline { background: transparent; color: #f5f5f7;
border-color: rgba(255,255,255,.22); }
.glass { background: rgba(255,255,255,.04); color: #c2d6c1; }
.dd-button:active { transform: scale(.97); opacity: .9; }
.dd-button:disabled { opacity: .4; cursor: not-allowed; }
</style>Segmented controls handle a small set of exclusive options. Selection chips handle filters that people scan quickly.
The selected state changes both the surface and aria-pressed. Use segments for 2–4 exclusive options. Use chips when there are more filters.
<div class="dd-segment" role="group" aria-label="Meal">
<button aria-pressed="true">Breakfast</button>
<button aria-pressed="false">Lunch</button>
<button aria-pressed="false">Dinner</button>
</div>
<button class="dd-chip" aria-pressed="true">Light activity</button>
<style>
.dd-segment { display: flex; gap: 3px; padding: 4px;
border-radius: 15px; background: rgba(255,255,255,.06); }
.dd-segment button { flex: 1; min-height: 38px; border: 0;
border-radius: 11px; background: transparent; color: #a8a8ad; }
.dd-segment [aria-pressed="true"] { background: #f5f5f7; color: #000; }
.dd-chip { min-height: 40px; padding: 0 16px; border-radius: 999px;
border: 1px solid rgba(255,255,255,.12); background: transparent; }
.dd-chip[aria-pressed="true"] { background: #f5f5f7; color: #000; }
</style>A switch applies a setting immediately. Checkboxes select multiple items. Radio buttons choose one from a group.
The entire row is clickable. Label the setting itself, not the act of toggling. A radio group always keeps one option selected.
<label class="dd-switch">Automatic sync
<input type="checkbox" checked><i aria-hidden="true"></i>
</label>
<label class="dd-check"><input type="checkbox" checked><i></i>Breakfast</label>
<style>
.dd-switch { display: flex; align-items: center; justify-content: space-between;
min-height: 48px; cursor: pointer; }
.dd-switch input { position: absolute; opacity: 0; }
.dd-switch i { width: 48px; height: 28px; padding: 3px; border-radius: 999px;
background: #3a3a3c; transition: background .2s; }
.dd-switch i::after { content: ""; display: block; width: 22px; height: 22px;
border-radius: 50%; background: #fff; transition: transform .2s; }
.dd-switch input:checked + i { background: #a8bfa8; }
.dd-switch input:checked + i::after { transform: translateX(20px); }
.dd-switch input:focus-visible + i { outline: 2px solid #c2d6c1; outline-offset: 3px; }
.dd-check { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; }
.dd-check input { width: 20px; height: 20px; accent-color: #a8bfa8; }
</style>A persistent label sits above the field; placeholder text never replaces it. Units and values share a baseline.
Enter the kilojoule value per serving. The result updates automatically.
Focus changes the border and cursor only. An error message sits next to its field and never clears what was entered. Disabled fields remain readable.
<label for="energy">Energy from the nutrition label</label>
<div class="dd-field"><input id="energy" type="number" value="1000"><span>kJ</span></div>
<small>Enter the kilojoule value per serving.</small>
<div class="dd-field transparent"><input type="text" value="Rice"></div>
<div class="dd-field error"><input type="email" value="name@" aria-invalid="true"></div>
<style>
.dd-field { min-height: 54px; display: flex; align-items: center; gap: 10px;
padding: 0 16px; border: 1px solid rgba(255,255,255,.12);
border-radius: 17px; background: rgba(255,255,255,.05); }
.dd-field:focus-within { border-color: #c2d6c1; }
.dd-field input { flex: 1; min-width: 0; border: 0; outline: 0;
background: transparent; color: #f5f5f7; font: 16px sans-serif; }
.dd-field.transparent { background: transparent; }
.dd-field.error { border-color: #e29379; }
.dd-field input:disabled { opacity: .45; }
</style>Search fields offer a clear action. Sliders adjust a value within a known range and always show the current value.
Sliders support arrow keys and expose a readable value. Pair a slider with a number field when exact input matters.
<div class="dd-search"><input type="search" placeholder="Search foods"></div>
<label for="portion">Portion <output id="value">150 g</output></label>
<input id="portion" type="range" min="0" max="500" step="10" value="150"
oninput="document.querySelector('#value').value = this.value + ' g'">
<style>
.dd-search { padding: 0 16px; border-radius: 17px;
background: rgba(255,255,255,.05); }
.dd-search input { width: 100%; min-height: 52px; border: 0; outline: 0;
background: transparent; color: #f5f5f7; }
input[type="range"] { width: 100%; height: 5px; accent-color: #a8bfa8; }
</style>Related content shares one parent surface. Rows use thin dividers; do not give every row its own card.
Primary label first, supporting detail below, value at the end. Dividers align with the text column. Clickable rows provide hover, focus, and pressed states.
<section class="dd-card">
<button class="dd-row"><span>Breakfast<small>08:20 · 3 foods</small></span><strong>386 kcal</strong></button>
<button class="dd-row"><span>Lunch<small>12:35 · 4 foods</small></span><strong>562 kcal</strong></button>
</section>
<section class="dd-card outline">Outline card</section>
<section class="dd-card flat">Flat card</section>
<style>
.dd-card { padding: 0 16px; border: 1px solid rgba(255,255,255,.12);
border-radius: 24px; background: #141416; }
.dd-row { width: 100%; min-height: 68px; display: flex; align-items: center;
justify-content: space-between; border: 0; background: transparent; color: #f5f5f7; }
.dd-row + .dd-row { border-top: 1px solid rgba(255,255,255,.1); }
.dd-row small { display: block; margin-top: 4px; color: #a8a8ad; }
.dd-card.outline { background: transparent; }
.dd-card.flat { border-color: transparent; }
</style>Badges show a category or short status. Avatars identify a person, group, or source.
Keep status badges to one or two words. Use initials when there is no photo, and never rely on color alone to identify members.
<span class="dd-badge success">Complete</span>
<span class="dd-badge outline">Outline</span>
<span class="dd-avatar">D</span>
<style>
.dd-badge { display: inline-flex; padding: 5px 9px; border-radius: 10px;
background: rgba(255,255,255,.07); color: #f5f5f7; font-size: 11px; }
.dd-badge.success { background: rgba(168,191,168,.14); color: #c2d6c1; }
.dd-badge.outline { background: transparent; border: 1px solid rgba(255,255,255,.18); }
.dd-avatar { width: 40px; height: 40px; display: inline-grid; place-items: center;
border-radius: 50%; background: rgba(168,191,168,.14); color: #c2d6c1; }
</style>Show a value when progress is known. Use an indeterminate bar or skeleton while the wait is unknown, with no layout shift.
The progress bar is 6px high with a 3px radius. When reduced motion is on, the indeterminate bar and spinner become static.
<div class="dd-progress" role="progressbar" aria-valuenow="69" aria-valuemin="0" aria-valuemax="100">
<i style="width:69%"></i>
</div>
<span class="dd-spinner" role="status" aria-label="Loading"></span>
<style>
.dd-progress { height: 6px; border-radius: 3px; background: rgba(255,255,255,.07); }
.dd-progress i { display: block; height: 100%; border-radius: inherit; background: #a8bfa8; }
.dd-spinner { width: 22px; height: 22px; border: 2px solid rgba(255,255,255,.12);
border-top-color: #a8bfa8; border-radius: 50%; animation: spin .8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .dd-spinner { animation: none; } }
</style>Charts sit below the value and its interpretation. Remove extra grid lines and axes so the trend reads immediately.
Provide a text summary and exact values. Label the limits of estimated data next to the estimate.
<figure><figcaption>This week's average: 1,642 kcal / day</figcaption>
<svg viewBox="0 0 320 116" role="img" aria-label="Weight down 0.4 kilograms over seven days">
<path d="M4 32C80 28 100 62 155 58S250 78 316 74"
fill="none" stroke="#a8bfa8" stroke-width="2.4"/>
<circle cx="316" cy="74" r="5" fill="#a8bfa8"/>
</svg>
</figure>
<div class="dd-bars" aria-label="Seven-day intake bar chart">
<i style="height:62%"></i><i style="height:82%"></i><i style="height:68%"></i>
</div>
<style>
figure { margin: 0; } figcaption { margin-bottom: 16px; color: #a8a8ad; }
.dd-bars { height: 118px; display: flex; align-items: flex-end; gap: 8px; }
.dd-bars i { width: 16px; border-radius: 5px; background: #a8bfa8; }
</style>For comparing rows of the same kind. Numbers align right, headers stay visible, and narrow screens scroll horizontally.
| Food | Amount | Calories | Status |
|---|---|---|---|
| Rice | 150 g | 232 kcal | Logged |
| Chicken breast | 300 g | 495 kcal | Logged |
| Broccoli | 200 g | 126 kcal | Draft |
Tables are for comparison, not for arranging ordinary forms. Do not hide important actions behind hover. On phones, switch to a grouped list.
<div class="dd-table-scroll"><table>
<thead><tr><th>Food</th><th>Amount</th><th>Calories</th></tr></thead>
<tbody><tr><th scope="row">Rice</th><td>150 g</td><td>232 kcal</td></tr></tbody>
</table></div>
<style>
.dd-table-scroll { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; text-align: left; }
th, td { padding: 14px 16px; border-bottom: 1px solid rgba(255,255,255,.1); }
thead th { color: #a8a8ad; font-size: 12px; font-weight: 400; }
td:last-child, th:last-child { text-align: right; font-variant-numeric: tabular-nums; }
</style>Inline alerts carry persistent information. Toasts report a result. Dialogs interrupt for a decision. Bottom sheets add mobile actions.
Records here are not uploaded or saved.
Every device is up to date.
Enter a target weight to continue.
Tooltips add a short clarification and never contain a required action. Confirm only irreversible actions; routine saves do not need repeated prompts.
<div class="dd-alert" role="status">Every device is up to date.</div>
<button id="open">Open dialog</button>
<dialog id="confirm"><h2>Delete this record?</h2><p>This cannot be undone.</p>
<button onclick="confirm.close()">Cancel</button><button>Delete</button>
</dialog>
<div class="dd-toast" role="status">Changes saved</div>
<style>
.dd-alert { padding: 14px 16px; border: 1px solid rgba(168,191,168,.2);
border-radius: 16px; background: rgba(168,191,168,.1); color: #c2d6c1; }
dialog { max-width: 360px; padding: 24px; border: 1px solid rgba(255,255,255,.16);
border-radius: 32px; background: #141416; color: #f5f5f7; }
dialog::backdrop { background: rgba(0,0,0,.72); }
.dd-toast { padding: 13px 18px; border-radius: 18px; background: #1c1c1e; }
</style>04 / PATTERNS
Navigation, accessibility, and tokens. Platforms may change how an action is performed, but component states and reading order stay the same.
Mobile uses a four-item floating capsule navigation with an inset pill for the selected item. Wide screens keep the same order and state.
SizeThe floating bar is 66px high and 296–420px wide. Touch targets are at least 44px.
HierarchyDo not combine multiple navigation systems at one level. Keep content clear of the floating area.
<nav class="dd-tabs" aria-label="Primary">
<a href="/today" aria-current="page">Today</a><a href="/records">Records</a>
<a href="/trends">Trends</a><a href="/me">Me</a>
</nav>
<nav class="dd-page-tabs" aria-label="Filter">
<button aria-pressed="true">All</button><button aria-pressed="false">Unread</button>
</nav>
<style>
.dd-tabs { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px;
width: 390px; max-width: 100%; padding: 6px; border-radius: 999px; background: #000; }
.dd-tabs a { min-height: 54px; display: grid; place-items: center;
border-radius: 999px; color: #a8a8ad; text-decoration: none; }
.dd-tabs [aria-current="page"] { background: #1c1c1e; color: #c2d6c1; }
.dd-page-tabs { display: flex; gap: 24px; border-bottom: 1px solid rgba(255,255,255,.12); }
.dd-page-tabs [aria-pressed="true"] { color: #c2d6c1; border-bottom: 2px solid #a8bfa8; }
</style>Accessibility is part of every component, not a separate review step.
Use a deep sage focus ring. Focus order follows reading order.
Check text contrast on the actual composited surface, not on theoretical translucent values.
Pair color with text, shape, icons, and status wording.
Support text scaling, reduced motion, keyboard input, and screen readers.
Color, type, spacing, radius, motion, and component sizes are provided as platform-neutral JSON.
{
"color": {
"dark": {
"background": "#FF000000",
"ink": "#FFF5F5F7",
"accent": "#FFA8BFA8"
}
},
"radius": {
"card": 28,
"field": 18,
"pill": 999
},
"motion": {
"press": { "durationMs": 170, "scale": 0.97 }
}
}
Reuse an existing semantic role before adding one. When a new role is necessary, define its dark and light values, purpose, and surface instead of hard-coding a color in a component.