| name | frontend-design |
| description | Flutter/Wind/Magic-native UI design skill: design systems, visual hierarchy, bold aesthetics, and semantic token usage for this project. Covers component authoring, DESIGN.md-driven theming, and dark/light parity. Use for any UI, component, or screen work in depools. |
| when_to_use | TRIGGER when: UI, pages, components, screens, design. DO NOT TRIGGER when: backend, auth logic, or non-visual. |
Frontend Design (Flutter/Wind/Magic)
Production-grade UI design for Flutter apps built on the Wind utility system. This is a project-specific fork of the generic frontend-design skill, pinned to MOBILE/Flutter mode. All guidance is Wind className and WindRecipe based; web/CSS-only directives have been reconciled or removed.
MODE
This skill is MOBILE/Flutter only. There is no web/CSS mode for this project.
Platform: Flutter (mobile-first, responsive via Wind breakpoint prefixes).
Styling: Wind className strings only. No raw Colors.*, no hardcoded hex in component code.
Theme: DESIGN.md is the single source of truth for all tokens. See the colors, typography, rounded, and spacing sections.
Components: use the project component library under lib/ui/components/. Never build inline one-offs when a library component covers the case.
DESIGN PROCESS (BEFORE CODING)
Before writing any widget code, commit to a bold aesthetic direction by answering four questions:
- Purpose: What problem does this screen or component solve? Who uses it?
- Tone: Choose a clear direction and commit fully (brutally minimal, refined precision, warm/approachable, editorial, etc.).
- Constraints: Touch targets, safe areas, responsive breakpoints (
sm/md/lg via Wind), accessibility (4.5:1 WCAG AA).
- Differentiation: What is the one thing a user will remember about this surface?
Then implement working code that is production-grade, visually distinctive, and cohesive.
Design-First Workflow
- Design the actual piece of functionality first, not the navigation shell.
- Work in grayscale first; add color after hierarchy is clear.
- Establish token bindings (spacing, type, color via semantic aliases) before detailed styling.
- Iterate in cycles; details come last.
For the full workflow loop (including screenshot + verify), see the design-first-workflow skill.
DESIGN SYSTEMS
Spacing Scale
Defer to DESIGN.md's spacing section and the Wind utility scale. The project 4px logical scale:
| Token | Size | Wind class | Use case |
|---|
| xs | 4px | p-1 / gap-1 | Micro gaps, icon padding |
| sm | 8px | p-2 / gap-2 | Within components |
| md | 16px | p-4 / gap-4 | Standard screen padding |
| lg | 24px | p-6 / gap-6 | Between sections |
| xl | 40px | p-10 / gap-10 | Major separation |
| 2xl | 64px | p-16 / gap-16 | Hero areas |
| gutter | 16px | px-4 | Horizontal content margin (narrow screens) |
| section | 32px | py-8 | Stacked section separation |
Do not use arbitrary pixel values (p-[13px]); stay on the 4px scale. For semantic spacing tokens, use the alias keys from DESIGN.md.
Type Scale
DESIGN.md's typography block is the scale. Read the sizes there; the Wind text utility is how you spell one, not where the number comes from.
The steps, by role: display, headline-lg, headline-md, title-lg, body-lg, body-md, label-md, label-sm, and metric.
Two things about this scale that a generic mapping gets wrong:
- It follows iOS, not a web ladder. Body is 17px rather than 16, because that is the iOS default and it is what makes the app feel native. So do not reach for the Wind step whose name sounds closest; check the pixel value.
title-lg and body-lg are the SAME size and differ only in weight, which is exactly how iOS separates Headline from Body. A size match alone does not tell you which step you are looking at.
Two families, with different jobs. Inter for everything, and metric is Geist Mono for quantities, prices and barcodes, so a column of figures lines up by construction. font-mono is reached for deliberately on those; everything else inherits Inter. The generic "never Inter" rule does not apply here: DESIGN.md records why one neo-grotesque family plus a mono for figures is the right pairing for this product.
Shadow and Elevation
Wind DOES parse shadow, shadow-sm, shadow-md, shadow-lg, shadow-xl, shadow-2xl and shadow-none (shadow_parser.dart). What it does not support is the CSS filter and backdrop-filter families.
So the constraint here is a DESIGN choice rather than a parser limit, and DESIGN.md states it: express depth through tonal surfaces, and reserve a shadow for something that genuinely floats.
- Tonal shift first:
bg-surface -> bg-surface-container -> bg-surface-container-high. Read the direction off DESIGN.md rather than assuming light-to-lighter; this palette's page is darker than its cards in light mode, and getting the direction backwards is a defect that has already shipped here once.
- A hairline where a tonal shift will not do:
border border-color-border.
shadow-sm for a genuinely floating element, shadow-md for a dropdown, shadow-lg for a modal. Never stacked, and never on a card that is already distinguished by its fill.
For elevation semantics, see docs/design-culture/material-design-3.md.
Transforms and Filters
Wind does not support CSS transform, rotate, scale, translate, filter, backdrop-filter, group-*, or peer-* utilities. For motion and transitions, use Flutter's animation system directly (AnimatedContainer, AnimatedOpacity, TweenAnimationBuilder), not Wind className strings.
VISUAL HIERARCHY
Every element sits at one of three levels:
- Primary:
text-fg + heavy weight (font-bold) headlines, key actions (one per section)
- Secondary:
text-fg-muted supporting text, dates, descriptions
- Tertiary:
text-fg-disabled metadata, timestamps, copyright
Key Principles
- Size is not everything: use weight and color before increasing font size.
- Emphasize by de-emphasizing: soften competing elements instead of loudening the target.
- Labels are a last resort: combine with values ("12 left in stock" beats "Stock: 12").
- Icons are visually heavy: give them
text-fg-muted or text-fg-disabled to balance with text.
Button Hierarchy
| Level | Intent | Rule |
|---|
| Primary | MSButton(intent: ButtonIntent.primary) | One per section maximum |
| Secondary | MSButton(intent: ButtonIntent.secondary) | Clear but not competing |
| Ghost | MSButton(intent: ButtonIntent.ghost) | Discoverable, unobtrusive |
| Destructive | MSButton(intent: ButtonIntent.destructive) | Only on destructive actions |
Destructive actions do not have to be big, red and bold on every screen. Where delete is a secondary action on a content page, use ghost or secondary styling and reserve the full destructive treatment for the confirmation.
Two measured facts about this button that change how you use it. Its disabled state produces no visible change in the primary intent, so a disabled primary button is indistinguishable from a live one: remove the control, or put the blocking reason where it would have been, rather than greying it out. And min-h-11 is the wrong way to reach a 44pt target on it, because it grows the box without re-centring the label; use padding, and check the arithmetic against the button's own size. .claude/rules/design.md carries both.
COLOR SYSTEM
Use Semantic Tokens, Not Hex
All color decisions go through semantic alias tokens defined in DESIGN.md. Never put raw hex or Colors.* in component code.
| Role | Wind alias | Use |
|---|
bg-surface | Page background | |
bg-surface-container | Card, panel background | |
bg-surface-container-high | Input background, nested panels | |
text-fg | Primary text | |
text-fg-muted | Secondary text | |
text-fg-disabled | Disabled/meta text | |
bg-primary / text-on-primary | Brand action / on-brand text | |
bg-primary-container | Tinted brand surface | |
bg-accent | Secondary accent | |
border-color-border | Dividers, card borders | |
border-color-border-subtle | Hairline borders | |
bg-destructive / text-on-destructive | Danger action / on-danger text | |
bg-success / bg-warning | Status tones | |
design:sync writes each alias as an arbitrary-value pair (bg-[#RRGGBB] dark:bg-[#RRGGBB]) into lib/config/wind_theme.g.dart. Read the values there or in DESIGN.md; never quote one from memory.
Beyond the canonical table, this app hand-authors four token supplements that design:sync cannot emit, merged into the alias map in lib/main.dart: the inventory status vocabulary, paper and ink, the overlay stroke pair, and the control edge. DESIGN.md documents each and why it is a supplement rather than frontmatter.
Dark/Light Parity
The alias system carries the pair: each key already expands to light plus dark, so write the alias alone and never add a dark: beside it. Never set a colour without going through an alias.
Three token families hold the SAME hex on both sides of dark:, on purpose. Paper and ink render a picture of paper, and the overlay strokes sit over a photograph; neither is a surface the app controls, and a printed sheet is white at two in the morning. So "light and dark differ" is the rule for everything else and NOT a defect signal for those. DESIGN.md records it as D44 and D65.
A screen is not verified until it has been seen in light AND dark. The two appearances are not brightness variants of each other: elevation direction inverts, so a pair that is correct in one can be actively wrong in the other. Use the /preview catalog and the component-visual-reviewer subagent.
Accessibility
| Text type | Minimum contrast | Checked by |
|---|
| Normal text (<18px) | 4.5:1 | design:lint |
| Large text (18px+ bold or 24px+) | 3:1 | design:lint |
Never rely on color alone for meaning. Add icons, text, or patterns alongside color cues.
TYPOGRAPHY
Inter for text and Geist Mono for figures (see DESIGN.md's typography block, and the metric step). Render with WText carrying the Wind size and weight that match a declared step, never an ad-hoc size picked at the call site.
Line-Height and Spacing
- Small text: taller line-height (1.5-2.0 equivalent).
- Large headlines: shorter line-height (1.0-1.2 equivalent).
- These are already encoded in the DESIGN.md
typography entries; use them as-is.
Alignment
- Default: left-aligned.
- Center: only for headlines and short blocks (under 2-3 lines).
- Right-align numbers in column comparison contexts.
LAYOUT AND SPACING
Mobile-First
Design for narrow screens first. Use Wind breakpoint prefixes to expand at sm (640px) and md (768px):
// narrow: stacked columns
// md and wider: row layout
WDiv(className: 'flex flex-col md:flex-row gap-4')
Touch Targets
| Minimum | Comfortable |
|---|
| 44x44 pt (iOS) | 48x48 dp (Android) |
Add invisible padding if an icon or label is smaller than the minimum target. Use min-h-11 min-w-11 as a floor.
Safe Areas
Respect device notches and home indicators via Flutter's SafeArea widget. Never place interactive elements in unsafe areas.
Navigation Patterns
| Pattern | Wind/Magic component | Use case |
|---|
| Bottom navigation | Navbar | 3-5 primary destinations |
| Tab bar | Tabs | Content categories |
| Navigation drawer | AppLayout sidebar | Many destinations |
| Bottom sheet | BottomSheet | Contextual actions |
Spacing Discipline
More space between groups than within groups:
- Form labels sit closer to their input than to the preceding element.
- Section headings have more space above than below.
- List items within a group are tighter than the group gap.
DEPTH AND MOTION
Tonal Depth (Wind-compatible)
Wind does not support box-shadow or filter. Express depth through tonal backgrounds:
- Raised: use a lighter background (
bg-surface-container) against the page (bg-surface).
- Inset: use a darker/deeper background (
bg-surface-container-high) for inputs and nested panels.
Motion
Flutter animation system handles motion; Wind className strings do not carry transitions/transforms. Focus on high-impact moments:
- Page transitions: use
MagicRoute transition settings, not custom animations per-view.
- State changes (loading/error): use the
Skeleton component for loading states; avoid inline spinners.
- Reduced motion: respect
MediaQuery.of(context).disableAnimations in all custom animations.
For detailed easing/duration guidance, see docs/design-culture/motion-interaction.md.
SPATIAL COMPOSITION
- Asymmetry and unexpected layouts can add character; do not default to symmetric grids.
- Generous negative space reads as premium; controlled density reads as rich/capable.
- Avoid filling the whole screen when the content only needs part of it.
MOBILE-SPECIFIC PATTERNS
Loading States
Use the Skeleton component (preferred over spinners). Shape variants: block, text, circle.
Empty States
Use the EmptyState component:
- Illustration or icon to grab attention.
- Clear title and helpful description.
- A call-to-action
Button.
- Hide irrelevant UI (filters, tabs) when they have no effect yet.
Forms
- Full-width
Input with horizontal gutter padding.
FormField handles label + hint + error state (never roll inline).
- Primary action: full-width
Button(intent: ButtonIntent.primary) at the bottom.
- Error state:
border-color-border turns border-color-destructive; use FormField's error slot.
ANTI-PATTERNS
| Anti-pattern | Fix |
|---|
Raw Color(0xFF...) or Colors.* in component code | Use Wind semantic token alias |
Hardcoded pixel values (SizedBox(height: 13)) | Use Wind spacing utilities on the 4px scale |
| Font family chosen outside DESIGN.md | DESIGN.md typography is authoritative; Inter is the font |
| Purple gradients on white without dark counterpart | Every color token needs its dark: pair |
| Filling the whole screen when content needs less | Add max-w-* or mx-auto; let content breathe |
| Ambiguous spacing between groups | More space between groups than within |
| Touch targets under 44pt/48dp | Add min-h-11 min-w-11 or invisible padding |
| Ignoring safe areas | Wrap top-level screens in SafeArea |
| Color as sole communication channel | Add icon, text, or pattern alongside color |
| Multiple previews in one file | One preview class per *.preview.dart file |
CSS-only utilities (box-shadow, filter, transform, group-*) | These are unsupported by Wind; use Flutter APIs instead |