Skip to content

API reference: package kit

import "github.com/kuronosu/tela/kit"

This page is generated from the doc comments of the package; the same text is on pkg.go.dev. For guides and examples, start at the wiki home.

Package kit provides styled components for Tela apps: a button, a card, a text field, in the look of shadcn/ui (style “new-york-v4”, at the registry commit ShadcnCommit), built on the behavior hooks of package aria and the classes of package h. It is the layer an app is written with; reach for aria and h when a component of the kit does not fit.

Every component is a struct component (ARCHITECTURE ADR-088): its props are the fields of the struct, and Build returns its element.

kit.Button{Variant: kit.Outline, Size: kit.Sm, OnClick: save, Children: h.Text("Save")}

Every component has the same few fields, with the same meaning:

  • Class is added to the classes of the root element after the component’s own, and Tela’s class order is last-wins, so Class overrides the look with no merging: Class: “w-full” widens a Card, Class: “rounded-full” rounds a Button.
  • Props is a tela.Child with more props for the root element, such as h.Key, h.TestID or h.OnMouseEnter(…). It takes one value: merge several with tela.MergeProps(h.TestID(“save”), h.OnMouseEnter(warm)).
  • Children is a tela.Component, the content of the component. A component with several parts has a slot for each as a tela.Component field (kit.Card has Title, Description, Action, Children and Footer); nil leaves a slot out. A slot that is only text for assistive technologies, such as a Field’s Label, Description and Error, is a string. The Class of a part is named after it: HeaderClass, FooterClass.
  • Behavior fields are named after the HTML or React prop they stand for: Disabled, OnClick, Type, Placeholder, Name, ID, ReadOnly, AutoFocus, MaxLength. Variant and Size choose among the looks of the recipe.
  • Icons are slots: every icon a component draws is a tela.Icon field (Button.Icon and IconEnd, Badge.Icon), so an app uses the icon set it likes, such as icons/lucide. Where shadcn draws an icon itself, the field has Lucide’s as its default (the X of Dialog.CloseIcon).

Because a struct component compares its fields to skip a build, a component given a func (OnClick) or a slice builds on every parent build; that is cheap, and tela.Callback makes it skippable when it matters.

A component that has a value follows one rule (ADR-101), whatever the value is. Give a pointer to your state to control it, a default to leave it to the component, and a callback to be told of changes:

name, setName := tela.UseState("")
kit.Input{Value: &name, OnValueChange: setName.Set} // controlled
kit.Input{DefaultValue: "Ana"} // uncontrolled

The same names recur: Value/DefaultValue/OnValueChange for a string, Checked (*bool)/DefaultChecked/OnCheckedChange for an on/off control, Open (*bool)/DefaultOpen/OnOpenChange for what opens and closes. A control that holds a choice among keys takes Value *K, DefaultValue *K and OnValueChange func(K).

A DefaultValue is a pointer when the zero value of its type is a valid value, so that “no default” and “the zero value” differ: a choice among keys (the int 0 can be a tab), a slider (0 can be where it starts) and a color (the zero Color is transparent black) take new(…) of the value (kit.Tabs[int]{DefaultValue: new(0)}), and nil leaves the choice to the component. Where the zero value means “none” it stays a value: a string, an aria.Date, an aria.DateRange.

1. Theme tokens. Theme is shadcn’s neutral theme with a 10 px radius; change its colors and every component follows (bg-primary, text-muted-foreground and the other classes of the recipes are tokens):

theme := kit.Theme()
theme.Colors["primary"] = tela.SchemeColor{Light: tela.Hex("#4f46e5"), Dark: tela.Hex("#818cf8")}
tela.Main(tela.App{Title: "Notes", Root: Notes{}, Theme: &theme,
RootClass: "bg-background text-foreground"})

2. Recipes. The whole look of a component is data: a Style has one struct of class strings per component (ButtonStyle, CardStyle…), with a Root for the root element and a field for each other part, variant and size. DefaultStyle returns a copy of the defaults to edit and StyleProvider applies the edited one to a subtree, as h.Scheme does for the color scheme:

s := kit.DefaultStyle()
s.Card.Root += " backdrop-blur-xl"
s.Button.Variants["glass"] = "bg-white/10 text-white hover:bg-white/20"
return kit.StyleProvider{Style: &s, Children: Dashboard{}}

A Variant or Size is valid when the active recipe defines it, so “glass” above is a variant of every Button under the provider. One the recipe does not define panics in dev mode, naming the ones it does and the closest, and uses the default look otherwise. Components of your own read the active style with UseStyle. Build a style once and do not edit it after giving it to a provider.

3. Per instance. Class, the Class of a part, Props and the icon slots change one component:

kit.Card{Class: "w-full max-w-sm", HeaderClass: "pb-6 border-b", TitleLevel: 2, Title: h.Text("Billing")}

Button, Badge, Card, Avatar, Label, Input, Textarea, Separator, Skeleton, Field, FieldSet, Select, Tabs, Table and Toaster; the overlays Dialog, AlertDialog, Popover and Tooltip; the menus DropdownMenu, Menubar and ContextMenu; and the controls that choose: Checkbox, Switch, RadioGroup, Toggle, ToggleGroup (the segmented control), Slider and Progress; and the dates: Calendar, RangeCalendar, DatePicker and DateRangePicker; and the TitleBar and WindowControls of a frameless window (below). A ColorPicker chooses a tela.Color with an area for the saturation and brightness, a hue slider, an optional alpha slider, a hex field and optional swatches; a ColorInput is the same behind a button that shows the color (the look of both is the kit’s own, shadcn/ui has no color picker):

kit.ColorPicker{Value: &color, OnValueChange: setColor.Set, Alpha: true,
Swatches: []tela.Color{tela.Hex("#ef4444"), tela.Hex("#22c55e")}}
kit.ColorInput{DefaultValue: new(tela.Hex("#6366f1")), OnValueCommit: save}

A Table shows rows of any type through columns, sorts by a header (aria-sort, Enter and Space) and keeps its header and leading columns in place when it scrolls:

kit.Table[Invoice]{Rows: invoices, RowKey: func(i Invoice) any { return i.ID },
Columns: []kit.Column[Invoice]{
{Header: "Invoice", Cell: func(i Invoice) tela.Node { return h.Text(i.ID) }, Sticky: true, Width: "120px",
Sort: func(a, b Invoice) int { return strings.Compare(a.ID, b.ID) }},
{Header: "Amount", Align: kit.AlignRight, Cell: showAmount, Sort: byAmount},
}}

A Calendar chooses a date and a RangeCalendar a span (the aria.Date and aria.DateRange of package aria, comparable days with no time or zone; the zero value is none), with limits (Min, Max, IsDateUnavailable), several months and the names, digits, first weekday and direction of the locale. A DatePicker and a DateRangePicker are shadcn’s date picker: an outline button showing the date that opens a popover with the calendar and closes when the date, or the range, is chosen:

day, setDay := tela.UseState(aria.Date{})
kit.DatePicker{Value: &day, OnValueChange: setDay.Set, Min: aria.Today()}

A Field is the usual way to put a label, a control, help text and an error together: it ties them with an id, so the label names the control and clicking it focuses the control, and the control is described by the error or the help text. Input, Textarea, Select, DatePicker, DateRangePicker, ColorInput, Checkbox and Switch take the field’s id; a ColorPicker gives it to its hex field.

kit.Field{Label: "Email", Description: "We never share it.", Error: errs.Email,
Children: kit.Input{Type: "email", Value: &email, OnValueChange: setEmail.Set}}

A horizontal Field puts the control first; ControlAfter puts it after the text, as in a row of switches. A FieldSet captions a group of controls with a legend and names a RadioGroup or ToggleGroup inside it:

kit.FieldSet{Legend: "Theme", Children: kit.RadioGroup[string]{Items: themes}}

A window that draws its own title bar (tela.Window and tela.App with Frameless, or a Wayland desktop without server-side decorations) uses a TitleBar: a drag area with a title, slots at the start, in the middle and at the end, and the WindowControls (minimize, maximize or restore, close) at the side of the platform, in the look of Windows, GNOME or macOS (a Variant: Windows, GNOME, MacOS; the platform’s by default). The OS handles drag and double click; buttons and fields in the slots stay clickable. The bar dims while the window is not focused and drops its outline while it is maximized.

kit.TitleBar{
Title: h.Text("Notes"),
Leading: kit.Button{Variant: kit.Ghost, Size: kit.IconSm, Icon: lucide.Menu, AriaLabel: "Menu"},
Trailing: kit.Button{Variant: kit.Ghost, Size: kit.IconSm, Icon: lucide.Settings, AriaLabel: "Settings"},
}

The recipes of the kit are written with logical utilities (ps-*, pe-*, start-*, end-*, ms-*, me-*, rounded-s-*, rounded-e-*, text-start, text-end) and the rtl: variant, which are the physical ones in left-to-right and mirror under h.Dir(“rtl”), so a kit page needs nothing but the direction to be mirrored: menu check marks and shortcuts, a select’s items, a dialog’s close button, a switch’s thumb, a table’s sticky columns, a progress bar’s fill. What stays physical is a position that names a side (a Toaster’s ToastBottomRight, a Popover’s Placement “right”), as sonner’s and Floating UI’s do; an Align of “start” follows the direction.

The components whose arrow keys or drag follow the direction (Slider, Tabs, ToggleGroup, RadioGroup, Menubar, ColorPicker, ColorInput and the calendars) read it at their position with tela.UseDirection, so an h.Dir around the app is all a right-to-left page needs: in it ArrowLeft goes to the next item and a slider’s minimum is at the right.

f := i18n.UseFormatter()
return h.Div(h.Dir(f.Dir().String()), App{})

Each of them also has a Direction field (“ltr” or “rtl”), which wins over the direction at its position and sets h.Dir on its root, for a component whose direction differs from its surroundings (a calendar has its Locale too).

A Dialog, AlertDialog, Popover and Tooltip draw in a Portal over the window, exist only while they are open (so what is inside them starts afresh each time) and enter and leave with a transition. Each has a Trigger slot, the element that opens it, and the Open (*bool), DefaultOpen and OnOpenChange of the controlled and uncontrolled rule.

kit.Dialog{
Trigger: kit.Button{Variant: kit.Outline, Children: h.Text("Edit profile")},
Title: h.Text("Edit profile"),
Description: h.Text("Make changes to your profile here."),
Children: form,
Footer: kit.Button{Children: h.Text("Save changes")},
}

One rule makes every trigger slot of the kit work, Radix’s asChild: the overlay needs to put props on the element that opens it (the press, the aria-expanded and aria-controls, the ref it is placed against, the hover of a tooltip), and it does not wrap the trigger in an element of its own. A trigger slot is a Trigger, a component with one method, WithTriggerProps(tela.Prop) tela.Component, that merges the props into its root element. Button is a Trigger, as are the kit components that say so; a component of your own becomes one by adding the method; and any other element is made one with TriggerFunc, which hands you the props to put where you like:

kit.Tooltip{
Trigger: kit.TriggerFunc(func(p tela.Prop) tela.Node {
return h.Button(p, h.Class("rounded-full"), h.Text("Docs"))
}),
Content: h.Text("Open the manual"),
}

A dialog is modal: the page is inert, Tab stays inside, Escape and a press on the scrim close it, and the focus returns to the trigger. An alert dialog is the same for a question that needs an answer, and is not closed by a press outside. A popover is not modal and closes when the focus or a press goes elsewhere. A tooltip opens on hover or keyboard focus and describes its trigger; the tooltips of a window are a group, so after one opened the next opens at once.

A Select and a Tabs take their items as slices of structs, generic over the key type, and follow the value rule:

kit.Select[string]{Placeholder: "Select a fruit", Items: []kit.SelectItem[string]{
{Value: "apple", Label: "Apple"}, {Value: "banana", Label: "Banana"}}}
kit.Tabs[string]{Tabs: []kit.Tab[string]{
{Value: "account", Label: "Account", Content: AccountForm{}},
{Value: "password", Label: "Password", Content: PasswordForm{}}}}

Toasts have no component to place: a Toaster goes once around the app, and any component below it gets a handle with UseToast and shows toasts with it, from a handler or from any goroutine (the handle is comparable and every method is safe to call from any goroutine; there is no global):

tela.Main(tela.App{Title: "Notes", Root: kit.Toaster{Children: Notes{}}})
toast := kit.UseToast()
id := toast.Loading("Uploading…")
go func() { upload(); toast.Success("Uploaded", kit.ToastReplace(id)) }()

A DropdownMenu (opens from a button), a Menubar (a row of menus) and a ContextMenu (opens on a right click inside an area) are built on the same model of items, MenuItem, and draw the same panel. A menu is a slice of items of a kind: a plain item (a label, an icon, a shortcut that is only shown, a destructive variant, OnSelect), a checkbox item, a radio group, a label, a separator or a group; every item that can be chosen has a Key, and the menu’s OnSelect reports the key of a plain item:

kit.DropdownMenu[string]{
Trigger: kit.Button{Variant: kit.Outline, Children: h.Text("Account")},
Align: "start",
Items: []kit.MenuItem[string]{
{Kind: kit.MenuLabel, Label: "My account"},
{Key: "profile", Label: "Profile", Shortcut: "⇧⌘P"},
{Kind: kit.MenuCheckbox, Key: "bars", Label: "Status bar", DefaultChecked: true},
{Kind: kit.MenuSeparator},
{Key: "logout", Label: "Log out", Variant: kit.Destructive},
},
OnSelect: func(key string) { run(key) },
}

The checkbox items and radio groups follow the value rule (Checked and OnCheckedChange, Value and OnValueChange, or a default), and an uncontrolled state is kept by the component, so it outlives the menu closing. Choosing an item closes the menu, as Radix’s does, unless the item has KeepOpen. There are no submenus yet: a menu is one level (ADR-141).

The class strings are shadcn/ui’s own, from the pinned commit ShadcnCommit, with the translations Tela needs: data-[state=…]: is checked: or an aria-*: variant, [&_svg]: classes are on the icon slot, a has-[>svg]: padding is a Go condition, and a border with no color (shadcn sets the border color of every element in its base layer) is border-border. Every component has a golden image in light and dark at scales 1 and 2 in testdata, and a page in testdata/chromium written with shadcn’s markup, rendered by Chromium with Tailwind 4.3.3, which the tests compare with Tela’s rendering: boxes to a few hundredths of a pixel and the pixels to the anti-aliasing of text. The color picker has no shadcn original: its gradients are compared with Chromium’s CSS gradients of the same colors, and the rest is checked by goldens and behavior tests.

const NoOffset float32 = -1

NoOffset as the Offset of a Popover puts the panel against its trigger: the zero value stands for shadcn’s default of 4 px.

const ShadcnCommit = "617134d5810bba521803b3ed787190bacbb80b35"

ShadcnCommit is the commit of github.com/shadcn-ui/ui (branch main) whose registry style “new-york-v4” (apps/v4/registry/new-york-v4/ui) the class strings of this package were ported from, and that the Chromium conformance pages in testdata/chromium are written against. Every component of the package follows this one commit (ADR-130).

const ToastForever time.Duration = -1

ToastForever as a duration keeps a toast until it is dismissed.

func Theme() tela.Theme

Theme returns the theme of the kit’s look: shadcn/ui’s neutral colors with a base radius of 10 px (shadcn’s 0.625rem). It is tela.DefaultTheme with the radius shadcn’s components were designed for, so the rounded-md, rounded-lg and rounded-xl of the recipes come out as 8, 10 and 14 px.

theme := kit.Theme()
tela.Main(tela.App{Title: "Notes", Root: Notes{}, Theme: &theme,
RootClass: "bg-background text-foreground"})

Change the tokens on the returned value (theme.Colors[“primary”] = …) to restyle every component at once; see DefaultStyle to restyle one.

type AlertDialog struct {
// Trigger is the element that opens the dialog; nil for a dialog
// opened only by its state.
Trigger Trigger
// Title and Description are the question and its explanation; both name
// the dialog for assistive technologies.
Title, Description tela.Component
// Cancel and Action are the labels of the buttons.
Cancel, Action tela.Component
// Destructive gives the Action button the destructive variant.
Destructive bool
// OnAction and OnCancel are called when the Action or Cancel button is
// pressed, before the dialog closes.
OnAction, OnCancel func()
// Size is the default or Sm, a narrow dialog with the buttons in two
// equal columns and centered text.
Size Size
// Open points to the open state of a controlled dialog; nil leaves the
// state to the dialog, which starts closed unless DefaultOpen is set.
Open *bool
DefaultOpen bool
OnOpenChange func(open bool)
// Disabled disables the trigger, which is then inert and dimmed by its
// recipe; the dialog does not open. Set it here: the trigger props
// override a Disabled given to the trigger component itself.
Disabled bool
// Class is added after the recipe's Root classes, to the panel. The
// other Class fields do the same for their part.
Class string
OverlayClass string
HeaderClass string
TitleClass string
DescriptionClass string
FooterClass string
CancelClass string
ActionClass string
// Props are more props for the dialog element (the panel).
Props tela.Child
}

AlertDialog is a modal dialog that interrupts the user with a question that needs an answer, shadcn/ui’s AlertDialog: a title, a description and two buttons, Cancel and Action. It has the alertdialog role, takes the focus on its first button (Cancel, so that Enter never confirms by accident), and, unlike a Dialog, is not closed by a press on the scrim: only the buttons and Escape close it.

kit.AlertDialog{
Trigger: kit.Button{Variant: kit.Outline, Children: h.Text("Delete project")},
Title: h.Text("Are you absolutely sure?"),
Description: h.Text("This action cannot be undone. The project and its deployments are deleted."),
Action: h.Text("Delete"),
Destructive: true,
OnAction: deleteProject,
}

Cancel and Action are the labels of the two buttons (“Cancel” and “Continue” when nil); a slot may be any component, such as a label with an icon. The buttons are [Button]s: Action is the default variant, or Destructive with Destructive set, and Cancel is the outline variant; ActionClass and CancelClass add classes to them. Both close the dialog; OnAction and OnCancel say which was chosen. Escape closes it as Cancel does, without calling OnCancel.

The Trigger slot is a Trigger. Open, DefaultOpen and OnOpenChange are the usual controlled and uncontrolled pair.

Example

t := &exampleT{}
defer t.close()
deleted := false
page := func() tela.Node {
return h.Div(h.Class("p-6"), kit.AlertDialog{
Trigger: kit.Button{Variant: kit.Outline, Children: h.Text("Delete project")},
Title: h.Text("Are you absolutely sure?"),
Description: h.Text("This action cannot be undone."),
Action: h.Text("Delete"),
Destructive: true,
OnAction: func() { deleted = true },
})
}
app := telatest.MountFunc(t, page)
app.Click(telatest.ByRole("button", "Delete project"))
app.Click(telatest.ByRole("button", "Delete"))
fmt.Println("deleted:", deleted)
// Output: deleted: true
func (a AlertDialog) Build() tela.Node

Build returns the trigger and the dialog’s portal.

type AlertDialogStyle struct {
Positioner string
Overlay string
OverlayExit string
// Root is the panel; Sizes adds the width of the size (the default and
// Sm), and Exit is what the panel transitions to as it closes.
Root string
Sizes map[Size]string
Exit string
// Header holds the title and the description, HeaderSizes adds the
// alignment of the size: shadcn centers the text, and aligns it to the
// start of the default size from the sm breakpoint up.
Header string
HeaderSizes map[Size]string
Title string
Description string
// Footer is the row of the two buttons, FooterSizes adds the size's
// layout (two equal columns for Sm).
Footer string
FooterSizes map[Size]string
}

AlertDialogStyle is the recipe of AlertDialog (shadcn/ui’s AlertDialogOverlay, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription and AlertDialogFooter). Its Positioner, Overlay and Exit work as the ones of DialogStyle.

type Avatar struct {
// Size is Default (32 px), Sm (24 px) or Lg (40 px).
Size Size
// Src is the picture: h.Src, h.SrcPath, h.SrcBytes or h.SrcFS. Nil
// shows the fallback only.
Src tela.Child
// Alt names the picture for assistive technologies.
Alt string
// Fallback is the text shown without a picture, normally initials.
Fallback string
// Class is added after the recipe's Root classes; FallbackClass does the
// same for the fallback disc, such as a gradient of its own per person.
Class string
FallbackClass string
// Props are more props for the root element.
Props tela.Child
}

Avatar is a round picture of a person or an account, shadcn/ui’s Avatar, with the initials to show while there is no picture. The picture is painted over the fallback, so when Src is not given or the image does not draw, the fallback is what shows.

kit.Avatar{Src: h.SrcPath("faces/mei.png"), Alt: "Mei Tanaka", Fallback: "MT"}
kit.Avatar{Size: kit.Lg, Fallback: "AB"}
func (a Avatar) Build() tela.Node

Build returns the avatar element.

type AvatarStyle struct {
Root string
// Sizes are the diameters: the default (32 px), Sm and Lg.
Sizes map[Size]string
// Image is the picture, laid over the fallback.
Image string
// Fallback is the initials on a muted disc, drawn under the image.
Fallback string
// FallbackSizes are the fallback's classes per avatar size (a smaller
// text for Sm); a size the map does not define adds none.
FallbackSizes map[Size]string
}

AvatarStyle is the recipe of Avatar (shadcn/ui’s Avatar, AvatarImage and AvatarFallback).

type Badge struct {
// Variant is Default, Secondary, Destructive, Outline, Ghost or Link.
Variant Variant
// Icon is drawn before the children.
Icon tela.Icon
// Children is the label.
Children tela.Component
// Class is added after the recipe's classes.
Class string
// Props are more props for the badge element.
Props tela.Child
}

Badge is a small label, shadcn/ui’s Badge: a status, a count, a tag.

kit.Badge{Variant: kit.Secondary, Children: h.Text("Draft")}
kit.Badge{Variant: kit.Destructive, Icon: lucide.CircleAlert, Children: h.Text("Failed")}

shadcn’s hover styles for a badge that is a link ([a&]:hover:) are not part of the recipe: a Badge is not a link here; put it in an h.A or give it Props with an OnClick and a Class with hover: classes.

func (b Badge) Build() tela.Node

Build returns the badge element.

type BadgeStyle struct {
// Root is what every badge has.
Root string
// Variants are the looks: Default, Secondary, Destructive, Outline,
// Ghost and Link.
Variants map[Variant]string
// Icon is what the Icon slot has.
Icon string
}

BadgeStyle is the recipe of Badge (shadcn/ui’s badgeVariants).

type Button struct {
// Variant is the look: Default, Secondary, Outline, Ghost, Destructive
// or Link.
Variant Variant
// Size is Default, Xs, Sm, Lg, Icon, IconXs, IconSm or IconLg.
Size Size
// Type is "button", "submit" or "reset" (h.Type). Empty leaves the
// element's default: "submit" inside an h.Form, else "button".
Type string
// Disabled makes the button inert (h.Disabled) and dims it.
Disabled bool
// OnClick is called when the button is clicked or activated with the
// keyboard.
OnClick func()
// AriaLabel names a button whose content is not text, such as an icon
// button.
AriaLabel string
// Icon is drawn before the children, IconEnd after them. Their size
// and shape come from the recipe's Icon and IconSizes.
Icon, IconEnd tela.Icon
// Children is the label.
Children tela.Component
// Class is added after the recipe's classes, so it wins over them.
Class string
// Props are more props for the button element: a Key, a TestID, a
// handler.
Props tela.Child
// contains filtered or unexported fields
}

Button is a button, shadcn/ui’s Button: the rounded, medium-weight control in six variants and eight sizes. It is an h.Button, so it takes focus with Tab, is activated with Enter and Space, shows hover and active states, and inside an h.Form submits it (Type “submit” is the HTML default there).

kit.Button{Variant: kit.Outline, Size: kit.Sm, OnClick: save, Children: h.Text("Save")}
// An icon before the label (your own tela.Icon), and an icon-only button.
kit.Button{Icon: lucide.Mail, Children: h.Text("Login with email")}
kit.Button{Variant: kit.Outline, Size: kit.Icon, Icon: lucide.ChevronRight, AriaLabel: "Next"}

A Variant or Size the active Style does not define panics in dev mode with the closest one.

Example

t := &exampleT{}
defer t.close()
saved := 0
page := func() tela.Node {
return h.Div(h.Class("p-6"),
kit.Button{Variant: kit.Outline, Size: kit.Sm, OnClick: func() { saved++ }, Children: h.Text("Save")})
}
app := telatest.MountFunc(t, page)
app.Click(telatest.ByRole("button", "Save"))
fmt.Println("saved", saved)
// Output: saved 1
func (b Button) Build() tela.Node

Build returns the button element.

func (b Button) WithTriggerProps(p tela.Prop) tela.Component

WithTriggerProps makes Button a Trigger: the overlay that has it as its Trigger slot puts its props on the button element, after the button’s own.

type ButtonStyle struct {
// Root is what every button has: layout, text, focus ring, disabled.
Root string
// Variants are the looks: Default, Secondary, Outline, Ghost,
// Destructive and Link.
Variants map[Variant]string
// Sizes are the dimensions: the default, Xs, Sm, Lg, Icon, IconXs,
// IconSm and IconLg.
Sizes map[Size]string
// HasIcon are the classes a size adds when the button has an Icon or
// an IconEnd (shadcn's has-[>svg]: padding), after Sizes.
HasIcon map[Size]string
// Icon is what the icon slots have, before IconSizes.
Icon string
// IconSizes are the icon's size per button size; a size the map does
// not define uses the default's.
IconSizes map[Size]string
}

ButtonStyle is the recipe of Button (shadcn/ui’s buttonVariants).

type Calendar struct {
// Value points to the selected date of a controlled calendar; nil
// leaves it to the calendar, which starts at DefaultValue.
Value *aria.Date
DefaultValue aria.Date
// OnValueChange is called with the date the user selects.
OnValueChange func(aria.Date)
// Min and Max bound the dates that can be focused or selected, both
// included; the zero Date is no limit. Dates outside them are dimmed
// and the month buttons stop at them.
Min, Max aria.Date
// IsDateUnavailable marks dates that are shown dimmed and can not be
// selected (booked days, holidays).
IsDateUnavailable func(aria.Date) bool
// Disabled disables every date; ReadOnly keeps them focusable but not
// selectable.
Disabled, ReadOnly bool
// FocusedDate points to the focused date of a controlled calendar, the
// one with the tab stop, whose month is on display; nil leaves it to the
// calendar, which starts at DefaultFocusedDate, else the selected date,
// else today. OnFocusChange is called when it changes.
FocusedDate *aria.Date
DefaultFocusedDate aria.Date
OnFocusChange func(aria.Date)
// VisibleMonths is the number of months shown side by side (1 to 12,
// default 1; the layout is a column below the md breakpoint).
VisibleMonths int
// FirstDayOfWeek is the first column; nil takes the locale's.
FirstDayOfWeek *time.Weekday
// FixedWeeks always shows six weeks per month, so the calendar keeps
// its height from month to month.
FixedWeeks bool
// HideOutsideDays leaves out the dates of the neighbouring months that
// fill the first and last week (shadcn shows them by default).
HideOutsideDays bool
// NarrowWeekdays writes the column headers with the narrowest weekday
// names (one letter, usually) instead of the short ones ("M" for "Mo"):
// for a locale whose short names do not fit a 32 px cell, such as
// Arabic's. The headers keep the full weekday as their name for
// assistive technologies.
NarrowWeekdays bool
// Today is the date drawn as today; the zero Date is the current day.
// Set it to make a test or a golden deterministic.
Today aria.Date
// Locale is the BCP 47 tag of the names, digits, first weekday and
// direction; empty is the component's locale (i18n.UseLocale).
// Direction is "ltr" or "rtl"; empty follows the Locale when one is
// given, else the direction at the calendar's position
// (tela.UseDirection: the nearest h.Dir above it). Either one given
// here also sets the direction of the calendar's layout.
Locale, Direction string
// AriaLabel names the calendar ("Appointment date"); the visible month
// is appended. PreviousLabel and NextLabel name the month buttons
// ("Previous" and "Next" by default: translate them with i18n.T).
AriaLabel string
PreviousLabel, NextLabel string
// PreviousIcon and NextIcon replace the chevrons of the month buttons.
PreviousIcon, NextIcon tela.Icon
// Class is added after the recipe's Root classes; the other Class fields
// do the same for their part: the months row, a month, the nav row, a
// caption, a weekday name, a day cell and a day button.
Class string
MonthsClass string
MonthClass string
NavClass string
CaptionClass string
WeekdayClass string
DayClass string
DayButtonClass string
// Props are more props for the root element.
Props tela.Child
// contains filtered or unexported fields
}

Calendar is a month grid that selects one date, shadcn/ui’s Calendar (react-day-picker’s look) on aria.UseCalendar: a caption with the month and year between the previous and next buttons, the weekday names, and a ghost button for each day, with today, the selected date, the dates of the neighbouring months and the dates that can not be selected each styled.

day, setDay := tela.UseState(aria.Date{})
kit.Calendar{Value: &day, OnValueChange: setDay.Set, Min: aria.Today()}

It is one tab stop: Tab reaches the focused date and the arrow keys move over the days (a week with up and down, swapped by a right-to-left direction), PageUp and PageDown over the months (a year with Shift), Home and End to the first and last day of the week, and Enter or Space select. The month on display follows the focus. Names, digits, the first day of the week and the direction come from package i18n: the component’s locale, or Locale.

The selected date follows the kit’s value rule with the Date type of package aria, a comparable day with no zone: Value (a pointer to your state) with OnValueChange controls it, DefaultValue starts an uncontrolled one; the zero Date is none. See RangeCalendar for a range and DatePicker for a calendar in a popover.

Deviations from shadcn: react-day-picker’s year and month dropdowns (captionLayout), week numbers and its other modes (several dates) are not ported; outside dates that belong to another visible month are hidden, as there; the date cell’s aria-label is the full localized date without “Today” or “selected” (aria-selected carries the state).

Example

t := &exampleT{}
defer t.close()
var picked aria.Date
page := func() tela.Node {
return h.Div(h.Class("p-6"), kit.Calendar{
// Today is injected so the example does not depend on the clock;
// leave it out in an app.
Today: aria.Date{Year: 2026, Month: time.October, Day: 25},
Min: aria.Date{Year: 2026, Month: time.October, Day: 8},
OnValueChange: func(day aria.Date) { picked = day },
})
}
app := telatest.MountFunc(t, page, telatest.Size(400, 400))
// Tab skips the previous button (disabled at Min) and reaches today's
// date; PageDown moves a month and Enter chooses.
app.Press("Tab")
app.Press("Tab")
app.Press("PageDown")
app.Press("Enter")
fmt.Println(picked)
// Output: 2026-11-25
func (c Calendar) Build() tela.Node

Build returns the calendar element.

type CalendarStyle struct {
// Root is the calendar: padding, background, the width of its content.
Root string
// Months holds the nav and the months, side by side from md up.
Months string
// Month is one month: its caption and its grid.
Month string
// Nav is the row of the previous and next buttons, over the captions.
Nav string
// ButtonPrevious and ButtonNext are added to the ghost button's classes
// (the Button recipe's Root, Ghost variant and default size).
ButtonPrevious, ButtonNext string
// Chevron is the arrow icon of the nav buttons; rtl:rotate-180 turns it
// around under a right-to-left direction.
Chevron string
// MonthCaption holds the month's title and CaptionLabel is the title.
MonthCaption, CaptionLabel string
// MonthGrid is the grid element of a month, Weekdays the row of weekday
// names, Weekday one name, and Week one row of days.
MonthGrid, Weekdays, Weekday, Week string
// Day is the cell of a date: it sizes the day and carries the
// background of today and of the ends of a range behind the button.
Day string
// Today, Outside, Disabled, Hidden, RangeStart, RangeMiddle and
// RangeEnd are added to Day for a date in that state (react-day-picker's
// modifiers). Disabled is a date that can not be selected (before Min,
// after Max, unavailable); Hidden an outside date that is not shown.
Today, Outside, Disabled, Hidden, RangeStart, RangeMiddle, RangeEnd string
// TodayWithSelected is added to Day when today is selected
// (shadcn's data-[selected=true]:rounded-none on the today modifier).
TodayWithSelected string
// DayButton is the button of a date, a ghost icon button resized to
// fill its cell.
DayButton string
// DayButtonWithSelected is added to DayButton for the selected date of
// a Calendar (shadcn's data-[selected-single=true]:), and
// DayButtonWithRangeStart, DayButtonWithRangeMiddle and
// DayButtonWithRangeEnd for the parts of a selected range.
DayButtonWithSelected, DayButtonWithRangeStart, DayButtonWithRangeMiddle, DayButtonWithRangeEnd string
}

CalendarStyle is the recipe of Calendar and RangeCalendar (shadcn/ui’s Calendar, whose parts are react-day-picker’s: its classNames). The root element is the calendar; a day is a cell (Day) that holds a ghost icon button (DayButton), as in react-day-picker’s td and button.

shadcn sizes the calendar with the CSS variable –cell-size (2rem); the engine has no variables, so the recipe writes size-8, h-8, px-8 and min-w-8 where shadcn writes size-(–cell-size) and the like: change them together to resize the cells.

type Card struct {
// Title and Description are the header's heading and its muted line.
Title, Description tela.Component
// TitleLevel makes the Title a heading of that level, 1 to 6 (h1 to
// h6); 0 keeps shadcn's div. A level outside 0 to 6 panics in dev mode
// and is clamped otherwise.
TitleLevel int
// Action sits at the header's right end: a button, a menu.
Action tela.Component
// Children is the content.
Children tela.Component
// Footer is the card's last row.
Footer tela.Component
// Class is added after the recipe's Root classes. The other Class
// fields do the same for their part.
Class string
HeaderClass string
TitleClass string
DescriptionClass string
ActionClass string
ContentClass string
FooterClass string
// Props are more props for the root element.
Props tela.Child
}

Card is a surface with a header, content and a footer, shadcn/ui’s Card. Its parts are slots; an absent slot (nil) draws nothing, and the header is drawn when the card has a Title, a Description or an Action.

kit.Card{
Title: h.Text("Create project"),
Description: h.Text("Deploy your new project in one click."),
Action: kit.Button{Variant: kit.Ghost, Size: kit.IconSm, Icon: icons.X, AriaLabel: "Close"},
Children: form,
Footer: h.Fragment(kit.Button{Variant: kit.Outline, Children: h.Text("Cancel")}, ...),
}

The Title is a div, as in shadcn. TitleLevel from 1 to 6 makes it a heading (h1 to h6) with the same classes, so that assistive technologies list it and a screen reader user can jump to it; give it the level that fits the page’s outline, with plain text in Title:

kit.Card{TitleLevel: 2, Title: h.Text("Notifications")}

The recipe’s Header, Title, Description, Action, Content and Footer classes style the parts; the fields ending in Class add to them for one card.

shadcn’s [.border-b]:pb-6 and [.border-t]:pt-6 (a header or footer that has a border brings its own padding) are not in the recipe: add pb-6 or pt-6 with HeaderClass and FooterClass when you add the border.

func (c Card) Build() tela.Node

Build returns the card element.

type CardStyle struct {
Root string
// Header holds the title, the description and the action.
Header string
// HeaderWithAction is added to Header when the card has an Action: the
// header becomes two columns (shadcn's has-data-[slot=card-action]:).
HeaderWithAction string
Title string
Description string
Action string
Content string
Footer string
}

CardStyle is the recipe of Card (shadcn/ui’s Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent and CardFooter).

type Checkbox struct {
// Checked points to the state of a controlled checkbox; nil leaves it
// to the checkbox, which starts at DefaultChecked.
Checked *bool
// DefaultChecked is the initial state of an uncontrolled checkbox.
DefaultChecked bool
// OnCheckedChange is called with the next state when the user toggles
// the checkbox. For a controlled checkbox it is how the change is asked
// for.
OnCheckedChange func(bool)
// Disabled makes the checkbox inert and dims it.
Disabled bool
// Required marks the checkbox as one the user must check
// (aria-required).
Required bool
// Invalid marks the value as wrong: assistive technologies announce it
// and the aria-invalid: classes (a red border and ring) apply. A
// [Field] with an Error sets it on its control.
Invalid bool
// ID is the checkbox's id, for a [Label]'s For. Inside a [Field] it
// defaults to the field's.
ID string
// AriaLabel names a checkbox that has no label of its own.
AriaLabel string
// Icon replaces the check mark (Lucide's check).
Icon tela.Icon
// Class is added after the recipe's classes; IndicatorClass and
// IconClass do the same for their parts.
Class string
IndicatorClass string
IconClass string
// Props are more props for the checkbox element.
Props tela.Child
}

Checkbox is a box that is checked or not, shadcn/ui’s Checkbox, on aria.UseCheckbox: a click or Space toggles it, Tab reaches it, and it has the checkbox role and aria-checked. Its state follows the kit’s rule: give Checked (a pointer to your state) and OnCheckedChange to keep it yourself, or DefaultChecked to let the checkbox keep it.

agree, setAgree := tela.UseState(false)
kit.Checkbox{Checked: &agree, OnCheckedChange: setAgree.Set, AriaLabel: "Accept the terms"}

A Label with the checkbox’s ID names it and toggles it when clicked, and a Field ties its label to it with no ids to write:

kit.Field{Orientation: kit.Horizontal, Label: "Accept the terms",
Children: kit.Checkbox{DefaultChecked: true}}

The indeterminate state is not supported (aria has none yet).

Example

t := &exampleT{}
defer t.close()
agreed := false
page := func() tela.Node {
return h.Div(h.Class("flex items-center gap-3"),
kit.Checkbox{ID: "terms", OnCheckedChange: func(b bool) { agreed = b }},
kit.Label{For: "terms", Children: h.Text("Accept the terms")})
}
app := telatest.MountFunc(t, page)
app.Click(telatest.ByText("Accept the terms")) // the label toggles the checkbox
fmt.Println("agreed", agreed)
// Output: agreed true
func (c Checkbox) Build() tela.Node

Build returns the checkbox element.

type CheckboxStyle struct {
// Root is the box: size, border, the checked colors, focus ring,
// disabled and invalid. It has the class "peer", so a sibling after it
// can use the peer-* variants. It is inline-flex with its content centered,
// as a browser draws the content of a button (Tela's button centers it
// horizontally only).
Root string
// Indicator holds the check mark and is only there while checked.
Indicator string
// Icon is the check mark.
Icon string
}

CheckboxStyle is the recipe of Checkbox (shadcn/ui’s Checkbox).

type ColorInput struct {
// The color, as in [ColorPicker].
Value *tela.Color
DefaultValue *tela.Color
OnValueChange func(tela.Color)
OnValueCommit func(tela.Color)
Alpha bool
Swatches []tela.Color
// Disabled makes the button inert and dims it, and the panel never opens.
Disabled bool
// Trigger is the element that opens the panel; nil is an outline
// [Button] with the swatch and the code.
Trigger Trigger
// Placement, Align and Offset place the panel as a [Popover]'s do: below
// the button, at its start, by default.
Placement, Align string
Offset float32
// Open points to the open state of a controlled panel, as a
// [Popover]'s does.
Open *bool
DefaultOpen bool
OnOpenChange func(open bool)
// AreaLabel, HueLabel, AlphaLabel and HexLabel name the picker's
// controls, as in [ColorPicker].
AreaLabel, HueLabel, AlphaLabel, HexLabel string
// AriaLabel names the button, followed by its color ("Brand color,
// #6366f1"), and the panel ("Color picker" when it is empty); inside a
// [Field] with a Label that label is used. The color in the name is the
// one of the last build, while the code shown on the button follows a
// drag: an uncontrolled drag names the color it ends on when it ends
// (or when the pointer rests), as the picker's thumbs do with their
// aria-valuetext, so assistive technology is not told every move; a
// controlled ColorInput builds on every change. ID is the button's id,
// for a [Label]'s For, and inside a Field defaults to the field's. With
// a Trigger of your own, these are for the panel only: name the button
// and give it its id yourself.
AriaLabel string
ID string
// Direction is "ltr" or "rtl"; empty is the direction at the component's
// position (tela.UseDirection: the nearest h.Dir above it, left to right
// without one). Under "rtl" the saturation grows toward the left
// of the area, the hue and the opacity toward the left of their sliders,
// and ArrowLeft increases them; a Direction given here also sets the
// direction of the panel's layout, not the button's.
Direction string
// Class is added to the default button's classes; PanelClass to the
// picker's root; PopoverClass to the popover's panel.
Class string
PanelClass string
PopoverClass string
// Props are more props for the default button.
Props tela.Child
}

ColorInput is a button that shows a color and opens a ColorPicker in a Popover to change it: the swatch and the hex code on a button, the picker in a panel under it. It takes the values of a ColorPicker and the open state of a Popover.

kit.ColorInput{DefaultValue: new(tela.Hex("#6366f1")), Alpha: true, OnValueCommit: save}

The color lives in the ColorInput, not in the panel, so the swatch and the code on the button follow a drag in the panel without building the component on every move, and the color survives the panel being closed. The panel is a dialog; the focus goes into it when it opens and returns to the button when it closes (Escape, or a press outside it).

Trigger replaces the button, with the rule of every trigger slot (see Trigger): the button is the one you write, with the popover’s props. Then nothing on it shows the color unless you draw it from OnValueChange.

Example

t := &exampleT{}
defer t.close()
page := func() tela.Node {
return h.Div(h.Class("p-6"), kit.ColorInput{DefaultValue: new(tela.Hex("#6366f1")), Swatches: []tela.Color{tela.Hex("#ef4444"), tela.Hex("#22c55e")}})
}
app := telatest.MountFunc(t, page)
app.Click(telatest.ByRole("button", "#6366f1"))
app.Click(telatest.ByRole("button", "#22c55e"))
app.Press("Escape")
app.Expect(telatest.ByRole("button", "#22c55e"), telatest.Expanded(false))
// Output:
func (c ColorInput) Build() tela.Node

Build returns the button and its popover.

type ColorInputStyle struct {
// Trigger is added to the classes of the outline [Button] that is the
// default trigger.
Trigger string
// Swatch is the small square of the color inside the trigger.
Swatch string
// Code is the hex code inside the trigger. Inter is proportional, so
// its digits are tabular (tabular-nums) and its minimum width is that
// of the widest code, "#" and six digits (the digits are wider than the
// letters a to f, which the code is written with), so the button does
// not change size while the color does. CodeAlpha is added to Code
// while the input has Alpha, with the width of "#" and eight digits.
// The widths are in em, for Inter at any size; another font needs its
// own.
Code string
CodeAlpha string
}

ColorInputStyle is the recipe of ColorInput.

type ColorPicker struct {
// Value points to the color of a controlled picker; nil leaves it to
// the picker, which starts at DefaultValue. OnValueChange is called with
// the next color on every change.
Value *tela.Color
// DefaultValue is the color of an uncontrolled picker at the start; nil
// starts at opaque black (tela.Black). It is a pointer because the zero
// Color is a color too, transparent black.
DefaultValue *tela.Color
OnValueChange func(tela.Color)
// OnValueCommit is called with the color when the user stops changing
// it, and not for a change that left it as it was.
OnValueCommit func(tela.Color)
// Alpha adds the opacity slider and lets the hex field take and show
// the opacity (#rrggbbaa). Without it the field shows #rrggbb and the
// color keeps whatever opacity it had.
Alpha bool
// Swatches are preset colors: buttons that set the color.
Swatches []tela.Color
// Disabled dims the picker and makes it ignore the user.
Disabled bool
// AreaLabel, HueLabel, AlphaLabel and HexLabel name the controls for
// assistive technologies ("Color area", "Hue", "Alpha" and "Hex" by
// default); set them for another language. The value texts ("Hue
// 180°") are aria's English ones.
AreaLabel, HueLabel, AlphaLabel, HexLabel string
// Direction is "ltr" or "rtl"; empty is the direction at the component's
// position (tela.UseDirection: the nearest h.Dir above it, left to right
// without one). Under "rtl" the saturation grows toward the left
// of the area, the hue and the opacity toward the left of their sliders,
// and ArrowLeft increases them; a Direction given here also sets the
// direction of the panel's layout.
Direction string
// ID is the id of the picker's hex field, for a [Label]'s For. Inside
// a [Field] it defaults to the field's, so the field's label points to
// it and a press on the label focuses it.
ID string
// AriaLabel names the picker as a group for assistive technologies;
// inside a Field with a Label that label is used. A picker with neither
// is not a group.
AriaLabel string
// Class is added after the recipe's Root classes. Props are more props
// for the root element.
Class string
Props tela.Child
}

ColorPicker lets people choose a color: an area for saturation and brightness, a slider for the hue, optionally one for the opacity, a field for the hex code, and optionally a row of preset colors. The value is a tela.Color, and the picker follows the kit’s rule for values.

c, setC := tela.UseState(tela.Hex("#6366f1"))
kit.ColorPicker{Value: &c, OnValueChange: setC.Set, Alpha: true,
Swatches: []tela.Color{tela.Hex("#ef4444"), tela.Hex("#22c55e"), tela.Hex("#3b82f6")}}

It is built on the color hooks of package aria (aria.UseColorState, the area, the sliders and the field), so the area and the sliders are dragged with a mouse, a finger or a pen, the arrow keys move a thumb by one step (Shift or Page Up and Page Down by more) and each control is announced with its value. The hue is kept when the color goes through a gray or black, so dragging to the left edge of the area does not lose it.

Dragging only repaints: the thumbs, the area, the preview, the hex field (unless it is being typed in) and the alpha track follow the pointer without the picker being built on every move. OnValueChange is called on every change, so a controlled picker builds as often as the state it is given does; OnValueCommit is called once when the user stops (a drag ends, a key, a swatch or a hex code changed the color), which is the moment to save it.

The hex field accepts “#rgb”, “#rrggbb” (and with Alpha the forms with four or eight digits) with or without the “#”, commits on Enter and when it loses the focus, and goes back to the color when the text is not a code. Use ColorInput for a picker that opens from a button.

Example

t := &exampleT{}
defer t.close()
var saved []string
page := func() tela.Node {
c, setC := tela.UseState(tela.Hex("#6366f1"))
return h.Div(h.Class("p-4"),
kit.ColorPicker{Value: &c, OnValueChange: setC.Set, Alpha: true,
Swatches: []tela.Color{tela.Hex("#ef4444"), tela.Hex("#22c55e")},
OnValueCommit: func(c tela.Color) { saved = append(saved, aria.FormatHexColor(c, true)) }},
h.P(h.Textf("Chosen: %s", aria.FormatHexColor(c, false))))
}
app := telatest.MountFunc(t, page)
app.Click(telatest.ByRole("button", "#22c55e"))
app.Focus(telatest.ByRole("slider", "Alpha"))
app.Press("PageDown")
app.ExpectText("Chosen: #22c55e")
fmt.Println(strings.Join(saved, " "))
// Output: #22c55eff #22c55ee5
func (p ColorPicker) Build() tela.Node

Build returns the picker. Inside a Field the hex field takes the field’s id, description and invalid state, and the picker is named by the field’s label.

type ColorPickerStyle struct {
// Root is the panel: a column that is 16rem wide, which is the room
// inside a [Popover]'s p-4 and w-72.
Root string
// Disabled is added to the root while the picker is disabled.
Disabled string
// Area is the saturation and brightness area. Its background is the
// hue at full saturation and brightness, which the component paints
// live; AreaSaturation and AreaBrightness are the white and black
// gradients over it.
Area string
AreaSaturation string
AreaBrightness string
// AreaThumb is the round handle on the area.
AreaThumb string
// Slider is the row of a hue or alpha slider: the track, and the thumb
// on top of it.
Slider string
// Track is the groove of a slider; HueTrack adds the rainbow to it and
// AlphaTrack adds the checkerboard (the color's gradient is drawn over
// it by AlphaFill).
Track string
HueTrack string
AlphaTrack string
AlphaFill string
// Thumb is the round handle of a slider. It is centered on the point
// the value stands for, so it hangs over the ends of the track.
Thumb string
// Checkerboard is the pattern under everything that can be
// transparent: the alpha track, the preview and the swatches. It is a
// conic gradient that repeats every 12px (two squares of 6px).
Checkerboard string
// Row is the line of the preview and the hex field.
Row string
// Preview is the swatch next to the hex field; it holds the color.
Preview string
// Input is added to the classes of the kit's [Input] on the hex field.
Input string
// Swatches is the wrapping row of preset colors; Swatch is one of
// them and SwatchSelected is added to the one that is the color now.
Swatches string
Swatch string
SwatchSelected string
}

ColorPickerStyle is the recipe of ColorPicker and of the panel ColorInput opens. shadcn/ui has no color picker: the look is the kit’s own, written in shadcn’s language (ADR-143).

type Column[T any] struct {
// ID names the column in a [SortState]; it defaults to Header and must
// be unique in the table.
ID string
// Header is the column's title, and the name of its sort button.
Header string
// HeaderContent is drawn in place of the Header text, for a header that
// is not plain text. Header still names the column.
HeaderContent tela.Component
// Cell returns the content of the column's cell for a row, such as
// h.Text(u.Name). It is a flex item of the cell: return one node.
Cell func(row T) tela.Node
// Sort orders two rows by this column (negative when a comes first, as
// strings.Compare and cmp.Compare). A column with a Sort is sortable:
// its header is a button that cycles the order.
Sort func(a, b T) int
// Sortable makes a column without a Sort sortable, for rows that come
// sorted (from a server): the table shows the sort state and reports a
// change with OnSortChange, and leaves the rows in the order given.
Sortable bool
// Width is the column's grid track: "200px", "1fr", "auto", "minmax(120px, 1fr)",
// "min-content"... The default is "auto".
Width string
// Align is AlignLeft (the default), AlignCenter or AlignRight; the first
// and the last are the start and the end of the line (see ColumnAlign).
Align ColumnAlign
// Sticky keeps the column in place when the table scrolls
// horizontally. Sticky columns come first, and every sticky column
// but the last has a Width in px ("160px"), since the next one sticks
// right after it.
Sticky bool
// Footer is the column's cell of the footer row; the row is drawn when
// any column has one. FooterSpan, 2 or more, makes it span that many
// columns (shadcn's colSpan), and the columns it covers have no
// footer cell of their own.
Footer tela.Component
FooterSpan int
// Class is added to the column's body cells, HeaderClass to its header
// and FooterClass to its footer cell.
Class, HeaderClass, FooterClass string
// CellProps returns more props for the column's cell of a row, such as
// h.TestID or a handler; HeaderProps and FooterProps are the props of
// the header and footer cells.
CellProps func(row T) tela.Child
HeaderProps, FooterProps tela.Child
}

Column describes a column of a Table of rows of type T.

type ColumnAlign string

ColumnAlign is the horizontal alignment of a Column’s header, cells and footer. The zero value is left, as shadcn’s text-left. Left and right are the start and the end of the line: under a right-to-left direction (h.Dir(“rtl”)) the table mirrors and AlignRight is at the left, where numbers belong, as in shadcn’s right-to-left registry (text-start and text-end).

const (
AlignLeft ColumnAlign = ""
AlignCenter ColumnAlign = "center"
AlignRight ColumnAlign = "right"
)
type ContextMenu[K comparable] struct {
// Children is the area: what a right click opens the menu on.
Children tela.Component
// Items are the entries of the menu, in order.
Items []MenuItem[K]
// OnSelect is called with the key of a plain item when it is chosen,
// after the item's own OnSelect.
OnSelect func(K)
// Disabled leaves the area's right click to the platform and to an
// outer context menu.
Disabled bool
// NoLongPress keeps a touch or a pen that stays down from opening the
// menu, so that it clicks what it is on (a held mouse button always
// does: see aria.ContextMenuTriggerOptions).
NoLongPress bool
// AriaLabel names the menu, which has no name of its own: the area is
// not a label.
AriaLabel string
// CheckIcon and RadioIcon replace the check of a checkbox item and the
// dot of a radio item.
CheckIcon, RadioIcon tela.Icon
// Open points to the open state of a controlled menu; nil leaves it to
// the component. OnOpenChange is called with the next state.
Open *bool
OnOpenChange func(open bool)
// Class is added to the area (it has no classes of its own);
// ContentClass is added after the recipe's Content classes, to the
// menu's panel.
Class string
ContentClass string
// Props are more props for the area element.
Props tela.Child
}

ContextMenu is a menu that opens where the user right-clicks inside an area, shadcn/ui’s ContextMenu, built on aria.UseContextMenuTrigger: a right click opens it at the pointer, a touch or a pen that stays down opens it under the finger, and Shift+F10 or the ContextMenu key opens it, on its first item, at the area’s top-left corner. The keys reach the area from the element that has the focus inside it, as in the browser: an area with nothing focusable inside opens from the keyboard only when it can take the focus itself (Props: h.TabIndex(0), with a focus-visible: ring in Class). The menu closes with Escape, a press outside it with any mouse button or choosing an item, and the focus returns to where it was. A right click elsewhere in the area while the menu is open closes it and opens a new one at the new point (Radix), a right click in another area opens that area’s menu instead, and in nested areas the innermost wins.

kit.ContextMenu[string]{
AriaLabel: "Actions",
Class: "flex h-40 w-72 items-center justify-center rounded-md border border-dashed text-sm",
Children: h.Text("Right click here"),
Items: []kit.MenuItem[string]{
{Key: "back", Label: "Back", Shortcut: "⌘["},
{Key: "reload", Label: "Reload", Shortcut: "⌘R"},
{Kind: kit.MenuSeparator},
{Kind: kit.MenuCheckbox, Key: "bookmarks", Label: "Show bookmarks", DefaultChecked: true},
},
}

The items are the [MenuItem]s of DropdownMenu, with no submenus yet (ADR-141). The root element is the area, a div that holds Children; the area is not focusable by itself, the keys reach it from what inside it has the focus, and a keyboard menu opens at the area’s corner, not at that element (ADR-133). Open and OnOpenChange are the usual controlled pair (a menu opens at a point, so there is no DefaultOpen).

Example

t := &exampleT{}
defer t.close()
page := func() tela.Node {
return h.Div(h.Class("p-6"), kit.ContextMenu[string]{
AriaLabel: "Actions",
Class: "flex h-40 w-72 items-center justify-center rounded-md border border-dashed text-sm",
Children: h.Button(h.Text("Right click here")),
Items: []kit.MenuItem[string]{
{Key: "back", Label: "Back", Shortcut: "⌘["},
{Key: "reload", Label: "Reload", Shortcut: "⌘R"},
},
OnSelect: func(key string) { fmt.Println("chose", key) },
})
}
app := telatest.MountFunc(t, page)
// Shift+F10 opens the menu from the keyboard; a right click opens it at
// the pointer.
app.Focus(telatest.ByRole("button", "Right click here"))
app.Press("Shift+F10")
app.Press("ArrowDown")
app.Press("Enter")
// Output: chose reload
func (c ContextMenu[K]) Build() tela.Node

Build returns the area and, in a portal, the menu.

type DatePicker struct {
// Value points to the chosen date of a controlled picker; nil leaves it
// to the picker, which starts at DefaultValue. OnValueChange is called
// with the date the user chooses.
Value *aria.Date
DefaultValue aria.Date
OnValueChange func(aria.Date)
// Placeholder is shown while no date is chosen; the default is "Pick a
// date".
Placeholder string
// Format writes the chosen date on the button; nil is the formatter's
// long date ("October 25, 2026").
Format func(f i18n.Formatter, d aria.Date) string
// Min, Max, IsDateUnavailable, VisibleMonths, FirstDayOfWeek,
// FixedWeeks, HideOutsideDays, NarrowWeekdays, Today, Locale, Direction,
// PreviousLabel and NextLabel are those of [Calendar].
Min, Max aria.Date
IsDateUnavailable func(aria.Date) bool
VisibleMonths int
FirstDayOfWeek *time.Weekday
FixedWeeks bool
HideOutsideDays bool
NarrowWeekdays bool
Today aria.Date
Locale, Direction string
PreviousLabel, NextLabel string
// Disabled makes the picker inert and dims it.
Disabled bool
// Invalid marks the picker as wrong: assistive technologies announce it
// and the button's aria-invalid: classes (a red border and ring) apply.
// A [Field] with an Error sets it.
Invalid bool
// ID is the button's id, for a [Label]'s For. Inside a [Field] it
// defaults to the field's.
ID string
// AriaLabel names the picker, and the popover; inside a Field with a
// Label that label is used. The chosen date is appended to the button's
// name.
AriaLabel string
// Icon replaces the calendar icon of the button.
Icon tela.Icon
// Align is where the popover sits along the button: "start" (the
// default), "center" or "end".
Align string
// KeepOpen leaves the popover open after a date is chosen.
KeepOpen bool
// Open points to the open state of a controlled popover; nil leaves it
// to the picker, which starts at DefaultOpen. OnOpenChange is called with
// the next state.
Open *bool
DefaultOpen bool
OnOpenChange func(open bool)
// Class is added after the recipe's Root classes, to the button; the
// other Class fields do the same for the popover's panel and the
// calendar.
Class string
PopoverClass string
CalendarClass string
// Props are more props for the button element.
Props tela.Child
}

DatePicker is a button that shows a date and opens a calendar to choose it, shadcn/ui’s date picker pattern: an outline Button with a calendar icon and the date, or a placeholder, that toggles a Popover holding a Calendar. Choosing a date closes the popover and puts the focus back on the button; Escape and a press outside close it without choosing.

day, setDay := tela.UseState(aria.Date{})
kit.DatePicker{Value: &day, OnValueChange: setDay.Set, Min: aria.Today()}

The date follows the kit’s value rule with the aria.Date type (the zero Date is none): Value (a pointer to your state) with OnValueChange controls it, DefaultValue starts an uncontrolled picker. The button shows the date as the component’s locale writes it in its long form (“October 25, 2026”); Format changes that. The focus moves into the calendar, onto the date, when the popover opens, and the calendar follows the picker’s locale for its names, digits, first weekday and direction (Locale).

Inside a Field the button takes the field’s id, so the label points to it and names it, the field’s description or error describes it, and an error marks it invalid.

Limits (Min, Max), IsDateUnavailable, FirstDayOfWeek, FixedWeeks, HideOutsideDays and Today are those of Calendar; for several months or a range see DateRangePicker. There is no time of day and no typed date: the date is chosen in the calendar.

Example

t := &exampleT{}
defer t.close()
page := func() tela.Node {
return h.Div(h.Class("p-6"), kit.DatePicker{
AriaLabel: "Birthday",
Today: aria.Date{Year: 2026, Month: time.October, Day: 25},
OnValueChange: func(day aria.Date) {
fmt.Println("chose", day)
},
})
}
app := telatest.MountFunc(t, page, telatest.Size(400, 480))
app.Click(telatest.ByRole("button", "Birthday"))
app.Click(telatest.ByRole("gridcell", "Friday, October 9, 2026"))
// The popover closed and the button shows the date.
app.ExpectText("October 9, 2026")
fmt.Println("open:", app.Count(telatest.ByRole("application", "")) == 1)
// Output:
// chose 2026-10-09
// open: false
func (p DatePicker) Build() tela.Node

Build returns the button and its popover.

type DatePickerStyle struct {
// Root is added to the trigger button's classes: the width of the
// button and the alignment of its icon and text.
Root string
// Range is added to Root for a [DateRangePicker]: its text is longer.
Range string
// Empty is added while no date is chosen, so the placeholder is muted.
Empty string
// Content is added to the popover's panel classes: shadcn's w-auto p-0.
Content string
// Calendar is added to the calendar's classes: it has no background of
// its own inside the popover (shadcn's [[data-slot=popover-content]_&]:).
Calendar string
}

DatePickerStyle is the recipe of DatePicker and DateRangePicker (shadcn/ui’s date picker pattern: an outline Button that opens a Popover that holds a Calendar). The root element is the trigger button; its look is the ButtonStyle’s outline variant plus these classes, and the calendar and the popover are styled by their own recipes (CalendarStyle and PopoverStyle).

type DateRangePicker struct {
// Value points to the chosen range of a controlled picker; nil leaves it
// to the picker, which starts at DefaultValue. OnValueChange is called
// with the range the user completes.
Value *aria.DateRange
DefaultValue aria.DateRange
OnValueChange func(aria.DateRange)
// Placeholder is shown while no range is chosen; the default is "Pick a
// date range".
Placeholder string
// Format writes the chosen range on the button; nil writes both ends in
// the formatter's medium form joined by " - ".
Format func(f i18n.Formatter, r aria.DateRange) string
// AllowsNonContiguousRanges lets a range cross unavailable dates.
AllowsNonContiguousRanges bool
Min, Max aria.Date
IsDateUnavailable func(aria.Date) bool
VisibleMonths int
FirstDayOfWeek *time.Weekday
FixedWeeks bool
HideOutsideDays bool
NarrowWeekdays bool
Today aria.Date
Locale, Direction string
PreviousLabel, NextLabel string
Disabled bool
Invalid bool
ID string
AriaLabel string
Icon tela.Icon
Align string
KeepOpen bool
Open *bool
DefaultOpen bool
OnOpenChange func(open bool)
Class string
PopoverClass string
CalendarClass string
Props tela.Child
}

DateRangePicker is a DatePicker for a span of dates, shadcn/ui’s date range picker: the button shows the two ends (“Oct 20, 2026 - Nov 5, 2026”) and the popover holds a RangeCalendar of two months. The popover closes when the second date completes the range (or stays open with KeepOpen); the first Escape in the calendar cancels an anchored range, and the next closes the popover.

var stay aria.DateRange
kit.DateRangePicker{OnValueChange: func(r aria.DateRange) { stay = r }}

The value follows the kit’s rule with aria.DateRange. VisibleMonths defaults to 2 here, as in shadcn’s demo; the other fields are those of DatePicker and RangeCalendar.

Example

t := &exampleT{}
defer t.close()
page := func() tela.Node {
return h.Div(h.Class("p-6"), kit.DateRangePicker{
AriaLabel: "Stay",
Today: aria.Date{Year: 2026, Month: time.October, Day: 25},
OnValueChange: func(r aria.DateRange) {
fmt.Println("stay", r.Start, r.End)
},
})
}
app := telatest.MountFunc(t, page, telatest.Size(800, 480))
app.Click(telatest.ByRole("button", "Stay"))
app.Click(telatest.ByRole("gridcell", "Tuesday, October 27, 2026"))
app.Click(telatest.ByRole("gridcell", "Saturday, October 31, 2026"))
// Output: stay 2026-10-27 2026-10-31
func (p DateRangePicker) Build() tela.Node

Build returns the button and its popover.

type Dialog struct {
// Trigger is the element that opens the dialog; nil for a dialog
// opened only by its state.
Trigger Trigger
// Title and Description name and describe the dialog for assistive
// technologies and sit in its header. Give a dialog that has no visible
// title an AriaLabel.
Title, Description tela.Component
// Children is the content, between the header and the footer.
Children tela.Component
// Footer is the row of actions at the bottom, usually buttons.
Footer tela.Component
// HideClose leaves out the close button in the top-right corner, which
// is drawn by default (shadcn's showCloseButton={false}). CloseIcon and
// CloseLabel are for the button when it is drawn.
HideClose bool
// CloseIcon replaces the X of the close button; CloseLabel is its
// accessible name ("Close").
CloseIcon tela.Icon
CloseLabel string
// AriaLabel names a dialog that has no Title.
AriaLabel string
// Open points to the open state of a controlled dialog; nil leaves the
// state to the dialog, which starts closed unless DefaultOpen is set.
// OnOpenChange is called with the next state when the trigger, Escape,
// a press on the scrim or the close button asks for a change.
Open *bool
DefaultOpen bool
OnOpenChange func(open bool)
// Disabled disables the trigger, which is then inert and dimmed by its
// recipe; the dialog does not open. Set it here: the trigger props
// override a Disabled given to the trigger component itself.
Disabled bool
// Class is added after the recipe's Root classes, to the panel. The
// other Class fields do the same for their part.
Class string
OverlayClass string
HeaderClass string
TitleClass string
DescriptionClass string
FooterClass string
CloseClass string
// Props are more props for the dialog element (the panel).
Props tela.Child
}

Dialog is a modal window over the page, shadcn/ui’s Dialog: a trigger, and a panel with a title, a description, content and a footer that opens in the middle of the window over a scrim. While it is open the page is inert, Tab stays inside the dialog, Escape and a press on the scrim close it, and the focus goes back to the trigger. It fades and scales in and out.

kit.Dialog{
Trigger: kit.Button{Variant: kit.Outline, Children: h.Text("Edit profile")},
Title: h.Text("Edit profile"),
Description: h.Text("Make changes to your profile here."),
Children: form,
Footer: kit.Button{Children: h.Text("Save changes")},
}

The Trigger slot is a Trigger: a Button, or anything made one with TriggerFunc. Without a Trigger the dialog is opened from your state:

kit.Dialog{Open: &open, OnOpenChange: setOpen.Set, Title: h.Text("Welcome")}

The panel exists only while the dialog is open, so the state of what is inside it starts afresh each time. Open, DefaultOpen and OnOpenChange are the usual controlled and uncontrolled pair. Close the dialog from inside with a button whose handler asks for it through OnOpenChange, or with the state you control.

A dialog that asks for a decision and must not close by a press outside is an AlertDialog.

Example

t := &exampleT{}
defer t.close()
page := func() tela.Node {
return h.Div(h.Class("p-6"), kit.Dialog{
Trigger: kit.Button{Variant: kit.Outline, Children: h.Text("Edit profile")},
Title: h.Text("Edit profile"),
Description: h.Text("Make changes to your profile here."),
Children: kit.Input{Placeholder: "Name"},
Footer: kit.Button{Children: h.Text("Save changes")},
})
}
app := telatest.MountFunc(t, page)
app.Click(telatest.ByRole("button", "Edit profile"))
fmt.Println("open:", app.Count(telatest.ByRole("dialog", "Edit profile")) == 1)
app.Press("Escape")
fmt.Println("open:", app.Count(telatest.ByRole("dialog", "Edit profile")) == 1)
// Output:
// open: true
// open: false

Example (controlled)

t := &exampleT{}
defer t.close()
open := true
// A controlled dialog shows the state it is given and reports what the
// user asked for, here an Escape; the app decides whether it closes.
app := telatest.MountFunc(t, func() tela.Node {
return h.Fragment(kit.Dialog{
Open: &open,
OnOpenChange: func(next bool) { fmt.Println("asked to open:", next) },
Title: h.Text("Welcome"),
Description: h.Text("Controlled by the app's own state."),
})
})
app.Press("Escape")
fmt.Println("still open:", app.Count(telatest.ByRole("dialog", "Welcome")) == 1)
// Output:
// asked to open: false
// still open: true
func (d Dialog) Build() tela.Node

Build returns the trigger and the dialog’s portal.

type DialogStyle struct {
// Positioner is the layer over the window that holds the overlay and
// the dialog and centers the dialog, leaving shadcn's 1rem of room on
// each side (its max-w-[calc(100%-2rem)]).
Positioner string
// Overlay is the scrim behind the dialog, and OverlayExit the classes
// it transitions to while the dialog closes.
Overlay string
OverlayExit string
// Root is the dialog panel, and Exit the classes it transitions to
// while it closes (the reverse of its starting: classes).
Root string
Exit string
// Header holds the title and the description; it is drawn when the
// dialog has either.
Header string
Title string
Description string
Footer string
// Close is the button in the corner (unless HideClose) and CloseIcon its icon.
Close string
CloseIcon string
}

DialogStyle is the recipe of Dialog (shadcn/ui’s DialogOverlay, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter and the close button of DialogContent).

Root is the dialog element, the panel with the role. shadcn centers it with fixed top-[50%] left-[50%] and a translation; here the Positioner, a fixed layer over the window, centers it with flex, which gives the same box and lets the panel keep its own transform for the entrance.

type DropdownMenu[K comparable] struct {
// Trigger is the element that opens the menu.
Trigger Trigger
// Items are the entries of the menu, in order.
Items []MenuItem[K]
// OnSelect is called with the key of a plain item when it is chosen,
// after the item's own OnSelect.
OnSelect func(K)
// Placement is the side of the trigger the menu opens on: "bottom"
// (the default), "top", "left" or "right"; a Floating UI placement such
// as "bottom-start" also works. Align is where it sits along that side:
// "start", "center" (the default) or "end".
Placement, Align string
// Offset is the distance in pixels between the trigger and the menu;
// zero is 4 and [NoOffset] puts them together.
Offset float32
// Disabled makes the trigger inert.
Disabled bool
// AriaLabel names the menu when the trigger's text is not enough; the
// menu is otherwise named by its trigger.
AriaLabel string
// CheckIcon and RadioIcon replace the check of a checkbox item and the
// dot of a radio item.
CheckIcon, RadioIcon tela.Icon
// Open points to the open state of a controlled menu; nil leaves it to
// the component, which starts closed unless DefaultOpen is set.
// OnOpenChange is called with the next state.
Open *bool
DefaultOpen bool
OnOpenChange func(open bool)
// Class is added after the recipe's Content classes, to the menu's
// panel (the root element of a dropdown menu is its trigger, the app's).
Class string
// Props are more props for the panel element.
Props tela.Child
}

DropdownMenu is a menu of actions that opens from a button, shadcn/ui’s DropdownMenu: a press on the trigger, or Enter, Space or ArrowDown (and ArrowUp for the last item) when it has the focus, opens it below; the arrow keys, Home and End move through the items, typing jumps to one, Enter, Space or a click chooses it and Escape closes it. It is built on aria.UseMenuTrigger, aria.UsePopover and aria.UseMenu. The page is inert while it is open (it is modal, as Radix’s), and the focus returns to the trigger when it closes.

kit.DropdownMenu[string]{
Trigger: kit.Button{Variant: kit.Outline, Children: h.Text("Open")},
Align: "start",
Items: []kit.MenuItem[string]{
{Kind: kit.MenuLabel, Label: "My account"},
{Kind: kit.MenuSeparator},
{Key: "profile", Label: "Profile", Shortcut: "⇧⌘P"},
{Key: "billing", Label: "Billing", Shortcut: "⌘B"},
{Kind: kit.MenuSeparator},
{Key: "logout", Label: "Log out", Variant: kit.Destructive},
},
OnSelect: func(key string) { run(key) },
}

The items are [MenuItem]s: plain items with an icon, a shortcut and a destructive variant, checkbox items, radio groups, labels, separators and groups. There are no submenus yet (a known gap, ADR-141): a menu is one level.

The Trigger slot is a Trigger (see there): a Button, or anything made one with TriggerFunc. Open, DefaultOpen and OnOpenChange are the usual controlled and uncontrolled pair; the panel exists only while the menu is open. It opens on the side Placement says (“bottom” by default, “top”, “left” or “right”) and Align puts it along that side (“start”, “center” or “end”; Radix’s default is the center), 4 px away unless Offset says otherwise, and it flips and shifts to stay in the window; a menu taller than the room left scrolls.

Example

t := &exampleT{}
defer t.close()
dark := true
page := func() tela.Node {
return h.Div(h.Class("p-6"), kit.DropdownMenu[string]{
Trigger: kit.Button{Variant: kit.Outline, Children: h.Text("Account")},
Align: "start",
Items: []kit.MenuItem[string]{
{Kind: kit.MenuLabel, Label: "My account"},
{Kind: kit.MenuSeparator},
{Key: "profile", Label: "Profile", Shortcut: "⇧⌘P"},
{Kind: kit.MenuCheckbox, Key: "dark", Label: "Dark mode", Checked: &dark,
OnCheckedChange: func(on bool) { dark = on }},
{Kind: kit.MenuSeparator},
{Key: "logout", Label: "Log out", Variant: kit.Destructive},
},
OnSelect: func(key string) { fmt.Println("chose", key) },
})
}
app := telatest.MountFunc(t, page)
app.Click(telatest.ByRole("button", "Account"))
app.Click(telatest.ByRole("menuitem", "Profile"))
app.Click(telatest.ByRole("button", "Account"))
app.Click(telatest.ByRole("menuitemcheckbox", "Dark mode"))
fmt.Println("dark:", dark)
// Output:
// chose profile
// dark: false
func (d DropdownMenu[K]) Build() tela.Node

Build returns the trigger and the menu’s portal.

type Field struct {
// Label, Description and Error are plain text; empty draws nothing.
Label, Description, Error string
// Invalid makes the field invalid with no error message, such as when
// the message is shown elsewhere. A field with an Error is invalid
// already.
Invalid bool
// Orientation is Vertical (the zero value) or Horizontal.
Orientation Orientation
// ControlAfter puts the control after the label and the description, as
// in shadcn's switch rows (FieldContent first, then the Switch). It only
// affects a Horizontal field: a Vertical one is always the label, the
// control, the description and the error. The DOM order, and so the
// reading order of a screen reader, is the visual one; the label still
// points to the control, and the error and the invalid state still reach
// it.
ControlAfter bool
// Children is the control.
Children tela.Component
// Class is added after the recipe's Root classes. The other Class
// fields do the same for their part.
Class string
LabelClass string
ContentClass string
DescriptionClass string
ErrorClass string
// Props are more props for the root element.
Props tela.Child
}

Field is a form control with its label, its help text and its error, shadcn/ui’s Field. It does the wiring: the label points to the control (the control takes an id made with tela.UseID and the label’s For is the same), and the control is described by the error when there is one, else by the description. The control is the Children, an Input or a Textarea (more controls of the kit take the id the same way as they arrive).

kit.Field{
Label: "Email",
Description: "We never share it.",
Error: errs.Email, // empty: no error
Children: kit.Input{Type: "email", Value: &email, OnValueChange: setEmail.Set},
}

The error shows as an alert below the control, and the field is invalid while it shows one: the control is marked h.AriaInvalid (a red border and ring from the recipe’s aria-invalid: classes) and the label turns red.

A Horizontal field puts the control before its text, as shadcn does for a checkbox or a radio. A switch row has the text first: set ControlAfter.

kit.Field{
Orientation: kit.Horizontal,
ControlAfter: true,
Label: "Marketing emails",
Description: "News about new features and offers.",
Class: "items-center gap-6",
Children: kit.Switch{Checked: &on, OnCheckedChange: setOn.Set},
}

To name a group of controls (a RadioGroup, a ToggleGroup) use a FieldSet and its legend: a Field’s label points to one control.

Example

t := &exampleT{}
defer t.close()
app := telatest.Mount(t, emailForm{})
app.Fill(telatest.ByRole("textbox", "Email"), "ana@example.com")
app.ExpectValue(telatest.ByRole("textbox", "Email"), "ana@example.com")
fmt.Println("the label names the input")
// Output: the label names the input
func (f Field) Build() tela.Node

Build returns the field element: a group holding the label, the control, the description and the error.

type FieldSet struct {
// Legend is the caption of the group, plain text; empty draws none.
Legend string
// LegendVariant is [LegendVariantLegend] (the zero value, a title) or
// [LegendVariantLabel] (a caption the size of a field's label).
LegendVariant LegendVariant
// Description is help text under the legend; empty draws none.
Description string
// Children are the controls, laid out in a column.
Children tela.Component
// Class is added after the recipe's Root classes. The other Class
// fields do the same for their part; ContentClass is for the column of
// the description and the children (set its gap here).
Class string
LegendClass string
DescriptionClass string
ContentClass string
// Props are more props for the root element.
Props tela.Child
}

FieldSet is a group of controls with a caption, shadcn/ui’s FieldSet with its FieldLegend and FieldDescription. It is a group (role “group”) named by the legend and described by the description. A RadioGroup or a ToggleGroup inside it takes its name from the legend, so it needs no AriaLabel and no Label that points at nothing; a group of its own AriaLabel keeps it.

kit.FieldSet{
Legend: "Theme",
LegendVariant: kit.LegendVariantLabel,
Description: "Pick how the app looks.", // optional
Children: kit.RadioGroup[string]{Items: themes, Value: &theme, OnValueChange: setTheme.Set},
}

The children are laid out in a column with a gap of 24 px (12 px for a RadioGroup given directly as Children, as in shadcn). Give several as h.Fragment, such as a few [Field]s:

kit.FieldSet{Legend: "Address", Description: "Where we send the invoice.",
Children: h.Fragment(
kit.Field{Label: "Street", Children: kit.Input{}},
kit.Field{Label: "City", Children: kit.Input{}})}

shadcn’s FieldGroup, the column that stacks fields with a 28 px gap, has no component: a FieldSet is already a column, and for any other place h.Div(h.Class(“flex w-full flex-col gap-7”), …) is the same.

The legend is a block above the children whose margin (mb-3) separates it from them; the description is pulled up under a legend of the default variant (-mt-1.5). shadcn’s nth-last-2:-mt-1, which pulls a description that has exactly one item after it, is not applied to the label variant: add -mt-1 to DescriptionClass for it.

Example

t := &exampleT{}
defer t.close()
plan := "free"
page := func() tela.Node {
return h.Div(h.Class("p-6"),
kit.FieldSet{Legend: "Plan", LegendVariant: kit.LegendVariantLabel,
Children: kit.RadioGroup[string]{Items: plans, Value: &plan, OnValueChange: func(k string) { plan = k }}})
}
app := telatest.MountFunc(t, page)
app.Click(telatest.ByRole("radio", "Pro"))
fmt.Println("plan", plan)
// Output: plan pro
func (f FieldSet) Build() tela.Node

Build returns the group element: the legend, then the description and the children in a column.

type FieldSetStyle struct {
// Root is the group: the legend and, under it, the column of parts. A
// fieldset is a block, and its legend is not one of the column's items,
// so the legend's margin alone separates the two (as in a browser).
Root string
// Content is the column that holds the description and the children
// (shadcn's fieldset: flex flex-col gap-6).
Content string
// ContentWithRadioGroup is added to Content when the children are a
// [RadioGroup] (shadcn's has-[>[data-slot=radio-group]]:gap-3).
ContentWithRadioGroup string
// Legend is the legend's classes. It has w-fit because a browser's
// legend is as wide as its text.
Legend string
// LegendVariants are the sizes of the legend.
LegendVariants map[LegendVariant]string
// Description is the help text under the legend.
Description string
// DescriptionAfterLegend is added to Description for each legend
// variant that pulls the description up under it (shadcn's
// [[data-variant=legend]+&]:-mt-1.5, which only the legend variant has).
DescriptionAfterLegend map[LegendVariant]string
}

FieldSetStyle is the recipe of FieldSet (shadcn/ui’s FieldSet, FieldLegend and FieldDescription).

type FieldStyle struct {
Root string
// Orientations are the arrangement of the control and its text:
// Vertical (the label above the control) and Horizontal (the control
// first, then the text; the text first with Field.ControlAfter).
Orientations map[Orientation]string
// HorizontalWithContent is added to Root when a Horizontal field has a
// Content part (shadcn's has-[>[data-slot=field-content]]:).
HorizontalWithContent string
// Label is added to the [Label] recipe's classes for the field's label.
Label string
// HorizontalLabel is added to Label when a Horizontal field has no
// Content part and the label sits next to the control.
HorizontalLabel string
// Content holds the label, description and error of a Horizontal
// field that has a description or an error.
Content string
Description string
// DescriptionWithError is added to Description when the field shows an
// error below it (shadcn's nth-last-2:).
DescriptionWithError string
// Invalid is added to Root when the field is invalid (shadcn's
// data-[invalid=true]:): the label and the text turn red.
Invalid string
Error string
}

FieldStyle is the recipe of Field (shadcn/ui’s Field, FieldLabel, FieldContent, FieldDescription and FieldError).

type Input struct {
// Value points to the text of a controlled field; nil leaves the text
// to the field, which starts with DefaultValue.
Value *string
// DefaultValue is the initial text of an uncontrolled field.
DefaultValue string
// OnValueChange is called with the new text after every edit. For a
// controlled field it is how the change is asked for.
OnValueChange func(string)
// Type is "text" (the default), "password", "email", "search", "url",
// "tel" or "number" (h.Type).
Type string
Placeholder string
// Name names the field in its h.Form.
Name string
// ID is the field's id, for a [Label]'s For. Inside a [Field] it
// defaults to the field's.
ID string
// AriaLabel names a field that has no label of its own, such as a
// search box; a [Field]'s Label or a [Label] pointing to the field
// names it otherwise.
AriaLabel string
Disabled bool
ReadOnly bool
// Invalid marks the value as wrong: assistive technologies announce it
// and the recipe's aria-invalid: classes (a red border and ring) apply.
// A [Field] with an Error sets it on its control.
Invalid bool
AutoFocus bool
// MaxLength limits what the user can type, in UTF-16 code units; 0 is
// no limit.
MaxLength int
// Class is added after the recipe's classes.
Class string
// Props are more props for the input element.
Props tela.Child
}

Input is a single-line text field, shadcn/ui’s Input. Its value follows the controlled and uncontrolled rule of the whole kit: give Value (a pointer to the state) and OnValueChange to keep the value yourself, or DefaultValue to let the field keep it.

name, setName := tela.UseState("")
kit.Input{Placeholder: "Name", Value: &name, OnValueChange: setName.Set}
kit.Input{Type: "email", DefaultValue: "ana@example.com", Name: "email"}

Inside a Field the input takes the field’s ID, so the field’s label names it, and is described by the field’s description or error.

func (i Input) Build() tela.Node

Build returns the input element.

type InputStyle struct {
Root string
}

InputStyle is the recipe of Input (shadcn/ui’s Input).

type Label struct {
// For is the ID of the control the label names (h.For).
For string
// Disabled dims the label and makes it ignore the pointer; a control
// that is a sibling after a label with the class "peer" dims it with
// peer-disabled: without it.
Disabled bool
// Children is the text.
Children tela.Component
// Class is added after the recipe's classes.
Class string
// Props are more props for the label element.
Props tela.Child
}

Label is the caption of a form control, shadcn/ui’s Label. Clicking it focuses the control it labels, which also gets its accessible name from it. It labels the control whose ID is its For, or else the field or button inside it.

kit.Label{For: "email", Children: h.Text("Email")}
kit.Input{ID: "email", Type: "email"}

A Field wires a label and its control with no ids to write.

func (l Label) Build() tela.Node

Build returns the label element.

type LabelStyle struct {
Root string
// Disabled is added when the label is Disabled (shadcn's
// group-data-[disabled=true]:).
Disabled string
}

LabelStyle is the recipe of Label (shadcn/ui’s Label).

type LegendVariant string

LegendVariant is the look of a FieldSet’s legend (shadcn’s FieldLegend variant): the size of its text. Like Variant it is open: an app adds a variant by naming it in the recipe’s LegendVariants map.

const (
// LegendVariantLegend is the default: a title, text-base. The zero
// value is the same variant.
LegendVariantLegend LegendVariant = "legend"
// LegendVariantLabel is a caption for a group of controls, text-sm, as
// a [Field]'s label is.
LegendVariantLabel LegendVariant = "label"
)
type MenuItem[K comparable] struct {
// Kind is what the entry is; the zero value is a plain item.
Kind MenuKind
// Key identifies a plain, checkbox or radio item, and a radio group.
Key K
// Label is the item's text, a label's text, or a group's accessible name.
Label string
// Icon is drawn before the label of a plain item.
Icon tela.Icon
// Shortcut is text shown at the end of the item, such as "⌘T". It is
// only shown: binding the keys is the app's.
Shortcut string
// Variant is Default or Destructive (a plain item, as shadcn's
// variant="destructive").
Variant Variant
// Disabled makes the item (for a radio group or a group, all of its
// items) impossible to choose.
Disabled bool
// Inset pads a plain item or a label like the ones with a check or an
// icon, so that their texts line up (shadcn's inset).
Inset bool
// KeepOpen keeps the menu open when this item is chosen, such as a
// checkbox toggled in a row.
KeepOpen bool
// OnSelect is called when a plain item is chosen.
OnSelect func()
// Checked points to the state of a controlled checkbox item; nil leaves
// it to the menu, which starts at DefaultChecked. OnCheckedChange is
// called with the next state.
Checked *bool
DefaultChecked bool
OnCheckedChange func(checked bool)
// Value points to the chosen item of a controlled radio group; nil
// leaves it to the menu, which starts at DefaultValue (an item whose
// Key it equals; nil: none chosen, a pointer so that the zero K can be
// a key). OnValueChange is called with the Key of the item the user
// chose.
Value *K
DefaultValue *K
OnValueChange func(K)
// Items are the radio items of a radio group, or the entries of a
// group.
Items []MenuItem[K]
// Class is added after the recipe's classes, to the item.
Class string
}

MenuItem is one entry of a menu, whatever its kind: the model that DropdownMenu, Menubar and ContextMenu share. K is the type of the keys, which name the items that can be chosen. A menu is a slice of them:

[]kit.MenuItem[string]{
{Kind: kit.MenuLabel, Label: "My account"},
{Key: "profile", Label: "Profile", Shortcut: "⇧⌘P", OnSelect: openProfile},
{Key: "billing", Label: "Billing"},
{Kind: kit.MenuSeparator},
{Kind: kit.MenuCheckbox, Key: "bars", Label: "Status bar", DefaultChecked: true},
{Kind: kit.MenuSeparator},
{Kind: kit.MenuRadioGroup, Key: "theme", DefaultValue: new("light"), Items: []kit.MenuItem[string]{
{Key: "light", Label: "Light"}, {Key: "dark", Label: "Dark"}}},
{Key: "logout", Label: "Log out", Variant: kit.Destructive},
}

Every item that can be chosen (a plain item, a checkbox item, a radio item) needs a Key distinct from the others of its menu; it is what the menu’s OnSelect reports and what the keyboard’s typeahead and the accessibility tree tell items apart by. A menu with two items of the same Key panics in dev mode. Fields that a kind does not use are ignored.

Choosing a plain item calls its OnSelect and the menu’s, and closes the menu; a checkbox item toggles and a radio item chooses, and they close the menu too (Radix’s default) unless KeepOpen is set. A disabled item is skipped by the keyboard and dimmed.

Checkbox items and radio groups follow the kit’s value rule: Checked (a pointer to your state) with OnCheckedChange controls a checkbox item, DefaultChecked starts an uncontrolled one; Value with OnValueChange controls a radio group, DefaultValue starts one. An uncontrolled state is kept by the menu component, so it survives the menu closing and opening.

type MenuKind int

MenuKind is what a MenuItem is: an item that does something, a checkbox item, a group of radio items, a label, a separator or a group.

const (
// MenuPlain is an item that does something when it is chosen
// (shadcn's DropdownMenuItem): Key, Label, Icon, Shortcut, Variant,
// Disabled, Inset, OnSelect.
MenuPlain MenuKind = iota
// MenuCheckbox is an item that is on or off (DropdownMenuCheckboxItem):
// Key, Label, Shortcut, Disabled and Checked/DefaultChecked/
// OnCheckedChange.
MenuCheckbox
// MenuRadioGroup is a set of exclusive items (DropdownMenuRadioGroup
// with its RadioItems): Key names the group and Items are the radio
// items, whose Key is their value. Value/DefaultValue/OnValueChange say
// which is chosen.
MenuRadioGroup
// MenuLabel is a heading (DropdownMenuLabel): Label and Inset.
MenuLabel
// MenuSeparator is a line between items (DropdownMenuSeparator).
MenuSeparator
// MenuGroup gathers Items under the role "group" (DropdownMenuGroup);
// Label, when set, is its accessible name (it is not drawn: put a
// MenuLabel in Items for a visible heading).
MenuGroup
)

The kinds of MenuItem. MenuPlain is the zero value, so an item that only has a Key and a Label is an ordinary one.

type MenuStyle struct {
// Content is the panel: surface, border, shadow, padding, its minimum
// width and the entrance. It scrolls when there is no room for it.
Content string
// Exit is what the panel transitions to as it closes.
Exit string
// Sides adds classes for the side of the trigger the panel is on
// ("top", "right", "bottom" or "left"): the transform origin and the
// slide it enters with, as [PopoverStyle].Sides.
Sides map[string]string
// Item is a plain item, ItemVariants adds the classes of a Variant
// (Destructive, and the empty key for the default), ItemInset the
// padding of an inset item.
Item string
ItemVariants map[Variant]string
ItemInset string
// ItemIcon is the icon of a plain item and ItemIconVariants its color
// per Variant.
ItemIcon string
ItemIconVariants map[Variant]string
// CheckboxItem and RadioItem are the items with an indicator at the
// start; Indicator is the box that holds it, CheckIcon the check and
// RadioIcon the dot.
CheckboxItem string
RadioItem string
Indicator string
CheckIcon string
RadioIcon string
// Label is a heading, LabelInset its padding when inset.
Label string
LabelInset string
// Separator is the line between items.
Separator string
// Shortcut is the text at the end of an item.
Shortcut string
}

MenuStyle is the recipe of the panel of a menu and its items, shared by DropdownMenu and ContextMenu (shadcn/ui’s DropdownMenuContent and ContextMenuContent and what they hold) and used by Menubar for its menus.

type Menubar[K comparable] struct {
// Menus are the bar's menus, in order.
Menus []MenubarMenu[K]
// OnSelect is called with the key of the menu and of the plain item
// that was chosen, after the item's own OnSelect.
OnSelect func(menu, item K)
// AriaLabel names the bar.
AriaLabel string
// Direction is "ltr" or "rtl"; empty is the direction at the component's
// position (tela.UseDirection: the nearest h.Dir above it, left to right
// without one). Under "rtl" ArrowLeft goes to the next menu and ArrowRight to the previous one; a Direction given here also
// sets the direction of the layout.
Direction string
// CheckIcon and RadioIcon replace the check of a checkbox item and the
// dot of a radio item.
CheckIcon, RadioIcon tela.Icon
// Open points to the open menu of a controlled bar; nil leaves it to
// the component, which starts with DefaultOpen (nil: all closed).
// OnOpenChange is called with the menu that opens (open true) or the one
// that closes; moving from one menu to another is one call, with the one
// that opens.
Open *K
DefaultOpen *K
OnOpenChange func(menu K, open bool)
// Class is added after the recipe's Root classes, to the bar;
// TriggerClass does the same for the menus' buttons and ContentClass for
// the menus' panels.
Class string
TriggerClass string
ContentClass string
// Props are more props for the bar element.
Props tela.Child
}

Menubar is a row of menus, such as the File, Edit and View of an application, shadcn/ui’s Menubar, built on aria.UseMenuBar: the bar is one tab stop, ArrowLeft and ArrowRight move between the menus’ buttons (and, with a menu open, open the neighbor’s), Home and End jump, ArrowDown, Enter and Space open the menu on its first item and ArrowUp on its last; once a menu is open, hovering another button opens that one. A press on the open menu’s button, Escape, a press outside and Tab close it.

kit.Menubar[string]{
AriaLabel: "Application",
Menus: []kit.MenubarMenu[string]{
{Key: "file", Label: "File", Items: []kit.MenuItem[string]{
{Key: "new", Label: "New Tab", Shortcut: "⌘T"},
{Key: "window", Label: "New Window", Shortcut: "⌘N"},
{Kind: kit.MenuSeparator},
{Key: "print", Label: "Print...", Shortcut: "⌘P"},
}},
{Key: "edit", Label: "Edit", Items: edit},
},
OnSelect: func(menu, item string) { run(menu, item) },
}

The items are the [MenuItem]s of DropdownMenu, with no submenus yet (ADR-141). The keys of the menus and of the items have the same type K; an item key needs to be distinct only within its menu. The open menu follows the kit’s value rule with Open (a pointer to the key of the open menu; a key no menu has, such as the zero K, means all closed), DefaultOpen and OnOpenChange.

Example

t := &exampleT{}
defer t.close()
page := func() tela.Node {
return h.Div(h.Class("p-6"), kit.Menubar[string]{
AriaLabel: "Application",
Menus: []kit.MenubarMenu[string]{
{Key: "file", Label: "File", Items: []kit.MenuItem[string]{
{Key: "new", Label: "New Tab", Shortcut: "⌘T"},
{Kind: kit.MenuSeparator},
{Key: "print", Label: "Print...", Shortcut: "⌘P"},
}},
{Key: "edit", Label: "Edit", Items: []kit.MenuItem[string]{
{Key: "undo", Label: "Undo", Shortcut: "⌘Z"},
}},
},
OnSelect: func(menu, item string) { fmt.Println(menu, item) },
})
}
app := telatest.MountFunc(t, page)
app.Click(telatest.ByRole("menuitem", "File"))
// With a menu open, hovering another button opens its menu.
app.Hover(telatest.ByRole("menuitem", "Edit"))
app.Click(telatest.ByRole("menuitem", "Undo"))
// Output: edit undo
func (m Menubar[K]) Build() tela.Node

Build returns the bar and, in a portal, the open menu.

type MenubarMenu[K comparable] struct {
// Key identifies the menu (what Menubar.Open and OnOpenChange name).
Key K
// Label is the text of the menu's button in the bar.
Label string
// Disabled makes the menu impossible to open.
Disabled bool
// Items are the entries of the menu.
Items []MenuItem[K]
}

MenubarMenu is one menu of a Menubar: the label on the bar and the items it opens.

type MenubarStyle struct {
// Root is the bar.
Root string
// Trigger is a menu's button in the bar; aria-expanded: styles the one
// whose menu is open.
Trigger string
// Menu is the recipe of the menus and their items.
Menu MenuStyle
}

MenubarStyle is the recipe of Menubar (shadcn/ui’s Menubar, MenubarTrigger and MenubarContent with the items they hold).

type Orientation string

Orientation is the direction a component lays its parts out in. The zero value is the component’s own default: Horizontal for a Separator, Vertical for a Field.

const (
Horizontal Orientation = "horizontal"
Vertical Orientation = "vertical"
)
type Popover struct {
// Trigger is the element the popover is anchored to and that opens it.
Trigger Trigger
// Children is the content of the panel.
Children tela.Component
// Placement is the side of the trigger the panel opens on: "bottom"
// (the default), "top", "left" or "right"; a Floating UI placement such
// as "bottom-start" also works. Align is where it sits along that side:
// "start", "center" (the default) or "end".
Placement, Align string
// Offset is the distance in pixels between the trigger and the panel;
// zero is 4 and [NoOffset] puts them together.
Offset float32
// Modal makes the rest of the window inert while the popover is open,
// as a dialog does. The default is a popover that leaves the page
// usable.
Modal bool
// AriaLabel names the panel (it is a dialog for assistive technologies).
AriaLabel string
// Open points to the open state of a controlled popover; nil leaves the
// state to the popover, which starts closed unless DefaultOpen is set.
// OnOpenChange is called with the next state when the trigger, Escape or
// a press outside asks for a change.
Open *bool
DefaultOpen bool
OnOpenChange func(open bool)
// Disabled disables the trigger, which is then inert and dimmed by its
// recipe; the popover does not open. Set it here: the trigger props
// override a Disabled given to the trigger component itself.
Disabled bool
// Class is added after the recipe's Root classes, to the panel.
Class string
// Props are more props for the panel element.
Props tela.Child
}

Popover is a panel anchored to its trigger, shadcn/ui’s Popover: it opens next to the trigger, scales and fades in, and closes with Escape, a press outside it or the focus moving elsewhere. It does not block the page (it is not modal): the rest of the window stays in the accessibility tree and can take the focus. The press that closes it is not delivered to what is under it, as in React Aria. The focus moves into the panel when it opens and returns to the trigger when it closes. A press on the trigger toggles it.

kit.Popover{
Trigger: kit.Button{Variant: kit.Outline, Children: h.Text("Dimensions")},
Placement: "bottom", Align: "start",
Children: dimensionsForm,
}

The Trigger slot is a Trigger: a Button, or anything made one with TriggerFunc. The panel is placed with Floating UI’s algorithm: it flips to the other side when there is no room and shifts to stay in the window. Open, DefaultOpen and OnOpenChange are the usual controlled and uncontrolled pair; the panel exists only while it is open.

Example

t := &exampleT{}
defer t.close()
page := func() tela.Node {
return h.Div(h.Class("p-6"), kit.Popover{
Trigger: kit.Button{Variant: kit.Outline, Children: h.Text("Dimensions")},
Placement: "bottom", Align: "start",
AriaLabel: "Dimensions",
Children: h.P(h.Text("Set the dimensions for the layer.")),
})
}
app := telatest.MountFunc(t, page)
trigger := telatest.ByRole("button", "Dimensions")
app.Click(trigger)
fmt.Println("open:", app.Count(telatest.ByRole("dialog", "Dimensions")) == 1)
app.Click(trigger) // the trigger toggles it
fmt.Println("open:", app.Count(telatest.ByRole("dialog", "Dimensions")) == 1)
// Output:
// open: true
// open: false
func (p Popover) Build() tela.Node

Build returns the trigger and the popover’s portal.

type PopoverStyle struct {
// Root is the panel: surface, border, shadow, padding and the
// entrance. Its width is w-72 (18rem); override it with Class.
Root string
// Exit is what the panel transitions to as it closes.
Exit string
// Sides adds classes for the side of the trigger the panel is on
// ("top", "right", "bottom" or "left"): the transform origin of the
// entrance, which scales out of the trigger's edge, and the slide of
// 0.5rem it starts from (shadcn's data-[side=bottom]:slide-in-from-top-2
// and the like).
Sides map[string]string
}

PopoverStyle is the recipe of Popover (shadcn/ui’s PopoverContent).

type Progress struct {
// Value is how far the task is, from 0 to Max.
Value float64
// Max is the value of a finished task (100 by default).
Max float64
// Indeterminate says that the amount of progress is unknown: the bar
// has no value and pulses.
Indeterminate bool
// FormatValue returns Value as people read it (aria-valuetext); the
// default is the percentage, "40%".
FormatValue func(float64) string
// AriaLabel names the bar.
AriaLabel string
// Class is added after the recipe's classes; IndicatorClass does the
// same for the bar.
Class string
IndicatorClass string
// Props are more props for the track element.
Props tela.Child
}

Progress is a bar that shows how far a task is, shadcn/ui’s Progress, on aria.UseProgressBar: it has the progressbar role, its range and aria-valuenow, and the bar moves with a transition when Value changes.

kit.Progress{Value: uploaded, AriaLabel: "Upload"} // 0 to 100
kit.Progress{Value: 512, Max: 2048, FormatValue: func(v float64) string { return fmt.Sprintf("%.0f MB", v) }}

Use Indeterminate while the amount is unknown. For a measure that is not the progress of a task, such as disk usage, use aria.UseMeter.

Example

t := &exampleT{}
defer t.close()
page := func() tela.Node {
return h.Div(h.Class("w-64 p-6"), kit.Progress{Value: 40, AriaLabel: "Upload"})
}
app := telatest.MountFunc(t, page)
app.ExpectRole("progressbar", "Upload")
fmt.Println("progress shown")
// Output: progress shown
func (p Progress) Build() tela.Node

Build returns the progress element.

type ProgressStyle struct {
// Root is the track, which clips the indicator.
Root string
// Indicator is the bar; it is as wide as the track and moves toward the
// start (left, or right under a right-to-left direction, with the
// rtl: class the component adds) by the part that is not done.
Indicator string
// Indeterminate is added to the indicator while the amount of progress
// is unknown (a pulsing, full bar; shadcn draws an empty one).
Indeterminate string
}

ProgressStyle is the recipe of Progress (shadcn/ui’s Progress).

type RadioGroup[K comparable] struct {
// Items are the options, in order.
Items []RadioItem[K]
// Value points to the checked key of a controlled group; nil leaves it
// to the group, which starts at DefaultValue (nil: nothing checked).
Value *K
DefaultValue *K
// OnValueChange is called with the next key when the user checks an
// option. For a controlled group it is how the change is asked for.
OnValueChange func(K)
// Orientation is Vertical (the default) or Horizontal; it is the
// group's aria-orientation and the arrangement of the options.
Orientation Orientation
// Direction is "ltr" or "rtl"; empty is the direction at the component's
// position (tela.UseDirection: the nearest h.Dir above it, left to right
// without one). Under "rtl" ArrowLeft goes to the next option and ArrowRight to the previous one; a Direction given here also
// sets the direction of the layout.
Direction string
// Disabled disables every option.
Disabled bool
// ReadOnly keeps the options focusable but not changeable.
ReadOnly bool
// Required marks the group as one the user must answer.
Required bool
// Invalid marks the group wrong: the circles take the invalid look. A
// [Field] with an Error sets it.
Invalid bool
// AriaLabel names the group; without it a [FieldSet]'s legend does.
AriaLabel string
// Render draws one option in place of the default (circle, label and
// description); see [RadioState]. It runs while the group builds.
Render func(item RadioItem[K], state RadioState) tela.Node
// Class is added after the recipe's classes. OptionClass, ItemClass,
// LabelClass and DescriptionClass do the same for their parts of the
// default look.
Class string
OptionClass string
ItemClass string
LabelClass string
DescriptionClass string
// Props are more props for the group element.
Props tela.Child
}

RadioGroup is a set of options of which one is chosen, shadcn/ui’s RadioGroup, on aria.UseRadioGroup: the arrow keys (all four) move the focus to the next option and check it, Space checks, and Tab enters the group at the checked option. The value follows the kit’s rule for a choice among keys: Value (a pointer to the key), DefaultValue and OnValueChange; nothing is checked while there is no value.

plan, setPlan := tela.UseState("pro")
kit.RadioGroup[string]{
AriaLabel: "Plan", Value: &plan, OnValueChange: setPlan.Set,
Items: []kit.RadioItem[string]{
{Value: "free", Label: "Free"},
{Value: "pro", Label: "Pro", Description: "For small teams."},
},
}

To draw the options yourself, such as theme cards, give Render: it is called for each option with the state and the props of its radio, and the group keeps the behavior.

kit.RadioGroup[string]{AriaLabel: "Theme", DefaultValue: new("light"), Orientation: kit.Horizontal,
Items: themes, Render: func(it kit.RadioItem[string], st kit.RadioState) tela.Node {
return h.Button(st.Props, cls("rounded-lg border p-4 aria-checked:border-primary"),
h.Text(it.Label))
}}

Inside a Field the group takes the field’s description and invalid state. Name it with AriaLabel, or put it in a FieldSet whose Legend names it.

Example

t := &exampleT{}
defer t.close()
plan := "free"
page := func() tela.Node {
return h.Div(h.Class("p-6"), kit.RadioGroup[string]{
AriaLabel: "Plan", DefaultValue: &plan, OnValueChange: func(k string) { plan = k },
Items: []kit.RadioItem[string]{
{Value: "free", Label: "Free"},
{Value: "pro", Label: "Pro", Description: "For small teams."},
}})
}
app := telatest.MountFunc(t, page)
app.Click(telatest.ByRole("radio", "Pro"))
fmt.Println("plan", plan)
// Output: plan pro
func (g RadioGroup[K]) Build() tela.Node

Build returns the group element.

type RadioGroupStyle struct {
// Root is the group: the options in a grid.
Root string
// Orientations are the arrangement of the options: Vertical (a column)
// and Horizontal (a row).
Orientations map[Orientation]string
// Option is the row of one option: the circle and its text.
Option string
// OptionWithDescription is added to an option that has a Description:
// the circle sits at the top, beside the text.
OptionWithDescription string
// Item is the radio circle (shadcn's RadioGroupItem): size, border,
// focus ring, disabled and invalid. It has the class "peer", so the
// label dims with it, and its content (the dot) is centered, as a
// browser centers a button's.
Item string
// ItemWithDescription is added to the circle of an option that has a
// Description (shadcn's mt-px of a Field's radio).
ItemWithDescription string
// Indicator holds the dot and is only there while checked.
Indicator string
// Icon is the dot.
Icon string
// Label is added to the [Label] recipe's classes for an option's label.
Label string
// LabelWithDescription is added to Label when the option has a
// Description (a [Field]'s label: snug leading).
LabelWithDescription string
// Content holds the label and the description of an option that has a
// Description.
Content string
// Description is the help text under an option's label.
Description string
}

RadioGroupStyle is the recipe of RadioGroup (shadcn/ui’s RadioGroup and RadioGroupItem, with the label arrangement of its documentation and of Field).

type RadioItem[K comparable] struct {
// Value is the option's key, what the group's value holds.
Value K
// Label is the option's text, which names the radio.
Label string
// Description is help text under the label.
Description string
// Disabled makes the option inert and dims it.
Disabled bool
}

RadioItem is one option of a RadioGroup.

type RadioState struct {
// Checked is whether the option is the group's value.
Checked bool
// Disabled is whether the option, or the whole group, is disabled.
Disabled bool
// Invalid is whether the group is invalid.
Invalid bool
// ID is the id of the radio element, for a [Label]'s For.
ID string
// Props make an element the radio: its role, aria-checked, id, tab
// stop and handlers (a click, Space and the arrow keys check it). Give
// them to the one element the user presses, usually an h.Button, and
// style it with the checked: and aria-checked: variants.
Props tela.Prop
// Indicator is the circle of the default look (with its dot while
// Checked), not interactive: put it in your item to keep the familiar
// mark, or leave it out.
Indicator tela.Component
}

RadioState is what a RadioGroup’s Render func is given about one option, to build a custom item that the group still drives.

type RangeCalendar struct {
// Value points to the selected range of a controlled calendar; nil
// leaves it to the calendar, which starts at DefaultValue.
Value *aria.DateRange
DefaultValue aria.DateRange
// OnValueChange is called with the range the user completes.
OnValueChange func(aria.DateRange)
// AllowsNonContiguousRanges lets a range cross unavailable dates.
AllowsNonContiguousRanges bool
Min, Max aria.Date
IsDateUnavailable func(aria.Date) bool
Disabled, ReadOnly bool
FocusedDate *aria.Date
DefaultFocusedDate aria.Date
OnFocusChange func(aria.Date)
VisibleMonths int
FirstDayOfWeek *time.Weekday
FixedWeeks bool
HideOutsideDays bool
NarrowWeekdays bool
Today aria.Date
Locale, Direction string
AriaLabel string
PreviousLabel, NextLabel string
PreviousIcon, NextIcon tela.Icon
Class string
MonthsClass string
MonthClass string
NavClass string
CaptionClass string
WeekdayClass string
DayClass string
DayButtonClass string
Props tela.Child
// contains filtered or unexported fields
}

RangeCalendar is a Calendar that selects a span of dates, shadcn/ui’s Calendar in range mode on aria.UseRangeCalendar. Selecting takes two presses: the first anchors the range, the pointer over a date or the focus on it previews the range up to it (the band of accent color between the two ends), and the second completes it, the earlier date first. Escape cancels an anchored range. While one is anchored, dates beyond the nearest unavailable one are not selectable, unless AllowsNonContiguousRanges.

var stay aria.DateRange
kit.RangeCalendar{VisibleMonths: 2, OnValueChange: func(r aria.DateRange) { stay = r }}

The value follows the kit’s rule with aria.DateRange: Value (a pointer) with OnValueChange controls it, DefaultValue starts an uncontrolled one; OnValueChange is called when a range is completed, not when it is anchored. The other fields are those of Calendar.

Example

t := &exampleT{}
defer t.close()
var stay aria.DateRange
page := func() tela.Node {
return h.Div(h.Class("p-6"), kit.RangeCalendar{
Today: aria.Date{Year: 2026, Month: time.October, Day: 25},
VisibleMonths: 2,
OnValueChange: func(r aria.DateRange) { stay = r },
})
}
app := telatest.MountFunc(t, page, telatest.Size(800, 400))
// The first press anchors the range, the second completes it.
app.Click(telatest.ByRole("gridcell", "Tuesday, October 27, 2026"))
app.Click(telatest.ByRole("gridcell", "Thursday, November 5, 2026"))
fmt.Println(stay.Start, stay.End)
// Output: 2026-10-27 2026-11-05
func (c RangeCalendar) Build() tela.Node

Build returns the calendar element.

type Select[K comparable] struct {
// Items are the options, in order.
Items []SelectItem[K]
// Value points to the chosen option of a controlled Select; nil leaves
// the choice to the component, which starts at DefaultValue.
Value *K
// DefaultValue points to the option an uncontrolled Select starts with;
// nil starts with nothing chosen.
DefaultValue *K
// OnValueChange is called with the key of the option the user chose. For
// a controlled Select it is how the change is asked for.
OnValueChange func(K)
// Open points to the open state of a controlled list; nil leaves it to
// the component, which starts at DefaultOpen. OnOpenChange is called with
// the next state.
Open *bool
DefaultOpen bool
OnOpenChange func(open bool)
// Placeholder is shown while nothing is chosen.
Placeholder string
// Size is Default (36 px) or Sm (32 px).
Size Size
// Disabled makes the select inert and dims it.
Disabled bool
// Invalid marks the choice as wrong: assistive technologies announce it
// and the recipe's aria-invalid: classes (a red border and ring) apply.
// A [Field] with an Error sets it.
Invalid bool
// ID is the trigger's id, for a [Label]'s For. Inside a [Field] it
// defaults to the field's.
ID string
// AriaLabel names the select when no label is tied to it; inside a Field
// with a Label that label is used.
AriaLabel string
// Icon replaces the chevron at the end of the trigger, CheckIcon the
// check that marks the chosen option.
Icon, CheckIcon tela.Icon
// Class is added after the recipe's Root classes (the trigger's); the
// other Class fields do the same for their part: the popover, an item,
// and a group's label.
Class string
ContentClass string
ItemClass string
LabelClass string
// Props are more props for the trigger element.
Props tela.Child
}

Select is a dropdown that picks one of a list of options, shadcn/ui’s Select: a trigger that shows the chosen option and a popover list that opens below it. It is built on aria.UseSelect: a press, Enter, Space and the arrow keys open the list, the arrows and Home and End move through it, typing jumps to an option, Enter or a click chooses it, and Escape closes it. On the closed trigger, typing and the left and right arrows choose without opening.

kit.Select[string]{
Placeholder: "Select a fruit",
Items: []kit.SelectItem[string]{
{Value: "apple", Label: "Apple"},
{Value: "banana", Label: "Banana"},
{Value: "grape", Label: "Grape", Disabled: true},
},
OnValueChange: func(v string) { fmt.Println("chose", v) },
}

The chosen option follows the kit’s value rule: Value (a pointer to your state) with OnValueChange controls it, DefaultValue starts an uncontrolled one. Value and DefaultValue choose the option whose Value equals them; when none does, or DefaultValue is nil, nothing is chosen and the placeholder shows. Both are pointers, so that the zero value of K (the int 0, say) can be an option: new(0) chooses it, nil chooses nothing.

Inside a Field the trigger takes the field’s id, so the label points to it and names it, the field’s description or error describes it, and an error marks it invalid.

The list is a deviation from shadcn in one place: it always opens below (or above, when there is no room) the trigger, like shadcn’s position=“popper”, where shadcn’s default aligns the chosen option over the trigger; and it has no scroll buttons, since the list scrolls.

Example

t := &exampleT{}
defer t.close()
var chosen string
page := kit.Select[string]{
Placeholder: "Select a fruit",
AriaLabel: "Fruit",
Items: []kit.SelectItem[string]{
{Value: "apple", Label: "Apple"},
{Value: "banana", Label: "Banana"},
},
OnValueChange: func(v string) { chosen = v },
}
app := telatest.Mount(t, page)
app.Click(telatest.ByRole("button", "Select a fruit Fruit"))
app.Click(telatest.ByRole("option", "Banana"))
fmt.Println("chose", chosen)
// Output: chose banana
func (s Select[K]) Build() tela.Node

Build returns the trigger and, in a portal, the list while it is open.

type SelectItem[K comparable] struct {
// Value is the option's key: what Select.Value holds and OnValueChange
// reports.
Value K
// Label is the option's text, shown in the list and, once selected, in
// the trigger. It is also what typing on the closed select matches.
Label string
// Icon is drawn before the label, in the list and in the trigger.
Icon tela.Icon
// Disabled makes the option impossible to choose.
Disabled bool
// Group names the group the option belongs to. Consecutive options with
// the same Group are drawn together under that label, and a line
// separates a group from the options before it; options with no Group
// have no label.
Group string
}

SelectItem is one option of a Select.

type SelectStyle struct {
// Root is the trigger: the button that shows the selected item.
Root string
// Sizes are the trigger's heights: the default (36 px) and Sm (32 px).
Sizes map[Size]string
// Placeholder is added to the trigger while nothing is selected
// (shadcn's data-[placeholder]:).
Placeholder string
// Value is the element inside the trigger that holds the selected
// item's icon and label, or the placeholder.
Value string
// Icon is the chevron at the end of the trigger.
Icon string
// ValueIcon is the selected item's icon inside the trigger.
ValueIcon string
// Content is the popover that holds the list. It has no relative: the
// popover's props make it fixed to the window and place it.
Content string
// ContentSides are the classes the content adds per side of the
// trigger it opens on ("top", "bottom", "left", "right"): the origin of
// its zoom.
ContentSides map[string]string
// ContentEnter and ContentExit are the classes of its entrance (the
// starting: style) and of its exit. Their side-specific slide is
// ContentEnterSides.
ContentEnter, ContentExit string
ContentEnterSides map[string]string
// Viewport is the list that scrolls inside the content.
Viewport string
// Label is a group's heading.
Label string
// Item is an option: Selected is added to the selected one.
Item string
// ItemIcon is an item's icon, ItemText the element that holds it and
// the label.
ItemIcon, ItemText string
// ItemIndicator is the box at the end of the item that holds the check
// and CheckIcon is the check.
ItemIndicator, ItemCheck string
// Separator is the line drawn between groups.
Separator string
}

SelectStyle is the recipe of Select (shadcn/ui’s SelectTrigger, SelectValue, SelectContent, SelectLabel, SelectItem and SelectSeparator). The root element of a Select is its trigger.

type Separator struct {
// Orientation is Horizontal (the default) or Vertical.
Orientation Orientation
// Semantic makes the separator part of the accessibility tree, with
// the separator role and its orientation, for a line that divides
// sections of the content and not just decorates it.
Semantic bool
// Class is added after the recipe's classes.
Class string
// Props are more props for the separator element.
Props tela.Child
}

Separator is a thin line that divides content, shadcn/ui’s Separator. By default it is decorative and hidden from assistive technologies, like Radix’s decorative separator; Semantic gives it the separator role.

kit.Separator{}
h.Div(cls("flex h-5 items-center gap-4"), h.Text("Blog"), kit.Separator{Orientation: kit.Vertical}, h.Text("Docs"))
func (s Separator) Build() tela.Node

Build returns the separator element.

type SeparatorStyle struct {
Root string
// Orientations are a 1 px line across the available width
// (Horizontal) and one down the available height (Vertical).
Orientations map[Orientation]string
}

SeparatorStyle is the recipe of Separator (shadcn/ui’s Separator).

type Size string

Size is the size of a component among the ones its recipe defines. Like Variant it is open: an app adds a size by naming it in a recipe’s Sizes map.

const (
Xs Size = "xs"
Sm Size = "sm"
Lg Size = "lg"
Icon Size = "icon"
IconXs Size = "icon-xs"
IconSm Size = "icon-sm"
IconLg Size = "icon-lg"
)

The sizes shadcn/ui’s components share. The zero value is the default size. Icon, IconSm, IconLg and IconXs are the square sizes of a button that holds only an icon.

type Skeleton struct {
// Class is added after the recipe's classes.
Class string
// Props are more props for the element.
Props tela.Child
}

Skeleton is a placeholder that pulses while content loads, shadcn/ui’s Skeleton. Size it with Class, to the shape of what it stands for.

h.Div(cls("flex items-center gap-4"),
kit.Skeleton{Class: "size-12 rounded-full"},
h.Div(cls("flex flex-col gap-2"),
kit.Skeleton{Class: "h-4 w-[250px]"},
kit.Skeleton{Class: "h-4 w-[200px]"}))

Mark the region that loads with h.AriaBusy(true) so assistive technologies know it is not final.

func (s Skeleton) Build() tela.Node

Build returns the skeleton element.

type SkeletonStyle struct {
Root string
}

SkeletonStyle is the recipe of Skeleton (shadcn/ui’s Skeleton).

type Slider struct {
// Value points to the value of a controlled one-knob slider; nil leaves
// it to the slider, which starts at DefaultValue (Min when it is nil: a
// pointer, so that 0 can be a start, as RadioGroup.DefaultValue is).
// OnValueChange is called with the new value on every change.
Value *float64
DefaultValue *float64
OnValueChange func(float64)
// ValueSource is Value for a slider whose value is moved by something
// other than the user, many times a second, such as a seek bar a clock
// advances: it points to a tela.Value that holds the value, and the
// knob and the range follow it by repainting, with no build of the
// slider or its parent on each change. The slider is controlled by it:
// OnValueChange must store the value the user asks for in it. It is read
// when the slider builds (aria-valuenow), so a parent that builds from
// time to time, say once a second, keeps the semantics current. It
// takes the place of Value and DefaultValue; use it with one knob.
ValueSource *tela.Value[float64]
// Values, DefaultValues and OnValuesChange do the same for any number of
// knobs (one per value, in ascending order): they take the place of
// the three fields above when given.
Values *[]float64
DefaultValues []float64
OnValuesChange func([]float64)
// OnValueCommit is called with the values when the user stops changing
// them: the pointer is released, or a key changed a value.
OnValueCommit func([]float64)
// Min and Max are the ends of the track: 0 and 100 by default.
Min, Max float64
// Step is the distance between the values the knobs can take (1 by
// default).
Step float64
// MinStepsBetweenThumbs is the least number of Steps between two
// knobs; 0 lets them touch.
MinStepsBetweenThumbs int
// Orientation is Horizontal (the default) or Vertical.
Orientation Orientation
// Direction is "ltr" or "rtl"; empty is the direction at the slider's
// position (tela.UseDirection: the nearest h.Dir above it, left to right
// without one). Under "rtl" a horizontal slider has its
// Min at the right, ArrowLeft increases the value and a drag follows
// the pointer; a Direction given here also sets the direction of the
// slider's layout.
Direction string
// Disabled dims the slider and makes it ignore the user.
Disabled bool
// FormatValue returns a value as people read it (aria-valuetext).
FormatValue func(float64) string
// AriaLabel names the slider; ThumbLabels name each knob.
AriaLabel string
ThumbLabels []string
// Class is added after the recipe's classes. TrackClass, RangeClass and
// ThumbClass do the same for their parts.
Class string
TrackClass string
RangeClass string
ThumbClass string
// Props are more props for the root element.
Props tela.Child
}

Slider picks a number, or a range, by dragging a knob along a track, shadcn/ui’s Slider, on aria.UseSlider: a press on the track moves the nearest knob there and goes on dragging it, the arrow keys move the focused knob by Step (PageUp and PageDown by a tenth of the range, Home and End to its ends), and each knob is a slider with aria-valuenow. Dragging only repaints: the knob and the range follow the pointer without the component being built on every move.

With one knob, use Value, DefaultValue and OnValueChange, the kit’s rule for a value:

vol, setVol := tela.UseState(30.0)
kit.Slider{Value: &vol, OnValueChange: setVol.Set, Max: 100, Step: 5, AriaLabel: "Volume"}

An uncontrolled one starts where DefaultValue points (new(50.0) makes the pointer), and at Min without one, so that a slider from -50 to 50 can start at 0.

With several (a price range has two), the values are a slice, as in Radix and shadcn/ui, and the knobs never pass each other:

kit.Slider{DefaultValues: []float64{20, 80}, ThumbLabels: []string{"Minimum price", "Maximum price"}}

A Vertical slider has its Min at the bottom and takes the height of its container (at least 11rem).

Example

t := &exampleT{}
defer t.close()
volume := 30.0
page := func() tela.Node {
return h.Div(h.Class("w-64 p-6"), kit.Slider{AriaLabel: "Volume", DefaultValue: &volume, Step: 10,
OnValueChange: func(v float64) { volume = v }})
}
app := telatest.MountFunc(t, page)
app.Focus(telatest.ByRole("slider", "Volume"))
app.Press("ArrowRight")
fmt.Println("volume", volume)
// Output: volume 40
func (s Slider) Build() tela.Node

Build returns the slider element.

type SliderStyle struct {
// Root is the whole slider, which a press anywhere on moves the nearest
// thumb.
Root string
// Orientations are the root's classes per orientation: Horizontal
// spans the width, Vertical the height (shadcn's
// data-[orientation=vertical]:).
Orientations map[Orientation]string
// Disabled is added to the root while the slider is disabled.
Disabled string
// Track is the groove, which clips the range.
Track string
// TrackOrientations are the groove's dimensions per orientation.
TrackOrientations map[Orientation]string
// Range is the filled part of the groove.
Range string
// RangeOrientations are the range's dimensions per orientation.
RangeOrientations map[Orientation]string
// Thumb is the knob. shadcn's disabled: classes of the thumb are
// left out: Radix's thumb is a span, which never matches them.
Thumb string
}

SliderStyle is the recipe of Slider (shadcn/ui’s Slider).

type SortDirection string

SortDirection is the order a Table shows its rows in by a column: the values of aria-sort. The zero value is unsorted.

const (
Ascending SortDirection = "ascending"
Descending SortDirection = "descending"
)
type SortState struct {
// Column is the ID of the sorted column ("" with no sort).
Column string
// Direction is Ascending or Descending ("" with no sort).
Direction SortDirection
}

SortState is how a Table is sorted: by the column with the ID Column, in Direction. The zero value is unsorted.

type Style struct {
AlertDialog AlertDialogStyle
Avatar AvatarStyle
Badge BadgeStyle
Button ButtonStyle
Calendar CalendarStyle
Card CardStyle
Checkbox CheckboxStyle
ColorInput ColorInputStyle
ColorPicker ColorPickerStyle
ContextMenu MenuStyle
DatePicker DatePickerStyle
Dialog DialogStyle
DropdownMenu MenuStyle
Field FieldStyle
FieldSet FieldSetStyle
Input InputStyle
Label LabelStyle
Menubar MenubarStyle
Popover PopoverStyle
Progress ProgressStyle
RadioGroup RadioGroupStyle
Select SelectStyle
Separator SeparatorStyle
Skeleton SkeletonStyle
Slider SliderStyle
Switch SwitchStyle
Table TableStyle
Tabs TabsStyle
Textarea TextareaStyle
TitleBar TitleBarStyle
Toast ToastStyle
Toggle ToggleStyle
ToggleGroup ToggleGroupStyle
Tooltip TooltipStyle
WindowControls WindowControlsStyle
}

Style holds the recipe of every component: the whole look of the kit as data (ADR-129, K17). DefaultStyle returns a copy of the defaults to edit, and StyleProvider applies an edited one to a subtree.

A recipe is a struct of class strings. Its fields follow one rule:

  • Root is the class of the component’s root element, and is always there;
  • one field per other part, named after the part: Header, Title, Description, Action, Content, Footer, Image, Fallback, Label, Error, Icon;
  • Variants, Sizes (and Orientations where there are two) are maps from a Variant, Size or Orientation to classes, so that new keys are new variants;
  • a state a component can be in without a variable of its own (Disabled) is a string named after it.

The classes go on the element in the order Root, variant, size, state, then the Class field of the component, and Tela’s last-wins rule makes the last one decide, so a recipe or an instance overrides what comes before with no merging.

Part names are public API; the strings are defaults and may change with the look.

func DefaultStyle() Style

DefaultStyle returns a copy of the default recipes: the maps are copied too, so editing the result never changes the defaults or the style of another caller.

s := kit.DefaultStyle()
s.Card.Root += " backdrop-blur-xl"
s.Button.Variants["glass"] = "bg-white/10 text-white hover:bg-white/20"
kit.StyleProvider{Style: &s, Children: app}
func UseStyle() *Style

UseStyle returns the style the component is under: the nearest StyleProvider’s, else the defaults. It is a hook. The result is shared: read it, and copy it with a recipe’s own fields before changing anything; to make a style of your own start from DefaultStyle.

The kit components call it; call it yourself to build components that follow the active style:

func (b Banner) Build() tela.Node {
s := kit.UseStyle()
return h.Div(cls(s.Card.Root, "bg-accent"), b.Children)
}
type StyleProvider struct {
Style *Style
Children tela.Component
}

StyleProvider gives Style to the kit components below it, in place of the defaults: the style of a subtree, like h.Scheme is its color scheme. Providers nest; the nearest one wins. A nil Style provides the defaults.

s := kit.DefaultStyle()
s.Button.Variants["glass"] = "bg-white/10 text-white backdrop-blur hover:bg-white/20"
return kit.StyleProvider{Style: &s, Children: Dashboard{}}

Give it a style once, for example from tela.UseMemo or a package variable, and do not edit that style afterwards: the components below rebuild when the pointer changes, not when what it points to does.

Example

t := &exampleT{}
defer t.close()
// A recipe is data: copy the defaults, add a variant, give the copy to
// a subtree.
style := kit.DefaultStyle()
style.Button.Variants["glass"] = "bg-white/10 text-white backdrop-blur hover:bg-white/20"
page := func() tela.Node {
return h.Fragment(kit.StyleProvider{Style: &style, Children: h.Div(h.Class("bg-slate-900 p-6"),
kit.Button{Variant: "glass", Children: h.Text("Glass")})})
}
app := telatest.MountFunc(t, page)
fmt.Println(app.Count(telatest.ByClass("backdrop-blur")))
// Output: 1
func (p StyleProvider) Build() tela.Node

Build provides the style to the children.

type Switch struct {
// Size is Default or Sm.
Size Size
// Checked points to the state of a controlled switch; nil leaves it to
// the switch, which starts at DefaultChecked.
Checked *bool
// DefaultChecked is the initial state of an uncontrolled switch.
DefaultChecked bool
// OnCheckedChange is called with the next state when the user toggles
// the switch. For a controlled switch it is how the change is asked
// for.
OnCheckedChange func(bool)
// Disabled makes the switch inert and dims it.
Disabled bool
// Required marks the switch as one the user must turn on
// (aria-required).
Required bool
// Invalid marks the value as wrong (aria-invalid). A [Field] with an
// Error sets it on its control.
Invalid bool
// ID is the switch's id, for a [Label]'s For. Inside a [Field] it
// defaults to the field's.
ID string
// AriaLabel names a switch that has no label of its own.
AriaLabel string
// Class is added after the recipe's classes; ThumbClass does the same
// for the knob.
Class string
ThumbClass string
// Props are more props for the switch element.
Props tela.Child
}

Switch is an on/off control that slides, shadcn/ui’s Switch, on aria.UseSwitch: a click or Space toggles it and it has the switch role and aria-checked. Its state follows the kit’s rule: Checked and OnCheckedChange to keep it yourself, DefaultChecked to let the switch keep it.

wifi, setWifi := tela.UseState(true)
kit.Switch{Checked: &wifi, OnCheckedChange: setWifi.Set, AriaLabel: "Wi-Fi"}
kit.Field{Orientation: kit.Horizontal, Label: "Airplane mode", Children: kit.Switch{Size: kit.Sm}}

The knob moves with a transition. A Size the active Style does not define panics in dev mode with the closest one.

Example

t := &exampleT{}
defer t.close()
wifi := true
page := func() tela.Node {
return h.Div(h.Class("p-6"),
kit.Switch{AriaLabel: "Wi-Fi", DefaultChecked: wifi, OnCheckedChange: func(on bool) { wifi = on }})
}
app := telatest.MountFunc(t, page)
app.Click(telatest.ByRole("switch", "Wi-Fi"))
fmt.Println("wifi", wifi)
// Output: wifi false
func (s Switch) Build() tela.Node

Build returns the switch element.

type SwitchStyle struct {
// Root is the track: shape, the off and on colors, focus ring and
// disabled. It has the classes "peer" and "group", for the thumb and
// for siblings that use the peer-* variants.
Root string
// Sizes are the track's dimensions: the default and Sm.
Sizes map[Size]string
// Thumb is the knob: its colors and its move from the off position to
// the on one (shadcn's data-[state=checked]:translate-x-[calc(100%-2px)],
// which goes the other way under a right-to-left direction, as in
// shadcn's right-to-left registry).
Thumb string
// ThumbSizes are the knob's size per switch size.
ThumbSizes map[Size]string
}

SwitchStyle is the recipe of Switch (shadcn/ui’s Switch).

type Tab[K comparable] struct {
// Value is the tab's key: what Tabs.Value holds and OnValueChange
// reports.
Value K
// Label is the text of the tab; it names the tab for assistive
// technologies and its panel.
Label string
// Icon is drawn before the label; the zero Icon draws nothing.
Icon tela.Icon
// Disabled makes the tab inert; the arrow keys skip it.
Disabled bool
// Content is the panel shown while the tab is selected. Only the
// selected tab's panel is built.
Content tela.Component
}

Tab is one tab of a Tabs: its key, its label in the list and the content of its panel.

type Table[T any] struct {
// Rows are the data, in the order to show them when unsorted.
Rows []T
// RowKey returns a comparable key that is unique among Rows (an ID);
// nil keys rows by position.
RowKey func(row T) any
// Columns are the table's columns, in order.
Columns []Column[T]
// Sort controls the sort (nil: the table keeps it), DefaultSort starts
// an uncontrolled table sorted, and OnSortChange is called with the new
// state when a header is pressed.
Sort *SortState
DefaultSort SortState
OnSortChange func(SortState)
// Selected tells whether a row is selected: the row gets the selected
// look and aria-selected. Nil leaves the rows unselectable.
Selected func(row T) bool
// RowProps returns more props for the element of a body row, such as
// h.AriaLabel, h.TestID or a handler (h.OnClick, h.OnKeyDown): what
// Column.CellProps does for one cell, for the whole row. The row is a
// display: contents element, which has no box of its own (see
// [TableStyle]), and the engine treats it as the cells it holds: its
// pointer and key handlers are called for events on any of its cells,
// as events bubble through the tree, and a ref on it, h.Ref, reports
// the bounds of its cells together. Classes on the row do not paint (it
// has no box): style a row through the cells, with CellClass or Column.Class.
RowProps func(row T) tela.Child
// Caption is drawn below the rows, and names the table for assistive
// technologies. AriaLabel names a table that has none.
Caption string
AriaLabel string
// Empty is drawn in a row of its own when there are no rows (shadcn's
// "No results."). Nil draws the header alone.
Empty tela.Component
// SortIcon, SortAscIcon and SortDescIcon replace the arrows of a
// sortable header that is unsorted, ascending and descending.
SortIcon, SortAscIcon, SortDescIcon tela.Icon
// Class is added after the recipe's Root classes. The other Class
// fields do the same for their part: the grid, every header cell,
// every body cell and the caption.
Class string
TableClass string
HeadClass string
CellClass string
CaptionClass string
// Props are more props for the root element.
Props tela.Child
}

Table is a data table, shadcn/ui’s Table with the sorting and the sticky header and columns of its data-table and scroll examples, as a typed component: columns say how to draw a row, and rows are the data.

kit.Table[Invoice]{
Caption: "A list of your recent invoices.",
Rows: invoices,
RowKey: func(i Invoice) any { return i.ID },
Columns: []kit.Column[Invoice]{
{Header: "Invoice", Cell: func(i Invoice) tela.Node { return h.Text(i.ID) },
Sort: func(a, b Invoice) int { return strings.Compare(a.ID, b.ID) }},
{Header: "Status", Cell: func(i Invoice) tela.Node { return h.Text(i.Status) }},
{Header: "Amount", Align: kit.AlignRight, Cell: func(i Invoice) tela.Node { return h.Text(i.Amount) },
Sort: func(a, b Invoice) int { return cmp.Compare(a.Cents, b.Cents) }},
},
}

The header of a column with a Sort (or Sortable) is a button, so Tab reaches it and Enter and Space press it. Pressing it cycles the column through ascending, descending and unsorted, as TanStack Table (the engine of shadcn’s data table) does by default, and pressing another column starts that one ascending; only one column is sorted at a time. The sorted header carries aria-sort and shows an arrow. The sort is stable: rows that compare equal keep their order in Rows (also when descending). Sorting follows ADR-101’s value rule: Sort (a pointer to your state) controls it, DefaultSort sets the start and OnSortChange reports every change.

sort, setSort := tela.UseState(kit.SortState{Column: "amount", Direction: kit.Descending})
kit.Table[Invoice]{Sort: &sort, OnSortChange: setSort.Set, ...}

The table is a CSS grid, one track per column from its Width. A column of Width “auto” (the default) is as wide as its widest cell and shares the space the container leaves; “1fr” columns divide it by weight, and “200px” ones keep their size. The header sticks to the top of the container, so a table in a container with a height (Class: “max-h-80”) scrolls its rows under the header; columns with Sticky stay at the left when the table scrolls sideways.

Rows are keyed by RowKey, so the state of the components in a row’s cells follows the row when the order changes. Without RowKey a row’s key is its position in the sorted rows.

The look is the pinned shadcn commit’s table, with the deviations in TableStyle. A table has no row selection or editing; Selected only styles the rows it names, with aria-selected.

Example

package kit_test
import (
"cmp"
"fmt"
"strings"
"github.com/kuronosu/tela"
"github.com/kuronosu/tela/h"
"github.com/kuronosu/tela/kit"
"github.com/kuronosu/tela/telatest"
)
// Invoice is the row type of ExampleTable.
type Invoice struct {
ID, Status string
Cents int
}
func ExampleTable() {
t := &exampleT{}
defer t.close()
invoices := []Invoice{{"INV001", "Paid", 25000}, {"INV002", "Pending", 15000}, {"INV003", "Unpaid", 35000}}
page := func() tela.Node {
return h.Div(h.Class("w-[420px] p-6"), kit.Table[Invoice]{
Caption: "A list of your recent invoices.",
Rows: invoices,
RowKey: func(i Invoice) any { return i.ID },
Columns: []kit.Column[Invoice]{
{Header: "Invoice", Cell: func(i Invoice) tela.Node { return h.Text(i.ID) },
Sort: func(a, b Invoice) int { return strings.Compare(a.ID, b.ID) }},
{Header: "Status", Cell: func(i Invoice) tela.Node { return h.Text(i.Status) }},
{Header: "Amount", Align: kit.AlignRight,
Cell: func(i Invoice) tela.Node { return h.Text(fmt.Sprintf("$%.2f", float64(i.Cents)/100)) },
Sort: func(a, b Invoice) int { return cmp.Compare(a.Cents, b.Cents) }},
},
})
}
app := telatest.MountFunc(t, page)
app.Click(telatest.ByRole("button", "Amount")) // ascending
app.Click(telatest.ByRole("button", "Amount")) // descending
app.Expect(telatest.ByRole("columnheader", "Amount"), telatest.Sort("descending"))
app.ExpectAria(`
- table "A list of your recent invoices.":
- caption: A list of your recent invoices.
- rowgroup:
- row "Invoice Status Amount":
- columnheader "Invoice":
- button "Invoice"
- columnheader "Status"
- columnheader "Amount":
- button "Amount"
- rowgroup:
- row "INV003 Unpaid $350.00":
- cell "INV003"
- cell "Unpaid"
- cell "$350.00"
- row "INV001 Paid $250.00":
- cell "INV001"
- cell "Paid"
- cell "$250.00"
- row "INV002 Pending $150.00":
- cell "INV002"
- cell "Pending"
- cell "$150.00"
`)
fmt.Println("sorted by amount, largest first")
// Output: sorted by amount, largest first
}
func (t Table[T]) Build() tela.Node

Build returns the table.

type TableStyle struct {
// Root is the scrolling container (shadcn's table-container). Give the
// table a height with Class (max-h-96) to scroll it vertically with its
// header kept in place.
Root string
// Table is the grid; the column tracks are added by the component.
// It is as wide as its columns and at least as wide as the container,
// so that a sticky column has a containing block as wide as the
// scrolled content.
Table string
// Header, Body and Footer are the row groups (thead, tbody, tfoot) and
// Row a row (tr); they have no box, so their classes are the inherited
// ones: text color, font, and the `group` the cells' variants read.
Header, Body, Footer, Row string
// Head is a header cell (th) and Cell a body cell (td). Both are flex
// containers that center their content vertically (`align-middle`). The
// border under every row is here.
Head, Cell string
// CellWithLastRow is added to the cells of the last body row (shadcn's
// [&_tr:last-child]:border-0).
CellWithLastRow string
// FooterCell is added to the footer's cells: its top border and
// background.
FooterCell string
// Aligns are the classes of a column's alignment, by [ColumnAlign].
Aligns map[ColumnAlign]string
// HeadSticky is added to every header cell, which sticks to the top
// of the scrolling container, over the rows that scroll under it; and
// HeadStickyColumn to the header cell of a sticky column too.
HeadSticky, HeadStickyColumn string
// StickyColumn is added to the body cells of a sticky column, with an
// opaque background so that the cells that scroll under it do not show;
// StickyColumnHover keeps it opaque on a hovered row (a translucent
// bg-muted/50 would show them), StickyColumnSelected on a selected
// one and StickyColumnFooter in the footer.
StickyColumn, StickyColumnHover, StickyColumnSelected, StickyColumnFooter string
// Caption is the caption (shadcn's caption-bottom: below the rows).
Caption string
// Empty is added to the cell that holds the [Table]'s Empty content
// when there are no rows.
Empty string
// SortButton is added to the classes of the button inside the header
// of a sortable column, which is a ghost [Button] with the sort arrow
// after the title, as in shadcn's data-table example. The header cell
// is a `group` with aria-sort, for group-aria-[sort=ascending]: and
// group-aria-[sort=descending]: classes.
SortButton string
}

TableStyle is the recipe of Table (shadcn/ui’s Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell and TableCaption).

shadcn’s table is an HTML table; Tela has no table layout (ARCHITECTURE §10), so this one is a CSS grid whose rows are display: contents elements, as ARIA’s table pattern allows: the cells are the grid’s items and the rows, row groups and header exist for the semantics tree and for the classes the cells inherit. The classes shadcn puts on a row or a row group and that paint (a row’s border, its hover and selected backgrounds, the footer’s top border and background) are on the cells here, with the group-hover: and group-aria-selected: variants of the row, since a display: contents element has no box to paint.

type Tabs[K comparable] struct {
// Tabs are the tabs, in order.
Tabs []Tab[K]
// Value points to the selected tab of a controlled Tabs; nil leaves
// the selection to the component, which starts at DefaultValue.
Value *K
// DefaultValue points to the tab an uncontrolled Tabs starts on; the
// first enabled tab when it is nil or not one of Tabs. It is a pointer
// so that the zero value of K (the int 0, say) can be a tab:
// new(0) selects it.
DefaultValue *K
// OnValueChange is called with the key of the tab the user selected. For
// a controlled Tabs it is how the change is asked for.
OnValueChange func(K)
// Orientation is Horizontal (the zero value) or Vertical.
Orientation Orientation
// Variant is Default (the muted pill) or TabsLine.
Variant Variant
// Direction is "ltr" or "rtl"; empty is the direction at the component's
// position (tela.UseDirection: the nearest h.Dir above it, left to right
// without one). Under "rtl" ArrowLeft goes to the next tab and ArrowRight to the previous one; a Direction given here also
// sets the direction of the layout.
Direction string
// ManualActivation makes the arrow keys only move the focus; Enter or
// Space selects the focused tab. By default the focused tab is selected.
ManualActivation bool
// AriaLabel names the tab list.
AriaLabel string
// Indicator replaces the underline of the TabsLine variant (and adds
// one to any other variant): it is drawn inside the selected tab only,
// so it is mounted by the tab that gains the selection and unmounted by
// the one that loses it, and a LayoutID on its element makes it slide
// between the tabs (a shared element transition). Give it an element
// that is positioned against the tab, such as
// h.Div(h.LayoutID("underline"), h.Class("absolute inset-x-0 -bottom-px h-0.5 bg-primary")).
Indicator tela.Component
// Class is added after the recipe's Root classes; the other Class
// fields do the same for their part.
Class string
ListClass string
TriggerClass string
ContentClass string
// Props are more props for the root element.
Props tela.Child
}

Tabs is a tab list with its panels, shadcn/ui’s Tabs: a strip of triggers and the content of the selected one. It is built on aria.UseTabs, so the arrow keys move between the tabs and select them, Home and End jump to the first and last, Tab moves from the selected tab to its panel, and disabled tabs are skipped.

kit.Tabs[string]{
DefaultValue: new("account"),
Tabs: []kit.Tab[string]{
{Value: "account", Label: "Account", Content: AccountForm{}},
{Value: "password", Label: "Password", Content: PasswordForm{}},
},
}

The selected tab follows the kit’s value rule: Value (a pointer to your state) with OnValueChange controls it, DefaultValue starts an uncontrolled one. There is always one selected tab: the first enabled one when no value is given, or the given one is missing or disabled.

Orientation Vertical lays the list out as a column beside the panel and moves with the up and down arrows. Variant TabsLine draws no pill and underlines the selected tab.

Example

t := &exampleT{}
defer t.close()
page := kit.Tabs[string]{
DefaultValue: new("account"),
Tabs: []kit.Tab[string]{
{Value: "account", Label: "Account", Content: h.Text("Make changes to your account.")},
{Value: "password", Label: "Password", Content: h.Text("Change your password.")},
},
}
app := telatest.Mount(t, page)
app.Click(telatest.ByRole("tab", "Password"))
app.ExpectText("Change your password.")
fmt.Println("password tab shown")
// Output: password tab shown
func (t Tabs[K]) Build() tela.Node

Build returns the tabs element: the list of triggers and the selected panel.

type TabsStyle struct {
// Root is the arrangement of the list and the panel.
Root string
// Orientations are the root's classes per orientation: Horizontal puts
// the list above the panel, Vertical beside it.
Orientations map[Orientation]string
// List is the strip of triggers.
List string
// ListVariants are the looks of the list: Default (a muted pill) and
// TabsLine (no background).
ListVariants map[Variant]string
// ListOrientations are the list's dimensions per orientation.
ListOrientations map[Orientation]string
// Trigger is what every tab has; its selected look is the aria-selected:
// classes, and TriggerVariants (the list's variant) go after them.
Trigger string
// TriggerVariants are the classes a tab adds per variant of its list,
// after Trigger (shadcn's group-data-[variant=…]/tabs-list:).
TriggerVariants map[Variant]string
// TriggerOrientations are the tab's classes per orientation (the
// vertical list's tabs are full-width and start-aligned).
TriggerOrientations map[Orientation]string
// Icon is what a tab's icon has.
Icon string
// Indicator is the underline of the TabsLine list's selected tab (the
// pseudo-element shadcn draws with after:, here a real element);
// IndicatorOrientations place it per orientation.
Indicator string
IndicatorOrientations map[Orientation]string
// Content is the panel.
Content string
}

TabsStyle is the recipe of Tabs (shadcn/ui’s Tabs, TabsList, TabsTrigger and TabsContent).

type Textarea struct {
// Value points to the text of a controlled field; nil leaves the text
// to the field, which starts with DefaultValue.
Value *string
DefaultValue string
// OnValueChange is called with the new text after every edit.
OnValueChange func(string)
Placeholder string
Name string
// ID is the field's id, for a [Label]'s For. Inside a [Field] it
// defaults to the field's.
ID string
// AriaLabel names a field that has no label of its own (see [Input]).
AriaLabel string
// Rows is the number of lines the field is tall (h.Rows); 0 leaves the
// recipe's minimum height and the text to size it.
Rows int
Disabled bool
ReadOnly bool
// Invalid marks the value as wrong (see [Input]); a [Field] with an
// Error sets it.
Invalid bool
AutoFocus bool
MaxLength int
// Class is added after the recipe's classes.
Class string
// Props are more props for the textarea element.
Props tela.Child
}

Textarea is a multi-line text field, shadcn/ui’s Textarea. It grows with its text (field-sizing-content) from a minimum height of 64 px, and scrolls once a max-h-* class in Class caps it. Its value follows the same rule as Input’s.

kit.Textarea{Placeholder: "Write a message", Value: &msg, OnValueChange: setMsg.Set}
func (t Textarea) Build() tela.Node

Build returns the textarea element.

type TextareaStyle struct {
Root string
}

TextareaStyle is the recipe of Textarea (shadcn/ui’s Textarea).

type TitleBar struct {
// Variant is the look: Default (the platform's), Windows, GNOME or
// MacOS, or one the active [Style] adds.
Variant Variant
// Title is the window's name, truncated when there is no room.
Title tela.Component
// Leading is drawn at the start, before the title: a menu button, the
// app's icon, a back button. On MacOS it follows the window controls.
Leading tela.Component
// Children is the content beside the title that takes the room
// left: a search field, tabs, a menu bar. On Windows it is centered
// in that room.
Children tela.Component
// Trailing is drawn at the end, before the window controls: actions
// such as share or settings.
Trailing tela.Component
// ControlsSide is "start" or "end": the side of the bar that has the
// window controls. Empty is the look's own (the end, except MacOS).
ControlsSide string
// HideMinimize and HideMaximize leave those buttons out (see
// [WindowControls]).
HideMinimize, HideMaximize bool
// State, when set, is the window state to draw in place of the
// window's own: Maximized, Fullscreen and Focused. It is for previews
// and tests; leave it nil in an app.
State *tela.WindowState
// TitleClass, LeadingClass, ContentClass and TrailingClass are added
// to the part after the recipe's classes.
TitleClass, LeadingClass, ContentClass, TrailingClass string
// Class is added to the bar after the recipe's classes ("bg-card").
Class string
// Props are more props for the bar element.
Props tela.Child
}

TitleBar is the title bar of a window that draws its own: a tela.Window or tela.App with Frameless, or a window on a Wayland desktop whose compositor draws no title bars (the window’s State().ClientSideDecorations). It is a drag area (h.WindowDrag): the OS moves the window when it is dragged, maximizes or restores it on a double click and opens the window menu on a right click, so the bar does nothing for those, and every interactive thing inside it (a button, a search field, tabs) stays clickable, because the buttons, fields and anything with a handler are holes in the drag area by themselves.

func Notes() tela.Node {
return h.Div(h.Class("flex h-screen flex-col"),
kit.TitleBar{
Leading: kit.Button{Variant: kit.Ghost, Size: kit.IconSm, Icon: lucide.Menu, AriaLabel: "Menu"},
Title: h.Text("Notes"),
Trailing: kit.Button{Variant: kit.Ghost, Size: kit.IconSm, Icon: lucide.Settings, AriaLabel: "Settings"},
},
h.Div(h.Class("min-h-0 grow"), content),
)
}

The bar draws WindowControls at the side the platform keeps them (the end, in the reading direction, and the start on macOS; ControlsSide overrides it), so under h.Dir(“rtl”) the whole bar mirrors. Its look is the platform’s by default (Windows, GNOME or MacOS: the title at the start on Windows, centered on the others; Variant picks another), it dims while the window is not focused, and loses its outline while the window is maximized or fullscreen.

To show the bar only where the window has no title bar of its own, read the window state and leave it out otherwise:

if tela.UseWindow().State().ClientSideDecorations { ... kit.TitleBar{...} ... }

A fullscreen window usually has no bar either: leave it out on State().Fullscreen. In tela snapshot there is no window: the bar is drawn focused and not maximized, and its buttons do nothing.

func (t TitleBar) Build() tela.Node

Build returns the bar.

type TitleBarStyle struct {
// Root is what every look has: the width, no text selection.
Root string
// Variants are the looks of the bar: Windows, GNOME and MacOS. Their
// heights are 32, 47 and 40 px, the ones of the OS's own bars.
Variants map[Variant]string
// Windowed and Maximized are added to the bar while the window is
// neither maximized nor fullscreen, and while it is: the outline a
// floating window has and a maximized one does not.
Windowed, Maximized map[Variant]string
// Inactive is added while the window is not focused.
Inactive map[Variant]string
// Leading, Middle and End are the three regions of the bar: the start
// (with the window controls on a look that puts them there), the title
// and content, and the end (the trailing slot and the controls).
Leading, Middle, End map[Variant]string
// Title, Content and Trailing are the parts inside them: the title's
// text, the content slot's box and the trailing slot's box.
Title, Content, Trailing map[Variant]string
}

TitleBarStyle is the recipe of TitleBar. Every field but Root and Variants is a map from a look (Windows, GNOME, MacOS, or one the recipe adds) to the classes of one part, so a look is one entry in each.

type ToastHandle struct {
// contains filtered or unexported fields
}

ToastHandle shows and dismisses the toasts of the Toaster above the component that called UseToast. It is a small comparable value, so it can sit in a struct component’s fields without making it rebuild, and every method is safe from any goroutine: an HTTP callback or a worker can call Success when it is done.

func UseToast() ToastHandle

UseToast returns the handle of the nearest Toaster above the component. It is a hook. It panics when there is none: put kit.Toaster{Children: …} around the app, once.

func Save() tela.Node {
toast := kit.UseToast()
return kit.Button{OnClick: func() {
toast.Success("Saved", kit.ToastDescription("Your changes are live."))
}, Children: h.Text("Save")}
}
func (t ToastHandle) Dismiss(id ToastID)

Dismiss closes the toast with the id; an id that is gone is ignored.

func (t ToastHandle) DismissAll()

DismissAll closes every toast.

func (t ToastHandle) Error(title string, opts ...ToastOption) ToastID

Error shows a toast with an error icon. It is announced as an alert.

func (t ToastHandle) Info(title string, opts ...ToastOption) ToastID

Info shows a toast with an info icon.

func (t ToastHandle) Loading(title string, opts ...ToastOption) ToastID

Loading shows a toast with a spinner that stays until it is dismissed or replaced (see ToastReplace).

id := toast.Loading("Uploading…")
// … later, from any goroutine:
toast.Success("Uploaded", kit.ToastReplace(id))
func (t ToastHandle) Show(title string, opts ...ToastOption) ToastID

Show shows a toast with the title and returns its id. It is safe from any goroutine.

id := toast.Show("Event created", kit.ToastDescription("Sunday at 9:00 AM"),
kit.ToastAction("Undo", undo))
func (t ToastHandle) Success(title string, opts ...ToastOption) ToastID

Success shows a toast with a check icon.

func (t ToastHandle) Warning(title string, opts ...ToastOption) ToastID

Warning shows a toast with a warning icon.

type ToastID uint64

ToastID names one toast of a Toaster: ToastHandle.Show returns it, and Dismiss and ToastReplace take it.

type ToastIcons struct {
Success, Info, Warning, Error, Loading tela.Icon
}

ToastIcons are the icons of the toast kinds; a zero Icon keeps shadcn’s.

type ToastOption func(*toastData)

ToastOption customizes a toast shown with ToastHandle.Show and its variants.

func ToastAction(label string, onClick func()) ToastOption

ToastAction adds a button to the toast. Clicking it calls onClick and dismisses the toast.

func ToastDescription(text string) ToastOption

ToastDescription adds a second, lighter line under the title.

func ToastDuration(d time.Duration) ToastOption

ToastDuration sets how long the toast stays: after it the toast is dismissed. ToastForever keeps it until it is dismissed by hand. The default is the Toaster’s Duration (4 seconds), and a loading toast never closes by itself. The time is the window’s frame clock, so it stops while the pointer rests over the toasts.

func ToastOnAutoClose(f func()) ToastOption

ToastOnAutoClose calls f, on the UI thread, when the toast closes because its time ran out.

func ToastOnDismiss(f func()) ToastOption

ToastOnDismiss calls f, on the UI thread, when the toast is dismissed by the user or by ToastHandle.Dismiss.

func ToastReplace(id ToastID) ToastOption

ToastReplace makes the toast replace the one with this id, in the same place and with its timer restarted, as sonner’s id option does: a loading toast becomes a success or an error when the work is over. When no toast has the id, a new one is shown.

type ToastPosition string

ToastPosition is the corner or edge of the window a Toaster puts its toasts at. The zero value is ToastBottomRight. Left and right are the window’s, whatever the direction, as sonner’s positions are (the toast’s own layout, its icon, action and close button, follows the direction).

const (
ToastTopLeft ToastPosition = "top-left"
ToastTopCenter ToastPosition = "top-center"
ToastTopRight ToastPosition = "top-right"
ToastBottomLeft ToastPosition = "bottom-left"
ToastBottomCenter ToastPosition = "bottom-center"
ToastBottomRight ToastPosition = "bottom-right"
)
type ToastStyle struct {
// Root is the viewport, Fixed or Inline is added to it (the viewport is
// fixed to the window, or absolute inside the nearest positioned
// ancestor for an Inline Toaster) and Positions say where.
Root, Fixed, Inline string
Positions map[ToastPosition]string
// Item is one toast: all of them are absolute at the viewport's edge, and
// the Toaster moves them with a translation and a scale. Sides add the
// edge ("top" or "bottom") and x the side they are laid against.
Item string
Sides map[string]string
// Enter and Exit are the classes an entering toast starts from and an
// exiting one goes to, per edge.
Enter, Exit map[string]string
// Hidden is added to the toasts beyond VisibleToasts.
Hidden string
// Body is the row inside a toast that holds the icon, the text and the
// action; Collapsed is added to the body of a toast behind the first
// while the stack is collapsed (its content is invisible, only its card
// peeks out).
Body, Collapsed string
// Bridge is the invisible strip above an expanded toast that keeps the
// pointer over the viewport while it crosses the gap between two toasts
// (sonner's ::after).
Bridge string
// Icon is the box of a toast's icon, IconSvg the drawing, and Spinner is
// added to the loading icon.
Icon, IconSvg, Spinner string
Content string
Title string
Description string
// Action is the toast's button, Close the round button of a Toaster
// with a CloseButton and CloseIcon its icon.
Action, Close, CloseIcon string
}

ToastStyle is the recipe of Toaster (shadcn/ui’s Sonner wrapper over sonner’s default styles, sonner 2.x). The root element of a Toaster is its viewport: the box at the edge of the window the toasts stack in.

type Toaster struct {
// Position is where the toasts stack: ToastBottomRight (the zero
// value), ToastBottomLeft, ToastBottomCenter, ToastTopRight,
// ToastTopLeft or ToastTopCenter.
Position ToastPosition
// Duration is how long a toast stays by default, 4 seconds when zero;
// ToastForever keeps toasts until they are dismissed.
Duration time.Duration
// VisibleToasts is how many toasts show at once, 3 when zero.
VisibleToasts int
// Expand keeps the stack fanned out, as when the pointer is over it.
Expand bool
// CloseButton gives every toast a close button.
CloseButton bool
// Inline draws the toasts inside the nearest positioned ancestor of the
// Toaster instead of in a layer fixed to the window: a toaster for a
// panel or a preview. The position classes then place it in that box.
Inline bool
// Icons replace the icons of the kinds; CloseIcon the one of the close
// button.
Icons ToastIcons
CloseIcon tela.Icon
// Children is the app: the components below that call UseToast.
Children tela.Component
// Class is added after the recipe's Root classes (the viewport's);
// ToastClass is added to every toast.
Class string
ToastClass string
// Props are more props for the viewport element.
Props tela.Child
}

Toaster shows the toasts of its app: shadcn/ui’s Sonner. It goes once, around the app, and the components below it show toasts with a handle from UseToast:

tela.Main(tela.App{Title: "Notes", Root: kit.Toaster{Children: Notes{}}, Theme: &theme})
toast := kit.UseToast()
toast.Success("Saved")

The toasts stack at a corner of the window (Position; bottom right by default). The newest is in front, with the ones before it peeking out behind it, scaled down; at most VisibleToasts (3) show. While the pointer is over the toasts they fan out to show each in full and their timers stop, and they go on from where they were when it leaves, as sonner does. A toast closes by itself after its Duration, 4 seconds by default; the clock is the window’s frame clock, so telatest’s Advance moves it and an app with only a waiting toast wakes up no more often than every 100 ms. Toasts enter and leave sliding from the edge of the window.

Toasts are announced to assistive technologies: each is a “status” and an error an “alert”.

Compared with sonner: there is no swipe to dismiss, no promise helper (show a Loading toast and replace it), no rich colors, and the toasts do not change layout on narrow windows.

Example

t := &exampleT{}
defer t.close()
// The Toaster goes once around the app; any component below it shows
// toasts with the handle of kit.UseToast.
app := telatest.Mount(t, kit.Toaster{Children: seedOnClick{}})
app.Click(telatest.ByRole("button", "Save"))
app.ExpectText("Your changes are live.")
fmt.Println("toast shown")
// Output: toast shown
func (t Toaster) Build() tela.Node

Build returns the app and the viewport that shows the toasts.

type Toggle struct {
// Variant is Default or Outline.
Variant Variant
// Size is Default, Sm or Lg.
Size Size
// Pressed points to the state of a controlled toggle; nil leaves it to
// the toggle, which starts at DefaultPressed.
Pressed *bool
// DefaultPressed is the initial state of an uncontrolled toggle.
DefaultPressed bool
// OnPressedChange is called with the next state when the user toggles
// the button. For a controlled toggle it is how the change is asked
// for.
OnPressedChange func(bool)
// Disabled makes the toggle inert and dims it.
Disabled bool
// Invalid marks the value as wrong (aria-invalid).
Invalid bool
// AriaLabel names a toggle whose content is not text.
AriaLabel string
// Icon is drawn before the children.
Icon tela.Icon
// Children is the label.
Children tela.Component
// Class is added after the recipe's classes.
Class string
// Props are more props for the button element.
Props tela.Child
}

Toggle is a button that stays pressed, shadcn/ui’s Toggle, on aria.UseToggleButton: a click, Enter or Space turns it on and off, and it has aria-pressed. Its state follows the kit’s rule: Pressed and OnPressedChange to keep it yourself, DefaultPressed to let the toggle keep it.

bold, setBold := tela.UseState(false)
kit.Toggle{Variant: kit.Outline, Pressed: &bold, OnPressedChange: setBold.Set,
Icon: lucide.Bold, AriaLabel: "Bold"}

For a row of toggles that share a choice, see ToggleGroup. A Variant or Size the active Style does not define panics in dev mode with the closest one.

Example

t := &exampleT{}
defer t.close()
bold := false
page := func() tela.Node {
return h.Div(h.Class("p-6"), kit.Toggle{Variant: kit.Outline, Icon: iconBold, AriaLabel: "Bold",
OnPressedChange: func(on bool) { bold = on }})
}
app := telatest.MountFunc(t, page)
app.Click(telatest.ByRole("button", "Bold"))
fmt.Println("bold", bold)
// Output: bold true
func (t Toggle) Build() tela.Node

Build returns the toggle element.

type ToggleGroup[K comparable] struct {
// Items are the buttons, in order.
Items []ToggleGroupItem[K]
// Variant (Default or Outline) and Size (Default, Sm or Lg) are the
// toggle looks of every item.
Variant Variant
Size Size
// Spacing is the space between items, in steps of 4 px. 0 joins the
// items into one segmented bar (shadcn's default).
Spacing int
// Multiple lets any number of items be on.
Multiple bool
// Value points to the key that is on in a controlled single-choice
// group; nil leaves it to the group, which starts at DefaultValue (nil:
// none on). OnValueChange is called with the key that has just been
// turned on.
Value *K
DefaultValue *K
OnValueChange func(K)
// Selected is the selection of a controlled group, DefaultSelected the
// starting one of an uncontrolled group and OnSelectionChange is called
// with the next selection. The zero aria.Selection leaves the selection
// to the group.
Selected aria.Selection[K]
DefaultSelected aria.Selection[K]
OnSelectionChange func(aria.Selection[K])
// DisallowEmptySelection keeps one item on: pressing the only item that
// is on does nothing.
DisallowEmptySelection bool
// Orientation is Horizontal (the default) or Vertical, which the arrow
// keys follow; the items are laid out in a row either way (use Class
// "flex-col" for a column).
Orientation Orientation
// Direction is "ltr" or "rtl"; empty is the direction at the component's
// position (tela.UseDirection: the nearest h.Dir above it, left to right
// without one). Under "rtl" ArrowLeft goes to the next item and ArrowRight to the previous one; a Direction given here also
// sets the direction of the layout.
Direction string
// Disabled disables every item.
Disabled bool
// AriaLabel names the group; without it a [FieldSet]'s legend does.
AriaLabel string
// Class is added after the recipe's classes; ItemClass is added to every
// item.
Class string
ItemClass string
// Props are more props for the group element.
Props tela.Child
}

ToggleGroup is a row of toggle buttons that share a choice, shadcn/ui’s ToggleGroup, on aria.UseToggleGroup: the segmented control, and a formatting toolbar. With one choice (the default) the group is a radio group of toggle buttons; with Multiple any number of items are on and the group is a toolbar. The group is one tab stop: the arrows move the focus between the items, Home and End jump, and Enter or Space turns an item on or off.

A single-choice group follows the kit’s value rule with Value (a pointer to a key), DefaultValue and OnValueChange; pressing the item that is on turns it off unless DisallowEmptySelection, which a segmented control sets:

view, setView := tela.UseState("list")
kit.ToggleGroup[string]{
Value: &view, OnValueChange: setView.Set, DisallowEmptySelection: true, AriaLabel: "View",
Items: []kit.ToggleGroupItem[string]{
{Value: "list", Label: "List"},
{Value: "grid", Label: "Grid"},
},
}

A group with Multiple holds an aria.Selection instead, with Selected, DefaultSelected and OnSelectionChange:

kit.ToggleGroup[string]{Multiple: true, Variant: kit.Outline, DefaultSelected: aria.SelectionOf("bold"),
Items: []kit.ToggleGroupItem[string]{{Value: "bold", Icon: lucide.Bold, AriaLabel: "Bold"}, ...}}

Selected, DefaultSelected and OnSelectionChange also work on a single-choice group, for a controlled group that may have no item on.

Example

t := &exampleT{}
defer t.close()
theme := "system"
page := func() tela.Node {
return h.Div(h.Class("p-6"), kit.ToggleGroup[string]{
AriaLabel: "Theme", Variant: kit.Outline, DefaultValue: &theme, DisallowEmptySelection: true,
OnValueChange: func(k string) { theme = k },
Items: []kit.ToggleGroupItem[string]{
{Value: "light", Label: "Light"}, {Value: "dark", Label: "Dark"}, {Value: "system", Label: "System"},
}})
}
app := telatest.MountFunc(t, page)
app.Click(telatest.ByRole("radio", "Dark"))
fmt.Println("theme", theme)
// Output: theme dark
func (g ToggleGroup[K]) Build() tela.Node

Build returns the group element.

type ToggleGroupItem[K comparable] struct {
// Value is the item's key, what the group's selection holds.
Value K
// Label is the item's text.
Label string
// Icon is drawn before the label; give an AriaLabel to an item that has
// only an icon.
Icon tela.Icon
// Children replaces the Label with any content.
Children tela.Component
// AriaLabel names an item whose content is not text.
AriaLabel string
// Disabled makes the item inert and dims it.
Disabled bool
}

ToggleGroupItem is one item of a ToggleGroup: a toggle button with an icon, a label, or both.

type ToggleGroupStyle struct {
// Root is the row that holds the items (shadcn's group/toggle-group class is
// left out: Tela has no named groups, and nothing selects on it).
Root string
// Item is added to the toggle classes of every item.
Item string
// ItemWithoutSpacing is added to an item of a group with no space
// between its items (shadcn's data-[spacing=0]:): square corners except
// the ends, no shadow.
ItemWithoutSpacing string
// OutlineItemWithoutSpacing is added to an Outline item of a group with
// no space between its items: one border between neighbors.
OutlineItemWithoutSpacing string
}

ToggleGroupStyle is the recipe of ToggleGroup (shadcn/ui’s ToggleGroup and ToggleGroupItem). The look of an item comes from ToggleStyle: its Root, the group’s Variant and Size, and On.

type ToggleStyle struct {
// Root is what every toggle has: layout, text, hover, focus ring,
// disabled and invalid. The unconditional classes (bg-transparent, the
// border color) come before the conditional ones, as Tela's last-wins
// order needs where CSS has the variant order.
Root string
// Variants are the looks: Default and Outline.
Variants map[Variant]string
// Sizes are the dimensions: the default, Sm and Lg.
Sizes map[Size]string
// On is added while the toggle is on (shadcn's
// data-[state=on]:), after the variant, so that it wins over hover.
// The toggle button is aria-pressed and the item of a single-selection
// group aria-checked, so it styles both.
On string
// Icon is what the icon slot has (shadcn's [&_svg]: classes).
Icon string
}

ToggleStyle is the recipe of Toggle (shadcn/ui’s toggleVariants), which the items of a ToggleGroup use too.

type Tooltip struct {
// Trigger is the element the tooltip is about.
Trigger Trigger
// Content is what the tooltip says: text, or a short component.
Content tela.Component
// Placement is the side of the trigger the tooltip is on: "top" (the
// default), "bottom", "left" or "right"; a Floating UI placement such as
// "top-start" also works. Align is where it sits along that side:
// "start", "center" (the default) or "end". The tooltip flips to the
// other side when there is no room.
Placement, Align string
// Offset is the distance in pixels between the trigger and the tooltip
// (zero by default: the arrow touches the trigger).
Offset float32
// Delay is how long the pointer must rest on the trigger before the
// tooltip opens. Zero opens it at once, as shadcn's provider does.
// Keyboard focus never waits.
Delay time.Duration
// Disabled keeps the tooltip from opening.
Disabled bool
// Open points to the open state of a controlled tooltip; nil leaves the
// state to the tooltip, which starts closed unless DefaultOpen is set.
Open *bool
DefaultOpen bool
OnOpenChange func(open bool)
// Class is added after the recipe's Root classes, to the tooltip;
// ArrowClass does the same for the arrow.
Class string
ArrowClass string
// Props are more props for the tooltip element.
Props tela.Child
}

Tooltip is a small label that explains its trigger, shadcn/ui’s Tooltip: it appears when the pointer rests on the trigger or the keyboard focuses it, points at it with an arrow, and goes away when the pointer leaves, a key is pressed, the trigger is pressed or Escape is. The trigger is described by it (aria-describedby), so assistive technologies read it after the trigger’s own name.

kit.Tooltip{
Trigger: kit.Button{Variant: kit.Outline, Size: kit.Icon, AriaLabel: "Add", Icon: plus},
Content: h.Text("Add to library"),
}

The Trigger slot is a Trigger. The tooltips of a window are a group, as in shadcn’s TooltipProvider and React Aria: only one is open, and once one has opened, moving to another trigger shows its tooltip at once, whatever its Delay; there is nothing to wrap them in.

A tooltip is for elements that already have a name and a tab stop; it should hold text, never controls. The tooltip exists only while it is open. Open, DefaultOpen and OnOpenChange are the usual controlled and uncontrolled pair.

Example

t := &exampleT{}
defer t.close()
page := func() tela.Node {
return h.Div(h.Class("p-6"), kit.Tooltip{
Trigger: kit.Button{Variant: kit.Outline, Size: kit.Icon, AriaLabel: "Add", Children: h.Text("+")},
Content: h.Text("Add to library"),
})
}
app := telatest.MountFunc(t, page, telatest.NoSettle())
app.Hover(telatest.ByRole("button", "Add"))
fmt.Println("tooltip:", app.Count(telatest.ByRole("tooltip", "Add to library")) == 1)
app.MouseMove(300, 300)
app.Advance(time.Second)
fmt.Println("tooltip:", app.Count(telatest.ByRole("tooltip", "Add to library")) == 1)
// Output:
// tooltip: true
// tooltip: false
func (t Tooltip) Build() tela.Node

Build returns the trigger and the tooltip’s portal.

type TooltipStyle struct {
// Root is the tooltip: the foreground color as background, small text
// and the entrance.
Root string
// Exit is what the tooltip transitions to as it closes.
Exit string
// Sides adds classes for the side of the trigger the tooltip is on
// ("top", "right", "bottom" or "left"): the transform origin of the
// entrance and the slide of 0.5rem it starts from.
Sides map[string]string
// Arrow is the arrow: a square turned 45 degrees, half under the
// tooltip. ArrowSides adds, per side, the margin that sinks it 2 px
// into the tooltip (shadcn's translate-y-[calc(-50%_-_2px)]); Floating
// UI puts the arrow's center on the edge.
Arrow string
ArrowSides map[string]string
}

TooltipStyle is the recipe of Tooltip (shadcn/ui’s TooltipContent and its arrow).

type Trigger interface {
// WithTriggerProps returns the component with p applied to its root
// element, after the props the component sets itself, so that p's
// handlers run along with the component's own.
WithTriggerProps(p tela.Prop) tela.Component
}

Trigger is what a trigger slot takes: the Trigger field of Dialog, AlertDialog, Popover and Tooltip, and of the menus, selects and pickers that open something. It is a component whose root element can receive the props of a trigger: the press that opens the overlay, the aria-haspopup, aria-expanded and aria-controls that say what it does, the ref the overlay is placed against, the hover and focus handlers of a tooltip. The overlay builds those props with its aria hook and calls WithTriggerProps to put them on the trigger.

One rule holds for every trigger slot of the kit:

  • a kit component that can be a trigger implements Trigger, and Button does: Trigger: kit.Button{Variant: kit.Outline, Children: h.Text(“Open”)};
  • a component of your own implements it with one method, merging p into the props of its root element (the way a kit component merges it with its Props field);
  • anything else, an element or a function component, is made a Trigger with TriggerFunc, which receives the props and puts them on the element it chooses.

A Trigger is the equivalent of Radix’s asChild: the overlay does not wrap the trigger in an element of its own, so the button you wrote is the one the screen reader finds, with its name and its tab stop.

type TriggerFunc func(p tela.Prop) tela.Node

TriggerFunc makes a function a Trigger: it is given the props of the trigger and returns the element that carries them. Use it for a trigger that is not a kit component, such as an avatar or a link:

kit.Popover{
Trigger: kit.TriggerFunc(func(p tela.Prop) tela.Node {
return h.Button(p, cls("rounded-full"), kit.Avatar{Fallback: "AB"})
}),
Children: profile,
}

The function runs as a component of its own, so it may use hooks. The props make the element a button that can be pressed and focused; give a different role with h.Role after p when the element is something else.

Example

t := &exampleT{}
defer t.close()
// Any element can be a trigger: it is given the props and puts them
// where it wants.
page := func() tela.Node {
return h.Div(h.Class("p-6"), kit.Popover{
Trigger: kit.TriggerFunc(func(p tela.Prop) tela.Node {
return h.Button(p, h.Class("rounded-full"), h.AriaLabel("Account"), kit.Avatar{Fallback: "AB"})
}),
AriaLabel: "Account",
Children: h.P(h.Text("Signed in as Ana.")),
})
}
app := telatest.MountFunc(t, page)
app.Click(telatest.ByRole("button", "Account"))
fmt.Println("open:", app.Count(telatest.ByRole("dialog", "Account")) == 1)
// Output: open: true
func (f TriggerFunc) WithTriggerProps(p tela.Prop) tela.Component

WithTriggerProps returns the component that calls f with p.

type Variant string

Variant is the look of a component among the ones its recipe defines: a Button is Default, Secondary, Outline, Ghost, Destructive or Link. The constants are shared by the components that have the same variants; an app defines its own by adding a name to a recipe’s Variants map (s.Button.Variants[“glass”] = “…”) and using Variant(“glass”).

const (
Default Variant = ""
Secondary Variant = "secondary"
Outline Variant = "outline"
Ghost Variant = "ghost"
Destructive Variant = "destructive"
Link Variant = "link"
)

The variants shadcn/ui’s components share. Default is the zero value, so leaving Variant out gives the default look.

const (
// Windows is Windows 11's: rectangular 46 px buttons that fill the
// title bar's height, thin glyphs, and a close button that turns red.
Windows Variant = "windows"
// GNOME is Adwaita's: round icon buttons in a tall header bar, with
// the title centered.
GNOME Variant = "gnome"
// MacOS is Aqua's: the red, yellow and green dots at the start of the
// bar, whose glyphs show when the pointer is over them.
MacOS Variant = "macos"
)

The looks of the window controls and the title bar (ADR-182). They are [Variant]s, like the looks of every other component: the recipes of WindowControlsStyle and TitleBarStyle define them, and an app adds a look by adding a name to their maps. Default, the zero value, is the look of the platform the app runs on: Windows on Windows, MacOS on macOS, GNOME everywhere else.

const TabsLine Variant = "line"

TabsLine is the Variant of a Tabs list that draws no background and underlines the selected tab (shadcn’s tabsListVariants “line”). The other variant, Default, is the muted pill.

type WindowControls struct {
// Variant is the look: Default (the platform's), Windows, GNOME or
// MacOS, or one the active [Style] adds.
Variant Variant
// HideMinimize and HideMaximize leave a button out, for a window that
// cannot be minimized or maximized (a dialog, a fixed-size window).
// Close is always there.
HideMinimize, HideMaximize bool
// State, when set, is the state to draw in place of the window's:
// Maximized changes the maximize button, Focused false dims the
// group. It is for previews and tests; leave it nil in an app.
State *tela.WindowState
// MinimizeIcon, MaximizeIcon, RestoreIcon and CloseIcon replace the
// glyphs of the look.
MinimizeIcon, MaximizeIcon, RestoreIcon, CloseIcon tela.Icon
// MinimizeLabel, MaximizeLabel, RestoreLabel and CloseLabel are the
// buttons' accessible names ("Minimize", "Maximize", "Restore",
// "Close"); set them to translate.
MinimizeLabel, MaximizeLabel, RestoreLabel, CloseLabel string
// AriaLabel names the group ("Window controls").
AriaLabel string
// Class is added after the recipe's classes.
Class string
// Props are more props for the group element.
Props tela.Child
}

WindowControls are the minimize, maximize (restore while the window is maximized) and close buttons of a custom title bar: the ones of Windows 11, of GNOME’s Adwaita or of macOS, by the platform the app runs on or by Variant. TitleBar draws them at the side of its platform; use WindowControls alone to place them yourself.

kit.WindowControls{} // the platform's look
kit.WindowControls{Variant: kit.Windows, HideMinimize: true}

Each button is a real button with the window-control prop of h.WindowControl, so a click does what its name says without any handler, the OS knows what the button is (Windows 11 shows its snap layouts over the maximize button), and the keyboard works. The maximize button’s name and glyph follow the window’s state: “Maximize” and “Restore”. The group takes no space of its own beyond its buttons, and is not a drag area: put it inside a bar that has h.WindowDrag (the buttons are holes in it by themselves).

Without a window, as in tela snapshot, the buttons draw and do nothing.

The glyphs, the names and the look are all slots and recipes: the icon and label fields replace one, and WindowControlsStyle restyles a look or adds one.

func (w WindowControls) Build() tela.Node

Build returns the group of buttons.

type WindowControlsStyle struct {
// Root is the group the buttons are in.
Root string
// Variants are the looks of the group: Windows, GNOME and MacOS.
Variants map[Variant]string
// Button is what every button has: layout, transition and focus ring.
Button string
// ButtonVariants are the looks of a button: its size, shape, color and
// hover.
ButtonVariants map[Variant]string
// MinimizeVariants, MaximizeVariants and CloseVariants are added to
// the button of that control, per look: the dot colors of MacOS, the
// red hover of Windows' close button.
MinimizeVariants, MaximizeVariants, CloseVariants map[Variant]string
// Inactive is added to every button while the window is not focused.
Inactive map[Variant]string
// Icon is what the icons have, before IconVariants.
Icon string
// IconVariants are the icon's size and color, per look.
IconVariants map[Variant]string
}

WindowControlsStyle is the recipe of WindowControls.