Buttons
CDX UI ships one unified Button sealed class plus three button-shaped controls: the split trigger of a CdxMenu, CdxSegmentedTabs, and the small CdxDismissAffordance glyph used inside chips and inputs.
All read CdxConfig.controlHeight, controlBorderRadius, and the canonical interaction tokens (hoverTint, selectedTint).
Live Example
Open the full route: Buttons.
Button
Button is a sealed class with five factories. Every variant auto-handles its own loading state — pass an async callback and the button shows a spinner while it's in flight.
| Factory | Render | Use |
|---|---|---|
Button.filled | FilledButton | Primary action |
Button.outlined | OutlinedButton | Secondary action |
Button.text | TextButton | Tertiary / link action |
Button.icon | IconButton | Icon-only — size, isActive, IconButtonStyle.filled / .outlined / .standard |
Button.toggle | CDX toggle control | Boolean on/off control |
// Primary action with async loading
Button.filled(
label: 'Save',
icon: FluentIcons.save_24_regular,
onPressed: () async => store.save(),
)
// Destructive secondary
Button.outlined(
label: 'Delete',
icon: FluentIcons.delete_24_regular,
destructive: true,
onPressed: () async => store.delete(),
)
// Compact icon button (toolbar / list row)
Button.icon(
icon: FluentIcons.edit_24_regular,
size: CdxControlSize.compact,
tooltip: 'Edit',
onPressed: () async => editor.open(),
)
// Active toolbar toggle (current selection)
Button.icon(
icon: FluentIcons.list_24_regular,
isActive: viewMode == ViewMode.list,
onPressed: () async => store.setView(ViewMode.list),
)
// Boolean toggle control
Button.toggle(
label: 'Active only',
icon: FluentIcons.filter_24_regular,
isActive: filter.activeOnly,
onChanged: (value) => filter.setActiveOnly(value),
)Parameters
| Param | Type | Default | Notes |
|---|---|---|---|
label | String | required (filled/outlined/text/toggle) | |
icon | IconData? | null | Optional leading icon |
onPressed | Future<void> Function()? | null | Async — auto-loading; null disables |
isLoading | bool | false | External loading override (vs. internal) |
destructive | bool | false | error palette, red border / fill |
size | CdxControlSize | standard | standard is controlHeight tall; compact is compactControlSize tall (a square with zero padding for icon buttons) |
iconSize (icon) | double? | null | The glyph's size; the button's own size is size |
isActive (icon) | bool | false | selectedTint background |
style (icon) | IconButtonStyle | standard | filled / outlined / standard |
tooltip (icon) | String? | null |
Why a sealed class?
Switch-exhaustive factories prevent the "27-prop button" trap. Each variant carries only the params that make sense for it.
CdxLink
An inline text link: words in the primary colour, at the height of the text around them. It has no padding, minimum height or fill, so it sits in a line of text, a record field or a grid cell without changing the line height. It takes the ambient DefaultTextStyle merged with style.
Hover and keyboard focus underline it (keyboard focus also lays the focus tint behind it), painted, so nothing moves. Enter and Space press it. Without onTap it is disabled: muted, unfocusable, but its tooltip still shows. maxLines (1) cuts a long label with an ellipsis; in a Row, wrap it in a Flexible.
CdxLink(label: 'Revert', tooltip: 'Put back the saved value', onTap: revert)
CdxLink(
label: 'Rules (2)',
icon: Icons.bolt, // leading, as tall as the text
trailingIcon: Icons.arrow_forward,
onTap: openRules,
)A link is not a Button variant: every Button is a control-height box sized by CdxControlSize, with an async loading state; a link is a run of text sized by its style.
Split button
A primary action visually joined to a caret that opens a menu: a CdxMenu of commands behind CdxMenuTrigger.split. Three styles.
CdxMenu.actions(
trigger: CdxMenuTrigger.split(
label: 'Save',
icon: FluentIcons.save_24_regular,
onPressed: _save,
style: CdxMenuButtonStyle.outlined,
),
items: [
CdxMenuItem.action(title: 'Save as draft', onTap: _saveDraft),
CdxMenuItem.action(title: 'Save and duplicate', onTap: _saveAndDup),
],
)
// A body of your own is a custom row. An icon-only split needs a tooltip.
CdxMenu.actions(
trigger: CdxMenuTrigger.split(
icon: FluentIcons.filter_24_regular,
tooltip: 'Filter',
onPressed: _applyFilter,
),
items: [CdxMenuItem.custom(child: MyFilterPresetPopover())],
)CdxMenuButtonStyle
| Style | Visual | When |
|---|---|---|
filled | Solid primary background on both halves | Single most important CTA on a surface |
outlined | One shared border around two borderless halves (default) | Secondary CTAs, dense rows |
text | No background, no border | In-card quick actions, repeated row actions |
size: CdxControlSize.compact forces the compact height (icon-only mode implies compact automatically).
CdxSegmentedTabs
Pill-style segmented tab bar with an animated sliding indicator. Mirrors Flutter's TabBar shape (tabs + value + onChanged + isScrollable) but renders as a single rounded container.
CdxSegmentedTabs<BehaviorType>(
tabs: [
CdxSegmentedTab(
value: BehaviorType.maintenance,
label: const Text('Maintenance'),
icon: FluentIcons.wrench_24_regular,
),
CdxSegmentedTab(value: BehaviorType.usage, label: const Text('Usage')),
CdxSegmentedTab(
value: BehaviorType.problem,
label: const Text('Problem'),
badge: const _RedDot(),
),
],
value: selected,
onChanged: (v) => setState(() => selected = v),
isScrollable: true,
variant: CdxSegmentedTabsVariant.subtle, // .primary for top-of-page workspace switcher
)| Variant | Visual |
|---|---|
subtle | surfaceBright floating pill on a dim track. Quiet — default. |
primary | Saturated primary fill with shadow. Loud — use for top-of-page switchers. |
CdxSegmentedTab.label is a Widget?; pass Text(...) for ordinary labels, a small Row when the label needs an inline count chip, or null for an icon-only tab. It also carries optional badge (corner-pinned, IgnorePointer), icon, tooltip, loading, and the typed value.
collapse lets a strip whose tabs all have an icon go icon-only when it is given less width than its labelled tabs need. The same tabs drop their labels and hug their icon and badge, and the label becomes the tooltip; the name screen readers hear does not change.
collapse | With room | Short of room |
|---|---|---|
never (default) | as built | labels ellipsize |
dropLabels | icon and label | icon |
labelsToIcons | label | icon |
Each tab needs an icon, and a plain Text label or a tooltip to be named by. A strip cannot collapse when it is scrollable or has a tabWidth; it asserts in debug builds and says why.
isScrollable: true is the other answer to a strip short of room: the tabs keep their labels, size to their content, and the strip scrolls. A strip takes one answer, scrolling or collapse. A scrolling strip:
- fades each edge that has more tabs beyond it, with
CdxEdgeFade; - brings the selected tab to the middle when it is first shown and when the selection changes, and keeps a selected tab that is on show on show when its room changes (a tab the user scrolled away stays away);
- scrolls a tab that takes the keyboard focus clear of the fades; the focus beats the selection;
- is one focus traversal group: Tab goes through its tabs in order wherever they are scrolled to, and the left and right arrow keys move the focus between them, stopping at the ends.
CdxDismissAffordance
Compact close glyph used inside chips, search inputs, token fields, and parenthesized filter groups. Avoids IconButton's 48 px floor.
CdxDismissAffordance(
icon: FluentIcons.dismiss_12_regular,
size: CdxDismissAffordanceSize.compact, // .regular (6 px hit padding) for standalone use
tooltip: 'Remove',
onPressed: () => removeChip(),
)AlertsNotificationsButton
Bell IconButton with an unread-count Badge (suppressed when count is null/0; shows 99+ past 99).
AlertsNotificationsButton(
unreadCount: store.pendingCount,
tooltip: 'Notifications',
onPressed: () => context.go('/notifications'),
)Cross-links
- Density & Sizing —
controlHeight,compactControlSize - Theming —
selectedTint, status palette - Menus —
CdxMenu,CdxMenuItemand every trigger