/create-modal
Create declarative modals using the modal library API. Covers modal types (confirm, input, select, form), sections (Text, Buttons, Input, Textarea, Checkbox, Select, List, Combo, When, Custom), rendering with OverlayModal, and keyboard/mouse handling. Use when adding modals or
Claude Code
$ npx -y skills add marcus/sidecar --skill create-modal --agent claude-code
Codex
$ npx skills add marcus/sidecar --skill create-modal -a codex -y
Cursor
$ npx skills add marcus/sidecar --skill create-modal -a cursor -y
Gemini CLI
$ npx skills add marcus/sidecar --skill create-modal -a gemini-cli -y
GitHub Copilot
$ npx skills add marcus/sidecar --skill create-modal -a github-copilot -y
OpenCode
$ npx skills add marcus/sidecar --skill create-modal -a opencode -y
Windsurf
$ npx skills add marcus/sidecar --skill create-modal -a windsurf -y
SKILL.md
create-modal.SKILL.mdname: create-modal
description: Create declarative modals using the modal library API. Covers modal types (confirm, input, select, form), sections (Text, Buttons, Input, Textarea, Checkbox, Select, List, Combo, When, Custom), rendering with OverlayModal, and keyboard/mouse handling. Use when adding modals or dialogs to the application.
Creating Declarative Modals
Use the `internal/modal` package. The library handles keyboard navigation, mouse hit regions, hover states, and scrolling automatically.
Quick Start
import "github.com/marcus/sidecar/internal/modal"
// 1. Create the modal
m := modal.New("Delete Worktree?",
modal.WithWidth(58),
modal.WithVariant(modal.VariantDanger),
modal.WithPrimaryAction("delete"),
).
AddSection(modal.Text("Name: " + wt.Name)).
AddSection(modal.Spacer()).
AddSection(modal.Buttons(
modal.Btn(" Delete ", "delete", modal.BtnDanger()),
modal.Btn(" Cancel ", "cancel"),
))
// 2. Render in View
func (p *Plugin) View(width, height int) string {
background := p.renderListView(width, height)
rendered := p.myModal.Render(width, height, p.mouseHandler)
return ui.OverlayModal(background, rendered, width, height)
}
// 3. Handle input in Update
case tea.KeyMsg:
action, cmd := p.myModal.HandleKey(msg)
if action != "" {
return p.handleAction(action) // "delete", "cancel", etc.
}
return p, cmd
case tea.MouseMsg:
action := p.myModal.HandleMouse(msg, p.mouseHandler)
if action != "" {
return p.handleAction(action)
}
return p, nilCritical: Modal Initialization Pattern
The modal must exist before input handling. Create an `ensure` function called in **both** View and Update:
func (p *Plugin) ensureMyModal() {
if p.targetItem == nil {
return // Required state missing
}
modalW := 50
if modalW > p.width-4 {
modalW = p.width - 4
}
if modalW < 20 {
modalW = 20
}
// Only rebuild if needed
if p.myModal != nil && p.myModalWidthCache == modalW {
return
}
p.myModalWidthCache = modalW
p.myModal = modal.New("Title", modal.WithWidth(modalW), ...).
AddSection(...)
}**Call `ensureModal()` before the nil check in key handlers:**
func (p *Plugin) handleMyModalKeys(msg tea.KeyMsg) tea.Cmd {
p.ensureMyModal() // CRITICAL: Before nil check
if p.myModal == nil {
return nil
}
action, cmd := p.myModal.HandleKey(msg)
// ...
}Without this, the first keypress after opening drops because View runs after Update in bubbletea.
Constructor and Options
m := modal.New(title string, opts ...Option)
| Option | Description | Default | |--------|-------------|---------| | `WithWidth(int)` | Modal width in characters | 50 | | `WithVariant(Variant)` | Visual style | `VariantDefault` | | `WithPrimaryAction(string)` | Action ID for Enter on inputs | "" | | `WithHints(bool)` | Show "Tab to switch..." hint | true | | `WithCloseOnBackdropClick(bool)` | Backdrop click returns "cancel" | true |
**Variants:** `VariantDefault`, `VariantDanger` (red), `VariantWarning` (yellow), `VariantInfo` (blue)
Built-in Sections
Text and Spacer
modal.Text("Static text with auto line wrapping")
modal.Spacer() // Single blank lineButtons
modal.Buttons(
modal.Btn(" Save ", "save"), // Standard button
modal.Btn(" Delete ", "delete", modal.BtnDanger()), // Red
modal.Btn(" Submit ", "submit", modal.BtnPrimary()), // Primary
modal.Btn(" Cancel ", "cancel"),
)- Include padding in labels: `" Save "` not `"Save"`
- Button IDs are returned as actions
- Tab/Shift+Tab cycles focus
Input
var nameInput textinput.Model
modal.Input("name-input", &nameInput)
modal.InputWithLabel("name-input", "Name:", &nameInput)
modal.Input("name-input", &nameInput,
modal.WithSubmitOnEnter(true), // Default: true
modal.WithSubmitAction("submit"), // Override primary action
)Textarea
var msgArea textarea.Model
modal.Textarea("message", &msgArea, 5) // height in lines
modal.TextareaWithLabel("message", "Label:", &msgArea, 5)- Enter inserts newlines (never submits)
Combo (floating dropdown)
items := []modal.DropdownItem{
{ID: "main", Label: "main", Value: "main"},
{ID: "dev", Label: "dev", Value: "dev"},
}
var selectedIdx int
modal.Combo("branch", &branchInput, items, &selectedIdx)- Single-line input; filtered results float over later sections (modal height does not change)
- `selected` is an **items** index (same as List)
- Typing filters and selects the top match; up/down move the highlight
- Enter commits the highlight and, by default, returns the modal primary action
- Tab commits and moves focus; Esc closes the overlay without cancelling the modal
- Click an overlay row to commit without submitting
Checkbox
var includeFiles bool
modal.Checkbox("include-files", "Include untracked files", &includeFiles)- Space toggles
- Enter does **not** toggle; it submits the modal primary action (if any)
Select (one choice out of a set)
items := []modal.SelectItem{
{ID: "shell", Label: "Shell", Description: "new agent/shell session"},
{ID: "worktree", Label: "Worktree", Description: "shell in a new worktree"},
}
var selectedIdx int
modal.Select("kind", items, &selectedIdx,
modal.WithMaxVisible(6),
modal.WithDisabled(func(i int) string { return reasons[i] }),
modal.WithOnSelect(func(i int) { rebuildAround(i) }),
)- The default control for a single choice: sort, filter, kind. `modal.List` is the low-level column of rows for lists that are not a single choice.
- Two shapes, chosen by count: a segmented `[ A | B | C ]` under five choices, a `❯`-cursor full-width list with an aligned description column at five or more (and the list whenever the segments would not fit the width). `WithShape(modal.Shap
Read more
name: create-modal description: Create declarative modals using the modal library API. Covers modal types (confirm, input, select, form), sections (Text, Buttons, Input, Textarea, Checkbox, Select, List, Combo, When, Custom), rendering with OverlayModal, and keyboard/mouse handling. Use when adding modals or dialogs to the application.
Creating Declarative Modals
Use the `internal/modal` package. The library handles keyboard navigation, mouse hit regions, hover states, and scrolling automatically.
Quick Start
import "github.com/marcus/sidecar/internal/modal"
// 1. Create the modal
m := modal.New("Delete Worktree?",
modal.WithWidth(58),
modal.WithVariant(modal.VariantDanger),
modal.WithPrimaryAction("delete"),
).
AddSection(modal.Text("Name: " + wt.Name)).
AddSection(modal.Spacer()).
AddSection(modal.Buttons(
modal.Btn(" Delete ", "delete", modal.BtnDanger()),
modal.Btn(" Cancel ", "cancel"),
))
// 2. Render in View
func (p *Plugin) View(width, height int) string {
background := p.renderListView(width, height)
rendered := p.myModal.Render(width, height, p.mouseHandler)
return ui.OverlayModal(background, rendered, width, height)
}
// 3. Handle input in Update
case tea.KeyMsg:
action, cmd := p.myModal.HandleKey(msg)
if action != "" {
return p.handleAction(action) // "delete", "cancel", etc.
}
return p, cmd
case tea.MouseMsg:
action := p.myModal.HandleMouse(msg, p.mouseHandler)
if action != "" {
return p.handleAction(action)
}
return p, nilCritical: Modal Initialization Pattern
The modal must exist before input handling. Create an `ensure` function called in **both** View and Update:
func (p *Plugin) ensureMyModal() {
if p.targetItem == nil {
return // Required state missing
}
modalW := 50
if modalW > p.width-4 {
modalW = p.width - 4
}
if modalW < 20 {
modalW = 20
}
// Only rebuild if needed
if p.myModal != nil && p.myModalWidthCache == modalW {
return
}
p.myModalWidthCache = modalW
p.myModal = modal.New("Title", modal.WithWidth(modalW), ...).
AddSection(...)
}**Call `ensureModal()` before the nil check in key handlers:**
func (p *Plugin) handleMyModalKeys(msg tea.KeyMsg) tea.Cmd {
p.ensureMyModal() // CRITICAL: Before nil check
if p.myModal == nil {
return nil
}
action, cmd := p.myModal.HandleKey(msg)
// ...
}Without this, the first keypress after opening drops because View runs after Update in bubbletea.
Constructor and Options
m := modal.New(title string, opts ...Option)
| Option | Description | Default | |--------|-------------|---------| | `WithWidth(int)` | Modal width in characters | 50 | | `WithVariant(Variant)` | Visual style | `VariantDefault` | | `WithPrimaryAction(string)` | Action ID for Enter on inputs | "" | | `WithHints(bool)` | Show "Tab to switch..." hint | true | | `WithCloseOnBackdropClick(bool)` | Backdrop click returns "cancel" | true |
**Variants:** `VariantDefault`, `VariantDanger` (red), `VariantWarning` (yellow), `VariantInfo` (blue)
Built-in Sections
Text and Spacer
modal.Text("Static text with auto line wrapping")
modal.Spacer() // Single blank lineButtons
modal.Buttons(
modal.Btn(" Save ", "save"), // Standard button
modal.Btn(" Delete ", "delete", modal.BtnDanger()), // Red
modal.Btn(" Submit ", "submit", modal.BtnPrimary()), // Primary
modal.Btn(" Cancel ", "cancel"),
)- Include padding in labels: `" Save "` not `"Save"`
- Button IDs are returned as actions
- Tab/Shift+Tab cycles focus
Input
var nameInput textinput.Model
modal.Input("name-input", &nameInput)
modal.InputWithLabel("name-input", "Name:", &nameInput)
modal.Input("name-input", &nameInput,
modal.WithSubmitOnEnter(true), // Default: true
modal.WithSubmitAction("submit"), // Override primary action
)Textarea
var msgArea textarea.Model
modal.Textarea("message", &msgArea, 5) // height in lines
modal.TextareaWithLabel("message", "Label:", &msgArea, 5)- Enter inserts newlines (never submits)
Combo (floating dropdown)
items := []modal.DropdownItem{
{ID: "main", Label: "main", Value: "main"},
{ID: "dev", Label: "dev", Value: "dev"},
}
var selectedIdx int
modal.Combo("branch", &branchInput, items, &selectedIdx)- Single-line input; filtered results float over later sections (modal height does not change)
- `selected` is an **items** index (same as List)
- Typing filters and selects the top match; up/down move the highlight
- Enter commits the highlight and, by default, returns the modal primary action
- Tab commits and moves focus; Esc closes the overlay without cancelling the modal
- Click an overlay row to commit without submitting
Checkbox
var includeFiles bool
modal.Checkbox("include-files", "Include untracked files", &includeFiles)- Space toggles
- Enter does **not** toggle; it submits the modal primary action (if any)
Select (one choice out of a set)
items := []modal.SelectItem{
{ID: "shell", Label: "Shell", Description: "new agent/shell session"},
{ID: "worktree", Label: "Worktree", Description: "shell in a new worktree"},
}
var selectedIdx int
modal.Select("kind", items, &selectedIdx,
modal.WithMaxVisible(6),
modal.WithDisabled(func(i int) string { return reasons[i] }),
modal.WithOnSelect(func(i int) { rebuildAround(i) }),
)- The default control for a single choice: sort, filter, kind. `modal.List` is the low-level column of rows for lists that are not a single choice.
- Two shapes, chosen by count: a segmented `[ A | B | C ]` under five choices, a `❯`-cursor full-width list with an aligned description column at five or more (and the list whenever the segments would not fit the width). `WithShape(modal.Shap
Always check if you are running in Sidecar: run sidecar agents for capabilities. You might never open your editor again. Status: Ready for daily use. Please report any issues you encounter. Documentation · Getting Started · Comprehensive List of Features
Other skills on sidecar.
More skills from other plugins.
10x
Personalized audit that teaches the advanced Paradev (formerly Superset) features the user isn't using yet (automations, parallel…
ao-actions
Direct actions on one Orchestrator session or PR: stop or interrupt, resume, merge a PR, start a review, screenshot a session's…
capabilities
Your capability catalog — read this at boot. Lists the temporal date-range skills and the external integrations (reached via the…
Questions about create-modal.
- Which coding agents can use create-modal?
- create-modal installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.

