Skip to Content

TextInput

TextInput edits one line of text. Use it for a search box, a name field, or the composer in a list. For several lines, use TextArea.

import 'package:noir/noir.dart';

Minimal working field

This complete widget owns the controller, reads the text on submit, and releases the controller in dispose:

class SearchField extends StatefulWidget { const SearchField({required this.onSearch, super.key}); final ValueChanged<String> onSearch; @override State<SearchField> createState() => _SearchFieldState(); } class _SearchFieldState extends State<SearchField> { final _controller = TextEditingController(); @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) => TextInput( controller: _controller, autofocus: true, placeholder: 'package name', onSubmit: () => widget.onSearch(_controller.text.trim()), ); }

Properties you will reach for first

PropertyDefaultBehavior
controllernullThe TextEditingController that owns the text, selection, and caret.
valuenullText the field owns for you. It sets the starting text, and a new value from a rebuild replaces the text. Passing both controller and value throws.
placeholdernullDimmed hint text shown while the field is empty.
onChangednullCalled with the raw text after the reader edits it. Programmatic controller changes do not call it.
onSubmitnullCalled when the reader presses Enter.
maxLength100Maximum accepted characters. Raise it for a long line.
focusNodenullSupply one when a parent must request or observe focus.
autofocusfalseRequests primary focus when the field first mounts.
obscureText / obscuringCharacterfalse / '*'Paints each grapheme as the obscuring character. The stored text stays raw.
cursorStyleCursorStyle.blockShape drawn for the caret.
color, backgroundColor, cursorColorTheme valuesOverride only for a deliberate exception to the Theme.

Keyboard and pointer behavior

  • Printable input inserts at the caret while the field has focus.
  • Backspace and Delete remove one grapheme cluster.
  • Left, Right, Home, and End move the caret. Ctrl+Home and Ctrl+End jump to the start and end of the text.
  • Enter calls onSubmit. The field never inserts a newline; TextArea does.
  • Tab is not consumed, so focus traversal keeps working.
  • A primary click takes focus. It does not move the caret to the clicked column. A field that gains focus with no usable selection puts the caret at the end of its text.

Who owns what

The code that creates a TextEditingController or a FocusNode also disposes it. TextInput creates an internal focus node only when you do not pass one, and it disposes what it created. It never disposes a controller or node you supplied.

Read controller.text inside the callback that needs it. Do not keep a second copy of the text in State and try to keep the two in sync.

onChanged reports the reader’s edits: typing, deleting, and pasting. Changing controller.text from your own code updates the field and repaints it, but it does not call onChanged. When a filter or a search must also react to a programmatic change, listen to the controller instead:

_controller.addListener(() => _applyFilter(_controller.text));

Remove that listener, or dispose the controller, in the same State that added it. This is why clearing a search field in code does not rerun a filter that is attached only to onChanged.

Common mistake

Passing both controller and value throws an assertion. Choose one: controller when your code must read or change the text and selection, value when the parent supplies the text and onChanged is enough.

Last updated on