Responsive layouts
A responsive layout changes its arrangement as the space changes: one card
per row on a phone, four across a wide desktop window. Bellui’s main tool for
this is layout.ResponsiveLayout.
Size classes
The responsive layout sorts the width it is given into one of five size
classes. The default boundaries, in
layout.DefaultResponsiveBreakpoints,
follow the Material Design 3 window size classes:
| Class | Width |
|---|---|
| 1. compact | below 600 |
| 2. medium | 600 to below 840 |
| 3. expanded | 840 to below 1200 |
| 4. large | 1200 to below 1600 |
| 5. extra-large | 1600 and above |
The width is the container’s width, not the window’s. A responsive layout inside one pane of a split responds to that pane.
Give each child a width per class
Wrap each child with
layout.Responsive(object, ratios...).
Each ratio is the share of the full width the child takes in one size class,
in class order. Then put the children in
layout.NewResponsiveLayout:
var cards []core.CanvasObject
for i := 1; i <= 4; i++ {
// Width ratios per size class: compact, medium, expanded.
// Classes after the last ratio given reuse it.
cards = append(cards, layout.Responsive(card(fmt.Sprintf("Card %d", i)), 1, 0.5, 0.25))
}
// A plain object takes a full row in every size class.
cards = append(cards, card("Full-width footer"))
grid := layout.NewResponsiveLayout(cards...)Here each card is full width when compact, half width when medium, and a quarter width when expanded. The large and extra-large classes have no ratio given, so they reuse the last one, a quarter.
The layout fills rows from left to right. When the next child’s ratio would take the row past the full width, it starts a new row. Each row is as tall as its tallest child.
Rules for the ratios:
- A ratio must be greater than 0 and at most 1. Other values are logged and treated as 1.
- A class with no ratio uses the previous class’s ratio. With no ratios at all, the child is full width in every class.
- A child that is not wrapped with
Responsivetakes a full row in every class. - Ratios beyond the fifth are ignored, with a logged error.
NewResponsiveLayout returns a container, not a layout. Put it wherever a
canvas object goes. On a long page, put it in a
vertical scroll so the rows can grow downwards.
Your own breakpoints
Set the Breakpoints field to use different boundaries, such as Bootstrap’s.
Each value is the upper bound of one class; widths at or past the last value
fall in the class after it.
// Use your own size-class boundaries instead of the defaults.
grid.Layout.(*layout.ResponsiveLayout).Breakpoints = []float32{576, 768, 992, 1200}A ratio configuration always has five entries. With fewer than four breakpoints the extra entries are unused; with more, the last one repeats.
The gallery app’s Responsive page shows the classes live: it labels the current width and class as you resize the window.
Adaptive grid
container.NewAdaptiveGrid(n, ...)
is a grid of n columns when the device is in landscape orientation and n
rows when it is in portrait. On a phone or tablet it changes as the device
rotates.
// Three columns while the device is landscape, three rows in portrait.
adaptive := container.NewAdaptiveGrid(3, card("A"), card("B"), card("C"))n columns there, however you shape the window. To respond to window
width on desktop, use the responsive layout.Row wrap
layout.NewRowWrapLayout()
places children left to right at their own minimum width, and starts a new
row when the next one does not fit. It suits items of similar height and
different widths, such as tags or chips. Unlike
GridWrap, it does not force every child to one
size.
// Each child keeps its own width; rows wrap when the width runs out.
tags := container.New(layout.NewRowWrapLayout())
for _, t := range []string{"go", "ui", "desktop", "mobile", "web", "webgpu", "layout"} {
tags.Add(widget.NewButton(t, func() {}))
}NewRowWrapLayoutWithCustomPadding(h, v) sets the gaps between items and
between rows.
Every row is as tall as the tallest child. Until the layout has run once, its minimum height assumes one child per row; after that it reports the height of the rows it actually made.
The whole program
// A dashboard that reflows with the window width: one card per row on a
// narrow window, two on a medium one, four on a wide one.
package main
import (
"fmt"
"image/color"
"github.com/belluikit/bellui"
"github.com/belluikit/bellui/app"
"github.com/belluikit/bellui/canvas"
"github.com/belluikit/bellui/container"
"github.com/belluikit/bellui/core"
"github.com/belluikit/bellui/geo"
"github.com/belluikit/bellui/layout"
"github.com/belluikit/bellui/widget"
)
func card(title string) core.CanvasObject {
bg := canvas.NewRectangle(color.NRGBA{R: 0x3b, G: 0x6e, B: 0xc8, A: 0x50})
bg.CornerRadius = 8
bg.SetMinSize(geo.NewSize(0, 80))
return container.NewStack(bg, container.NewCenter(widget.NewLabel(title)))
}
func main() {
a := app.NewWithID("com.example.responsive")
w := a.NewWindow("Responsive")
var cards []core.CanvasObject
for i := 1; i <= 4; i++ {
// Width ratios per size class: compact, medium, expanded.
// Classes after the last ratio given reuse it.
cards = append(cards, layout.Responsive(card(fmt.Sprintf("Card %d", i)), 1, 0.5, 0.25))
}
// A plain object takes a full row in every size class.
cards = append(cards, card("Full-width footer"))
grid := layout.NewResponsiveLayout(cards...)
// Use your own size-class boundaries instead of the defaults.
grid.Layout.(*layout.ResponsiveLayout).Breakpoints = []float32{576, 768, 992, 1200}
// Three columns while the device is landscape, three rows in portrait.
adaptive := container.NewAdaptiveGrid(3, card("A"), card("B"), card("C"))
// Each child keeps its own width; rows wrap when the width runs out.
tags := container.New(layout.NewRowWrapLayout())
for _, t := range []string{"go", "ui", "desktop", "mobile", "web", "webgpu", "layout"} {
tags.Add(widget.NewButton(t, func() {}))
}
w.SetContent(container.NewVScroll(container.NewVBox(grid, adaptive, tags)))
w.Resize(geo.NewSize(900, 400))
bellui.ShowAndRun(w)
}