Skip to content

SplitButton

A split button combines a primary action with a dropdown menu of secondary actions. The main button area triggers the default action, while a dropdown arrow reveals additional options.

Split buttons are ideal when you have a common default action but need to expose related alternatives without cluttering your UI with multiple buttons.

Basic Usage

Create split buttons using SplitButton(), then chain PrimaryAction() and SecondaryAction() methods:

csharp
using Hex1b;

var state = new EditorState();

await using var terminal = Hex1bTerminal.CreateBuilder()
    .WithHex1bApp((app, options) => ctx => ctx.VStack(v => [
        v.Text("Split Button Demo"),
        v.Text(""),
        v.Text($"Last action: {state.LastAction}"),
        v.Text(""),
        v.SplitButton()
           .PrimaryAction("Save", _ => state.LastAction = "Saved file")
           .SecondaryAction("Save As...", _ => state.LastAction = "Save As dialog")
           .SecondaryAction("Save All", _ => state.LastAction = "Saved all files")
           .SecondaryAction("Save Copy", _ => state.LastAction = "Saved copy"),
        v.Text(""),
        v.Text("Click the button or press ▼ for more options")
    ]))
    .Build();

await terminal.RunAsync();

class EditorState
{
    public string LastAction { get; set; } = "(none)";
}

Interaction

  • Click the label or press Enter to trigger the primary action
  • Click the arrow (▼) or press Down to open the dropdown menu
  • Escape closes the dropdown without selecting

How It Works

The split button renders as [ Label ▼ ] with two distinct click regions:

RegionMouse ActionKeyboard Action
Label areaTriggers primary actionEnter or Space
Arrow (▼)Opens dropdown menuDown arrow

When the dropdown is open:

  • Up/Down arrows navigate between options
  • Enter selects the highlighted option
  • Escape closes without selecting

Event Handlers

Primary Action

Use PrimaryAction() to set the label and handler for the main button:

csharp
ctx.SplitButton()
   .PrimaryAction("Run", e => RunDefaultCommand())

The handler receives SplitButtonClickedEventArgs with:

  • Widget - The source SplitButtonWidget
  • Node - The underlying SplitButtonNode
  • Context - Access to notifications, focus, popups, and app services

Secondary Actions

Add dropdown menu items with SecondaryAction():

csharp
ctx.SplitButton()
   .PrimaryAction("Run", _ => RunDefault())
   .SecondaryAction("Run with Debugger", _ => RunDebug())
   .SecondaryAction("Run Tests", _ => RunTests())
   .SecondaryAction("Run Benchmarks", _ => RunBenchmarks())

Actions appear in the dropdown in the order they're added.

Async Handlers

Both primary and secondary actions support async handlers:

csharp
ctx.SplitButton()
   .PrimaryAction("Deploy", async e => {
       await DeployToProductionAsync();
       e.Context.Notifications.Post(
           new Notification("Deployed", "Successfully deployed to production"));
   })
   .SecondaryAction("Deploy to Staging", async e => {
       await DeployToStagingAsync();
   })

Use OnDropdownOpened() to react when the dropdown menu opens:

csharp
ctx.SplitButton()
   .PrimaryAction("Options", _ => { })
   .OnDropdownOpened(() => Analytics.Track("dropdown_opened"))
   .SecondaryAction("Option A", _ => { })
   .SecondaryAction("Option B", _ => { })

This is useful for:

  • Analytics tracking
  • Canceling timeouts (e.g., on notification cards)
  • Lazy-loading menu content

Multiple Split Buttons

You can use multiple split buttons together for complex toolbars:

csharp
using Hex1b;

var state = new TaskState();

await using var terminal = Hex1bTerminal.CreateBuilder()
    .WithHex1bApp((app, options) => ctx => ctx.Border(b => [
        b.VStack(v => [
            v.Text($"Task: {state.TaskName}"),
            v.Text($"Priority: {state.Priority}"),
            v.Text(""),
            v.HStack(h => [
                h.SplitButton()
                   .PrimaryAction("Create Task", _ => state.TaskName = "New Task")
                   .SecondaryAction("From Template", _ => state.TaskName = "Template Task")
                   .SecondaryAction("Duplicate Last", _ => state.TaskName = "Duplicated Task"),
                h.Text(" "),
                h.SplitButton()
                   .PrimaryAction("Set Priority", _ => state.Priority = "Normal")
                   .SecondaryAction("Low", _ => state.Priority = "Low")
                   .SecondaryAction("High", _ => state.Priority = "High")
                   .SecondaryAction("Urgent", _ => state.Priority = "Urgent")
            ])
        ])
    ], title: "Task Manager"))
    .Build();

await terminal.RunAsync();

class TaskState
{
    public string TaskName { get; set; } = "(none)";
    public string Priority { get; set; } = "Normal";
}

Use Cases

Split buttons work well for:

ScenarioExample
File operationsSave / Save As / Save All
DeploymentDeploy / Deploy to Staging / Rollback
Creation actionsNew File / From Template / Duplicate
Run commandsRun / Debug / Profile
Export optionsExport / Export as PDF / Export as CSV

Keyboard Navigation

KeyAction
TabFocus the split button
Enter / SpaceTrigger primary action
DownOpen dropdown menu
Up / Down (in menu)Navigate options
Enter (in menu)Select option
EscapeClose dropdown

Theming

A SplitButton chip is rendered as two colour regions on the same body:

  • Primary region" {Label} " cells. Styled by ButtonTheme (the same colours that style a regular Button), so a SplitButton's primary chip always reads as part of the same family as your Buttons.
  • Secondary affordance region" │ ▼ " cells (only rendered when secondary actions exist). Styled by SplitButtonTheme, defaulting to a half-shade darker tone than the matching primary colour so the dropdown affordance reads as a distinct sub-region of the chip without breaking the chip into two visual pieces.
csharp
using Hex1b;
using Hex1b.Theming;

var theme = new Hex1bTheme("Custom")
    // Primary region — same as Button
    .Set(ButtonTheme.BackgroundColor, Hex1bColor.FromRgb(25, 40, 60))
    .Set(ButtonTheme.FocusedBackgroundColor, Hex1bColor.FromRgb(0, 100, 180))
    .Set(ButtonTheme.FocusedForegroundColor, Hex1bColor.White)
    // Secondary affordance — half-shade darker than the primary
    .Set(SplitButtonTheme.ArrowBackgroundColor, Hex1bColor.FromRgb(20, 32, 48))
    .Set(SplitButtonTheme.FocusedArrowBackgroundColor, Hex1bColor.FromRgb(0, 80, 140))
    .Set(SplitButtonTheme.FocusedArrowForegroundColor, Hex1bColor.White);

Available Theme Elements

ElementTypeDefaultDescription
ArrowForegroundColorHex1bColorDefaultText colour for the divider + arrow text in the resting state. Inherits global text colour by default, matching the resting primary label.
ArrowBackgroundColorHex1bColorrgb(50, 50, 50)Resting background tint for the divider + arrow region. Half-shade darker than ButtonTheme.BackgroundColor so the dropdown affordance reads as a distinct sub-region of the chip.
FocusedArrowForegroundColorHex1bColorBlackText colour for the divider + arrow text when focused.
FocusedArrowBackgroundColorHex1bColorrgb(225, 225, 225)Background tint for the divider + arrow region when focused. Slightly dimmed white so the secondary affordance stays distinguishable from the brighter primary chip while remaining inside the high-contrast focused palette.
HoveredArrowForegroundColorHex1bColorBlackText colour for the divider + arrow text when hovered (mouse-only mid state).
HoveredArrowBackgroundColorHex1bColorrgb(160, 160, 160)Background tint for the divider + arrow region when hovered. Half-shade darker than ButtonTheme.HoveredBackgroundColor.

Uniform Chip

Setting any SplitButtonTheme colour to Hex1bColor.Default makes the arrow region inherit the matching primary colour, producing a uniform chip without a visible secondary tint. Useful when you want the dropdown affordance to be advertised by the glyph alone.

  • Button - Simple single-action buttons
  • Picker - Dropdown selection without primary action
  • List - Scrollable item selection

Released under the MIT License.