WInput
A utility-first form input that combines React-style controlled state management with Tailwind-like styling. It renders Material-free (EditableText core inside a Container) and works under any theming ancestor: Material, Cupertino, a custom app, or a bare WidgetsApp. No Material ancestor required (fixes #102).
- Basic Usage
- Constructor
- Props
- Layout Modes
- Keyboard Clearance
- Event Handling
- State Variants
- Accessibility
- Styling Examples
- All Supported Classes
- Customizing Theme
- Related Documentation
WInput(
value: _email,
onChanged: (value) => setState(() => _email = value),
type: InputType.email,
placeholder: 'Enter your email',
className: 'p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500',
placeholderClassName: 'text-gray-400 italic',
)
Basic Usage
Pass value and onChanged for controlled (React-style) binding. Omit both to let the field manage its own state internally. WInput works under any ancestor widget tree. No MaterialApp required.
WInput(
placeholder: 'Type something...',
className: 'w-full p-2 border rounded',
onChanged: (val) => print(val),
)
Constructor
const WInput({
Key? key,
String? value,
ValueChanged? onChanged,
InputType type = InputType.text,
String? className,
String? placeholderClassName,
String? placeholder,
bool enabled = true,
bool readOnly = false,
bool autofocus = false,
TextInputAction? textInputAction,
ValueChanged? onSubmitted,
VoidCallback? onEditingComplete,
VoidCallback? onTap,
TapRegionCallback? onTapOutside,
int? maxLines,
int minLines = 1,
FocusNode? focusNode,
TextEditingController? controller,
Set? states,
List? inputFormatters,
TextCapitalization textCapitalization = TextCapitalization.none,
bool autocorrect = true,
bool enableSuggestions = true,
Widget? prefix,
Widget? suffix,
String? semanticLabel,
})
Props
| Prop | Type | Default | Description |
|---|---|---|---|
className |
String? |
null |
Wind utility classes for the input field |
placeholderClassName |
String? |
null |
Utility classes for styling the hint text |
value |
String? |
null |
The controlled value of the input |
onChanged |
ValueChanged |
null |
Callback when text changes |
type |
InputType |
InputType.text |
Input keyboard and behavior (text, password, email, number, multiline). number restricts input to a signed decimal on every platform (web included); pass inputFormatters to override. |
placeholder |
String? |
null |
Hint text shown when input is empty |
enabled |
bool |
true |
Whether the input is interactive |
readOnly |
bool |
false |
Whether the input is read-only |
autofocus |
bool |
false |
Autofocus on mount |
textInputAction |
TextInputAction? |
null |
Keyboard action (e.g., .done, .next). When null, resolves to .done on a single-line field and .newline on a multiline one. |
onSubmitted |
ValueChanged |
null |
Callback when action button is pressed |
onTapOutside |
TapRegionCallback? |
null |
Callback when tapping outside (useful for blur) |
maxLines |
int? |
null |
Max lines for multiline input |
minLines |
int |
1 |
Min lines for multiline input |
prefix |
Widget? |
null |
Widget displayed before text |
suffix |
Widget? |
null |
Widget displayed after text |
controller |
TextEditingController? |
null |
Optional external controller |
states |
Set |
null |
Custom states for dynamic styling |
onEditingComplete |
VoidCallback? |
null |
Callback fired when the user finishes editing (loses focus, presses done, etc.) |
onTap |
VoidCallback? |
null |
Callback fired when the field is tapped |
focusNode |
FocusNode? |
null |
External focus controller for programmatic focus management |
inputFormatters |
List |
null |
Formatters applied as the user types (digit-only, masks, etc.) |
textCapitalization |
TextCapitalization |
TextCapitalization.none |
Auto-capitalize behavior (none / sentences / words / characters) |
autocorrect |
bool |
true |
Enable OS autocorrect suggestions |
enableSuggestions |
bool |
true |
Enable OS suggestion bar (Android) |
semanticLabel |
String? |
null |
Accessible name for the single textbox semantics node. Falls back to placeholder when null. Used by screen readers and getByLabel-style locators. |
Layout Modes
Multiline Input
For text areas, set the type to InputType.multiline. You can control the height using minLines or standard sizing classes.
WInput(
type: InputType.multiline,
minLines: 3,
maxLines: 5,
className: 'w-full p-4 border rounded-xl bg-gray-50',
placeholder: 'Enter your message...',
)
Keyboard Clearance
A focused WInput lifts the WHOLE field above the software keyboard, not just the line the caret is on.
Flutter reveals the caret rect with scrollPadding around it, which is the same box as the field while the field is one line tall. On a multiline field it is not: the caret sits on the first line, so line one clears the keyboard and every line below it stays under it. WInput widens scrollPadding by however much of the field sits below the caret, so Flutter's own caret scroll carries the rest of the field with it, and it recomputes as the caret moves down through the text.
The keyboard is not the only thing in the way. A WKeyboardActions toolbar is drawn on top of the keyboard and the engine reports nothing about it, so the region a field has to clear is the keyboard plus the bar. WInput reads the bar's published height and adds it, which means a field inside a WKeyboardActions needs no setup at all.
WInput(
type: InputType.multiline,
minLines: 3,
maxLines: 5,
className: 'w-full p-4 border rounded-xl',
placeholder: 'Write an update...',
)
[!NOTE] Nothing here is a prop. The clearance is computed from the field's own geometry, so a field that sits inside your own scroll view behaves the same as one that does not.
Event Handling
WInput provides standard callbacks for user interaction. Use onSubmitted for handling "Enter" or keyboard action buttons.
WInput(
className: 'border p-2',
textInputAction: TextInputAction.search,
onSubmitted: (value) {
_performSearch(value);
},
onTapOutside: (_) => FocusScope.of(context).unfocus(),
)
The Return key closes the keyboard by default. With no
textInputAction, a single-line field resolves toTextInputAction.doneand a multiline one to.newline. Passing.nextis a deliberate opt-in, because Flutter implements it asfocusNode.nextFocus(): the next FOCUSABLE widget, not the next text field. On a form whose traversal order runs through buttons, colour swatches and switches, the same Return key does two different things on one screen, dismissing the keyboard where the next focusable is a button and jumping past several fields where it happens to be editable. Reach for.nextonly where you know the following focusable IS the next input, which is a fact only the form itself has. For a toolbar that advances through a known field order instead, see WKeyboardActions.
State Variants
Wind automatically manages focus:, disabled: (when enabled: false), and readonly: (when readOnly: true) states. You can also trigger custom states like error: by passing them to the states property.
WInput(
states: _hasError ? {'error'} : {},
className: 'border border-gray-300 focus:border-blue-500 error:border-red-500',
)
[!NOTE]
focus:styles (likering-2) are applied automatically when the field gains focus.
Accessibility
WInput emits exactly one typeable textbox semantics node. Its accessible name is semanticLabel when provided, falling back to placeholder. This single-node shape ensures label-by-text locators (e.g., Playwright getByLabel, dusk label lookup) resolve cleanly with no duplicate accessible names.
InputType.password fields report isObscured: true in the semantics tree so screen readers announce the field as a password input.
When the field is enabled: false, it is non-editable and reports isReadOnly in the semantics tree.
[!NOTE]
WInputsupports native text selection (drag-select, double-tap word, long-press) with Cupertino-style selection handles on all platforms. This keepsWInputcupertino-only (nopackage:flutter/material.dartimport) and consistent with the rest of Wind's own look. Interactive selection requires anOverlayancestor (MaterialApp/CupertinoApp/WidgetsAppall provide one). Under a bare root with noOverlay(unusual in practice), typing and focus still work, but all interactive selection (drag-select, double-tap word, long-press, the drag handles, and the toolbar) is suppressed rather than throwing.
[!NOTE] Web: Flutter renders to a canvas (CanvasKit or Skwasm) and does not use a native browser
. Mouse drag-select, double-click word, and keyboard copy (Ctrl+C / Cmd+C) all work through Flutter's own selection layer. Right-click opens the browser's native context menu by default (not WInput's selection toolbar). Apps that want WInput's selection toolbar on right-click can callBrowserContextMenu.disableContextMenu()once at startup; this is an app-level opt-in and not WInput behavior. No magnifier is shown on web.
[!WARNING] Passing both
valueandcontrollerat the same time throws anAssertionErrorin debug mode. Use eithervalue+onChanged(controlled) orcontroller(external), not both.
Styling Examples
Search Input with Prefix
You can use the prefix prop to add icons while maintaining the utility-first styling of the input.
WInput(
prefix: Icon(Icons.search, color: Colors.gray),
className: 'pl-10 p-2 bg-gray-100 rounded-full border-transparent focus:bg-white focus:border-blue-500',
placeholder: 'Search...',
)
Password Field
Use InputType.password to obscure text. You can use the suffix prop to add a visibility toggle.
WInput(
type: _obscure ? InputType.password : InputType.text,
suffix: IconButton(
icon: Icon(_obscure ? Icons.visibility : Icons.visibility_off),
onPressed: () => setState(() => _obscure = !_obscure),
),
className: 'p-3 border rounded',
)
All Supported Classes
| Category | Classes |
|---|---|
| Layout | w-{size}, h-{size}, flex-1, w-full |
| Spacing | p-{n}, px-{n}, py-{n}, m-{n} |
| Typography | text-{size}, font-{weight}, italic, uppercase |
| Colors | bg-{color}, text-{color} |
| Borders | border, border-{n}, rounded-{size}, border-{color} |
| Effects | shadow-{size}, opacity-{n}, ring-{n}, ring-offset-{n} |
Customizing Theme
You can override the default input appearance through WindThemeData. This is useful for setting global border colors or padding defaults.
WindThemeData(
colors: {
'input-border': Colors.blueGrey,
},
// WInput respects global spacing units for p-x classes
baseSpacingUnit: 4.0,
)
Related Documentation
- WFormInput - Form-integrated input with labels and validation.
- WButton - Interactive button component.
- WText - Typography component.