Input Group
An input with an addon or a text prefix.
<inputgroup.InputGroup>
<inputgroup.InputGroupAddon><inputgroup.InputGroupText>$</inputgroup.InputGroupText></inputgroup.InputGroupAddon>
<inputgroup.InputGroupInput placeholder="0.00" attrs={gx.Attrs{{Key: "aria-label", Value: "Amount"}}} />
</inputgroup.InputGroup>Installation
Run the command in the app module.
gx add input-groupThe command also installs button, icons, input and textarea.
The command writes these files.
| File | Path in the app |
|---|---|
InputGroup.gx |
ui/input-group/InputGroup.gx |
InputGroupAddon.gx |
ui/input-group/InputGroupAddon.gx |
InputGroupInput.gx |
ui/input-group/InputGroupInput.gx |
InputGroupText.gx |
ui/input-group/InputGroupText.gx |
InputGroup.fixtures.go |
ui/input-group/InputGroup.fixtures.go |
styles.go |
ui/input-group/styles.go |
InputGroupAddon.fixtures.go |
ui/input-group/InputGroupAddon.fixtures.go |
InputGroupInput.fixtures.go |
ui/input-group/InputGroupInput.fixtures.go |
InputGroupText.fixtures.go |
ui/input-group/InputGroupText.fixtures.go |
InputGroupButton.gx |
ui/input-group/InputGroupButton.gx |
InputGroupButton.fixtures.go |
ui/input-group/InputGroupButton.fixtures.go |
InputGroupTextarea.gx |
ui/input-group/InputGroupTextarea.gx |
InputGroupTextarea.fixtures.go |
ui/input-group/InputGroupTextarea.fixtures.go |
Install button, icons, input and textarea first.
Copy each file to its path in the app. Change each import of a registry package to the path of that package in the app.
package inputgroup
props {
// Class adds classes to the root element.
Class string = ""
// Children is the content of the group: one control and its addons.
Children gx.Node
// Attrs adds HTML attributes to the root element.
Attrs gx.Attrs = nil
}
<div data-slot="input-group" role="group" class={gx.Cx("group/input-group relative flex w-full items-center rounded-md border border-input shadow-xs transition-[color,box-shadow] outline-none dark:bg-input/30 h-9 min-w-0 has-[>textarea]:h-auto has-[>[data-align=inline-start]]:[&>input]:pl-2 has-[>[data-align=inline-end]]:[&>input]:pr-2 has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40", p.Class)} {...p.Attrs}>{p.Children}</div>package inputgroup
props {
// Align sets the position of the addon: InlineStart, InlineEnd, BlockStart or BlockEnd.
Align Align = InlineStart
// Class adds classes to the root element.
Class string = ""
// Children is the content of the addon, for example an icon, text or a button.
Children gx.Node
// Attrs adds HTML attributes to the root element.
Attrs gx.Attrs = nil
}
<div role="group" data-slot="input-group-addon" data-align={p.align()} class={gx.Cx("flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4", alignClass[Align(p.align())], p.Class)} {...p.Attrs}>{p.Children}</div>package inputgroup
import "github.com/alternayte/gx/registry/input"
props {
// Id is the id of the root element. A label points at it.
Id string = ""
// Type is the type attribute. Empty means "text".
Type string = "text"
// Name is the name attribute. The form sends the value under this name.
Name string = ""
// Value is the value attribute: the start value of the input.
Value string = ""
// Placeholder is the placeholder attribute: the hint text of an empty control.
Placeholder string = ""
// Class adds classes to the root element.
Class string = ""
// Attrs adds HTML attributes to the root element.
Attrs gx.Attrs = nil
}
<input type={p.inputType()} name={p.Name} value={p.Value} placeholder={p.Placeholder} data-slot="input-group-control" class={gx.Cx(input.Control, "flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent", p.Class)} {...p.attrs()} />package inputgroup
props {
// Class adds classes to the root element.
Class string = ""
// Children is the content of the text, with or without an icon.
Children gx.Node
// Attrs adds HTML attributes to the root element.
Attrs gx.Attrs = nil
}
<span class={gx.Cx("flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4", p.Class)} {...p.Attrs}>{p.Children}</span>package inputgroup
import (
"github.com/alternayte/gx"
"github.com/alternayte/gx/registry/button"
"github.com/alternayte/gx/registry/icons"
)
// name returns the accessible name of a fixture control.
func name(label string) gx.Attrs {
return gx.Attrs{{Key: "aria-label", Value: label}}
}
var InputGroupFixtures = gx.Fixtures[InputGroupProps]{
"LeadingText": {Children: gx.Frag(
InputGroupAddon(InputGroupAddonProps{Children: InputGroupText(InputGroupTextProps{Children: gx.Text("$")})}),
InputGroupInput(InputGroupInputProps{Placeholder: "0.00", Attrs: name("Amount")}),
)},
"TrailingText": {Children: gx.Frag(
InputGroupInput(InputGroupInputProps{Placeholder: "example", Attrs: name("Domain")}),
InputGroupAddon(InputGroupAddonProps{Align: InlineEnd, Children: InputGroupText(InputGroupTextProps{Children: gx.Text(".com")})}),
)},
"Button": {Children: gx.Frag(
InputGroupInput(InputGroupInputProps{Placeholder: "Search", Attrs: name("Search")}),
InputGroupAddon(InputGroupAddonProps{Align: InlineEnd, Children: InputGroupButton(InputGroupButtonProps{Children: gx.Text("Search")})}),
)},
"IconButton": {Children: gx.Frag(
InputGroupInput(InputGroupInputProps{Value: "ada@example.com", Attrs: name("Email")}),
InputGroupAddon(InputGroupAddonProps{Align: InlineEnd, Children: InputGroupButton(InputGroupButtonProps{
Size: IconXs,
Attrs: name("Clear"),
Children: icons.X(icons.XProps{}),
})}),
)},
"Invalid": {Children: gx.Frag(
InputGroupAddon(InputGroupAddonProps{Children: icons.Info(icons.InfoProps{})}),
InputGroupInput(InputGroupInputProps{Value: "ada@", Attrs: gx.Attrs{{Key: "aria-label", Value: "Email"}, {Key: "aria-invalid", Value: "true"}}}),
)},
"Textarea": {Children: gx.Frag(
InputGroupTextarea(InputGroupTextareaProps{Placeholder: "Ask a question.", Attrs: name("Question")}),
InputGroupAddon(InputGroupAddonProps{Align: BlockEnd, Children: gx.Frag(
InputGroupText(InputGroupTextProps{Children: gx.Text("120 characters left")}),
InputGroupButton(InputGroupButtonProps{Variant: button.Default, Size: Sm, Class: "ml-auto", Children: gx.Text("Send")}),
)}),
)},
"BlockStart": {Children: gx.Frag(
InputGroupAddon(InputGroupAddonProps{Align: BlockStart, Children: InputGroupText(InputGroupTextProps{Children: gx.Text("Amount")})}),
InputGroupInput(InputGroupInputProps{Placeholder: "0.00", Attrs: name("Amount")}),
)},
}package inputgroup
import (
"github.com/alternayte/gx"
"github.com/alternayte/gx/registry/button"
)
// Align is the position of an addon inside an input group.
type Align string
// The alignments of inputgroup.InputGroupAddon.
const (
InlineStart Align = "inline-start"
InlineEnd Align = "inline-end"
BlockStart Align = "block-start"
BlockEnd Align = "block-end"
)
var alignClass = gx.Enum[Align]{
InlineStart: "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
InlineEnd: "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
BlockStart: "order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3",
BlockEnd: "order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3",
}
// align returns the data-align value; a zero value is InlineStart. The
// group reads this attribute to pad its control.
func (p InputGroupAddonProps) align() string {
if p.Align == "" {
return string(InlineStart)
}
return string(p.Align)
}
// Size is the height and padding of a button inside an input group.
type Size string
// The sizes of inputgroup.InputGroupButton.
const (
Xs Size = "xs"
Sm Size = "sm"
IconXs Size = "icon-xs"
IconSm Size = "icon-sm"
)
var sizeClass = gx.Enum[Size]{
Xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
Sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5",
IconXs: "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
IconSm: "size-8 p-0 has-[>svg]:p-0",
}
// buttonSize is the button size that each group size starts from: the
// group classes replace its height and padding.
var buttonSize = map[Size]button.Size{
Xs: button.Md,
Sm: button.Md,
IconXs: button.Icon,
IconSm: button.Icon,
}
// size returns the button size; a zero value is Xs.
func (p InputGroupButtonProps) size() Size {
if p.Size == "" {
return Xs
}
return p.Size
}
// variant returns the button variant; a zero value is Ghost.
func (p InputGroupButtonProps) variant() button.Variant {
if p.Variant == "" {
return button.Ghost
}
return p.Variant
}
// inputType returns the type of the input; a zero value is text.
func (p InputGroupInputProps) inputType() string {
if p.Type == "" {
return "text"
}
return p.Type
}
// attrs returns the id when the input has one, then the caller's
// attributes.
func (p InputGroupInputProps) attrs() gx.Attrs {
if p.Id == "" {
return p.Attrs
}
return append(gx.Attrs{{Key: "id", Value: p.Id}}, p.Attrs...)
}
// attrs returns the id when the textarea has one, then the caller's
// attributes.
func (p InputGroupTextareaProps) attrs() gx.Attrs {
if p.Id == "" {
return p.Attrs
}
return append(gx.Attrs{{Key: "id", Value: p.Id}}, p.Attrs...)
}package inputgroup
import "github.com/alternayte/gx"
var InputGroupAddonFixtures = gx.Fixtures[InputGroupAddonProps]{"Text": {Children: gx.Text("@")}}package inputgroup
import "github.com/alternayte/gx"
var InputGroupInputFixtures = gx.Fixtures[InputGroupInputProps]{"Placeholder": {Placeholder: "Value", Attrs: name("Value")}}
// InputGroupInputWrap renders the input in a group, which draws its border
// and its focus ring.
func InputGroupInputWrap(n gx.Node) gx.Node {
return InputGroup(InputGroupProps{Children: n})
}package inputgroup
import "github.com/alternayte/gx"
var InputGroupTextFixtures = gx.Fixtures[InputGroupTextProps]{"Text": {Children: gx.Text("Text")}}package inputgroup
import "github.com/alternayte/gx/registry/button"
props {
// Variant sets the visual style. It takes a button.Variant: Default, Secondary, Destructive,
// Outline, Ghost or Link.
Variant button.Variant = button.Ghost
// Size sets the height and padding: Xs, Sm, or IconXs or IconSm for a square button.
Size Size = Xs
// Type is the type attribute: "button", "submit" or "reset".
Type string = "button"
// Class adds classes to the root element.
Class string = ""
// Children is the button label.
Children gx.Node
// Attrs adds HTML attributes to the root element.
Attrs gx.Attrs = nil
}
<button.Button type={p.Type} variant={p.variant()} size={buttonSize[p.size()]} class={gx.Cx("flex items-center gap-2 text-sm shadow-none", sizeClass[p.size()], p.Class)} attrs={p.Attrs}>{p.Children}</button.Button>package inputgroup
import (
"github.com/alternayte/gx"
"github.com/alternayte/gx/registry/icons"
)
var InputGroupButtonFixtures = gx.Fixtures[InputGroupButtonProps]{
"ExtraSmall": {Children: gx.Text("Search")},
"Small": {Size: Sm, Children: gx.Text("Search")},
"IconXs": {Size: IconXs, Attrs: name("Clear"), Children: icons.X(icons.XProps{})},
"IconSm": {Size: IconSm, Attrs: name("Clear"), Children: icons.X(icons.XProps{})},
}package inputgroup
import "github.com/alternayte/gx/registry/textarea"
props {
// Id is the id of the root element. A label points at it.
Id string = ""
// Name is the name attribute. The form sends the value under this name.
Name string = ""
// Value is the start text of the textarea.
Value string = ""
// Placeholder is the placeholder attribute: the hint text of an empty control.
Placeholder string = ""
// Class adds classes to the root element.
Class string = ""
// Attrs adds HTML attributes to the root element.
Attrs gx.Attrs = nil
}
<textarea name={p.Name} placeholder={p.Placeholder} data-slot="input-group-control" class={gx.Cx(textarea.Control, "flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent", p.Class)} {...p.attrs()}>{p.Value}</textarea>package inputgroup
import "github.com/alternayte/gx"
var InputGroupTextareaFixtures = gx.Fixtures[InputGroupTextareaProps]{
"Placeholder": {Placeholder: "Ask a question.", Attrs: name("Question")},
}
// InputGroupTextareaWrap renders the textarea in a group, which draws its
// border and its focus ring.
func InputGroupTextareaWrap(n gx.Node) gx.Node {
return InputGroup(InputGroupProps{Children: n})
}The theme must define these tokens: --input, --ring, --muted-foreground, --destructive.
Usage
An input or a textarea with text, icons or buttons inside its border.
<inputgroup.InputGroup>
<inputgroup.InputGroupAddon><icons.Info /></inputgroup.InputGroupAddon>
<inputgroup.InputGroupInput name="q" placeholder="Search" />
<inputgroup.InputGroupAddon align={inputgroup.InlineEnd}>
<inputgroup.InputGroupButton>Search</inputgroup.InputGroupButton>
</inputgroup.InputGroupAddon>
</inputgroup.InputGroup>
<inputgroup.InputGroup>
<inputgroup.InputGroupTextarea name="message" placeholder="Ask a question." />
<inputgroup.InputGroupAddon align={inputgroup.BlockEnd}>
<inputgroup.InputGroupText>120 characters left</inputgroup.InputGroupText>
</inputgroup.InputGroupAddon>
</inputgroup.InputGroup>An addon has four positions: InlineStart, InlineEnd, BlockStart and BlockEnd. A block addon makes the group a column.
InputGroupButton is a ghost button with the sizes Xs, Sm, IconXs and IconSm.
The group shows the focus ring of its control. It shows the error border when the control has aria-invalid="true".
A click on an addon does not focus the control. Put the addon text in a <label for> when a click must focus it.
Examples
InputGroup: Leading text
<inputgroup.InputGroup>
<inputgroup.InputGroupAddon><inputgroup.InputGroupText>$</inputgroup.InputGroupText></inputgroup.InputGroupAddon>
<inputgroup.InputGroupInput placeholder="0.00" attrs={gx.Attrs{{Key: "aria-label", Value: "Amount"}}} />
</inputgroup.InputGroup>InputGroup: Trailing text
<inputgroup.InputGroup>
<inputgroup.InputGroupInput placeholder="example" attrs={gx.Attrs{{Key: "aria-label", Value: "Domain"}}} />
<inputgroup.InputGroupAddon align={inputgroup.InlineEnd}>
<inputgroup.InputGroupText>.com</inputgroup.InputGroupText>
</inputgroup.InputGroupAddon>
</inputgroup.InputGroup>InputGroup: Button
<inputgroup.InputGroup>
<inputgroup.InputGroupInput placeholder="Search" attrs={gx.Attrs{{Key: "aria-label", Value: "Search"}}} />
<inputgroup.InputGroupAddon align={inputgroup.InlineEnd}>
<inputgroup.InputGroupButton>Search</inputgroup.InputGroupButton>
</inputgroup.InputGroupAddon>
</inputgroup.InputGroup>InputGroup: Icon button
<inputgroup.InputGroup>
<inputgroup.InputGroupInput value="ada@example.com" attrs={gx.Attrs{{Key: "aria-label", Value: "Email"}}} />
<inputgroup.InputGroupAddon align={inputgroup.InlineEnd}>
<inputgroup.InputGroupButton size={inputgroup.IconXs} attrs={gx.Attrs{{Key: "aria-label", Value: "Clear"}}}>
<icons.X />
</inputgroup.InputGroupButton>
</inputgroup.InputGroupAddon>
</inputgroup.InputGroup>InputGroup: Invalid
<inputgroup.InputGroup>
<inputgroup.InputGroupAddon><icons.Info /></inputgroup.InputGroupAddon>
<inputgroup.InputGroupInput value="ada@" attrs={gx.Attrs{{Key: "aria-label", Value: "Email"}, {Key: "aria-invalid", Value: "true"}}} />
</inputgroup.InputGroup>InputGroup: Textarea
<inputgroup.InputGroup>
<inputgroup.InputGroupTextarea placeholder="Ask a question." attrs={gx.Attrs{{Key: "aria-label", Value: "Question"}}} />
<inputgroup.InputGroupAddon align={inputgroup.BlockEnd}>
<inputgroup.InputGroupText>120 characters left</inputgroup.InputGroupText>
<inputgroup.InputGroupButton variant={button.Default} size={inputgroup.Sm} class="ml-auto">
Send
</inputgroup.InputGroupButton>
</inputgroup.InputGroupAddon>
</inputgroup.InputGroup>InputGroup: Block start
<inputgroup.InputGroup>
<inputgroup.InputGroupAddon align={inputgroup.BlockStart}>
<inputgroup.InputGroupText>Amount</inputgroup.InputGroupText>
</inputgroup.InputGroupAddon>
<inputgroup.InputGroupInput placeholder="0.00" attrs={gx.Attrs{{Key: "aria-label", Value: "Amount"}}} />
</inputgroup.InputGroup>InputGroupAddon: Text
<inputgroup.InputGroupAddon>@</inputgroup.InputGroupAddon>InputGroupButton: Extra small
<inputgroup.InputGroupButton>Search</inputgroup.InputGroupButton>InputGroupButton: Small
<inputgroup.InputGroupButton size={inputgroup.Sm}>Search</inputgroup.InputGroupButton>InputGroupButton: Icon xs
<inputgroup.InputGroupButton size={inputgroup.IconXs} attrs={gx.Attrs{{Key: "aria-label", Value: "Clear"}}}>
<icons.X />
</inputgroup.InputGroupButton>InputGroupButton: Icon sm
<inputgroup.InputGroupButton size={inputgroup.IconSm} attrs={gx.Attrs{{Key: "aria-label", Value: "Clear"}}}>
<icons.X />
</inputgroup.InputGroupButton>InputGroupInput: Placeholder
<inputgroup.InputGroupInput placeholder="Value" attrs={gx.Attrs{{Key: "aria-label", Value: "Value"}}} />InputGroupText: Text
<inputgroup.InputGroupText>Text</inputgroup.InputGroupText>InputGroupTextarea: Placeholder
<inputgroup.InputGroupTextarea placeholder="Ask a question." attrs={gx.Attrs{{Key: "aria-label", Value: "Question"}}} />API reference
A tag sets a prop by its name with a lower-case first letter: Class is class.
inputgroup.InputGroup
| Prop | Type | Default | Description |
|---|---|---|---|
Class |
string |
"" |
Class adds classes to the root element. |
Children |
gx.Node |
Required | Children is the content of the group: one control and its addons. |
Attrs |
gx.Attrs |
nil |
Attrs adds HTML attributes to the root element. |
inputgroup.InputGroupAddon
| Prop | Type | Default | Description |
|---|---|---|---|
Align |
Align |
InlineStart |
Align sets the position of the addon: InlineStart, InlineEnd, BlockStart or BlockEnd. |
Class |
string |
"" |
Class adds classes to the root element. |
Children |
gx.Node |
Required | Children is the content of the addon, for example an icon, text or a button. |
Attrs |
gx.Attrs |
nil |
Attrs adds HTML attributes to the root element. |
inputgroup.InputGroupButton
| Prop | Type | Default | Description |
|---|---|---|---|
Variant |
button.Variant |
button.Ghost |
Variant sets the visual style. It takes a button.Variant: Default, Secondary, Destructive, Outline, Ghost or Link. |
Size |
Size |
Xs |
Size sets the height and padding: Xs, Sm, or IconXs or IconSm for a square button. |
Type |
string |
"button" |
Type is the type attribute: "button", "submit" or "reset". |
Class |
string |
"" |
Class adds classes to the root element. |
Children |
gx.Node |
Required | Children is the button label. |
Attrs |
gx.Attrs |
nil |
Attrs adds HTML attributes to the root element. |
inputgroup.InputGroupInput
| Prop | Type | Default | Description |
|---|---|---|---|
Id |
string |
"" |
Id is the id of the root element. A label points at it. |
Type |
string |
"text" |
Type is the type attribute. Empty means "text". |
Name |
string |
"" |
Name is the name attribute. The form sends the value under this name. |
Value |
string |
"" |
Value is the value attribute: the start value of the input. |
Placeholder |
string |
"" |
Placeholder is the placeholder attribute: the hint text of an empty control. |
Class |
string |
"" |
Class adds classes to the root element. |
Attrs |
gx.Attrs |
nil |
Attrs adds HTML attributes to the root element. |
inputgroup.InputGroupText
| Prop | Type | Default | Description |
|---|---|---|---|
Class |
string |
"" |
Class adds classes to the root element. |
Children |
gx.Node |
Required | Children is the content of the text, with or without an icon. |
Attrs |
gx.Attrs |
nil |
Attrs adds HTML attributes to the root element. |
inputgroup.InputGroupTextarea
| Prop | Type | Default | Description |
|---|---|---|---|
Id |
string |
"" |
Id is the id of the root element. A label points at it. |
Name |
string |
"" |
Name is the name attribute. The form sends the value under this name. |
Value |
string |
"" |
Value is the start text of the textarea. |
Placeholder |
string |
"" |
Placeholder is the placeholder attribute: the hint text of an empty control. |
Class |
string |
"" |
Class adds classes to the root element. |
Attrs |
gx.Attrs |
nil |
Attrs adds HTML attributes to the root element. |
Do and do not
Do
- Give the control an
aria-labelor a label. An addon is not a label. - Put a block addon with a textarea.
Don't
- Do not put more than one control in a group.
- Do not use an addon for an error message. Use a field error.
Keyboard
| Key | Action |
|---|---|
| Tab | Moves focus to the control, then to each button of the group. |
| Enter, Space | Activates the focused button. |