Skip to content

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.

FactoryRenderUse
Button.filledFilledButtonPrimary action
Button.outlinedOutlinedButtonSecondary action
Button.textTextButtonTertiary / link action
Button.iconIconButtonIcon-only — size, isActive, IconButtonStyle.filled / .outlined / .standard
Button.toggleCDX toggle controlBoolean on/off control
dart
// 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 ​

ParamTypeDefaultNotes
labelStringrequired (filled/outlined/text/toggle)
iconIconData?nullOptional leading icon
onPressedFuture<void> Function()?nullAsync — auto-loading; null disables
isLoadingboolfalseExternal loading override (vs. internal)
destructiveboolfalseerror palette, red border / fill
sizeCdxControlSizestandardstandard is controlHeight tall; compact is compactControlSize tall (a square with zero padding for icon buttons)
iconSize (icon)double?nullThe glyph's size; the button's own size is size
isActive (icon)boolfalseselectedTint background
style (icon)IconButtonStylestandardfilled / 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.

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.

dart
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.

dart
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 ​

StyleVisualWhen
filledSolid primary background on both halvesSingle most important CTA on a surface
outlinedOne shared border around two borderless halves (default)Secondary CTAs, dense rows
textNo background, no borderIn-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.

dart
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
)
VariantVisual
subtlesurfaceBright floating pill on a dim track. Quiet — default.
primarySaturated 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.

collapseWith roomShort of room
never (default)as builtlabels ellipsize
dropLabelsicon and labelicon
labelsToIconslabelicon

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.

dart
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).

dart
AlertsNotificationsButton(
  unreadCount: store.pendingCount,
  tooltip: 'Notifications',
  onPressed: () => context.go('/notifications'),
)
  • Density & Sizing — controlHeight, compactControlSize
  • Theming — selectedTint, status palette
  • Menus — CdxMenu, CdxMenuItem and every trigger