State Management & Interactive Styles
Style hover, focus, disabled, loading and custom states with prefixed classes and the states prop.
On this page
Introduction#
Interactive styles allow you to define how your UI reacts to user input without managing complex state variables. Instead of nesting multiple MouseRegion, GestureDetector, and Focus widgets, Wind provides a declarative way to handle states directly in your className string.
By using prefixes like hover:, focus: and disabled:, you can build responsive, tactile interfaces that feel native to both web and mobile platforms.
How It Works#
core-concepts/state_management_overview
A live demo of this example follows.
state_management_overview.dart
View source
Open demo
Wind's parsing engine identifies state prefixes in your class string and stores them in a separate style map. When the widget's internal state changes (e.g., a user hovers over it), the widget rebuilds and applies the corresponding styles.
Consider this basic button example:
WButton(
onTap: () => print('Action triggered'),
className: 'bg-blue-500 dark:bg-blue-600 hover:bg-blue-600 dark:hover:bg-blue-700 p-4 rounded-lg duration-200',
child: WText('Save Changes', className: 'text-white dark:text-white'),
)
In this case, the background shifts between shades based on the interaction state. A WButton applies the new colour at once and duration-200 only fades its opacity; to animate the colour itself, put the same classes on a WDiv, which wraps its decoration in an AnimatedContainer when a duration is set.
Built-in State Prefixes#
Wind supports several interactive states out of the box. These are automatically managed when using interactive widgets:
- hover: Triggered when a pointer enters the widget bounds.
- focus: Triggered when the widget receives keyboard or input focus.
- active: Reserved. It is not wired to the pointer: it applies only when you pass
states: {'active'}to the widget that carries theactive:classes (WText,WDiv,WButton). Passing it to aWAnchordoes not reach a descendantWText, because the anchor's custom states are not part of the context a descendant resolves its styles from. - disabled: Active when
WButton(disabled: true)orWAnchor(isDisabled: true); a nullonTapdoes not disable.WButton(isLoading: true)activatesloading:on the button's ownclassName, notdisabled:, so a class meant to cover both needs both prefixes (disabled:opacity-50 loading:opacity-50). A loading button's anchor is disabled, though, so aWTextorWIconinside it does resolvedisabled:.
Quick Reference#
| Prefix | Trigger | Typical Use Case |
|---|---|---|
hover: |
Pointer entry | Changing background or elevation on desktop. |
focus: |
Keyboard focus | Showing a ring or border on input fields. |
active: |
states: {'active'} only |
Press styling you drive yourself: track the press in your own state and pass states: {'active'} to the styled widget itself. |
disabled: |
disabled: true / isDisabled: true |
Reducing opacity or greying out actions. WButton(isLoading: true) applies loading: to the button itself and disabled: only to descendants such as WText. |
Interactive Widgets#
Wind provides specific widgets designed to manage and propagate these states.
WAnchor: The Core Wrapper#
WAnchor is the low-level engine behind Wind's interactivity. It provides no visual styling itself; its sole purpose is to detect gestures and share that state with its child tree. This is useful for making entire cards or complex layouts interactive.
WAnchor(
onTap: () {},
child: WDiv(
className: 'bg-white dark:bg-gray-800 hover:bg-slate-50 dark:hover:bg-gray-700 border p-6 rounded-xl duration-300',
children: [
WText('Interactive Card', className: 'text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400'),
],
),
)
WButton: The High-Level Action#
While WAnchor is for generic wrappers, WButton is optimised for actions. It includes built-in support for loading states, padding defaults, and button-specific accessibility.
WButton(
onTap: _submit,
className: 'bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 text-white px-8 py-3 rounded',
child: Text('Submit'),
)
WInput: Focus States#
WInput manages its own focus state. It is common to use focus: prefixes here to highlight the active field or show a focus ring.
WInput(
placeholder: 'Search...',
className: 'border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 p-3 rounded-lg',
)
Custom States#
Sometimes you need to style a widget based on a state that isn't built-in, such as whether an item is "selected" or "loading". You can pass custom strings to the states parameter.
WDiv(
className: 'border-2 border-gray-200 selected:border-blue-500 bg-white selected:bg-blue-50 p-4',
states: isSelected ? {'selected'} : {},
child: WText('Selection Item'),
)
If the states set contains "selected", any class prefixed with selected: will be applied.
State Propagation#
Interactivity in Wind is shared through WindContext. When an ancestor widget like WAnchor changes state, it propagates that state to all children in its subtree.
Let's look at a nested example:
WAnchor(
onTap: () {},
child: WDiv(
className: 'p-4 bg-white hover:bg-gray-50',
children: [
WText('Title', className: 'font-bold hover:text-blue-600'),
WText('Description', className: 'text-gray-500'),
],
),
)
Even if the mouse is only hovering over the padding of the WDiv, the WText widget will receive the "hover" state and update its colour accordingly.
Related Documentation#
- Responsive Design - Combining states with breakpoints.
- Dark Mode - Styling for night owls.
- WAnchor Widget - Technical reference for the anchor engine.