NakedTextField

Headless text input component with native editing behavior

Headless text input component. Preserves native text editing behavior (selection handles, keyboard shortcuts, IME). Use builder pattern for custom styling.

When to use this

  • Form inputs: Text fields in forms (name, email, message, etc.)
  • Search boxes: Search inputs with custom styling
  • Text areas: Multi-line text input with custom appearance
  • Specialized inputs: Custom styled inputs that standard TextFields can't provide

Basic implementation

dart
import 'package:flutter/material.dart';
import 'package:naked_ui/naked_ui.dart';

class HeadlessField extends StatefulWidget {
  const HeadlessField({super.key});

  @override
  State<HeadlessField> createState() => _HeadlessFieldState();
}

class _HeadlessFieldState extends State<HeadlessField> {
  final controller = TextEditingController();

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return NakedTextField(
      controller: controller,
      onChanged: (value) => debugPrint('Value: $value'),
      builder: (context, state, editable) {
        final borderColor = state.when(
          focused: Colors.blue,
          hovered: Colors.grey.shade400,
          orElse: Colors.grey.shade300,
        );
        final borderWidth = state.isFocused ? 2 : 1;

        return AnimatedContainer(
          duration: const Duration(milliseconds: 160),
          padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(10),
            color: Colors.white,
            border: Border.all(
              color: borderColor,
              width: borderWidth,
            ),
            boxShadow: state.isFocused
                ? [
                    BoxShadow(
                      color: Colors.blue.withOpacity(0.12),
                      blurRadius: 12,
                      offset: const Offset(0, 4),
                    ),
                  ]
                : null,
          ),
          child: editable,
        );
      },
    );
  }
}

Builder Contract

NakedTextFieldBuilder has the signature:

dart
typedef NakedTextFieldBuilder = Widget Function(
  BuildContext context,
  NakedTextFieldState state,
  Widget editableText,
);

The provided editableText widget is an EditableText configured with all parameters from NakedTextField. The state parameter gives you access to interaction states like state.isFocused, state.isHovered, and state.when() for conditional styling. Wrap the editable text with decoration, icons, paddings, or animations—just return the composed widget.

Constructor Highlights

dart
const NakedTextField({
  Key? key,
  this.groupId = EditableText,
  this.controller,
  this.focusNode,
  this.undoController,
  this.keyboardType,
  this.textInputAction,
  this.textCapitalization = TextCapitalization.none,
  this.textAlign = TextAlign.start,
  this.textDirection,
  this.readOnly = false,
  this.showCursor,
  this.autofocus = false,
  this.obscuringCharacter = '•',
  this.obscureText = false,
  this.autocorrect = true,
  SmartDashesType? smartDashesType,
  SmartQuotesType? smartQuotesType,
  this.enableSuggestions = true,
  this.maxLines = 1,
  this.minLines,
  this.expands = false,
  this.maxLength,
  this.maxLengthEnforcement,
  this.onChanged,
  this.onEditingComplete,
  this.onSubmitted,
  this.onAppPrivateCommand,
  this.inputFormatters,
  this.enabled = true,
  this.error = false,
  this.cursorWidth = 2.0,
  this.cursorHeight,
  this.cursorRadius,
  this.cursorOpacityAnimates,
  this.cursorColor,
  this.selectionHeightStyle = ui.BoxHeightStyle.tight,
  this.selectionWidthStyle = ui.BoxWidthStyle.tight,
  this.keyboardAppearance,
  this.scrollPadding = const EdgeInsets.all(20.0),
  this.dragStartBehavior = DragStartBehavior.start,
  this.enableInteractiveSelection = true,
  this.selectionControls,
  this.onTap,
  this.onTapAlwaysCalled = false,
  this.onTapChange,
  this.onTapOutside,
  this.scrollController,
  this.scrollPhysics,
  this.autofillHints = const <String>[],
  this.contentInsertionConfiguration,
  this.clipBehavior = Clip.hardEdge,
  this.restorationId,
  this.onTapUpOutside,
  this.stylusHandwritingEnabled = true,
  this.enableIMEPersonalizedLearning = true,
  this.contextMenuBuilder,
  this.canRequestFocus = true,
  this.spellCheckConfiguration,
  this.magnifierConfiguration,
  this.onHoverChange,
  this.onFocusChange,
  this.onPressChange,
  this.style,
  required this.builder,
  this.ignorePointers,
  this.semanticLabel,
  this.semanticHint,
  this.semanticErrorText,
  this.strutStyle,
  this.excludeSemantics = false,
})

Parameters mirror EditableText, so you can keep using the same configuration options you know from Flutter’s built-in text fields.

Styling & Pointer Control

  • style lets you override the text style without wrapping the builder output in a new DefaultTextStyle
  • ignorePointers temporarily disables pointer input while leaving focus/keyboard flow intact—handy when showing loading states around the same editable instance
  • groupId matches EditableText’s grouping behaviour (set it when coordinating multiple fields for the same input method)

Event Hooks

  • onTap, onTapChange, onTapOutside, onTapUpOutside → fine-grained gesture hooks
  • onChanged, onSubmitted, onEditingComplete → forward the underlying EditableText callbacks
  • onHoverChange, onFocusChange, onPressChange → interaction callbacks

Accessibility & Semantics

  • Supply semanticLabel and semanticHint when the visual chrome lacks textual context
  • Pair semanticErrorText with error: true; when error is false, the error text is not announced and the field is not a live region
  • Set excludeSemantics to hide the field and its descendants from accessibility services
  • NakedTextField wraps the builder result with proper Semantics for focusable text fields
  • Respect contrast and focus indicators in your builder to ensure the field remains discoverable

For a validation error, derive both parameters from the same state:

dart
NakedTextField(
  controller: emailController,
  error: emailError != null,
  semanticLabel: 'Email',
  semanticErrorText: emailError,
  builder: (context, state, editableText) => editableText,
);

Tips

  • For Material/Cupertino visual parity, wrap the builder output with your design system components
  • Use maxLines, minLines, and expands to create multi-line editors
  • Provide an UndoHistoryController when you need cross-field undo stacks
  • Remember to dispose any controllers you allocate alongside the widget