Skip to content
Responsive layouts

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:

ClassWidth
1. compactbelow 600
2. medium600 to below 840
3. expanded840 to below 1200
4. large1200 to below 1600
5. extra-large1600 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 Responsive takes 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"))
On desktop the device always reports landscape, so an adaptive grid is always 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)
}