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.
- Overview
- Constants
- Functions:
DefaultStyle·Theme·ToastAction·ToastDescription·ToastDuration·ToastOnAutoClose·ToastOnDismiss·ToastReplace·UseStyle·UseToast - Types
type AlertDialog:Buildtype AlertDialogStyletype Avatar:Buildtype AvatarStyletype Badge:Buildtype BadgeStyletype Button:Build·WithTriggerPropstype ButtonStyletype Calendar:Buildtype CalendarStyletype Card:Buildtype CardStyletype Checkbox:Buildtype CheckboxStyletype ColorInput:Buildtype ColorInputStyletype ColorPicker:Buildtype ColorPickerStyletype Columntype ColumnAligntype ContextMenu:Buildtype DatePicker:Buildtype DatePickerStyletype DateRangePicker:Buildtype Dialog:Buildtype DialogStyletype DropdownMenu:Buildtype Field:Buildtype FieldSet:Buildtype FieldSetStyletype FieldStyletype Input:Buildtype InputStyletype Label:Buildtype LabelStyletype LegendVarianttype MenuItemtype MenuKindtype MenuStyletype Menubar:Buildtype MenubarMenutype MenubarStyletype Orientationtype Popover:Buildtype PopoverStyletype Progress:Buildtype ProgressStyletype RadioGroup:Buildtype RadioGroupStyletype RadioItemtype RadioStatetype RangeCalendar:Buildtype Select:Buildtype SelectItemtype SelectStyletype Separator:Buildtype SeparatorStyletype Sizetype Skeleton:Buildtype SkeletonStyletype Slider:Buildtype SliderStyletype SortDirectiontype SortStatetype Style:DefaultStyle·UseStyletype StyleProvider:Buildtype Switch:Buildtype SwitchStyletype Tabtype Table:Buildtype TableStyletype Tabs:Buildtype TabsStyletype Textarea:Buildtype TextareaStyletype TitleBar:Buildtype TitleBarStyletype ToastHandle:UseToast·Dismiss·DismissAll·Error·Info·Loading·Show·Success·Warningtype ToastIDtype ToastIconstype ToastOption:ToastAction·ToastDescription·ToastDuration·ToastOnAutoClose·ToastOnDismiss·ToastReplacetype ToastPositiontype ToastStyletype Toaster:Buildtype Toggle:Buildtype ToggleGroup:Buildtype ToggleGroupItemtype ToggleGroupStyletype ToggleStyletype Tooltip:Buildtype TooltipStyletype Triggertype TriggerFunc:WithTriggerPropstype Varianttype WindowControls:Buildtype WindowControlsStyle
Overview
Section titled “Overview”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.
The shape of a component
Section titled “The shape of a component”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.
Controlled and uncontrolled
Section titled “Controlled and uncontrolled”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} // controlledkit.Input{DefaultValue: "Ana"} // uncontrolledThe 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.
Customization, in three levels
Section titled “Customization, in three levels”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")}What is there
Section titled “What is there”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}}Title bars
Section titled “Title bars”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"},}Right to left
Section titled “Right to left”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).
Overlays and triggers
Section titled “Overlays and triggers”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 look and its tests
Section titled “The look and its tests”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.
Constants
Section titled “Constants”const NoOffset float32 = -1NoOffset 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 = -1ToastForever as a duration keeps a toast until it is dismissed.
Functions
Section titled “Functions”func Theme
Section titled “func Theme”func Theme() tela.ThemeTheme 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
Section titled “type AlertDialog”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 := falsepage := 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: truefunc (AlertDialog) Build
Section titled “func (AlertDialog) Build”func (a AlertDialog) Build() tela.NodeBuild returns the trigger and the dialog’s portal.
type AlertDialogStyle
Section titled “type AlertDialogStyle”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
Section titled “type Avatar”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 (Avatar) Build
Section titled “func (Avatar) Build”func (a Avatar) Build() tela.NodeBuild returns the avatar element.
type AvatarStyle
Section titled “type AvatarStyle”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
Section titled “type Badge”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 (Badge) Build
Section titled “func (Badge) Build”func (b Badge) Build() tela.NodeBuild returns the badge element.
type BadgeStyle
Section titled “type BadgeStyle”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
Section titled “type Button”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 := 0page := 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 1func (Button) Build
Section titled “func (Button) Build”func (b Button) Build() tela.NodeBuild returns the button element.
func (Button) WithTriggerProps
Section titled “func (Button) WithTriggerProps”func (b Button) WithTriggerProps(p tela.Prop) tela.ComponentWithTriggerProps 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
Section titled “type ButtonStyle”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
Section titled “type Calendar”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.Datepage := 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-25func (Calendar) Build
Section titled “func (Calendar) Build”func (c Calendar) Build() tela.NodeBuild returns the calendar element.
type CalendarStyle
Section titled “type CalendarStyle”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
Section titled “type Card”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 (Card) Build
Section titled “func (Card) Build”func (c Card) Build() tela.NodeBuild returns the card element.
type CardStyle
Section titled “type CardStyle”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
Section titled “type Checkbox”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 := falsepage := 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 checkboxfmt.Println("agreed", agreed)// Output: agreed truefunc (Checkbox) Build
Section titled “func (Checkbox) Build”func (c Checkbox) Build() tela.NodeBuild returns the checkbox element.
type CheckboxStyle
Section titled “type CheckboxStyle”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
Section titled “type ColorInput”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 (ColorInput) Build
Section titled “func (ColorInput) Build”func (c ColorInput) Build() tela.NodeBuild returns the button and its popover.
type ColorInputStyle
Section titled “type ColorInputStyle”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
Section titled “type ColorPicker”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 []stringpage := 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 #22c55ee5func (ColorPicker) Build
Section titled “func (ColorPicker) Build”func (p ColorPicker) Build() tela.NodeBuild 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
Section titled “type ColorPickerStyle”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
Section titled “type Column”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
Section titled “type ColumnAlign”type ColumnAlign stringColumnAlign 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
Section titled “type ContextMenu”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 reloadfunc (ContextMenu[K]) Build
Section titled “func (ContextMenu[K]) Build”func (c ContextMenu[K]) Build() tela.NodeBuild returns the area and, in a portal, the menu.
type DatePicker
Section titled “type DatePicker”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: falsefunc (DatePicker) Build
Section titled “func (DatePicker) Build”func (p DatePicker) Build() tela.NodeBuild returns the button and its popover.
type DatePickerStyle
Section titled “type DatePickerStyle”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
Section titled “type DateRangePicker”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.DateRangekit.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-31func (DateRangePicker) Build
Section titled “func (DateRangePicker) Build”func (p DateRangePicker) Build() tela.NodeBuild returns the button and its popover.
type Dialog
Section titled “type Dialog”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: falseExample (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: truefunc (Dialog) Build
Section titled “func (Dialog) Build”func (d Dialog) Build() tela.NodeBuild returns the trigger and the dialog’s portal.
type DialogStyle
Section titled “type DialogStyle”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
Section titled “type DropdownMenu”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 := truepage := 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: falsefunc (DropdownMenu[K]) Build
Section titled “func (DropdownMenu[K]) Build”func (d DropdownMenu[K]) Build() tela.NodeBuild returns the trigger and the menu’s portal.
type Field
Section titled “type Field”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 inputfunc (Field) Build
Section titled “func (Field) Build”func (f Field) Build() tela.NodeBuild returns the field element: a group holding the label, the control, the description and the error.
type FieldSet
Section titled “type FieldSet”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 profunc (FieldSet) Build
Section titled “func (FieldSet) Build”func (f FieldSet) Build() tela.NodeBuild returns the group element: the legend, then the description and the children in a column.
type FieldSetStyle
Section titled “type FieldSetStyle”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
Section titled “type FieldStyle”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
Section titled “type Input”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 (Input) Build
Section titled “func (Input) Build”func (i Input) Build() tela.NodeBuild returns the input element.
type InputStyle
Section titled “type InputStyle”type InputStyle struct { Root string}InputStyle is the recipe of Input (shadcn/ui’s Input).
type Label
Section titled “type Label”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 (Label) Build
Section titled “func (Label) Build”func (l Label) Build() tela.NodeBuild returns the label element.
type LabelStyle
Section titled “type LabelStyle”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
Section titled “type LegendVariant”type LegendVariant stringLegendVariant 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
Section titled “type MenuItem”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
Section titled “type MenuKind”type MenuKind intMenuKind 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
Section titled “type MenuStyle”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
Section titled “type Menubar”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 undofunc (Menubar[K]) Build
Section titled “func (Menubar[K]) Build”func (m Menubar[K]) Build() tela.NodeBuild returns the bar and, in a portal, the open menu.
type MenubarMenu
Section titled “type MenubarMenu”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
Section titled “type MenubarStyle”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
Section titled “type Orientation”type Orientation stringOrientation 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
Section titled “type Popover”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 itfmt.Println("open:", app.Count(telatest.ByRole("dialog", "Dimensions")) == 1)// Output:// open: true// open: falsefunc (Popover) Build
Section titled “func (Popover) Build”func (p Popover) Build() tela.NodeBuild returns the trigger and the popover’s portal.
type PopoverStyle
Section titled “type PopoverStyle”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
Section titled “type Progress”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 shownfunc (Progress) Build
Section titled “func (Progress) Build”func (p Progress) Build() tela.NodeBuild returns the progress element.
type ProgressStyle
Section titled “type ProgressStyle”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
Section titled “type RadioGroup”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 profunc (RadioGroup[K]) Build
Section titled “func (RadioGroup[K]) Build”func (g RadioGroup[K]) Build() tela.NodeBuild returns the group element.
type RadioGroupStyle
Section titled “type RadioGroupStyle”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
Section titled “type RadioItem”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
Section titled “type RadioState”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
Section titled “type RangeCalendar”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.DateRangekit.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.DateRangepage := 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-05func (RangeCalendar) Build
Section titled “func (RangeCalendar) Build”func (c RangeCalendar) Build() tela.NodeBuild returns the calendar element.
type Select
Section titled “type Select”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 stringpage := 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 bananafunc (Select[K]) Build
Section titled “func (Select[K]) Build”func (s Select[K]) Build() tela.NodeBuild returns the trigger and, in a portal, the list while it is open.
type SelectItem
Section titled “type SelectItem”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
Section titled “type SelectStyle”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
Section titled “type Separator”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 (Separator) Build
Section titled “func (Separator) Build”func (s Separator) Build() tela.NodeBuild returns the separator element.
type SeparatorStyle
Section titled “type SeparatorStyle”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
Section titled “type Size”type Size stringSize 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
Section titled “type Skeleton”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 (Skeleton) Build
Section titled “func (Skeleton) Build”func (s Skeleton) Build() tela.NodeBuild returns the skeleton element.
type SkeletonStyle
Section titled “type SkeletonStyle”type SkeletonStyle struct { Root string}SkeletonStyle is the recipe of Skeleton (shadcn/ui’s Skeleton).
type Slider
Section titled “type Slider”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.0page := 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 40func (Slider) Build
Section titled “func (Slider) Build”func (s Slider) Build() tela.NodeBuild returns the slider element.
type SliderStyle
Section titled “type SliderStyle”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
Section titled “type SortDirection”type SortDirection stringSortDirection 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
Section titled “type SortState”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
Section titled “type Style”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,SizeorOrientationto 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
Section titled “func DefaultStyle”func DefaultStyle() StyleDefaultStyle 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
Section titled “func UseStyle”func UseStyle() *StyleUseStyle 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
Section titled “type StyleProvider”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: 1func (StyleProvider) Build
Section titled “func (StyleProvider) Build”func (p StyleProvider) Build() tela.NodeBuild provides the style to the children.
type Switch
Section titled “type Switch”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 := truepage := 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 falsefunc (Switch) Build
Section titled “func (Switch) Build”func (s Switch) Build() tela.NodeBuild returns the switch element.
type SwitchStyle
Section titled “type SwitchStyle”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
Section titled “type Tab”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
Section titled “type Table”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) }}, },}Sorting
Section titled “Sorting”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, ...}Layout
Section titled “Layout”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 (Table[T]) Build
Section titled “func (Table[T]) Build”func (t Table[T]) Build() tela.NodeBuild returns the table.
type TableStyle
Section titled “type TableStyle”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
Section titled “type Tabs”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 shownfunc (Tabs[K]) Build
Section titled “func (Tabs[K]) Build”func (t Tabs[K]) Build() tela.NodeBuild returns the tabs element: the list of triggers and the selected panel.
type TabsStyle
Section titled “type TabsStyle”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
Section titled “type Textarea”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 (Textarea) Build
Section titled “func (Textarea) Build”func (t Textarea) Build() tela.NodeBuild returns the textarea element.
type TextareaStyle
Section titled “type TextareaStyle”type TextareaStyle struct { Root string}TextareaStyle is the recipe of Textarea (shadcn/ui’s Textarea).
type TitleBar
Section titled “type TitleBar”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 (TitleBar) Build
Section titled “func (TitleBar) Build”func (t TitleBar) Build() tela.NodeBuild returns the bar.
type TitleBarStyle
Section titled “type TitleBarStyle”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
Section titled “type ToastHandle”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
Section titled “func UseToast”func UseToast() ToastHandleUseToast 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 (ToastHandle) Dismiss
Section titled “func (ToastHandle) Dismiss”func (t ToastHandle) Dismiss(id ToastID)Dismiss closes the toast with the id; an id that is gone is ignored.
func (ToastHandle) DismissAll
Section titled “func (ToastHandle) DismissAll”func (t ToastHandle) DismissAll()DismissAll closes every toast.
func (ToastHandle) Error
Section titled “func (ToastHandle) Error”func (t ToastHandle) Error(title string, opts ...ToastOption) ToastIDError shows a toast with an error icon. It is announced as an alert.
func (ToastHandle) Info
Section titled “func (ToastHandle) Info”func (t ToastHandle) Info(title string, opts ...ToastOption) ToastIDInfo shows a toast with an info icon.
func (ToastHandle) Loading
Section titled “func (ToastHandle) Loading”func (t ToastHandle) Loading(title string, opts ...ToastOption) ToastIDLoading 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 (ToastHandle) Show
Section titled “func (ToastHandle) Show”func (t ToastHandle) Show(title string, opts ...ToastOption) ToastIDShow 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 (ToastHandle) Success
Section titled “func (ToastHandle) Success”func (t ToastHandle) Success(title string, opts ...ToastOption) ToastIDSuccess shows a toast with a check icon.
func (ToastHandle) Warning
Section titled “func (ToastHandle) Warning”func (t ToastHandle) Warning(title string, opts ...ToastOption) ToastIDWarning shows a toast with a warning icon.
type ToastID
Section titled “type ToastID”type ToastID uint64ToastID names one toast of a Toaster: ToastHandle.Show returns it, and Dismiss and ToastReplace take it.
type ToastIcons
Section titled “type ToastIcons”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
Section titled “type ToastOption”type ToastOption func(*toastData)ToastOption customizes a toast shown with ToastHandle.Show and its variants.
func ToastAction
Section titled “func ToastAction”func ToastAction(label string, onClick func()) ToastOptionToastAction adds a button to the toast. Clicking it calls onClick and dismisses the toast.
func ToastDescription
Section titled “func ToastDescription”func ToastDescription(text string) ToastOptionToastDescription adds a second, lighter line under the title.
func ToastDuration
Section titled “func ToastDuration”func ToastDuration(d time.Duration) ToastOptionToastDuration 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
Section titled “func ToastOnAutoClose”func ToastOnAutoClose(f func()) ToastOptionToastOnAutoClose calls f, on the UI thread, when the toast closes because its time ran out.
func ToastOnDismiss
Section titled “func ToastOnDismiss”func ToastOnDismiss(f func()) ToastOptionToastOnDismiss calls f, on the UI thread, when the toast is dismissed by the user or by ToastHandle.Dismiss.
func ToastReplace
Section titled “func ToastReplace”func ToastReplace(id ToastID) ToastOptionToastReplace 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
Section titled “type ToastPosition”type ToastPosition stringToastPosition 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
Section titled “type ToastStyle”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
Section titled “type Toaster”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 shownfunc (Toaster) Build
Section titled “func (Toaster) Build”func (t Toaster) Build() tela.NodeBuild returns the app and the viewport that shows the toasts.
type Toggle
Section titled “type Toggle”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 := falsepage := 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 truefunc (Toggle) Build
Section titled “func (Toggle) Build”func (t Toggle) Build() tela.NodeBuild returns the toggle element.
type ToggleGroup
Section titled “type ToggleGroup”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 darkfunc (ToggleGroup[K]) Build
Section titled “func (ToggleGroup[K]) Build”func (g ToggleGroup[K]) Build() tela.NodeBuild returns the group element.
type ToggleGroupItem
Section titled “type ToggleGroupItem”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
Section titled “type ToggleGroupStyle”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
Section titled “type ToggleStyle”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
Section titled “type Tooltip”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: falsefunc (Tooltip) Build
Section titled “func (Tooltip) Build”func (t Tooltip) Build() tela.NodeBuild returns the trigger and the tooltip’s portal.
type TooltipStyle
Section titled “type TooltipStyle”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
Section titled “type Trigger”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
Buttondoes: 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
Section titled “type TriggerFunc”type TriggerFunc func(p tela.Prop) tela.NodeTriggerFunc 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: truefunc (TriggerFunc) WithTriggerProps
Section titled “func (TriggerFunc) WithTriggerProps”func (f TriggerFunc) WithTriggerProps(p tela.Prop) tela.ComponentWithTriggerProps returns the component that calls f with p.
type Variant
Section titled “type Variant”type Variant stringVariant 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
Section titled “type WindowControls”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 lookkit.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 (WindowControls) Build
Section titled “func (WindowControls) Build”func (w WindowControls) Build() tela.NodeBuild returns the group of buttons.
type WindowControlsStyle
Section titled “type WindowControlsStyle”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.