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

See the complete example in example/lib/api/naked_textfield.0.dart.

Basic implementation

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:

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

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.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.strutStyle,
})

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 / semanticHint when the visual chrome lacks textual context
  • 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

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