Overview
Bubble Tea provides comprehensive mouse support, allowing you to handle clicks, releases, motion, and wheel events. Mouse support is enabled per-view through the View.MouseMode property.
Mouse Messages
Mouse events are delivered through four message types:
MouseClickMsg - Mouse button pressed
MouseReleaseMsg - Mouse button released
MouseMotionMsg - Mouse moved
MouseWheelMsg - Mouse wheel scrolled
Mouse Structure
All mouse messages wrap the Mouse type:
type Mouse struct {
X , Y int // Zero-based coordinates (0,0 = top-left)
Button MouseButton // Which button was involved
Mod KeyMod // Modifier keys held during event
}
Bubble Tea supports all standard mouse buttons:
const (
MouseLeft // Left button
MouseMiddle // Middle button (scroll wheel click)
MouseRight // Right button
MouseWheelUp // Scroll wheel up
MouseWheelDown // Scroll wheel down
MouseWheelLeft // Scroll wheel left
MouseWheelRight // Scroll wheel right
MouseBackward // Browser back button
MouseForward // Browser forward button
MouseButton10
MouseButton11
)
Enabling Mouse Support
Enable mouse support by setting MouseMode on your view:
examples/mouse/main.go:40-44
func ( m model ) View () tea . View {
v := tea . NewView ( "Do mouse stuff. When you're done press q to quit. \n " )
v . MouseMode = tea . MouseModeAllMotion
return v
}
Mouse Modes
MouseModeAllMotion Track all mouse movement, even without buttons pressed
MouseModeButtonMotion Track mouse movement only when a button is held
MouseModeClickOnly Only receive click and release events
Basic Mouse Handling
Catching All Mouse Events
Use the MouseMsg interface to handle all mouse events:
examples/mouse/main.go:25-38
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch msg := msg .( type ) {
case tea . KeyPressMsg :
if s := msg . String (); s == "ctrl+c" || s == "q" || s == "esc" {
return m , tea . Quit
}
case tea . MouseMsg :
mouse := msg . Mouse ()
return m , tea . Printf ( "(X: %d , Y: %d ) %s " , mouse . X , mouse . Y , mouse )
}
return m , nil
}
Specific Event Types
Handle specific mouse events:
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch msg := msg .( type ) {
case tea . MouseClickMsg :
mouse := msg . Mouse ()
if mouse . Button == tea . MouseLeft {
return m , handleClick ( mouse . X , mouse . Y )
}
case tea . MouseReleaseMsg :
return m , handleRelease ()
case tea . MouseWheelMsg :
mouse := msg . Mouse ()
if mouse . Button == tea . MouseWheelUp {
return m , scrollUp ()
} else if mouse . Button == tea . MouseWheelDown {
return m , scrollDown ()
}
case tea . MouseMotionMsg :
mouse := msg . Mouse ()
return m , updateHover ( mouse . X , mouse . Y )
}
return m , nil
}
Mouse Event Details
Click Events
MouseClickMsg is sent when a mouse button is pressed:
type MouseClickMsg Mouse
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch msg := msg .( type ) {
case tea . MouseClickMsg :
mouse := msg . Mouse ()
// Check button
if mouse . Button == tea . MouseLeft {
fmt . Printf ( "Left click at ( %d , %d ) \n " , mouse . X , mouse . Y )
}
}
return m , nil
}
Release Events
MouseReleaseMsg is sent when a mouse button is released:
type MouseReleaseMsg Mouse
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch msg := msg .( type ) {
case tea . MouseReleaseMsg :
// Handle drag completion
if m . dragging {
m . dragging = false
return m , completeDrag ()
}
}
return m , nil
}
Wheel Events
MouseWheelMsg is sent for scroll wheel actions:
type MouseWheelMsg Mouse
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch msg := msg .( type ) {
case tea . MouseWheelMsg :
mouse := msg . Mouse ()
switch mouse . Button {
case tea . MouseWheelUp :
m . scroll -= 3
case tea . MouseWheelDown :
m . scroll += 3
case tea . MouseWheelLeft :
m . scrollX -= 3
case tea . MouseWheelRight :
m . scrollX += 3
}
}
return m , nil
}
Motion Events
MouseMotionMsg is sent when the mouse moves:
type MouseMotionMsg Mouse
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch msg := msg .( type ) {
case tea . MouseMotionMsg :
mouse := msg . Mouse ()
// Track hover state
m . hoverX = mouse . X
m . hoverY = mouse . Y
// Handle dragging
if mouse . Button != tea . MouseNone {
// Button is held during motion - this is a drag
m . dragging = true
}
}
return m , nil
}
Common Patterns
Click Detection
type model struct {
buttons [] Button
}
type Button struct {
X , Y , Width , Height int
Label string
}
func ( b Button ) Contains ( x , y int ) bool {
return x >= b . X && x < b . X + b . Width &&
y >= b . Y && y < b . Y + b . Height
}
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch msg := msg .( type ) {
case tea . MouseClickMsg :
mouse := msg . Mouse ()
for i , btn := range m . buttons {
if btn . Contains ( mouse . X , mouse . Y ) {
return m , handleButton ( i )
}
}
}
return m , nil
}
Drag and Drop
type model struct {
dragging bool
dragStartX , dragStartY int
dragItem int
}
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch msg := msg .( type ) {
case tea . MouseClickMsg :
mouse := msg . Mouse ()
m . dragging = true
m . dragStartX = mouse . X
m . dragStartY = mouse . Y
m . dragItem = m . itemAt ( mouse . X , mouse . Y )
case tea . MouseMotionMsg :
if m . dragging {
mouse := msg . Mouse ()
return m , updateDragPosition ( mouse . X , mouse . Y )
}
case tea . MouseReleaseMsg :
if m . dragging {
m . dragging = false
mouse := msg . Mouse ()
return m , dropItem ( m . dragItem , mouse . X , mouse . Y )
}
}
return m , nil
}
Hover Effects
type model struct {
hoverItem int
items [] string
}
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch msg := msg .( type ) {
case tea . MouseMotionMsg :
mouse := msg . Mouse ()
m . hoverItem = mouse . Y // Assuming one item per line
}
return m , nil
}
func ( m model ) View () tea . View {
var s string
for i , item := range m . items {
if i == m . hoverItem {
s += "> " + item + " < \n " // Highlight hovered item
} else {
s += " " + item + " \n "
}
}
v := tea . NewView ( s )
v . MouseMode = tea . MouseModeAllMotion
return v
}
type model struct {
offset int
content [] string
height int
}
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch msg := msg .( type ) {
case tea . MouseWheelMsg :
mouse := msg . Mouse ()
switch mouse . Button {
case tea . MouseWheelUp :
m . offset = max ( 0 , m . offset - 1 )
case tea . MouseWheelDown :
m . offset = min ( len ( m . content ) - m . height , m . offset + 1 )
}
}
return m , nil
}
Modifier Keys
Detect modifier keys held during mouse events:
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch msg := msg .( type ) {
case tea . MouseClickMsg :
mouse := msg . Mouse ()
if mouse . Mod & tea . ModCtrl != 0 {
// Ctrl was held during click
return m , handleCtrlClick ( mouse . X , mouse . Y )
}
if mouse . Mod & tea . ModShift != 0 {
// Shift was held - extend selection
return m , extendSelection ( mouse . X , mouse . Y )
}
}
return m , nil
}
Coordinate System
Mouse coordinates are zero-based with (0, 0) at the top-left corner:
// The X and Y coordinates are zero-based, with (0,0) being the upper left
// corner of the terminal.
switch msg := msg .( type ) {
case tea . MouseMsg :
m := msg . Mouse ()
fmt . Println ( "Mouse event:" , m . X , m . Y , m )
}
Coordinates are relative to the terminal window, not your view. If you’re rendering content with offsets, you’ll need to adjust coordinates accordingly.
Best Practices
Choose the Right Mouse Mode
Use MouseModeClickOnly for buttons and simple interactions
Use MouseModeButtonMotion for drag operations
Use MouseModeAllMotion only when you need hover effects
Provide Keyboard Alternatives
Always provide keyboard shortcuts for mouse actions: case tea . KeyPressMsg :
switch msg . String () {
case "enter" :
return m , handleClick ( m . selectedX , m . selectedY )
}
Always validate mouse coordinates against your UI bounds: if mouse . X < 0 || mouse . X >= m . width ||
mouse . Y < 0 || mouse . Y >= m . height {
return m , nil
}
Use the MouseMsg Interface
Catch all mouse events with MouseMsg when you don’t need to distinguish: examples/mouse/main.go:32-34
case tea . MouseMsg :
mouse := msg . Mouse ()
return m , tea . Printf ( "(X: %d , Y: %d ) %s " , mouse . X , mouse . Y , mouse )