Inputs
CDX UI ships purpose-built input controls for the cases Material's defaults don't cover well: a labeled slider, a live-formatting rich text field, and a consistent form-field label primitive.
Live examples
Open the full Inputs example or the focused Rich Text example.
CdxSlider
Canonical labeled slider for the design system. Wraps Material's Slider with a primary-coloured value label, optional leading icon, and configurable min/max end labels — replaces the ad-hoc "duration / days / count" sliders that each rolled their own chrome.
// Top-centered value with calendar icon, days formatting
CdxSlider(
value: days.toDouble(),
min: 0,
max: 365,
onChanged: (v) => setDays(v.round()),
format: (v) => '${v.round()} days',
icon: FluentIcons.calendar_24_regular,
)
// Compact row: trailing value, no end labels
CdxSlider(
value: rows.toDouble(),
min: 10, max: 30, divisions: 2,
onChanged: (v) => setRows(v.toInt()),
format: (v) => v.toInt().toString(),
labelPosition: CdxSliderLabelPosition.trailing,
showEndLabels: false,
)Parameters
| Param | Type | Default | Notes |
|---|---|---|---|
value | double | required | |
min / max | double | required | |
divisions | int? | null | |
onChanged | ValueChanged<double> | required | |
onChangeEnd | ValueChanged<double>? | null | Forwarded to Slider.onChangeEnd |
format | String Function(double) | required | Drives the prominent label + Material drag tooltip |
endLabelFormat | String Function(double)? | falls back to format | Min/max end labels |
icon | IconData? | null | Leading icon next to the value label |
labelPosition | CdxSliderLabelPosition | top | top / trailing / none |
showEndLabels | bool | true | Auto-suppressed on right when labelPosition is trailing |
size | CdxControlSize | standard | compact gives a smaller value label + icon for tight surfaces |
enabled | bool | true | False mutes the whole slider and ignores input |
CdxRangeSlider
CdxSlider with two thumbs, in the same chrome and under the same slider theme, so the two look alike. Picks a RangeValues between min and max; the prominent label reads start – end.
CdxRangeSlider(
values: RangeValues(start.toDouble(), end.toDouble()),
max: 12,
divisions: 12,
minGap: 1,
format: (v) => '${v.round()}',
labelPosition: CdxSliderLabelPosition.none,
showEndLabels: false,
onChanged: (range) => setRange(range.start.round(), range.end.round()),
)- Gap. The thumbs never come closer than
minGap: the thumb that moves stops at the gap. An unchanged range is not reported. - Keyboard. Each thumb is a focus stop; the arrow keys step it by one division (or a small step without
divisions). Screen readers hear each thumb's value throughformat. - Long labels. On both sliders the labels give way to the track rather than overflow: an end label takes at most a quarter of the row, a trailing value label two fifths, and the value label above the track the row's width, each ending in an ellipsis past that.
| Param | Type | Default | Notes |
|---|---|---|---|
values | RangeValues | required | start <= end, within min..max |
min / max | double | 0 / 1 | |
divisions | int? | null | Snaps both thumbs to steps |
minGap | double | 0 | Smallest end - start |
onChanged | ValueChanged<RangeValues> | required | Drag and keyboard |
onChangeEnd | ValueChanged<RangeValues>? | null | Forwarded to RangeSlider.onChangeEnd |
format | String Function(double) | required | One thumb's value: label, drag tooltip, semantics |
endLabelFormat, icon, labelPosition, showEndLabels, size, enabled | As on CdxSlider |
RemarksField
Rich-text input with live inline formatting. As the user types markers like **bold** or ==highlight==, they render formatted in-place. When the cursor leaves a span, the markers hide (MarkerVisibility.whenActive).
RemarksField(
controller: _remarksController,
labelText: 'Comments',
isRequired: true,
hintText: 'Why was this rejected?',
helperText: 'Visible to approvers and the submitter.',
maxLines: 4,
showToolbar: true,
errorText: form.errors['comments'],
)Parameters
| Param | Type | Default |
|---|---|---|
controller | TextEditingController? | null (creates internal TextfEditingController) |
labelText | String? | null |
isRequired | bool | false (adds red * next to label) |
hintText / helperText / errorText | String? | null |
maxLines | int | 4 |
maxLength | int? | null |
showToolbar | bool | true |
prefixIcon | Widget? | null |
autofocus / focusNode / onChanged | standard |
When the caller passes a plain TextEditingController, RemarksField syncs back to it whenever the internal TextfEditingController changes — so existing form-binding patterns work unchanged.
RemarksText
Read-only renderer for textf-formatted strings. Drop-in replacement for Text that parses the same markup as RemarksField:
RemarksText(
text: comment.body,
maxLines: 5,
overflow: TextOverflow.ellipsis,
)Markup
| Markup | Rendered |
|---|---|
**bold** / __bold__ | bold |
*italic* / _italic_ | italic |
~~strikethrough~~ | strike |
++underline++ | underline |
==highlight== | highlight (tertiary container background) |
`code` | inline code (monospace + surfaceContainerHighest) |
^superscript^ | superscript |
~subscript~ | subscript |
FormattingToolbar
The bare toolbar embedded inside RemarksField. Use it directly when building a custom rich-text container.
FormattingToolbar(
controller: myTextfController,
focusNode: myFocusNode,
)Renders a row of icon buttons for bold / italic / highlight / underline / strikethrough / code / superscript / subscript. Each wraps the current selection (or inserts empty markers at the cursor).
CdxFieldLabel
Standard form-field label — mirrors the fieldLabel(context) extension in vyuh_feature_forms. Use it for non-form widgets that still need to present the same visual contract (label in labelMedium, required * in error color, focus / error coloring).
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
CdxFieldLabel(
label: 'Assignee',
required: true,
hasFocus: _hasFocus,
hasError: _hasError,
trailing: Button.text(label: 'Create new', onPressed: _createAssignee),
),
MyCustomInput(...),
])| Param | Default |
|---|---|
required | false (red * next to label) |
hasFocus | false (drives label color to primary) |
hasError | false (drives label color to error) |
padding | EdgeInsets.only(bottom: 6) |
trailing | null (optional same-row affordance such as help or "Create new") |
CdxCodeEditor
A code box for editing code or, with readOnly: true, for showing code to select and copy. There is one widget for both. It is built on re_editor: it lays out and colours only the lines it paints, so thousands of lines type within a frame.
Web builds need an override
Every app that builds for the web adds dependency_overrides: isolate_manager: ^6.3.2 to its pubspec (or its workspace root's). re_editor's default isolate_manager 4.x breaks flutter build web --wasm; see the vyuh_cdx_ui README.
CdxCodeEditor(
language: CdxCodeLanguage.json,
value: prettyJson, // or controller: CdxCodeController()
onChanged: (text) => ..., // edits only
lineNumbers: true,
minLines: 8,
maxLines: 20, // or expands: true in a bounded parent
error: parseError, // CdxCodeError, underlined at its line
)- Languages.
CdxCodeLanguage.jsoncolours keys, strings, numbers andtrue/false/null, and folds{}and[]blocks.CdxCodeLanguage.yamlcolours keys, values, literals and comments, and folds the lines indented under a line.CdxCodeLanguage.plaincolours and folds nothing. Another language isCdxCodeLanguage(name:, tokenizer:, folding:)with aCdxCodeTokenizerthat splits one line intoCdxCodeTokens and, if its blocks fold, aCodeChunkAnalyzer:CdxBracketChunkAnalyzerfor bracketed blocks,CdxIndentChunkAnalyzerfor indented ones, or your own that knows its strings and comments. Keep a language in aconstor a field. - Folding. On by default (
folding: true) for a language that folds. A chevron in the gutter beside a block's first line folds it, pointing down while the block shows and to the side once it is folded; a tap on it again, or on the⋯after the folded line, unfolds it. Hovering a chevron names the action and its shortcut. Ctrl+Alt+[ folds the block at the caret and Ctrl+Alt+] unfolds it (Command+Option on Apple). Folding hides lines and never changes the text:text,onChanged, the line numbers and an error's line all count the folded lines, and a fold stays through edits outside it.folding: falseturns it off; a document longer thanCdxCodeEditor.foldingLineLimitlines does not fold. - Theme. Token colours come from the
ColorSchemeandCdxConfig, so they follow light and dark. The outline, radius, padding and focus ring followCdxConfig(Classic or Blue);framed: falseleaves the frame to a parent that already draws one. Code draws inCdxFonts.mono. - Keys. Tab indents to the next two-space stop and Shift+Tab outdents. Ctrl+Tab and Ctrl+Shift+Tab move focus out; in a read-only box Tab moves focus as usual.
- Errors.
CdxCodeError(message, line:, column:)counts from 1.CdxCodeError.fromFormatException(e, source)places ajsonDecodefailure;CdxCodeError.at(source, offset, message)places any offset. The editor underlines the token, marks the line in the gutter with the message as a tooltip, and shows the message under the code.
| Param | Default |
|---|---|
language | required (CdxCodeLanguage.json, .yaml, .plain, or your own) |
controller / value | one of them; value keeps the text in the box |
readOnly | false |
lineNumbers | false |
folding | true (folds when the language has blocks) |
minLines / maxLines / expands | 1 / null (grows) / false |
error | null |
placeholder, style, framed, autofocus, focusNode | — |
CdxFileDropZone
An area that takes files dropped on it. It is generic in the host's file type and calls onDropped with the files it accepts. While a drag is over it only the outline changes, from a dashed hairline to a solid primary line; the fill never does.
CdxFileDropZone<XFile>(
accept: (file) => file.name.endsWith('.pdf'),
multiple: false,
onDropped: (files) => controller.upload(files),
onRejected: (files) => toast('Only one PDF'),
onBrowse: () async => controller.upload(await pickFiles()),
dropTarget: (context, target, child) => DropTarget( // desktop_drop
onDragEntered: (_) => target.enter(),
onDragExited: (_) => target.exit(),
onDragDone: (detail) => target.drop(detail.files),
child: child,
),
)Flutter cannot take files from the operating system without a platform plugin, and vyuh_cdx_ui carries none. The host wires its own plugin through dropTarget, calling CdxFileDropTarget.enter, exit and drop as the plugin reports the drag. A Draggable in the app whose data is a file or a list of files drops on the zone without any wiring. With onBrowse the zone is also a button (tap, Enter or Space) for the host's file picker. Its words come from CdxStrings.dropFiles and dropFilesOrBrowse, or label and hint.
CdxFieldAffixButton
An icon button set into a field's prefixIcon or suffixIcon, the field's height so it never grows the field. shape says how its hover and focus fill meets the field:
CdxFieldAffixShape | Fill |
|---|---|
round (default) | A round fill on a field-height square |
flat | A square fill the field's height inside the outline, for a button beside another control at the field's end |
fieldEnd | Flat where it meets the text, the field's corners at its edge, inset by the outline so it never paints over it |
CdxTextField(
controller: date,
suffixIcon: CdxFieldAffixButton(
icon: CdxIcons.calendar,
tooltip: 'Pick a date',
shape: CdxFieldAffixShape.fieldEnd,
onPressed: openCalendar,
),
)The icon keeps its enabled colour without onPressed: a disabled field mutes itself and its affixes as one unit.
CdxFieldStepper
A number field's stepper arrows: an up half above a down half, behind a hairline divider, set into a field's suffixIcon. CdxNumberField sets one in; a field of another kind that steps a value sets one into its CdxTextField the same way.
CdxTextField(
controller: pageText,
suffixIcon: CdxFieldStepper(
incrementTooltip: 'Next page',
decrementTooltip: 'Previous page',
onIncrement: page < last ? () => goTo(page + 1) : null,
onDecrement: page > 1 ? () => goTo(page - 1) : null,
),
)The column fills the field's height one outline in and sits at the field's end: the right in left-to-right text, the left in right-to-left. A half with a null callback is inert and mutes its chevron; a disabled field (enabled: false) mutes the stepper with itself as one unit. Each half is a focus stop and a button named by its tooltip. size matches the field's; incrementIcon and decrementIcon default to the chevrons. reserveRoom (default true) pads the stepper to the field-height end slot so the column reaches the edge; set it false when a unit label shares the field's end.
CdxTextField inline variant
CdxTextFieldVariant.inline edits words where they stand, such as a heading on a canvas. It draws no outline, fill, padding or minimum height in any state, uses no strut, and sets its text in the caller's style over the ambient DefaultTextStyle, as a Text does. A Text swapped for the field leaves every glyph in place. It takes no label, helper, error, unit, affixes or maxLength: the host frames the words.
CdxTextField(
controller: controller,
focusNode: focus, // listen to it to stop editing on focus loss
style: headingStyle, // the same style the Text used
textAlign: TextAlign.start,
maxLines: null, // grows with its lines
variant: CdxTextFieldVariant.inline,
onChanged: write,
)