Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ Remix provides a comprehensive set of production-ready components:

### Input Components
- **TextField** - Text input with validation support
- **TextArea** - Multiline text input with safe auto-growing defaults
- **Select** - Dropdown selection with keyboard navigation

### Display Components
Expand Down
4 changes: 4 additions & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@
"title": "Tabs",
"href": "/components/tabs"
},
{
"title": "TextArea",
"href": "/components/textarea"
},
{
"title": "TextField",
"href": "/components/textfield"
Expand Down
268 changes: 268 additions & 0 deletions docs/components/textarea.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
---
title: TextArea
description: A multiline text editor with native auto-growth, labels, supporting text, validation, and Remix styling
keywords: [flutter, remix, textarea, multiline, textfield, form, editor]
---

`RemixTextArea` is the multiline constructor for Remix's existing text editor.
It uses the same `TextFieldStyler`, `TextFieldSpec`, controller, focus, input,
selection, and accessibility pipeline as `RemixTextField`.

## When to use this

- **Long-form input**: Comments, descriptions, notes, and messages
- **Multiple paragraphs**: Content where line breaks must be preserved
- **Growing forms**: Editors that should expand naturally inside parent constraints
- **Validated content**: Multiline fields with labels, helper text, and errors

Use `RemixTextField` for compact single-line values such as names, search, and
email addresses.

## Basic implementation

<CodeGroup title="Basic implementation" defaultLanguage="dart">
```dart
import 'package:flutter/material.dart';
import 'package:remix/remix.dart';

class TextAreaExample extends StatelessWidget {
const TextAreaExample({super.key});

@override
Widget build(BuildContext context) {
return const SizedBox(
width: 360,
child: RemixTextArea(
label: 'Notes',
hintText: 'Add details',
helperText: 'Line breaks are preserved',
),
);
}
}
```
</CodeGroup>

The default editor starts at two lines, uses the multiline keyboard and newline
action, and grows with its content because `maxLines` is `null`. Flutter lays it
out within the constraints supplied by its parent. TextArea does not add a
browser-style drag-resize handle or a fixed height.

## Controlled value, validation, and length

<CodeGroup title="Controlled value and validation" defaultLanguage="dart">
```dart
import 'package:flutter/material.dart';
import 'package:remix/remix.dart';

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

@override
State<FeedbackEditor> createState() => _FeedbackEditorState();
}

class _FeedbackEditorState extends State<FeedbackEditor> {
final controller = TextEditingController();
bool showError = false;

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

@override
Widget build(BuildContext context) {
return RemixTextArea(
controller: controller,
label: 'Feedback',
hintText: 'What could be better?',
helperText: showError ? 'Feedback is required' : 'Up to 500 characters',
error: showError,
maxLength: 500,
minLines: 3,
maxLines: 8,
onChanged: (value) {
if (showError && value.trim().isNotEmpty) {
setState(() => showError = false);
}
},
);
}
}
```
</CodeGroup>

When setting both line limits, `maxLines` must be greater than or equal to
`minLines`, and both values must be positive. Set both together when overriding
the default two-line minimum with a smaller maximum.

## Disabled and read-only

<CodeGroup title="Disabled and read-only states" defaultLanguage="dart">
```dart
import 'package:flutter/material.dart';
import 'package:remix/remix.dart';

class TextAreaStates extends StatelessWidget {
const TextAreaStates({super.key, required this.savedController});

final TextEditingController savedController;

@override
Widget build(BuildContext context) {
return Column(
spacing: 16,
children: [
const RemixTextArea(
label: 'Disabled notes',
hintText: 'Editing is unavailable',
enabled: false,
),
RemixTextArea(
controller: savedController,
label: 'Saved notes',
readOnly: true,
),
],
);
}
}
```
</CodeGroup>

A read-only TextArea remains focusable and selectable. A disabled TextArea does
not accept input or expose an editing action.

## Custom styling

TextArea deliberately reuses the TextField anatomy. Use `TextFieldStyler` for
fluent styling or provide a resolved `TextFieldSpec`; there are no separate
TextArea style or spec types. `RemixTextArea` intentionally has no `styleFrom`
callable because `TextFieldStyler`'s callable creates a `RemixTextField`; pass
the styler through `style` as shown below.

<Warning>
Do not pass `fortalTextFieldStyle()` to a TextArea. That preset pins the input
container to a single-line height, so the editor stops growing and taller
content scrolls inside a one-line box with no layout error to signal it. There
is no Fortal TextArea preset: Radix Themes' TextArea is not part of this
package's mapped parity surface, so its metrics have no reference data to
derive from. Style TextArea with `TextFieldStyler` directly, using `minHeight`
rather than `height` for any floor you need.
</Warning>

<CodeGroup title="Custom Remix styling" defaultLanguage="dart">
```dart
import 'package:flutter/material.dart';
import 'package:remix/remix.dart';

class StyledTextArea extends StatelessWidget {
const StyledTextArea({super.key});

@override
Widget build(BuildContext context) {
return RemixTextArea(
label: 'Project summary',
hintText: 'Describe the project',
helperText: 'Markdown is supported',
style: TextFieldStyler()
.backgroundColor(const Color(0xFFF8FAFC))
.textColor(const Color(0xFF172033))
.hintColor(const Color(0xFF667085))
.paddingAll(14)
.borderRadiusAll(const Radius.circular(12))
.border(
BoxBorderMix.all(
BorderSideMix(color: const Color(0xFF7C3AED), width: 1.5),
),
)
.onFocused(
TextFieldStyler().border(
BoxBorderMix.all(
BorderSideMix(color: const Color(0xFF8B5CF6), width: 2.5),
),
),
),
);
}
}
```
</CodeGroup>

## Constructor

<CodeGroup title="Constructor" defaultLanguage="dart">
```dart
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:remix/remix.dart';

RemixTextArea remixTextAreaConstructor({
Key? key,
TextEditingController? controller,
FocusNode? focusNode,
String? label,
String? hintText,
String? helperText,
bool error = false,
TextInputType? keyboardType = TextInputType.multiline,
TextInputAction? textInputAction = TextInputAction.newline,
TextCapitalization textCapitalization = TextCapitalization.none,
TextDirection? textDirection,
bool enabled = true,
bool readOnly = false,
bool autofocus = false,
int? maxLines,
int? minLines = 2,
int? maxLength,
MaxLengthEnforcement? maxLengthEnforcement,
ValueChanged<String>? onChanged,
VoidCallback? onEditingComplete,
ValueChanged<String>? onSubmitted,
AppPrivateCommandCallback? onAppPrivateCommand,
List<TextInputFormatter>? inputFormatters,
bool? showCursor,
bool autocorrect = true,
bool enableSuggestions = true,
SmartDashesType? smartDashesType,
SmartQuotesType? smartQuotesType,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
bool enableInteractiveSelection = true,
TextSelectionControls? selectionControls,
GestureTapCallback? onTap,
TapRegionCallback? onTapOutside,
TapRegionUpCallback? onPressUpOutside,
bool onTapAlwaysCalled = false,
ScrollController? scrollController,
ScrollPhysics? scrollPhysics,
Iterable<String>? autofillHints,
ContentInsertionConfiguration? contentInsertionConfiguration,
Clip clipBehavior = Clip.hardEdge,
String? restorationId,
bool stylusHandwritingEnabled = true,
bool enableIMEPersonalizedLearning = true,
EditableTextContextMenuBuilder? contextMenuBuilder,
SpellCheckConfiguration? spellCheckConfiguration,
TextMagnifierConfiguration? magnifierConfiguration,
bool canRequestFocus = true,
bool? ignorePointers,
UndoHistoryController? undoController,
Object groupId = EditableText,
Widget? leading,
Widget? trailing,
String? semanticLabel,
String? semanticHint,
bool excludeSemantics = false,
TextFieldStyler style = const TextFieldStyler.create(),
TextFieldSpec? styleSpec,
}) => throw UnimplementedError();
```
</CodeGroup>

`obscureText`, `obscuringCharacter`, and `expands` are intentionally absent.
TextArea always uses nonsecure, nonexpanding multiline behavior. Override
`keyboardType` or `textInputAction` when a multiline workflow needs a more
specific keyboard or a completion action.
31 changes: 23 additions & 8 deletions docs/components/textfield.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -452,9 +452,18 @@ Optional. The style configuration for the text field.

### Style Methods

The input anatomy is always a horizontal row. Forwarded container methods are
the full `BoxStyler` surface, while generated `spacing` and
`crossAxisAlignment` methods control that row directly. The row follows the
ambient text direction; direction and main-axis controls are intentionally not
part of its styling API. The separate `layout` styler wraps the label, input
row, and helper text in a real Flex layout. It defaults to a vertical, min-size,
start-aligned layout with 8 pixels of spacing, and its full Flex direction and
alignment surface is honored.

#### `color(Color value)`

Sets text color
Sets the container color. Use `textColor` for editable text.

#### `text(TextStyler value)`

Expand All @@ -464,9 +473,14 @@ Sets editable text style using a TextStyler.

Sets background color

#### `container(FlexBoxStyler value)`
#### `container(BoxStyler value)`

Styles the box around the fixed input row.

Sets container that wraps editable text area
#### `layout(FlexBoxStyler value)`

Styles the label/input/helper layout. Its full Flex direction, spacing, and
alignment controls are supported.

#### `borderRadius(BorderRadiusGeometryMix radius)`

Expand Down Expand Up @@ -538,7 +552,12 @@ Sets margin

#### `spacing(double value)`

Sets flex spacing
Sets spacing between the leading widget, editor, and trailing widget.

#### `crossAxisAlignment(CrossAxisAlignment value)`

Aligns the editor and accessories on the input row's cross axis.
`CrossAxisAlignment.baseline` uses `TextBaseline.alphabetic`.

#### `decoration(DecorationMix value)`

Expand Down Expand Up @@ -592,10 +611,6 @@ Sets a foreground decoration painted on top of the component.

Applies a matrix transformation to the component.

#### `flex(FlexStyler value)`

Configures the flex layout properties.

#### `labelStyle(TextStyleMix value)`

Sets label/text style using TextStyleMix directly
Expand Down
5 changes: 5 additions & 0 deletions packages/playground/lib/registry/component_registry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import 'entries/slider_entry.dart';
import 'entries/spinner_entry.dart';
import 'entries/switch_entry.dart';
import 'entries/textfield_entry.dart';
import 'entries/textarea_entry.dart';
import 'entries/tooltip_entry.dart';

// Map component slugs to a builder that returns the component inside FortalScope.
Expand All @@ -33,6 +34,10 @@ final Map<String, WidgetBuilder> components = {
brightness: Theme.of(context).brightness,
child: PreviewShell(child: buildTextFieldExample()),
),
'textarea': (context) => FortalScope(
brightness: Theme.of(context).brightness,
child: PreviewShell(child: buildTextAreaExample()),
),
'checkbox': (context) => FortalScope(
brightness: Theme.of(context).brightness,
child: PreviewShell(child: buildCheckboxExample()),
Expand Down
Loading
Loading