| name | wind-ui |
| description | fluttersdk_wind 1.5: utility-first Flutter styling with Tailwind-syntax className strings. 27 W-prefix widgets (WDiv, WText, WButton, WInput, WSelect, WDatePicker, WPopover, WCard, WTabs, plus five WForm* wrappers) parse className into a cached immutable WindStyle; WindRecipe and WindSlotRecipe compose variant classNames. Prefixes stack freely (dark: / hover: / focus: / md: / ios: / selected: / disabled: / custom), the last class in a family wins, an unrecognized token drops with a one-time kDebugMode hint, and every color token carries a dark: peer in the same className. TRIGGER when: writing or editing UI in a Flutter app that depends on fluttersdk_wind; any className string; any W-prefix widget; any WindTheme or WindThemeData reference; the user mentions Tailwind for Flutter, utility-first, className, or wind-ui. DO NOT TRIGGER when: backend, API, or state-management work that never touches a widget tree; a Flutter project without fluttersdk_wind in pubspec.yaml; Material-only widgets (Scaffold, AppBar, Dialog) with no Wind content inside. |
| when_to_use | Any task that produces, modifies, or audits Wind-styled UI: composing a className, picking the right W-widget, wiring a Form field, customizing WindThemeData, pairing dark-mode classes, debugging a layout or a RenderFlex overflow, building a popover, rendering a JSON tree via WDynamic, or composing a WindRecipe. Load it before the first line of new UI, and equally when auditing UI that already exists. |
| version | 2.13.0 |
Wind UI 1.5
Utility-first Flutter styling. Every visual decision lives in a className: String? parsed at build time into an immutable WindStyle and composed into a native Flutter widget tree. Tailwind syntax (flex, p-4, dark:bg-gray-800, hover:shadow-lg), Flutter physics.
This skill assumes the host app already depends on fluttersdk_wind and has WindTheme wrapping MaterialApp. If a fresh project needs setup (rare; the skill normally triggers on an already-installed project), see Quick install at the bottom.
0. Before writing UI in this project
Three quick checks. Each pays off across the whole session.
- Confirm Wind is installed. Look at
pubspec.yaml for fluttersdk_wind:. If absent, jump to §13 first.
- Read the project's
WindThemeData setup. Usually in lib/main.dart or lib/config/wind.dart. Note any custom color families (primary, accent, incident, etc.): those become available as bg-primary-500, text-incident-700, etc. without registration. Skip this and the agent risks writing tokens that silently no-op, or missing the brand palette entirely.
- Scan one existing view in
lib/ for the project's className idioms. Triple-quoted style? Single-line preferred? Custom states (pressed:, expanded:)? Match the surrounding code, don't invent a new dialect.
After these, the agent has the project's color landscape, breakpoint set, and className voice loaded.
The parser cache is near-100% hit-rate in production. Do not worry about className parse overhead; the same className parses exactly once for its (breakpoint, brightness, platform, states) tuple. Prefer expressive className over inline BoxDecoration / EdgeInsets for "performance" reasons; the cache handles it.
1. Core Laws
These hold for every line of Wind code. Apply each as a hard constraint, not a suggestion.
-
className is the styling surface. Inline Dart props (backgroundColor on WDiv, foregroundColor on WText) exist only as runtime-dynamic escape hatches for values the cache key cannot represent. Default to className. Never reach for BoxDecoration, EdgeInsets, TextStyle when a token covers it.
-
Every bg- / text- / border- / ring- / shadow- / fill- carries a dark: peer in the same className. Missing pair is a bug, not a style choice. Pair bg-white dark:bg-gray-800 on the same line, not at the top and bottom of a multi-line className. Wind's dark-mode contract: the agent never opts in; every color opts in by default.
-
Conditional styling routes through states: Set<String>? plus prefixed classes. Never interpolate Dart expressions into className. 'bg-${isOn ? "blue" : "gray"}-500' breaks the parser cache and is a bug. The right shape:
WDiv(
className: '''
rounded-lg p-4 border-2
border-gray-200 dark:border-gray-700
bg-white dark:bg-gray-800
selected:border-blue-500 selected:bg-blue-50
dark:selected:border-blue-400 dark:selected:bg-blue-950
''',
states: isSelected ? const {'selected'} : const {},
child: ...,
);
-
Inside a Row (flex flex-row), prefer flex-1 for a fill-the-row child. A bare w-full on a direct Row child is now treated as flex-1 (the row wraps it in Expanded), so it fills the available width instead of asserting RenderBox was not laid out; flex-1 stays the idiomatic, explicit choice and is what to reach for. (A prefixed md:w-full is NOT auto-expanded; use md:flex-1.) Inside a Column (flex flex-col), scrollable children use flex-1 overflow-y-auto plus the constructor prop scrollPrimary: true for iOS tap-to-top. h-full inside a scrollable parent still triggers "Vertical viewport was given unbounded height".
-
child XOR children on every W-widget that accepts both. Passing both fails an assertion at construction. Passing neither renders an empty SizedBox.
-
Unknown tokens are dropped; the debug hint fires only for tokens no parser recognizes. Two cases, and they behave differently. A token whose prefix matches NO parser (ps-4 logical-inline, -m-4 negative margin, a mistyped family like wibble-4) is dropped and, in kDebugMode, prints a one-time debugPrint naming it (deduped per unique token per session; release builds stay silent). A token whose family IS recognized but whose value is unsupported (text-7xl, past wind's text-6xl cap; flex-cow, which still matches flex-*) is claimed by that parser and drops SILENTLY, with no hint. So the hint catches unknown-family typos, not bad-value ones; spell-check values by hand or load references/tokens.md to verify the family.
-
Last class wins within a parser family. p-4 p-8 resolves to p-8. bg-red-500 bg-blue-500 resolves to bg-blue-500. Conflicts inside the same property are stable but silent; conflicts across properties (text-red-500 color + text-center alignment) coexist because they target different fields.
-
WindTheme lives BELOW MaterialApp in the runtime tree. The builder pattern inverts apparent order: WindTheme(data: ..., builder: (ctx, controller) => MaterialApp(...)). Consequence: OverlayEntry.builder contexts cannot reach WindTheme via ancestor walk. Capture the State's context before showing an overlay, then pass it to WindParser.parse from inside the overlay builder. WPopover / WSelect already handle this internally.
-
Wind composes with Flutter, not against it. Scaffold, AppBar, Dialog, BottomSheet, Drawer, SnackBar, Navigator, Hero, FutureBuilder, StreamBuilder, ValueListenableBuilder remain canonical. ListView / GridView.builder / CustomScrollView are the right choice for virtualised lists; WDiv with grid-cols-N produces a static Wrap (or, with items-stretch, equal-height rows), not a virtualised grid. See Wind ≠ Flutter rules of thumb.
-
active: prefix is reserved but not wired. WAnchor tracks hover and focus only; there is no onTapDown/onTapUp tracking. Don't rely on active:bg-blue-700 for press feedback. Use a transient state in the consumer's controller and states: {'pressed'} if you genuinely need press feedback today.
2. The 27 public widgets (+ WindRecipe) at a glance
fluttersdk_wind v1 ships 27 public widgets plus the WindRecipe / WindSlotRecipe variant-composition primitives, all imported from the single barrel package:fluttersdk_wind/fluttersdk_wind.dart. No sub-barrels exist; do not write import 'package:fluttersdk_wind/widgets.dart'.
The headline 25 (table below) are the ones an agent reaches for daily. Two more cover narrow surfaces and live outside the table: WKeyboardActions (iOS keyboard toolbar overlay for Done / Next actions on a focused TextField) and WindAnimationWrapper (the internal stateful wrapper that drives looping animate-* tokens; consumers normally do not instantiate it directly).
| Widget | Category | Required positional | One-line purpose |
|---|
WDiv | Layout / container | none | Universal container; auto-wraps in WAnchor when className contains hover: / focus: / active:. child XOR children. Inline color prop: backgroundColor. |
WSpacer | Layout | none | Lightweight SizedBox that reads only w-N / h-N. Skips every other token. |
WBreakpoint | Structural | none | Per-breakpoint WidgetBuilder map (base, sm, md, lg, xl, xxl, plus theme-defined custom keys). Escape hatch when className prefixes are not enough. |
WText | Display | data: String | Typography; supports selectable prop. Inline color prop: foregroundColor. No child / children. |
WIcon | Display | icon: IconData | Material icons; use Icons.*_outlined variants by convention. Reads text-* for size AND color (overloaded). Inherits from DefaultTextStyle when className is absent. Inline color prop: foregroundColor. |
WImage | Display | none (requires src or image) | Network (URL) or asset (prefix asset://path) or ImageProvider. object-cover default. |
WSvg / WSvg.string | Display | src / svg | Vector graphics. fill-* / stroke-* for color. preserve-colors token disables tint for multi-color SVGs (QR codes, logos). |
WAnchor | Interactive | child: Widget | Low-level gesture + focus + hover propagator. Emits Semantics(button: true) only when it carries a gesture, or when semanticLabel is set. |
WButton | Interactive | child: Widget | Wraps WAnchor + WDiv + built-in spinner. isLoading: true injects loading: state. disabled: true injects disabled: state and blocks taps. |
WPopover | Overlay | none (requires builders) | OverlayPortal-based; triggerBuilder(ctx, isOpen, isHovering) + contentBuilder(ctx, close) + optional PopoverController. Auto-flips alignment when bottom space is insufficient. |
WInput | Form (raw) | none | Material-free text input (EditableText core); works under Material, Cupertino, custom, or bare WidgetsApp (no Material ancestor required). value + onChanged for controlled binding, or controller for imperative needs; passing both throws AssertionError in debug. InputType enum: text / password / email / number / multiline (number restricts to a signed decimal on every platform incl. web; pass inputFormatters to override). readOnly: true activates a readonly: state like enabled: false activates disabled:. Native text selection: mouse-drag selects a substring, double-click/double-tap selects a word, tapping the box moves the cursor; selection handles are Cupertino-style on all platforms (keeps WInput cupertino-only, no material.dart import). An Overlay ancestor is required for interactive selection; without one, typing and focus still work but all interactive selection (drag-select, double-tap, long-press, handles, and toolbar) is suppressed. Emits exactly one typeable textbox semantics node carrying semanticLabel ?? placeholder; password reports obscured. |
WCheckbox | Form (raw) | none | Boolean checkbox; auto-injects checked: state when value: true. Default className includes checked:bg-primary (primary is a seeded default token aliased to blue; override it in WindThemeData.colors to rebrand). A null onChanged renders it display-only, exactly like disabled: true: no tap action, reported as not enabled, disabled: styles active. WRadio and WSwitch read a null callback the same way. |
WSelect<T> | Form (raw) | options: List<SelectOption<T>> | Single OR multi-select dropdown with overlay. Supports searchable, async search, async create (tagging), pagination via onLoadMore + hasMore. Auto-flips upward when bottom space < maxMenuHeight. |
WDatePicker | Form (raw) | none | single date, DateRange, OR dateTime mode; popover-based calendar; min/max constraints. dateTime keeps the time of day (stepped time row, minuteStep / timeLabel / doneLabel) where the other two strike every value to midnight. |
WFormInput | Form (FormField) | none | extends FormField<String>; auto-injects error: when validation fails; renders label / hint / error around WInput. |
WFormSelect<T> | Form (FormField) | options: List<SelectOption<T>> | extends FormField<T>; single-select with validation. |
WFormMultiSelect<T> | Form (FormField) | options: List<SelectOption<T>> | extends FormField<List<T>>; multi-select with validation; validator inspects the full list. |
WFormCheckbox | Form (FormField) | none | extends FormField<bool>; validation hook + label + error display. |
WFormDatePicker | Form (FormField) | none | extends FormField<DateTime>. Forwards mode / minuteStep / timeLabel / doneLabel, so dateTime gives the validator a full instant. Range mode stores range.start only in FormFieldState; validators only see the start date. |
WDynamic | Structural / SSR | json: Map | Renders a JSON node tree into Wind widgets. 13 Wind types + 16 Flutter core types allowed by default; builders: adds custom types; denyWidgets: blocks. Max recursion depth default 50. |
WBadge | Display | label: String | Inline status/label pill. Composes a rounded-full WDiv around WText(text-xs). All tone via className (bg-*, text-*, dark: pairs). No positional child; label is the only positional param. |
WCard | Layout / container | child: Widget | Surface container. header:, child: (required), footer: slots; delegates to WDiv(flex-col). No colors baked in; all tone via className. |
WSwitch | Form (raw) | none | Controlled toggle. value + onChanged. className styles the track; thumbClassName styles the indicator dot. checked: state activates when value: true. The thumb is a flex child of the track, so it slides via justify-start -> checked:justify-end on the track className (Wind has no transform parser; translate-x-* is a no-op). disabled: when disabled: true. |
WRadio<T> | Form (raw) | none | Controlled radio. value, groupValue, onChanged. selected: activates when value == groupValue. Outer ring: className. Inner dot: indicatorClassName (defaults to blue filled circle). Group exclusivity is the caller's responsibility. |
WTabs | Form (raw) / Layout | none | Controlled tabs. tabs: List<String>, selectedIndex, panelBuilder. selected: activates on the active tab. Slot classNames: listClassName, tabClassName, selectedTabClassName, panelClassName. fullWidthList (default true) prepends w-full so a border-b underline spans the container; set false for content-width / pill tabs. |
Full constructor surface, every named parameter, every default: ${CLAUDE_SKILL_DIR}/references/widgets.md.
WindRecipe / WindSlotRecipe (variant-composition primitives)
WindRecipe and WindSlotRecipe are callable objects (not widgets) that compose className strings from variant axes. Use them to centralise multi-variant component styling instead of scattering conditional className strings across the call sites.
final button = WindRecipe(
base: 'flex flex-row items-center rounded-lg font-medium',
variants: {
'intent': {'primary': 'bg-blue-600 dark:bg-blue-500 text-white', 'ghost': 'bg-transparent text-blue-600 dark:text-blue-400'},
'size': {'sm': 'px-3 py-1.5 text-sm', 'md': 'px-4 py-2 text-base', 'lg': 'px-6 py-3 text-lg'},
},
compoundVariants: [
WindCompoundVariant(conditions: {'intent': 'primary', 'size': 'lg'}, className: 'shadow-lg'),
],
defaultVariants: {'intent': 'primary', 'size': 'md'},
);
button() // uses defaults