bubbledropdown

package module
v0.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 23, 2026 License: MIT Imports: 5 Imported by: 0

README

bubble-dropdown

A Bubble Tea dropdown component: a [ Label ▼ ] trigger that opens a scrollable selection panel as a bubble-overlay modal. Full keyboard and mouse support with bubblezone hit-testing.

Dropdown demo

Installation

go get github.com/madicen/bubble-dropdown

Quick start

import bubbledropdown "github.com/madicen/bubble-dropdown"

// Create
d := bubbledropdown.New(
    bubbledropdown.WithOptions([]string{"Apple", "Banana", "Cherry"}),
    bubbledropdown.WithPlaceholder("Pick a fruit"),
)
d.SetZoneManager(zm) // optional; enable bubblezone hit-testing

// In View — call SetBounds every frame before building the zone mark
tw, th := d.TriggerSize()
d.SetBounds(row, col, tw, th)
mainView := zm.Mark("my-dropdown", d.TriggerView())
return d.ViewWithOverlay(mainView, width, height)
// (if using zm: wrap the final return in zm.Scan)

// In Update — forward all messages
d, cmd = d.Update(msg)

// Handle selection
case bubbledropdown.ItemChosenMsg:
    d.SetSelectedIndex(msg.Index)

See examples/basic for a full working example with three dropdowns and keyboard + mouse support, and examples/styled for fully customized appearance.

Styling

Every visible part of the dropdown can be styled via options. WithAccentColor is the quickest path — it recolors the panel border, the highlighted row, and the focused trigger arrow in one go. For finer control, override individual lipgloss styles. Custom styles take precedence over the accent color where they overlap.

Styled demo

d := bubbledropdown.New(
    bubbledropdown.WithOptions([]string{"Apple", "Banana", "Cherry"}),

    // One accent color recolors border + highlight + focused arrow
    bubbledropdown.WithAccentColor("#7D56F4"),

    // …or override individual styles for full control
    bubbledropdown.WithTriggerStyle(lipgloss.NewStyle().Bold(true)),
    bubbledropdown.WithListStyle(
        lipgloss.NewStyle().Border(lipgloss.ThickBorder()),
    ),
    bubbledropdown.WithItemStyle(
        lipgloss.NewStyle().Padding(0, 1).Foreground(lipgloss.Color("245")),
    ),
    bubbledropdown.WithCursorStyle(
        lipgloss.NewStyle().Padding(0, 1).Bold(true).
            Background(lipgloss.Color("#2ECC71")).Foreground(lipgloss.Color("16")),
    ),
)

Note: WithItemStyle and WithCursorStyle should keep symmetric horizontal padding (e.g. Padding(0, 1)) so item rows align with the panel's computed width.

Run it: go run ./examples/styled

Keyboard controls

Context Key Action
Trigger focused Enter / Space Open the dropdown panel
Trigger focused / Cycle selection without opening
Trigger focused Tab / Shift+Tab Move focus (handled by your app)
Panel open / Move cursor through options
Panel open Enter Confirm selection → ItemChosenMsg
Panel open Esc Cancel → ItemCanceledMsg

Mouse controls

Action Result
Click trigger Open the panel
Click option row Confirm selection → ItemChosenMsg
Hover over option row Move cursor highlight
Scroll wheel inside panel Move cursor
Click outside open panel Cancel → ItemCanceledMsg

API

New(opts ...Option) *Dropdown

Creates a new Dropdown. All configuration is done through options.

Options
Option Description
WithOptions([]string) Selectable items
WithInitialIndex(int) Initially selected item (default 0)
WithPlaceholder(string) Text shown when nothing is selected (default "Select…")
WithMaxVisible(int) Max items shown before scrolling (default 8)
WithAccentColor(string) Color used for the default border, highlight, and focused arrow (e.g. "62" or "#7D56F4")
WithTriggerStyle(lipgloss.Style) Override the closed trigger style
WithListStyle(lipgloss.Style) Override the open panel border style
WithItemStyle(lipgloss.Style) Override non-highlighted item rows
WithCursorStyle(lipgloss.Style) Override the highlighted (hover/cursor) item row
Methods
// Positioning — call every frame before building view
d.SetBounds(row, col, w, h int)
d.TriggerSize() (width, height int)

// Rendering
d.TriggerView() string
d.ViewWithOverlay(mainView string, viewWidth, viewHeight int) string

// State
d.Open() bool
d.Focused() bool
d.SetFocused(bool)
d.Selected() string
d.SelectedIndex() int
d.SetSelectedIndex(i int)

// Zone support
d.SetZoneManager(*zone.Manager)

// Message loop
d.Update(tea.Msg) (*Dropdown, tea.Cmd)
Messages
// Emitted when the user picks an item
type ItemChosenMsg struct {
    Index int    // zero-based index into the options slice
    Value string // the selected option string
}

// Emitted when the user dismisses without selecting
type ItemCanceledMsg struct{}

Embedding pattern

The standard embed pattern (mirrors bubble-color-picker's SwatchPicker):

type appModel struct {
    width, height int
    zm            *zone.Manager
    dropdowns     []*bubbledropdown.Dropdown
    focused       int
}

func (a *appModel) View() string {
    mainView := a.buildMainView() // calls SetBounds + zm.Mark inside
    for _, d := range a.dropdowns {
        mainView = d.ViewWithOverlay(mainView, a.width, a.height)
    }
    return a.zm.Scan(mainView)
}

func (a *appModel) buildMainView() string {
    tw, th := a.dropdowns[0].TriggerSize()
    a.dropdowns[0].SetBounds(3, 8, tw, th)
    return a.zm.Mark("fruit", a.dropdowns[0].TriggerView())
}

func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    // Route to the open dropdown exclusively
    for i, d := range a.dropdowns {
        if d.Open() {
            a.dropdowns[i], cmd = d.Update(msg)
            return a, cmd
        }
    }
    // Route mouse clicks to the zone-matched dropdown
    if m, ok := msg.(tea.MouseMsg); ok && m.Action == tea.MouseActionPress {
        if a.zm.Get("fruit").InBounds(m) {
            a.dropdowns[0], cmd = a.dropdowns[0].Update(msg)
            return a, cmd
        }
    }
    // Broadcast to focused dropdown for keys
    a.dropdowns[a.focused], cmd = a.dropdowns[a.focused].Update(msg)
    return a, cmd
}

Panel positioning

The panel opens below the trigger by default. When there is not enough room below (the panel would exceed the terminal height), it automatically flips above the trigger. bubble-overlay's ClampedOrigin ensures the panel stays within the viewport at all times.

Dependencies

License

MIT

Documentation

Overview

Package bubbledropdown provides Dropdown: a trigger element that opens a scrollable selection panel as a bubble-overlay modal. Embed it in your BubbleTea model, call SetBounds and forward messages, then call ViewWithOverlay to composite the open panel over your main view.

Usage overview:

d := bubbledropdown.New(
    bubbledropdown.WithOptions([]string{"Apple", "Banana", "Cherry"}),
    bubbledropdown.WithPlaceholder("Pick a fruit"),
)
d.SetZoneManager(zm) // optional; enables bubblezone hit-testing

In your View:

tw, th := d.TriggerSize()
d.SetBounds(row, col, tw, th)
mainView := zm.Mark("my-dropdown", d.TriggerView())
return d.ViewWithOverlay(mainView, width, height)

In your Update, forward all messages:

d, cmd = d.Update(msg)

On ItemChosenMsg, call d.SetSelectedIndex(msg.Index).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	Options      []string
	InitialIndex int
	Placeholder  string
	MaxVisible   int

	// Styling. Each style has a corresponding Custom* flag so New can tell an
	// intentionally-empty style apart from "not set".
	TriggerStyle lipgloss.Style
	ListStyle    lipgloss.Style
	ItemStyle    lipgloss.Style
	CursorStyle  lipgloss.Style
	AccentColor  string

	CustomTriggerStyle bool
	CustomListStyle    bool
	CustomItemStyle    bool
	CustomCursorStyle  bool
}

Config holds static options for a Dropdown built with New.

type Dropdown struct {
	// contains filtered or unexported fields
}

Dropdown is a trigger + panel component. Use New to create one; the zero value is not valid. All methods are safe to call on a nil pointer (they are no-ops / return zero values) so partial initialisation is safe.

func New

func New(opts ...Option) *Dropdown

New creates a Dropdown configured with the given options.

func (d *Dropdown) Focused() bool

Focused returns true when the trigger is marked as focused (highlighted arrow).

func (d *Dropdown) Open() bool

Open returns true when the dropdown panel is currently displayed.

func (d *Dropdown) Selected() string

Selected returns the currently selected option string, or the placeholder if nothing is selected.

func (d *Dropdown) SelectedIndex() int

SelectedIndex returns the zero-based index of the selected option.

func (d *Dropdown) SetBounds(row, col, w, h int)

SetBounds records where the trigger is drawn (0-based row, col) and its display size (w cells wide, h rows tall). Call this every frame before ViewWithOverlay so the panel is positioned correctly.

If w or h is zero, TriggerSize() is used for the missing dimension.

func (d *Dropdown) SetFocused(f bool)

SetFocused marks the trigger as focused or unfocused. When focused, the ▼ arrow is rendered in the accent color so keyboard-only users have a visible indicator.

func (d *Dropdown) SetSelectedIndex(i int)

SetSelectedIndex updates which item appears selected. Out-of-range values are clamped. This is typically called after receiving ItemChosenMsg.

func (d *Dropdown) SetZoneManager(zm *zone.Manager)

SetZoneManager sets the bubblezone manager. When set, the host marks the trigger with zm.Mark and the dropdown skips its own coordinate hit-test (trusting the zone was already checked). The host must call zm.Scan on the final view string.

func (d *Dropdown) TriggerSize() (width, height int)

TriggerSize returns the stable display-cell width and height (always 1) of the trigger. Width is computed from the longest option so the trigger does not resize as the selection changes.

Use this when building your layout and when calling SetBounds.

func (d *Dropdown) TriggerView() string

TriggerView renders the closed-state trigger: "[ Label ▼ ]". When focused, the ▼ arrow is rendered in the accent color.

func (d *Dropdown) Update(msg tea.Msg) (*Dropdown, tea.Cmd)

Update handles all BubbleTea messages. Forward every tea.Msg here.

  • When the dropdown is open it routes keys and mouse events to the list panel.
  • On ItemChosenMsg / ItemCanceledMsg it closes the dropdown; the host should also handle ItemChosenMsg to call SetSelectedIndex(msg.Index).
  • When closed, a mouse press on the trigger (or zone hit) opens the panel.
func (d *Dropdown) ViewWithOverlay(mainView string, viewWidth, viewHeight int) string

ViewWithOverlay returns the view to display. When the dropdown is open it composites the panel over mainView using bubble-overlay. Call this at the end of your View method; it also caches overlay geometry for Update.

mainView := buildMain()
mainView = d.ViewWithOverlay(mainView, width, height)
return zm.Scan(mainView)

type ItemCanceledMsg

type ItemCanceledMsg struct{}

ItemCanceledMsg is sent when the user dismisses the dropdown without selecting an item (Esc key or click outside the panel).

type ItemChosenMsg

type ItemChosenMsg struct {
	Index int    // zero-based index into the options slice
	Value string // the option string at that index
}

ItemChosenMsg is sent when the user selects an item from the dropdown (Enter key or mouse click on an option). Handle it in your root model; call SetSelectedIndex(msg.Index) on the dropdown to update displayed value.

type Option

type Option func(*Config)

Option configures a Dropdown via New(opts...).

func WithAccentColor added in v0.0.2

func WithAccentColor(color string) Option

WithAccentColor sets the accent color (any lipgloss color string, e.g. "62" or "#7D56F4") used for the default panel border, the highlighted item background, and the focused trigger arrow. Styles set via WithListStyle / WithCursorStyle take precedence over the accent color where they overlap.

func WithCursorStyle added in v0.0.2

func WithCursorStyle(s lipgloss.Style) Option

WithCursorStyle overrides the lipgloss style applied to the highlighted (hovered / keyboard-cursor) item row. The default inverts the accent color.

Note: the style should keep symmetric horizontal padding (e.g. Padding(0, 1)) so item rows align with the panel width computation.

func WithInitialIndex

func WithInitialIndex(i int) Option

WithInitialIndex sets which item is shown as selected when the dropdown is first rendered. Out-of-range values are clamped to [0, len(options)-1].

func WithItemStyle added in v0.0.2

func WithItemStyle(s lipgloss.Style) Option

WithItemStyle overrides the lipgloss style applied to non-highlighted item rows in the open panel. The default adds one cell of horizontal padding.

Note: the style should keep symmetric horizontal padding (e.g. Padding(0, 1)) so item rows align with the panel width computation.

func WithListStyle

func WithListStyle(s lipgloss.Style) Option

WithListStyle overrides the lipgloss style applied to the open dropdown panel border. The default is a rounded border in the accent color.

func WithMaxVisible

func WithMaxVisible(n int) Option

WithMaxVisible sets the maximum number of items shown in the open dropdown panel before scrolling kicks in. Defaults to 8.

func WithOptions

func WithOptions(opts []string) Option

WithOptions sets the list of selectable items. An empty slice produces a disabled dropdown that shows only the placeholder.

func WithPlaceholder

func WithPlaceholder(p string) Option

WithPlaceholder sets the text shown when no item is selected or the options slice is empty. Defaults to "Select…".

func WithTriggerStyle

func WithTriggerStyle(s lipgloss.Style) Option

WithTriggerStyle overrides the lipgloss style applied to the trigger row (the closed-state "[ Label ▼ ]" element).

Directories

Path Synopsis
examples
basic command
Basic example: a small form with three dropdowns — fruit, color, and size.
Basic example: a small form with three dropdowns — fruit, color, and size.
styled command
Styled example: two dropdowns with fully customized appearance.
Styled example: two dropdowns with fully customized appearance.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL