Buttons

July 9, 2026 · View on GitHub

Buttons are how users tell your app to do things. Proton has four kinds, plus clickable links and a way to make literally anything tappable.


The One Rule

Every button needs its own proton.Clickable field in your state struct.

type UI struct {
    save   proton.Clickable
    cancel proton.Clickable
    delete proton.Clickable
}

Don't share one between two buttons. If you do, clicking either one fires both — which is a fun bug to debug and a terrible UX.

Also, buttons must be inside a layout wrapper (Pad, Row, Column, etc.) for clicks to register. See Getting Started for why.


Button

Filled, solid, primary action. Use this for the thing you most want the user to click.

var save proton.Clickable

proton.Pad(ctx, 8, func(ctx proton.Context) {
    if proton.Button(ctx, &save, "Save") {
        doSave()
    }
})

Returns true on the frame it gets clicked. One click, one true. It doesn't keep firing while held down.

proton.Button(ctx proton.Context, state *proton.Clickable, label string) bool

OutlineButton

Ghost/outline style. Same behavior as Button but without the filled background. Use it for secondary actions — things the user might want to do, but that aren't the primary action.

var save   proton.Clickable
var cancel proton.Clickable

proton.Row(ctx,
    func(ctx proton.Context) {
        proton.Pad(ctx, 4, func(ctx proton.Context) {
            if proton.OutlineButton(ctx, &cancel, "Cancel") {
                handleCancel()
            }
        })
    },
    func(ctx proton.Context) { proton.Gap(ctx, 8) },
    func(ctx proton.Context) {
        proton.Pad(ctx, 4, func(ctx proton.Context) {
            if proton.Button(ctx, &save, "Save") {
                handleSave()
            }
        })
    },
)

The visual hierarchy here — outline for Cancel, filled for Save — tells users which one is the primary action without a single word of explanation.

proton.OutlineButton(ctx proton.Context, state *proton.Clickable, label string) bool

IconButton

An icon-only button. No text, just an icon. Common in toolbars.

// icon is a *proton.Icon — load one with widget.NewIcon() from gioui.org/widget
var closeBtn proton.Clickable

if proton.IconButton(ctx, &closeBtn, closeIcon, "Close window") {
    win.Close()
}

The fourth argument is the accessibility description — what a screen reader would say. Don't skip it.

proton.IconButton(ctx proton.Context, state *proton.Clickable, icon *proton.Icon, desc string) bool

Tappable

Makes any content clickable. The entire area you draw inside the callback becomes the hit target. Use it for cards, list rows, custom buttons, or anything where a standard button label isn't what you want.

var rowClick proton.Clickable

if proton.Tappable(ctx, &rowClick, func(ctx proton.Context) {
    proton.Card(ctx, proton.RGB(0x2a2a3e), 8, 12, func(ctx proton.Context) {
        proton.Label(ctx, "Click anywhere on this card")
        proton.Gap(ctx, 4)
        proton.Muted(ctx, "The whole thing is a button")
    })
}) {
    println("card clicked")
}
proton.Tappable(ctx proton.Context, state *proton.Clickable, content func(proton.Context)) bool

Underlined clickable text styled like a hyperlink. Handle the click yourself — Proton doesn't open URLs for you, it just tells you the user clicked.

var githubLink proton.Clickable

if proton.Link(ctx, &githubLink, "View on GitHub") {
    openBrowser("https://github.com/CzaxStudio/proton")
}

LinkSmall is the same thing but uses caption-sized text:

var termsLink proton.Clickable

if proton.LinkSmall(ctx, &termsLink, "Terms of service") {
    showTerms()
}
proton.Link(ctx proton.Context, state *proton.Clickable, text string) bool
proton.LinkSmall(ctx proton.Context, state *proton.Clickable, text string) bool

Common Patterns

Confirm / Cancel row (right-aligned)

type UI struct {
    save   proton.Clickable
    cancel proton.Clickable
}

proton.RowEnd(ctx,
    func(ctx proton.Context) {
        proton.Pad(ctx, 4, func(ctx proton.Context) {
            if proton.OutlineButton(ctx, &u.cancel, "Cancel") {
                handleCancel()
            }
        })
    },
    func(ctx proton.Context) { proton.Gap(ctx, 8) },
    func(ctx proton.Context) {
        proton.Pad(ctx, 4, func(ctx proton.Context) {
            if proton.Button(ctx, &u.save, "Save changes") {
                handleSave()
            }
        })
    },
)

RowEnd pushes everything to the right edge — standard placement for confirm/cancel pairs.

Toolbar

type UI struct {
    newFile  proton.Clickable
    openFile proton.Clickable
    saveFile proton.Clickable
}

proton.Row(ctx,
    func(ctx proton.Context) {
        proton.Pad(ctx, 4, func(ctx proton.Context) {
            if proton.Button(ctx, &u.newFile, "New") { handleNew() }
        })
    },
    func(ctx proton.Context) { proton.Gap(ctx, 4) },
    func(ctx proton.Context) {
        proton.Pad(ctx, 4, func(ctx proton.Context) {
            if proton.OutlineButton(ctx, &u.openFile, "Open") { handleOpen() }
        })
    },
    func(ctx proton.Context) { proton.Gap(ctx, 4) },
    func(ctx proton.Context) {
        proton.Pad(ctx, 4, func(ctx proton.Context) {
            if proton.OutlineButton(ctx, &u.saveFile, "Save") { handleSave() }
        })
    },
)

Clickable list rows

type UI struct {
    rows   [100]proton.Clickable
    chosen int
}

items := []string{"Alpha", "Beta", "Gamma", "Delta"}

proton.List(ctx, &u.scroll, len(items), func(ctx proton.Context, i int) {
    if proton.Tappable(ctx, &u.rows[i], func(ctx proton.Context) {
        proton.PadV(ctx, 10, func(ctx proton.Context) {
            proton.PadH(ctx, 12, func(ctx proton.Context) {
                proton.Label(ctx, items[i])
            })
        })
    }) {
        u.chosen = i
    }
    proton.Divider(ctx)
})