Input Group
An Input with fixed text before or after the value, such as a domain suffix.
Suffix
Fixed text after the input, such as the domain a site's address ends in. The label names the whole value.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-form-field/ui-form-field.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-input-group/ui-input-group.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-input/ui-input.html"
>
<ui-form-field>
<label
slot="label"
for="site-address"
>Site address</label>
<ui-input-group>
<ui-input
id="site-address"
name="site"
placeholder="your-site"
></ui-input>
<span
slot="suffix"
>.example.com</span>
</ui-input-group>
</ui-form-field><script setup>
import { UiFormField, UiInput, UiInputGroup } from "@threadlabs/looma";
</script>
<template>
<UiFormField>
<template #label>
<label
for="site-address"
>Site address</label>
</template>
<UiInputGroup>
<UiInput
id="site-address"
name="site"
placeholder="your-site"
/>
<template #suffix>
<span>.example.com</span>
</template>
</UiInputGroup>
</UiFormField>
</template>import { UiFormField, UiInput, UiInputGroup } from "@threadlabs/looma";
export default function Example() {
return <UiFormField
slots={{
label: <label
htmlFor="site-address"
>Site address</label>,
}}
>
<UiInputGroup
slots={{
suffix: <span>.example.com</span>,
}}
>
<UiInput
id="site-address"
name="site"
placeholder="your-site"
/>
</UiInputGroup>
</UiFormField>;
}/** @jsxImportSource preact */
import { UiFormField, UiInput, UiInputGroup } from "@threadlabs/looma";
export default function Example() {
return <UiFormField
slots={{
label: <label
htmlFor="site-address"
>Site address</label>,
}}
>
<UiInputGroup
slots={{
suffix: <span>.example.com</span>,
}}
>
<UiInput
id="site-address"
name="site"
placeholder="your-site"
/>
</UiInputGroup>
</UiFormField>;
}<script>
import { UiFormField, UiInput, UiInputGroup } from "@threadlabs/looma";
</script>
{#snippet label()}
<label
for="site-address"
>Site address</label>
{/snippet}
{#snippet suffix()}
<span>.example.com</span>
{/snippet}
<UiFormField
slots={{ label }}
>
<UiInputGroup
slots={{ suffix }}
>
<UiInput
id="site-address"
name="site"
placeholder="your-site"
/>
</UiInputGroup>
</UiFormField>import { Component } from "@angular/core";
import { UiFormField, UiInput, UiInputGroup } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiFormField, UiInputGroup, UiInput],
template: `
<div
ui-form-field
>
<ng-container ngProjectAs="[slot='label']">
<label
for="site-address"
>Site address</label>
</ng-container>
<div
ui-input-group
>
<input
ui-input
id="site-address"
name="site"
placeholder="your-site"
>
<ng-container ngProjectAs="[slot='suffix']">
<span>.example.com</span>
</ng-container>
</div>
</div>
`,
})
export default class Example {}Prefix and suffix
Fixed text on both sides, such as the slashes around a path. An invalid input marks the whole group.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-form-field/ui-form-field.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-input-group/ui-input-group.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-input/ui-input.html"
>
<ui-form-field
invalid
>
<label
slot="label"
for="route"
>Address</label>
<ui-input-group>
<span
slot="prefix"
>/</span>
<ui-input
id="route"
name="route"
value="team notes"
invalid
></ui-input>
<span
slot="suffix"
>/</span>
</ui-input-group>
<span
slot="error"
>Use letters, numbers, and hyphens.</span>
</ui-form-field><script setup>
import { UiFormField, UiInput, UiInputGroup } from "@threadlabs/looma";
</script>
<template>
<UiFormField
:invalid="true"
>
<template #label>
<label
for="route"
>Address</label>
</template>
<UiInputGroup>
<template #prefix>
<span>/</span>
</template>
<UiInput
id="route"
name="route"
value="team notes"
:invalid="true"
/>
<template #suffix>
<span>/</span>
</template>
</UiInputGroup>
<template #error>
<span>Use letters, numbers, and hyphens.</span>
</template>
</UiFormField>
</template>import { UiFormField, UiInput, UiInputGroup } from "@threadlabs/looma";
export default function Example() {
return <UiFormField
invalid
slots={{
label: <label
htmlFor="route"
>Address</label>,
error: <span>Use letters, numbers, and hyphens.</span>,
}}
>
<UiInputGroup
slots={{
prefix: <span>/</span>,
suffix: <span>/</span>,
}}
>
<UiInput
id="route"
name="route"
value="team notes"
invalid
/>
</UiInputGroup>
</UiFormField>;
}/** @jsxImportSource preact */
import { UiFormField, UiInput, UiInputGroup } from "@threadlabs/looma";
export default function Example() {
return <UiFormField
invalid
slots={{
label: <label
htmlFor="route"
>Address</label>,
error: <span>Use letters, numbers, and hyphens.</span>,
}}
>
<UiInputGroup
slots={{
prefix: <span>/</span>,
suffix: <span>/</span>,
}}
>
<UiInput
id="route"
name="route"
value="team notes"
invalid
/>
</UiInputGroup>
</UiFormField>;
}<script>
import { UiFormField, UiInput, UiInputGroup } from "@threadlabs/looma";
</script>
{#snippet label()}
<label
for="route"
>Address</label>
{/snippet}
{#snippet prefix()}
<span>/</span>
{/snippet}
{#snippet suffix()}
<span>/</span>
{/snippet}
{#snippet error()}
<span>Use letters, numbers, and hyphens.</span>
{/snippet}
<UiFormField
invalid
slots={{ label, error }}
>
<UiInputGroup
slots={{ prefix, suffix }}
>
<UiInput
id="route"
name="route"
value="team notes"
invalid
/>
</UiInputGroup>
</UiFormField>import { Component } from "@angular/core";
import { UiFormField, UiInput, UiInputGroup } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiFormField, UiInputGroup, UiInput],
template: `
<div
ui-form-field
[invalid]="true"
>
<ng-container ngProjectAs="[slot='label']">
<label
for="route"
>Address</label>
</ng-container>
<div
ui-input-group
>
<ng-container ngProjectAs="[slot='prefix']">
<span>/</span>
</ng-container>
<input
ui-input
id="route"
name="route"
[value]="'team notes'"
[invalid]="true"
>
<ng-container ngProjectAs="[slot='suffix']">
<span>/</span>
</ng-container>
</div>
<ng-container ngProjectAs="[slot='error']">
<span>Use letters, numbers, and hyphens.</span>
</ng-container>
</div>
`,
})
export default class Example {}Action
One button at the end of the field, inside its border, for the one thing the field is for. A small button keeps the field at its usual height.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-form-field/ui-form-field.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-input-group/ui-input-group.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-input/ui-input.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-button/ui-button.html"
>
<form>
<ui-form-field>
<label
slot="label"
for="site-address-action"
>Site address</label>
<ui-input-group>
<ui-input
id="site-address-action"
name="site"
placeholder="your-site"
></ui-input>
<span
slot="suffix"
>.example.com</span>
<ui-button
slot="action"
type="submit"
variant="solid"
size="sm"
>Continue</ui-button>
</ui-input-group>
</ui-form-field>
</form><script setup>
import { UiButton, UiFormField, UiInput, UiInputGroup } from "@threadlabs/looma";
</script>
<template>
<form>
<UiFormField>
<template #label>
<label
for="site-address-action"
>Site address</label>
</template>
<UiInputGroup>
<UiInput
id="site-address-action"
name="site"
placeholder="your-site"
/>
<template #suffix>
<span>.example.com</span>
</template>
<template #action>
<UiButton
type="submit"
variant="solid"
size="sm"
>Continue</UiButton>
</template>
</UiInputGroup>
</UiFormField>
</form>
</template>import { UiButton, UiFormField, UiInput, UiInputGroup } from "@threadlabs/looma";
export default function Example() {
return <form>
<UiFormField
slots={{
label: <label
htmlFor="site-address-action"
>Site address</label>,
}}
>
<UiInputGroup
slots={{
suffix: <span>.example.com</span>,
action: <UiButton
type="submit"
variant="solid"
size="sm"
>Continue</UiButton>,
}}
>
<UiInput
id="site-address-action"
name="site"
placeholder="your-site"
/>
</UiInputGroup>
</UiFormField>
</form>;
}/** @jsxImportSource preact */
import { UiButton, UiFormField, UiInput, UiInputGroup } from "@threadlabs/looma";
export default function Example() {
return <form>
<UiFormField
slots={{
label: <label
htmlFor="site-address-action"
>Site address</label>,
}}
>
<UiInputGroup
slots={{
suffix: <span>.example.com</span>,
action: <UiButton
type="submit"
variant="solid"
size="sm"
>Continue</UiButton>,
}}
>
<UiInput
id="site-address-action"
name="site"
placeholder="your-site"
/>
</UiInputGroup>
</UiFormField>
</form>;
}<script>
import { UiButton, UiFormField, UiInput, UiInputGroup } from "@threadlabs/looma";
</script>
{#snippet label()}
<label
for="site-address-action"
>Site address</label>
{/snippet}
{#snippet suffix()}
<span>.example.com</span>
{/snippet}
{#snippet action()}
<UiButton
type="submit"
variant="solid"
size="sm"
>Continue</UiButton>
{/snippet}
<form>
<UiFormField
slots={{ label }}
>
<UiInputGroup
slots={{ suffix, action }}
>
<UiInput
id="site-address-action"
name="site"
placeholder="your-site"
/>
</UiInputGroup>
</UiFormField>
</form>import { Component } from "@angular/core";
import { UiButton, UiFormField, UiInput, UiInputGroup } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiFormField, UiInputGroup, UiInput, UiButton],
template: `
<form>
<div
ui-form-field
>
<ng-container ngProjectAs="[slot='label']">
<label
for="site-address-action"
>Site address</label>
</ng-container>
<div
ui-input-group
>
<input
ui-input
id="site-address-action"
name="site"
placeholder="your-site"
>
<ng-container ngProjectAs="[slot='suffix']">
<span>.example.com</span>
</ng-container>
<ng-container ngProjectAs="[slot='action']">
<ng-template
ui-button
[type]="'submit'"
[variant]="'solid'"
[size]="'sm'"
>Continue</ng-template>
</ng-container>
</div>
</div>
</form>
`,
})
export default class Example {}Icon prefix
An icon shares the input surface with one compact gap to the value; text affixes keep their separate framing.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-form-field/ui-form-field.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-input-group/ui-input-group.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-icon/ui-icon.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-input/ui-input.html"
>
<ui-form-field>
<label
slot="label"
for="destination"
>Destination</label>
<ui-input-group
prefix-kind="icon"
>
<ui-icon
slot="prefix"
name="search"
></ui-icon>
<ui-input
id="destination"
placeholder="Search or paste a URL…"
></ui-input>
</ui-input-group>
</ui-form-field><script setup>
import { UiFormField, UiIcon, UiInput, UiInputGroup } from "@threadlabs/looma";
</script>
<template>
<UiFormField>
<template #label>
<label
for="destination"
>Destination</label>
</template>
<UiInputGroup
prefix-kind="icon"
>
<template #prefix>
<UiIcon
name="search"
/>
</template>
<UiInput
id="destination"
placeholder="Search or paste a URL…"
/>
</UiInputGroup>
</UiFormField>
</template>import { UiFormField, UiIcon, UiInput, UiInputGroup } from "@threadlabs/looma";
export default function Example() {
return <UiFormField
slots={{
label: <label
htmlFor="destination"
>Destination</label>,
}}
>
<UiInputGroup
prefixKind="icon"
slots={{
prefix: <UiIcon
name="search"
/>,
}}
>
<UiInput
id="destination"
placeholder="Search or paste a URL…"
/>
</UiInputGroup>
</UiFormField>;
}/** @jsxImportSource preact */
import { UiFormField, UiIcon, UiInput, UiInputGroup } from "@threadlabs/looma";
export default function Example() {
return <UiFormField
slots={{
label: <label
htmlFor="destination"
>Destination</label>,
}}
>
<UiInputGroup
prefixKind="icon"
slots={{
prefix: <UiIcon
name="search"
/>,
}}
>
<UiInput
id="destination"
placeholder="Search or paste a URL…"
/>
</UiInputGroup>
</UiFormField>;
}<script>
import { UiFormField, UiIcon, UiInput, UiInputGroup } from "@threadlabs/looma";
</script>
{#snippet label()}
<label
for="destination"
>Destination</label>
{/snippet}
{#snippet prefix()}
<UiIcon
name="search"
/>
{/snippet}
<UiFormField
slots={{ label }}
>
<UiInputGroup
prefixKind="icon"
slots={{ prefix }}
>
<UiInput
id="destination"
placeholder="Search or paste a URL…"
/>
</UiInputGroup>
</UiFormField>import { Component } from "@angular/core";
import { UiFormField, UiIcon, UiInput, UiInputGroup } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiFormField, UiInputGroup, UiIcon, UiInput],
template: `
<div
ui-form-field
>
<ng-container ngProjectAs="[slot='label']">
<label
for="destination"
>Destination</label>
</ng-container>
<div
ui-input-group
[prefixKind]="'icon'"
>
<ng-container ngProjectAs="[slot='prefix']">
<span
ui-icon
name="search"
></span>
</ng-container>
<input
ui-input
id="destination"
placeholder="Search or paste a URL…"
>
</div>
</div>
`,
})
export default class Example {}Multiline message
A message and its Send action share one frame. The action sits at the upper end, with text space reserved as its label changes.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-form-field/ui-form-field.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-input-group/ui-input-group.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-textarea/ui-textarea.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-button/ui-button.html"
>
<form>
<ui-form-field>
<label
slot="label"
for="multiline-message"
>Message</label>
<ui-input-group
multiline
action-position="top-end"
>
<ui-textarea
id="multiline-message"
name="message"
rows="3"
autosize
placeholder="Write a message…"
></ui-textarea>
<ui-button
slot="action"
type="submit"
variant="solid"
>Send</ui-button>
</ui-input-group>
</ui-form-field>
</form><script setup>
import { UiButton, UiFormField, UiInputGroup, UiTextarea } from "@threadlabs/looma";
</script>
<template>
<form>
<UiFormField>
<template #label>
<label
for="multiline-message"
>Message</label>
</template>
<UiInputGroup
:multiline="true"
action-position="top-end"
>
<UiTextarea
id="multiline-message"
name="message"
:rows="3"
:autosize="true"
placeholder="Write a message…"
/>
<template #action>
<UiButton
type="submit"
variant="solid"
>Send</UiButton>
</template>
</UiInputGroup>
</UiFormField>
</form>
</template>import { UiButton, UiFormField, UiInputGroup, UiTextarea } from "@threadlabs/looma";
export default function Example() {
return <form>
<UiFormField
slots={{
label: <label
htmlFor="multiline-message"
>Message</label>,
}}
>
<UiInputGroup
multiline
actionPosition="top-end"
slots={{
action: <UiButton
type="submit"
variant="solid"
>Send</UiButton>,
}}
>
<UiTextarea
id="multiline-message"
name="message"
rows={3}
autosize
placeholder="Write a message…"
/>
</UiInputGroup>
</UiFormField>
</form>;
}/** @jsxImportSource preact */
import { UiButton, UiFormField, UiInputGroup, UiTextarea } from "@threadlabs/looma";
export default function Example() {
return <form>
<UiFormField
slots={{
label: <label
htmlFor="multiline-message"
>Message</label>,
}}
>
<UiInputGroup
multiline
actionPosition="top-end"
slots={{
action: <UiButton
type="submit"
variant="solid"
>Send</UiButton>,
}}
>
<UiTextarea
id="multiline-message"
name="message"
rows={3}
autosize
placeholder="Write a message…"
/>
</UiInputGroup>
</UiFormField>
</form>;
}<script>
import { UiButton, UiFormField, UiInputGroup, UiTextarea } from "@threadlabs/looma";
</script>
{#snippet label()}
<label
for="multiline-message"
>Message</label>
{/snippet}
{#snippet action()}
<UiButton
type="submit"
variant="solid"
>Send</UiButton>
{/snippet}
<form>
<UiFormField
slots={{ label }}
>
<UiInputGroup
multiline
actionPosition="top-end"
slots={{ action }}
>
<UiTextarea
id="multiline-message"
name="message"
rows={3}
autosize
placeholder="Write a message…"
/>
</UiInputGroup>
</UiFormField>
</form>import { Component } from "@angular/core";
import { UiButton, UiFormField, UiInputGroup, UiTextarea } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiFormField, UiInputGroup, UiTextarea, UiButton],
template: `
<form>
<div
ui-form-field
>
<ng-container ngProjectAs="[slot='label']">
<label
for="multiline-message"
>Message</label>
</ng-container>
<div
ui-input-group
[multiline]="true"
[actionPosition]="'top-end'"
>
<textarea
ui-textarea
id="multiline-message"
name="message"
[rows]="3"
[autosize]="true"
placeholder="Write a message…"
></textarea>
<ng-container ngProjectAs="[slot='action']">
<ng-template
ui-button
[type]="'submit'"
[variant]="'solid'"
>Send</ng-template>
</ng-container>
</div>
</div>
</form>
`,
})
export default class Example {}API
Package: @threadlabs/looma · Native root: div
Attributes
| Name | Description | Property | Type | Default | Allowed values |
|---|---|---|---|---|---|
action-position | In a multiline group, below keeps the action under the text. top-end places it inside the textarea at its upper end and reserves text padding for its measured width. | actionPosition | keyword | below | below, top-end |
disabled | Fades the group's framing to match a disabled input inside it. Disable the input itself too. | disabled | boolean | false | — |
multiline | Groups a Textarea with an action below its text inside the shared frame. | multiline | boolean | false | — |
prefix-kind | Text affixes have their own framing. An icon prefix shares the field surface and uses one compact icon-to-value gap. | prefixKind | keyword | text | text, icon |
variant | outline uses the standard field edge and surface. subtle uses a quiet decorative edge and muted surface for a multiline composer. Both retain the same focus, invalid and disabled states, and the field inside shares the group's one frame. | variant | keyword | outline | outline, subtle |
Framework props
| Name | Type | Default | Allowed values |
|---|---|---|---|
actionPosition | "below" | "top-end" | below | below, top-end |
disabled | boolean | false | — |
multiline | boolean | false | — |
prefixKind | "text" | "icon" | text | text, icon |
variant | "outline" | "subtle" | outline | outline, subtle |
Events
No events.
Slots
| Name | Purpose |
|---|---|
prefix | Named content for the prefix region. |
default | Default child content. |
suffix | Named content for the suffix region. |
action | Named content for the action region. |
Design tokens
Set shared theme properties on a theme boundary. Component hooks apply only to their owning component.
Component tokens
| Token | Declared values | Fallback |
|---|---|---|
--ui-input-group-invalid-border | — | var(--ui-field-danger, var(--ui-danger-solid)) |
--ui-input-group-invalid-focus-shadow | — | var(--ui-control-inset),
0 0 0 calc(var(--ui-focus-width) * 1.5) color-mix(
in srgb,
var(--ui-input-group-invalid-border, var(--ui-field-danger, var(--ui-danger-solid))) 22%,
transparent
) |
--ui-input-group-radius | — | var(--ui-field-radius, var(--ui-radius-md)) |
Shared tokens
| Token | Fallback |
|---|---|
--ui-accent-solid | — |
--ui-border | — |
--ui-border-divider | — |
--ui-border-strong | — |
--ui-border-width | — |
--ui-control-border | var(--ui-border-strong) |
--ui-control-border-hover | var(--ui-text-secondary) |
--ui-control-focus | var(--ui-accent-solid) |
--ui-control-focus-halo-shadow | — |
--ui-control-inset | — |
--ui-control-size-lg | — |
--ui-control-size-md | — |
--ui-control-size-sm | — |
--ui-control-surface | var(--ui-surface-elevated) |
--ui-control-surface-disabled | var(--ui-surface-muted) |
--ui-danger-solid | — |
--ui-field-danger | var(--ui-danger-solid) |
--ui-field-radius | var(--ui-radius-md) |
--ui-focus-width | — |
--ui-font-family-mono | — |
--ui-font-size-sm | — |
--ui-motion-ease | — |
--ui-motion-fast | — |
--ui-radius-md | — |
--ui-space-1 | — |
--ui-space-2 | — |
--ui-space-3 | — |
--ui-surface-elevated | — |
--ui-surface-highlight | none |
--ui-surface-muted | — |
--ui-text-muted | — |
--ui-text-secondary | — |