lotusui the ikaito design language for Gio — desktop, mobile, web
GitHub

Changelog

Every release, every API change — written so an agent can migrate from it alone.

Changelog

This changelog is written for the AI agents that develop the apps consuming lotusui. From the next release on, it records every API-visible change — created, updated, renamed, removed — with exact symbols, old→new forms, and replacement guidance, precise enough to migrate a consuming app from this document alone. There is deliberately no migrate tool: read the entries for the versions you crossed, apply them, then run go build ./... until clean — the compiler is the safety net. lotusui update prints the relevant sections when upgrading vendored components.

Format: Keep a Changelog sections, SemVer versions (Go modules enforce it). Until v1.0.0 breaking changes may land in minor versions; each will be recorded here in full.

[Unreleased]

Nothing yet.

[0.5.1] - 2026-08-04

Fixed

Symbol Kind Notes
HoverCard multi-site behavior Calling Layout multiple times on one HoverCard in a frame (shared tip across every "GB" in a table) used one event tag and painted a floating card at every call site while open — stacked identical panels. Each call is now a distinct trigger site; only the hovered site paints.
Tooltip multi-site behavior Same shared-Layout stacking footgun as HoverCard; only the hovered site paints.
DropdownMenuSub multi-site behavior Same: only the hovered submenu site paints the side panel.
DropdownMenuTrigger multi-site behavior Same: each Layout call is a distinct trigger button; only the active site paints the floating menu.
Popover multi-site behavior Same: while Open, only the first Layout call in a frame paints the panel.
Select multi-site behavior Same: while open, only the first Layout call in a frame paints the options panel.

[0.5.0] - 2026-08-04

Added

Symbol Kind Notes
HoverCard type Hover-preview card on the portal layer (shadcn Hover Card): Layout(th, gtx, content, trigger). Stays open while the pointer is over trigger or card.
HoverCardSide enum HoverCardBottom (default), HoverCardTop, HoverCardLeft, HoverCardRight.
HoverCard.Side field Where the card floats relative to the trigger.
HoverCard.Align field PopoverAlign along the side (PopoverStart / Center / End).
HoverCard.OpenDelay / CloseDelay field Zero keeps Radix defaults (700ms / 300ms).
HoverCard.Width field Card width; zero means 320dp (shadcn w-80).
HoverCard.Disabled field Prevents opening (chakra).
DropdownMenuTrigger.Icon field Optional icon on the trigger; empty label + Icon = icon-only (Breadcrumb ellipsis → menu).
DropdownMenuTrigger.Size field Shared size preset for the trigger button.

Fixed

Symbol Kind Notes
Tooltip background paint Inverted chrome fill used Fill (paints Constraints.Min, often 0×0 in the floating layer) so only FgInverted ink showed; now paint.Fills the clipped label rect like Popover/Select.
Breadcrumb demo ellipsis docs Usage demo now matches shadcn breadcrumb-demo.tsx: the is a ghost icon DropdownMenuTrigger that opens Documentation / Themes / GitHub.

[0.4.0] - 2026-08-04

Added

Symbol Kind Notes
SelectOption.Meta field Optional secondary text on the far right of an option row (and the closed trigger). Empty omits it. The selected check still sits after Meta.

Changed

Symbol Kind Notes
Select Meta spacing layout Meta keeps 8dp left of the trailing chevron/check (was flush on the closed trigger; 4dp on option rows).
Select option gap layout Open panel options: Space.XS from the panel’s top/bottom, half of that between rows; hover/selected pills span the full panel width (horizontal panel inset removed — content padding stays on HoverRow).
DropdownMenu item gap layout Same vertical rhythm and full-width rows as Select.
Button disabled color paint Disabled walks a brighter step of the button's own scale (Soft/Accent → Subtle; Color solids → .200 fill / .600 ink; outline/ghost/link → lighter ink) — never a darker press-step. Cursor drops while disabled/loading.
Checkbox / RadioGroup / Switch / Slider disabled color paint Colored on/checked/fill states mute brighter along BrandScale / BrandSubtle (not darker, not FgDisabled grey). TabsLine honors Disabled (label + bar + cursor).

[0.3.0] - 2026-08-03

One root per concept. Two different ideas were both spelled Opts: a component's per-call prop bag (ButtonOpts) and the constructor for a list of choices (Opts, RadioOpts, TabOpts). The collision forced an exception the moment both existed on one component — Toggle's list constructor had to be called ToggleOptions because ToggleOpts was already its prop bag.

The prop bags become XxxProps, which frees the Opt* root entirely for the choice family. The rule is now one sentence: Props is how to render, Option/Options/Opts is what to choose from. Props is also the word the docs and the web component vocabulary this library follows already use.

This is a mechanical rename — no behaviour changes, no field changes. Your compiler finds every site.

Changed — breaking

Old New Migration
ButtonOpts ButtonProps rename the type; fields unchanged
BadgeOpts BadgeProps "
CardOpts CardProps "
AlertOpts AlertProps "
AlertDialogOpts AlertDialogProps "
AvatarOpts AvatarProps "
AvatarGroupOpts AvatarGroupProps "
ButtonGroupOpts ButtonGroupProps "
FieldOpts FieldProps "
TableOpts TableProps "
ToggleOpts (the prop bag) ToggleProps " — and note ToggleOpts now names the option constructor below
ButtonGroupItem.Opts ButtonGroupItem.Props the field holds a ButtonProps, so it is Props too
Opts(labels …string) SelectOpts(labels …string) the bare name never said which component's options it built; now the four are parallel
ToggleOptions(labels …string) ToggleOpts(labels …string) the exception is gone — ToggleOpts is free now that the prop bag is ToggleProps

After this, every choice component reads the same: SelectOpts / RadioOpts / TabOpts / ToggleOpts build []SelectOption / []RadioOption / []TabOption / []ToggleOption into an Options field, addressed by Value() / SetValue().

[0.2.0] - 2026-08-03

Every choice component now speaks VALUES instead of indexesSelect, RadioGroup, Tabs and ToggleGroup, on HTML's <option value="…"> model. An option index is a fact about one list's current order, so it must never reach app state, storage or a wire; each component's cursor is therefore unexported and callers read/write Value()/SetValue(). Reordering or rewording an option list can no longer change what stored data means.

The contract is shared as BEHAVIOUR, not as one type: each component keeps its own option struct carrying only what it renders (SelectOption, RadioOption, TabOption, ToggleOption), each with a label-only constructor. In every one of them:

  • Val() is the stored value: Value, or Label when Value is empty. Icon-only options must carry an explicit Value — an empty Label has no value to store.
  • The component exposes Value() / SetValue(v) / Clear() / Chosen(); ToggleGroup adds Values() / SetValues(vs) for its multi-select mode.
  • The zero value selects the FIRST option.
  • SetValue with an UNKNOWN value clears the choice rather than falling back to option 0, so a stored value that no longer exists can never silently mean something else.

Every index-aligned []bool / []string sidecar disappears into the option struct: what used to be three parallel slices that could fall out of step is now one list of options.

Added

Symbol Notes
SelectOption struct{ Label, Value string } — one choice: what the user reads, what the app stores. Empty Value means the Label IS the value (HTML's rule)
SelectOption.Val() the stored value: Value, or Label when Value is empty
Opts(labels ...string) []SelectOption shorthand for label-only lists (Opts("10","25","50")) — use explicit Values whenever the label is prose that might get reworded
Select.Value() string the chosen option's value; "" when nothing is chosen
Select.SetValue(v string) choose the option carrying v; an UNKNOWN value clears the choice (a stored value that no longer exists shows the placeholder instead of silently meaning option 0)
Select.Clear() drop back to the placeholder state (was: Selected = -1)
Select.Chosen() bool whether an option is currently selected
RadioOption struct{ Label, Value, Description string; Disabled bool } — one radio choice, carrying what RadioGroup used to keep in parallel slices
RadioOpts(labels ...string) []RadioOption label-only shorthand
RadioGroup.Value/SetValue/Clear/Chosen the choice contract, as on Select
TabOption struct{ Label, Value, Icon string; Disabled bool } — one tab
TabOpts(labels ...string) []TabOption label-only shorthand
Tabs.Value/SetValue/Clear/Chosen the choice contract, as on Select
ToggleOption struct{ Label, Value, Icon string; Content layout.Widget } — one toggle of a group
ToggleOptions(labels ...string) []ToggleOption label-only shorthand. It carries the LONGER name because ToggleOpts is already the Toggle widget's per-call options struct (the ButtonOpts/BadgeOpts convention) — Opts, RadioOpts and TabOpts are its siblings
ToggleGroup.Value/SetValue/Clear/Chosen single-select choice
ToggleGroup.Values() []string / SetValues([]string) multi-select choices, in Options order; unknown values ignored
IconLock mono fluent:lock-closed-24-regular ("lock") — the edit pen's sibling for LOCKED rows: rendered where the pencil would sit when a row is read-only by construction

New component families:

Symbol Notes
ButtonGroup(th, ButtonGroupOpts, …ButtonGroupItem) attached controls: neighbours overlap 1dp so their borders collapse to one line, inner corners square, only the group's outer corners rounded
ButtonGroupOpts{Vertical} stack the group top-to-bottom
ButtonGroupItem{Btn, Label, Opts, Separator, Widget, Flex} a button slot, the hairline seam, or any widget (an Input) with flexed weight
ButtonGroupSeparator() ready-made separator item
AttachedEdges{Start, End, Top, Bottom}, ButtonOpts.Attached squares the corners on attached sides and drops the seat shadow; ButtonGroup sets it for its children
InputOTP{Length, Groups, Filter, Disabled, Invalid} one-time-code slots driven by ONE hidden editor — click anywhere, type or paste
InputOTP.Value() / SetValue(s) the code typed so far
Kbd(th, key) keyboard-cap hint ("⌘K"), for Input End slots and prose
DropdownMenuSub, .Item(th, label, items…) nested submenu: a chevron row whose side panel opens while the pointer rests on the row or the panel

New capabilities on existing components:

Symbol Notes
AvatarOpts.Badge, AvatarBadge{Color, Icon} status dot on the rim, ringed in the panel background
AvatarGroup(th, AvatarGroupOpts, …AvatarOpts), AvatarGroupOpts{Size, Count, CountIcon} ringed overlap with an optional trailing "+N" bubble
BreadcrumbLink, BreadcrumbPage, BreadcrumbSep(th, icon), BreadcrumbEllipsis the composable trail pieces — custom separators, collapsed depth, a dropdown in the row
BadgeOpts.Start / End arbitrary leading/trailing widgets (a Spinner); the widget owns its colour
DropdownMenuCheckboxItemIcon, DropdownMenuRadioItemIcon gutter rows with a leading icon
DropdownMenuTrigger.Variant, .Align trigger button style (zero = outline); panel edge against the trigger
PopoverAlign (PopoverStart, PopoverCenter, PopoverEnd), Popover.Align which panel edge lines up with the anchor
TooltipSide (TooltipBottom, TooltipTop, TooltipLeft, TooltipRight), Tooltip.Side where the label floats
Slider.Values []float32 multi-thumb mode, kept ordered — two entries is the range slider
Slider.Vertical rotates the axis; the value grows upward
Toast.ID, Toast.Loading, Toaster.Update(id, Toast) the promise pattern: add a loading toast, replace it in place with the outcome
ToastSuccess, ToastInfo, ToastWarning joins ToastDefault / ToastDestructive
ToggleGroup.Outline / Disabled / Vertical / Spacing group-wide chrome, stacked axis, gap override (default 2dp)
ToggleOpts.Content, ToggleGroupItem.Content arbitrary content inside the toggle's chrome (the font-weight selector)
Input.Top / Bottom full-width rows INSIDE the frame, above/below the editor line — the block addons
Accordion.Bordered, AccordionItem.Disabled boxed outline with side padding; dimmed item that ignores clicks
Pagination.Simple numbered links only — no previous/next, no elision
TableOpts.Footer []string a final emphasized row (totals) above the caption
AlertDialogOpts.Size, .Media width preset; a medallion above the title
RadioGroup.Descriptions []string index-aligned muted description under each label
ClampCorner(r, size) int caps a corner radius at half the smaller side — past that a rounded rect grows spurs instead of staying a capsule
IconMail, IconMessage, IconBell, IconCreditCard, IconWallet, IconBuilding, IconShare, IconTrash, IconMoreHorizontal, IconDot, IconPlus, IconSearch, IconMinus, IconArrowLeft, IconArchive, IconClock mono Fluent icons for the new examples

Changed — breaking

Old New Migration
Select.Options []string Select.Options []SelectOption label-only lists: wrap with Opts("a","b"). Lists whose choice is stored: give each option an explicit Value ({Label: "One per environment (recommended)", Value: "per-env"})
SelectGroup.Options []string SelectGroup.Options []SelectOption same as above
Select.Selected int (exported) unexported cursor reading d.Selectedd.Value(); writing d.Selected = id.SetValue("…"); Selected: -1 (placeholder) → d.Clear(). The zero value still selects the first option
RadioGroup.Layout(th, gtx, labels ...string) RadioGroup.Layout(th, gtx) move the labels onto the struct: RadioGroup{Options: lotusui.RadioOpts("Default", "Compact")}, or []RadioOption{{Label: …, Value: …}} when the choice is stored
RadioGroup.Selected int (exported) unexported cursor r.Selectedr.Value(); r.Selected = ir.SetValue("…"); Selected: -1r.Clear()
RadioGroup.Disabled []bool RadioOption.Disabled Disabled: []bool{false, true} → set Disabled: true on the option itself
RadioGroup.Descriptions []string RadioOption.Description Descriptions: []string{"a", "b"} → set Description: on each option. (Added earlier in this same unreleased entry and never tagged, so it is removed outright rather than deprecated)
Tabs.Layout(th, gtx, labels ...string) Tabs.Layout(th, gtx) move the labels onto the struct: Tabs{Options: lotusui.TabOpts("Account", "Password")}
Tabs.Update(gtx, n int) Tabs.Update(gtx) drop the count — the options are on the struct now. The ordering contract is unchanged: Update must still run before anything reads the selection in a frame, and Layout still never processes clicks
Tabs.Sel int (exported) unexported cursor tabs.Seltabs.Value(); tabs.Sel = itabs.SetValue("…"); bodies[tabs.Sel] → switch on tabs.Value()
Tabs.Icons []string TabOption.Icon Icons: []string{a, b} → set Icon: on each option
Tabs.Disabled []bool TabOption.Disabled as RadioGroup above
ToggleGroup.Layout(th, gtx, size, items ...ToggleGroupItem) ToggleGroup.Layout(th, gtx, size) move the items onto the struct as Options []ToggleOption
ToggleGroupItem ToggleOption same fields plus Value; ToggleGroupItem{Icon: X}ToggleOption{Value: "…", Icon: X} — give icon-only options a Value
ToggleGroup.Sel int (exported) unexported cursor g.Selg.Value(); g.Sel = ig.SetValue("…"); Sel: -1g.Clear()
ToggleGroup.On []bool (exported) unexported state g.Ong.Values(); g.On = []bool{…}g.SetValues([]string{…}); if g.On[i]slices.Contains(g.Values(), "…")

Fixed

Event ordering — the class of bug behind all of these: Gio commits a keystroke inside the editor's own layout, so anything read before that call describes the PREVIOUS frame.

Fix Notes
Typed characters appeared one frame late InputOTP read its text before laying the editor out; the editor now runs first (into a macro) and paints last. Input.Transform rewrote the previous frame's text, so a typed "A" flashed before folding to "a" — the rewrite now lands in the same frame, re-laying out only when it actually changes the text
InputOTP focus ring drew a square border over rounded slot corners, and lost a 1dp sliver to the next slot's fill (reading thinner at the junction). It now strokes the slot's exact rounded shape in a pass after every slot
Rounded chrome grew corner spurs when a radius exceeded half the smaller side — fully-rounded Skeleton, small Badge, Progress fills, Kbd, Button, input frames. All clamp through ClampCorner
Separator / VerticalHairline claimed unbounded height a vertical rule inside a row measured at natural height reported the full 1<<20 constraint, so any box containing one grew enormous. It now falls back to the row's Min, or one line-height
Tooltip and Toaster woke every frame while waiting on a clock (the 450ms reveal delay, toast expiry) they invalidated per vsync. Both now schedule a single timed wake-up (op.InvalidateCmd{At:})
InputOTP length and caret the over-length guard rewrote the editor's text; it now uses Gio's Editor.MaxLen, and the active-slot ring follows the editor's real caret (Selection()) rather than the text length, so arrow keys and clicks move it
Digit-only InputOTP on mobile now sets key.HintNumeric, so phones raise the number pad

[0.1.0] - 2026-08-02

The initial release: the ikaito design language for Gio (gioui.org) — desktop, mobile and web from one Go codebase.

Components

Component vocabulary and docs anatomy follow the conventions the web settled on; the color engine is lotusui's own.

  • Button — variants ButtonDefault/Secondary/Destructive/Outline/ Ghost/Link; icon-only and with-icon; Rounded pill form; loading (spinner or LoadingText); disabled; 1dp press nudge; focus ring.
  • BadgeBadgeDefault/Secondary/Destructive/Outline/Ghost; Icon; raw Bg/Fg status pairs.
  • Input — variants outline/subtle/flushed; Filter/Transform input mechanisms; Error; Start/End slots; suffix addon.
  • Field — label, helper, error, required, around any control.
  • Checkbox — indeterminate, invalid, disabled.
  • Switch — animated on the shared clock; invalid, disabled.
  • Select — floating panel on the portal primitive; Groups with labels and separators; check-marked selection; scrollable (7-row cap) opening aligned to the selection; placeholder; invalid.
  • TabsTabsDefault (muted well, raised active tab), TabsLine, TabsSubtle; Vertical; Icons; per-tab Disabled; the explicit Update-before-Sel contract.
  • Dialog — full-window scrim, width presets per size, entrance motion (Appear()), Escape/backdrop dismissal via onClose, corner ✕ (suppress with HideClose).
  • DropdownMenuDropdownMenuItem, DropdownMenuItemIcon, DropdownMenuShortcutItem, DropdownMenuCheckboxItem, DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator.
  • CardCardOutline/Elevated/Subtle, size-scaled padding, equal-height rows via Min.Y; the image and edge-to-edge patterns documented.
  • Accordion, Alert (+Action, Color), AlertDialog, Avatar, Breadcrumb, Pagination, Popover, Progress, RadioGroup, Separator, Skeleton (+SkeletonCircle), Slider, Spinner, Table, Textarea, Toast/Toaster, Toggle/ToggleGroup, Tooltip — each with live per-example docs and an API table.
  • DropdownMenuTrigger — the anchored floating menu (close on selection/outside/Escape; KeepOpen for checkbox/radio menus).
  • One surface grammar: borders never change with elevation; resting surfaces carry the 1dp seat, floating layers the full shadow.
  • Strictly monotonic size ladders 2XS→2XL, pinned by a test.
  • Extensions beyond the usual web catalogs: Grid (spans, auto-flow), SimpleGrid (responsive columns, equal-height rows), Split + VSlide (the carousel motion grammar), Stack (VStack/HStack), ListView (virtualized; Scrollable, HoverRow), plus the page/layout utilities.
  • Floating — the portal primitive (end-of-frame layer on Gio's op.Defer): escapes parent clips, paints above everything, wins hit-testing. Select's panel is built on it.

Sizing and color

  • Shared Size enum: Size2XS…Size2XL, SizeMD default, one meaning across every component.
  • Semantic Palette tokens in paired fill/ink slots; Scheme with interaction steps (.500 base → .600 hover → .700 pressed); ColorScale C50–C900 with ten stock scales and ScaleFrom(anchor); per-instance Color ColorScale on Button/Badge — one anchor in, the whole ladder out. Precedence: Scheme > Color > the variant's role scheme.
  • DefaultDarkPalette — dark mode is a palette: NewTheme(WithPalette(lotusui.DefaultDarkPalette)).
  • Theme construction: NewTheme(WithPalette/WithRadius/WithSpace/ WithTextSize/WithFaces) — everything resolves once, nothing per frame.

Tooling (go run github.com/ikaito-com/lotusui/cmd/lotusui …)

  • add / update — the ownership model: vendor a component's source into the app (exported core identifiers auto-qualified, unexported helpers carried into a CLI-owned companion); update with a true three-way merge (base reconstructed from the Go module cache).
  • registry — generates registry.json, the build-time catalog for the CLI and AI agents (never read by app code at runtime).
  • skills — installs the agent skill files into .claude/skills/.
  • icons — Iconify fetch + normalization + typed constants; theme — palette generation from an anchor or theme.json, with WCAG contrast checks; verify — offline drift checks (icons, theme, API baseline, registry); api — the exported-API baseline; init — app scaffold; release — the version ritual.
  • First block: login-form (registry/blocks/login).

Every demo on this site is the real Go component compiled to WebAssembly — one gallery app, one bundle, addressed by URL hash.