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:
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)";
}dotnet runInteraction
- 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:
| Region | Mouse Action | Keyboard Action |
|---|---|---|
| Label area | Triggers primary action | Enter or Space |
| Arrow (▼) | Opens dropdown menu | Down 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:
ctx.SplitButton()
.PrimaryAction("Run", e => RunDefaultCommand())2
The handler receives SplitButtonClickedEventArgs with:
Widget- The source SplitButtonWidgetNode- The underlying SplitButtonNodeContext- Access to notifications, focus, popups, and app services
Secondary Actions
Add dropdown menu items with SecondaryAction():
ctx.SplitButton()
.PrimaryAction("Run", _ => RunDefault())
.SecondaryAction("Run with Debugger", _ => RunDebug())
.SecondaryAction("Run Tests", _ => RunTests())
.SecondaryAction("Run Benchmarks", _ => RunBenchmarks())2
3
4
5
Actions appear in the dropdown in the order they're added.
Async Handlers
Both primary and secondary actions support async handlers:
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();
})2
3
4
5
6
7
8
9
Dropdown Opened Callback
Use OnDropdownOpened() to react when the dropdown menu opens:
ctx.SplitButton()
.PrimaryAction("Options", _ => { })
.OnDropdownOpened(() => Analytics.Track("dropdown_opened"))
.SecondaryAction("Option A", _ => { })
.SecondaryAction("Option B", _ => { })2
3
4
5
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:
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";
}dotnet runUse Cases
Split buttons work well for:
| Scenario | Example |
|---|---|
| File operations | Save / Save As / Save All |
| Deployment | Deploy / Deploy to Staging / Rollback |
| Creation actions | New File / From Template / Duplicate |
| Run commands | Run / Debug / Profile |
| Export options | Export / Export as PDF / Export as CSV |
Keyboard Navigation
| Key | Action |
|---|---|
| Tab | Focus the split button |
| Enter / Space | Trigger primary action |
| Down | Open dropdown menu |
| Up / Down (in menu) | Navigate options |
| Enter (in menu) | Select option |
| Escape | Close dropdown |
Theming
A SplitButton chip is rendered as two colour regions on the same body:
- Primary region —
" {Label} "cells. Styled byButtonTheme(the same colours that style a regularButton), 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 bySplitButtonTheme, 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.
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);2
3
4
5
6
7
8
9
10
11
12
Available Theme Elements
| Element | Type | Default | Description |
|---|---|---|---|
ArrowForegroundColor | Hex1bColor | Default | Text colour for the divider + arrow text in the resting state. Inherits global text colour by default, matching the resting primary label. |
ArrowBackgroundColor | Hex1bColor | rgb(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. |
FocusedArrowForegroundColor | Hex1bColor | Black | Text colour for the divider + arrow text when focused. |
FocusedArrowBackgroundColor | Hex1bColor | rgb(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. |
HoveredArrowForegroundColor | Hex1bColor | Black | Text colour for the divider + arrow text when hovered (mouse-only mid state). |
HoveredArrowBackgroundColor | Hex1bColor | rgb(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.