Install
$ agentstack add skill-fluttersdk-wind-wind-ui ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Wind UI 1.1
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](#13-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.yamlforfluttersdk_wind:. If absent, jump to §13 first. - Read the project's
WindThemeDatasetup. Usually inlib/main.dartorlib/config/wind.dart. Note any custom color families (primary,accent,incident, etc.): those become available asbg-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 (
backgroundColoronWDiv,foregroundColoronWText) exist only as runtime-dynamic escape hatches for values the cache key cannot represent. Default to className. Never reach forBoxDecoration,EdgeInsets,TextStylewhen a token covers it.
- Every
bg-/text-/border-/ring-/shadow-/fill-carries adark:peer in the same className. Missing pair is a bug, not a style choice. Pairbg-white dark:bg-gray-800on 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?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:
``dart 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), preferflex-1for a fill-the-row child. A barew-fullon a direct Row child is now treated asflex-1(the row wraps it inExpanded), so it fills the available width instead of assertingRenderBox was not laid out;flex-1stays the idiomatic, explicit choice and is what to reach for. (A prefixedmd:w-fullis NOT auto-expanded; usemd:flex-1.) Inside a Column (flex flex-col), scrollable children useflex-1 overflow-y-autoplus the constructor propscrollPrimary: truefor iOS tap-to-top.h-fullinside a scrollable parent still triggers "Vertical viewport was given unbounded height".
childXORchildrenon every W-widget that accepts both. Passing both fails an assertion at construction. Passing neither renders an emptySizedBox.
- Unknown tokens fail silently.
text-7xl(Wind stops attext-6xl),flex-cow(typo),ps-4(logical-inline, unsupported),-m-4(negative margin, unsupported) all parse to nothing. The parser drops them without warning. Spell-check by hand or loadreferences/tokens.mdto verify the family.
- Last class wins within a parser family.
p-4 p-8resolves top-8.bg-red-500 bg-blue-500resolves tobg-blue-500. Conflicts inside the same property are stable but silent; conflicts across properties (text-red-500color +text-centeralignment) coexist because they target different fields.
WindThemelives BELOWMaterialAppin the runtime tree. The builder pattern inverts apparent order:WindTheme(data: ..., builder: (ctx, controller) => MaterialApp(...)). Consequence:OverlayEntry.buildercontexts cannot reachWindThemevia ancestor walk. Capture the State'scontextbefore showing an overlay, then pass it toWindParser.parsefrom inside the overlay builder.WPopover/WSelectalready handle this internally.
- Wind composes with Flutter, not against it.
Scaffold,AppBar,Dialog,BottomSheet,Drawer,SnackBar,Navigator,Hero,FutureBuilder,StreamBuilder,ValueListenableBuilderremain canonical.ListView/GridView.builder/CustomScrollVieware the right choice for virtualised lists;WDivwithgrid-cols-Nproduces a staticWrap(or, withitems-stretch, equal-height rows), not a virtualised grid. See [Wind ≠ Flutter rules of thumb](#9-wind--flutter-rules-of-thumb).
active:prefix is reserved but not wired.WAnchortracks hover and focus only; there is no onTapDown/onTapUp tracking. Don't rely onactive:bg-blue-700for press feedback. Use a transient state in the consumer's controller andstates: {'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). | | 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 (requires a primary color in theme). | | WSelect | Form (raw) | options: List> | Single OR multi-select dropdown with overlay. Supports searchable, async search, async create (tagging), pagination via onLoadMore + hasMore. Auto-flips upward when bottom space ; auto-injects error: when validation fails; renders label / hint / error around WInput. | | WFormSelect | Form (FormField) | options: List> | extends FormField; single-select with validation. | | WFormMultiSelect | Form (FormField) | options: List> | extends FormField>; multi-select with validation; validator inspects the full list. | | WFormCheckbox | Form (FormField) | none | extends FormField; validation hook + label + error display. | | WFormDatePicker | Form (FormField) | none | extends FormField. 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 | 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, 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: 'inline-flex 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
button(variants: {'intent': 'ghost'}) // override one axis
button(variants: {'size': null}) // null clears the default (no size classes)
button(className: 'w-full') // caller suffix, appended last
Resolution order (strict, never sorted): base ++ variant-classes(definition order) ++ matched-compound(array order) ++ caller.
Same-granularity contract: a variant must override a base token at the same granularity. Mixing p-4 (base) and px-2 (variant) is silent: the parser's per-family last-wins keeps the shorthand. Override px-* with px-* or p-* with p-*.
WindSlotRecipe returns Map (slot -> className). Use the slot keys directly on widget className: props.
Core Law addition for recipes: pass enum variant values as .name (ButtonIntent.ghost.name); recipe axes are plain strings.
3. The state system (three layers)
| Layer | Set by | Examples | |---|---|---| | Automatic | WAnchor from pointer / keyboard events | hover: (MouseRegion onEnter/onExit), focus: (FocusNode listener) | | Framework-managed | The widget itself, from its own props | loading: (WButton.isLoading), disabled: (any widget's disabled / enabled prop), checked: (WCheckbox.value, WSwitch.value), selected: (WRadio when value==groupValue, WTabs active tab), error: (WForm* when FormFieldState.hasError) | | Consumer-passed | states: Set? on the widget | selected: (card toggles), highlighted:, new:, any custom string. No registration required. |
WDiv and WButton auto-wrap themselves in WAnchor whenever className contains the literal substrings hover:, focus:, or active:. Other widgets do not. If you need hover detection on a WText, wrap it in WDiv or WAnchor explicitly.
All active states merge into a single Set inside WindContext.activeStates and contribute to the parser cache key. Manual states: {'hover'} and a real pointer hover produce the same cache entry by design.
Combining prefixes:
md:hover:bg-blue-500 // breakpoint AND hover
dark:md:hover:bg-blue-400 // dark AND breakpoint AND hover
ios:focus:ring-blue-500 // iOS only AND focused
md:dark:selected:border-2 // breakpoint AND dark AND selected
Prefix order does not matter at the parser level; all stacked prefixes must match for the class to activate.
4. The token landscape (high-frequency subset)
Inline this catalog as your default reach-for set. For the full per-parser regex catalog (every flag, every arbitrary-value pattern): `${CLAUDE_SK
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: fluttersdk
- Source: fluttersdk/wind
- License: MIT
- Homepage: https://wind.fluttersdk.com
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.