Usage
TextField lets users enter and edit text. It supports controlled (value) and uncontrolled (defaultValue) usage, and switches to a multiline textarea, a date picker, or a select menu depending on multiline/type. In Svelte, use bind:value for the bindable value surface.
Variants
filled(default)outlined
import { TextField } from '@udixio/ui-react';
export default function TextFieldVariantsReact() {
return (
<div className="flex flex-wrap items-end gap-6">
<TextField label="Name" name="name" variant="filled" />
<TextField label="Name" name="name2" variant="outlined" />
</div>
);
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { TextField } from '@udixio/ui-angular';
@Component({
selector: 'text-field-variants-angular-example',
standalone: true,
imports: [TextField],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-wrap items-end gap-6">
<udx-text-field label="Name" name="name" variant="filled" />
<udx-text-field label="Name" name="name2" variant="outlined" />
</div>
`,
})
export class TextFieldVariantsAngular {}<script lang="ts">
import { TextField } from '@udixio/ui-svelte';
</script>
<div class="flex flex-wrap items-end gap-6">
<TextField label="Name" name="name" variant="filled" />
<TextField label="Name" name="name2" variant="outlined" />
</div>Styling the root and internal elements
A string targets the root: className in React, the native class attribute in Angular, or class in Svelte. To reach an internal element (input, label, supportingText, …), pass an object keyed by element name — className={{ … }} in React, [classes]="{ … }" in Angular, or classes={{ … }} in Svelte. A function receiving the resolved props and states is also accepted for the rare cases that depend on what only the component knows. Consumer classes are merged after the defaults with tailwind-merge, so bg-*, text-* and similar utilities override cleanly.
Uppercase letters only
import { TextField } from '@udixio/ui-react';
export default function TextFieldElementClassesReact() {
return (
<div className="flex flex-wrap items-end gap-6">
<TextField label="Name" name="name" className="w-72" />
<TextField
label="Code"
name="code"
supportingText="Uppercase letters only"
className={{
input: 'uppercase tracking-widest',
supportingText: 'italic',
}}
/>
</div>
);
}Uppercase letters only
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { TextField } from '@udixio/ui-angular';
@Component({
selector: 'text-field-element-classes-angular-example',
standalone: true,
imports: [TextField],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-wrap items-end gap-6">
<udx-text-field label="Name" name="name" class="w-72" />
<udx-text-field
label="Code"
name="code"
supportingText="Uppercase letters only"
[classes]="{ input: 'uppercase tracking-widest', supportingText: 'italic' }"
/>
</div>
`,
})
export class TextFieldElementClassesAngular {}Uppercase letters only
<script lang="ts">
import { TextField } from '@udixio/ui-svelte';
</script>
<div class="flex flex-wrap items-end gap-6">
<TextField label="Name" name="name" class="w-72" />
<TextField
label="Code"
name="code"
supportingText="Uppercase letters only"
classes={{ input: 'uppercase tracking-widest', supportingText: 'italic' }}
/>
</div>Leading/trailing icon and suffix
Use leadingIcon, trailingIcon, or suffix for extra affordances. Both are agnostic Icon data, rendered identically in all three frameworks; there is no escape hatch for an arbitrary interactive node.
import { TextField } from '@udixio/ui-react';
import { iSearch } from '@udixio/icons-rounded-400/search';
import { iClose } from '@udixio/icons-rounded-400/close';
export default function TextFieldIconsReact() {
return (
<div className="flex flex-col gap-4">
<TextField label="Search" name="q" leadingIcon={iSearch} />
<TextField label="Amount" name="amount" suffix="kg" />
<TextField label="Search" name="q2" trailingIcon={iClose} />
<TextField
label="Search"
name="q3"
variant="outlined"
leadingIcon={iSearch}
trailingIcon={iClose}
/>
</div>
);
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { TextField } from '@udixio/ui-angular';
import { iSearch } from '@udixio/icons-rounded-400/search';
import { iClose } from '@udixio/icons-rounded-400/close';
@Component({
selector: 'text-field-icons-angular-example',
standalone: true,
imports: [TextField],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-col gap-4">
<udx-text-field label="Search" name="q" [leadingIcon]="iSearch" />
<udx-text-field label="Amount" name="amount" suffix="kg" />
<udx-text-field label="Search" name="q2" [trailingIcon]="iClose" />
<udx-text-field
label="Search"
name="q3"
variant="outlined"
[leadingIcon]="iSearch"
[trailingIcon]="iClose"
/>
</div>
`,
})
export class TextFieldIconsAngular {
protected readonly iSearch = iSearch;
protected readonly iClose = iClose;
}<script lang="ts">
import { TextField } from '@udixio/ui-svelte';
import { iClose } from '@udixio/icons-rounded-400/close';
import { iSearch } from '@udixio/icons-rounded-400/search';
</script>
<div class="flex flex-col gap-4">
<TextField label="Search" name="q" leadingIcon={iSearch} />
<TextField label="Amount" name="amount" suffix="kg" />
<TextField label="Search" name="q2" trailingIcon={iClose} />
<TextField
label="Search"
name="q3"
variant="outlined"
leadingIcon={iSearch}
trailingIcon={iClose}
/>
</div>Multi-line
Set multiline for an auto-growing textarea. The auto-grow behavior is implemented once as a shared @udixio/core/dom controller, so all three adapters resize identically.
import { TextField } from '@udixio/ui-react';
export default function TextFieldMultilineReact() {
return (
<div className="flex flex-col gap-4">
<TextField label="Description" name="desc1" multiline />
<TextField
label="Notes"
name="desc2"
multiline
defaultValue="Initial content"
/>
</div>
);
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { TextField } from '@udixio/ui-angular';
@Component({
selector: 'text-field-multiline-angular-example',
standalone: true,
imports: [TextField],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-col gap-4">
<udx-text-field label="Description" name="desc1" multiline />
<udx-text-field
label="Notes"
name="desc2"
multiline
defaultValue="Initial content"
/>
</div>
`,
})
export class TextFieldMultilineAngular {}<script lang="ts">
import { TextField } from '@udixio/ui-svelte';
</script>
<div class="flex flex-col gap-4">
<TextField label="Description" name="desc1" multiline />
<TextField label="Notes" name="desc2" multiline defaultValue="Initial content" />
</div>Error and supporting text
Show validation and helper text with errorText and supportingText.
Use 3–16 characters
Password is too short
import { TextField } from '@udixio/ui-react';
export default function TextFieldValidationReact() {
return (
<div className="flex flex-col gap-2">
<TextField
label="Username"
name="u1"
supportingText="Use 3–16 characters"
/>
<TextField
label="Password"
name="p1"
type="password"
errorText="Password is too short"
/>
</div>
);
}Use 3–16 characters
Password is too short
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { TextField } from '@udixio/ui-angular';
@Component({
selector: 'text-field-validation-angular-example',
standalone: true,
imports: [TextField],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-col gap-2">
<udx-text-field
label="Username"
name="u1"
supportingText="Use 3–16 characters"
/>
<udx-text-field
label="Password"
name="p1"
type="password"
errorText="Password is too short"
/>
</div>
`,
})
export class TextFieldValidationAngular {}Use 3–16 characters
Password is too short
<script lang="ts">
import { TextField } from '@udixio/ui-svelte';
</script>
<div class="flex flex-col gap-2">
<TextField label="Username" name="u1" supportingText="Use 3–16 characters" />
<TextField label="Password" name="p1" type="password" errorText="Password is too short" />
</div>Controlled vs uncontrolled
Use value with onChange in React, [(value)] in Angular, or bind:value in Svelte. defaultValue initializes uncontrolled usage.
import { useState } from 'react';
import { TextField } from '@udixio/ui-react';
export default function TextFieldControlledReact() {
const [value, setValue] = useState('hello');
return (
<div className="flex flex-col gap-3">
<TextField
label="Controlled"
name="c1"
value={value}
onChange={setValue}
/>
<TextField label="Uncontrolled" name="u2" defaultValue="Initial value" />
</div>
);
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { TextField } from '@udixio/ui-angular';
@Component({
selector: 'text-field-controlled-angular-example',
standalone: true,
imports: [TextField],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-col gap-3">
<udx-text-field label="Controlled" name="c1" [(value)]="value" />
<udx-text-field
label="Uncontrolled"
name="u2"
defaultValue="Initial value"
/>
</div>
`,
})
export class TextFieldControlledAngular {
protected value = 'hello';
}Value: hello
<script lang="ts">
import { TextField } from '@udixio/ui-svelte';
let value = $state('hello');
</script>
<div class="flex flex-col gap-3">
<TextField label="Controlled" name="c1" bind:value />
<TextField label="Uncontrolled" name="u2" defaultValue="Initial value" />
<p class="text-body-medium">Value: {value}</p>
</div>Select mode
type="select" opens a menu of options instead of a native input. React additionally accepts projected MenuItem children in place of options; Angular and Svelte use options.
import { TextField } from '@udixio/ui-react';
export default function TextFieldSelectReact() {
return (
<TextField
label="Country"
name="country"
type="select"
options={[
{ value: 'fr', label: 'France' },
{ value: 'de', label: 'Germany' },
{ value: 'jp', label: 'Japan' },
]}
/>
);
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { TextField } from '@udixio/ui-angular';
import type { TextFieldOption } from '@udixio/core';
@Component({
selector: 'text-field-select-angular-example',
standalone: true,
imports: [TextField],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<udx-text-field
label="Country"
name="country"
type="select"
[options]="options"
/>
`,
})
export class TextFieldSelectAngular {
protected readonly options: TextFieldOption[] = [
{ value: 'fr', label: 'France' },
{ value: 'de', label: 'Germany' },
{ value: 'jp', label: 'Japan' },
];
}<script lang="ts">
import { TextField } from '@udixio/ui-svelte';
const options = [
{ value: 'fr', label: 'France' },
{ value: 'de', label: 'Germany' },
{ value: 'jp', label: 'Japan' },
];
</script>
<TextField label="Country" name="country" type="select" {options} />Date mode
type="date" opens a DatePicker popover and stores the selection as an ISO YYYY-MM-DD string. The field stays directly typable in that same format; only select mode is read-only. Svelte owns the equivalent calendar surface inside TextField rather than exposing a separate DatePicker component.
import { TextField } from '@udixio/ui-react';
export default function TextFieldDateReact() {
return <TextField label="Birthday" name="birthday" type="date" />;
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { TextField } from '@udixio/ui-angular';
@Component({
selector: 'text-field-date-angular-example',
standalone: true,
imports: [TextField],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<udx-text-field label="Birthday" name="birthday" type="date" />`,
})
export class TextFieldDateAngular {}<script lang="ts">
import { TextField } from '@udixio/ui-svelte';
</script>
<TextField label="Birthday" name="birthday" type="date" />Custom mask
Pass mask to transform typed or pasted input on every keystroke – for any format an app needs, not just dates: a phone number, a card number, a multi-group ID. It defaults to the built-in YYYY-MM-DD mask for type="date"; providing one always overrides it, for that type or any other.
The field is controlled, so mask receives its own previous output back as raw on every subsequent keystroke – it must be idempotent (mask(mask(x)) === mask(x)). Inserted separators the user never typed (a space, a dash) are safe, since a naive filter already excludes them from the next pass – but a literal that contains digits (a +33 country code, a fixed digit group) will be mistaken for freshly typed input on the next call unless you strip it back out first.
import { TextField } from '@udixio/ui-react';
// Always renders in international form ("+33 6 85 51 14 11"): typing "+"
// opts in explicitly, and typing the national trunk "0" is recognized and
// dropped for the country code, the way a French number is dialed
// internationally -- either way the user only ever types digits they meant.
function maskFrenchPhone(raw: string): string {
const hasPlus = raw.trimStart().startsWith('+');
let digits = raw.replace(/\D/g, '');
if (!hasPlus && !digits.startsWith('0')) {
return (
digits
.slice(0, 10)
.match(/.{1,2}/g)
?.join(' ') ?? ''
);
}
if (!hasPlus) digits = `33${digits.slice(1)}`;
digits = digits.slice(0, 11);
const subscriber = digits.slice(2);
const groups = [
digits.slice(0, 2),
subscriber.slice(0, 1),
...(subscriber.slice(1).match(/.{1,2}/g) ?? []),
].filter(Boolean);
return `+${groups.join(' ')}`;
}
export default function TextFieldMaskReact() {
return <TextField label="Phone" mask={maskFrenchPhone} />;
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { TextField } from '@udixio/ui-angular';
// Always renders in international form ("+33 6 85 51 14 11"): typing "+"
// opts in explicitly, and typing the national trunk "0" is recognized and
// dropped for the country code, the way a French number is dialed
// internationally -- either way the user only ever types digits they meant.
function maskFrenchPhone(raw: string): string {
const hasPlus = raw.trimStart().startsWith('+');
let digits = raw.replace(/\D/g, '');
if (!hasPlus && !digits.startsWith('0')) {
return (
digits
.slice(0, 10)
.match(/.{1,2}/g)
?.join(' ') ?? ''
);
}
if (!hasPlus) digits = `33${digits.slice(1)}`;
digits = digits.slice(0, 11);
const subscriber = digits.slice(2);
const groups = [
digits.slice(0, 2),
subscriber.slice(0, 1),
...(subscriber.slice(1).match(/.{1,2}/g) ?? []),
].filter(Boolean);
return `+${groups.join(' ')}`;
}
@Component({
selector: 'text-field-mask-angular-example',
standalone: true,
imports: [TextField],
changeDetection: ChangeDetectionStrategy.OnPush,
template: ` <udx-text-field label="Phone" [mask]="mask" /> `,
})
export class TextFieldMaskAngular {
protected readonly mask = maskFrenchPhone;
}<script lang="ts">
import { TextField } from '@udixio/ui-svelte';
function maskFrenchPhone(raw: string): string {
const hasPlus = raw.trimStart().startsWith('+');
let digits = raw.replace(/\D/g, '');
if (!hasPlus && !digits.startsWith('0')) {
return digits.slice(0, 10).match(/.{1,2}/g)?.join(' ') ?? '';
}
if (!hasPlus) digits = `33${digits.slice(1)}`;
digits = digits.slice(0, 11);
const subscriber = digits.slice(2);
const groups = [
digits.slice(0, 2),
subscriber.slice(0, 1),
...(subscriber.slice(1).match(/.{1,2}/g) ?? []),
].filter(Boolean);
return `+${groups.join(' ')}`;
}
</script>
<TextField label="Phone" mask={maskFrenchPhone} />Accessibility
aria-describedby links the supporting text/error row to the input, and aria-invalid reflects errorText. The date/select trailing icon renders as a real <button> so it stays reachable by keyboard, independent of the field’s own click-to-open behavior. Svelte’s select popup exposes listbox/option semantics and its calendar exposes the shared grid/gridcell structure.