Input
ui-input wraps a native <input> in its shadow root and passes the usual attributes through: type,
placeholder, pattern, min, max, minlength and the rest. It is form-associated, so its value is
submitted under its name, its validity is the inner input's validity, and pressing Enter submits the form as in
a native text field.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Input demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
<style>
.demo-narrow {
width: min(100%, calc(var(--spacing) * 88));
}
</style>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/input.html"></import-element>
<div class="demo-narrow">
<ui-input aria-label="Email" type="email" placeholder="Email"></ui-input>
</div>
</body>
</html>Examples
Types
type accepts the text-like and date-like native types: text, email, password, number, search, tel,
url, date, time, datetime-local, month, week and color. For checkboxes, radios, ranges and files use
the dedicated components or a native input.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Input demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
<style>
.demo-narrow {
width: min(100%, calc(var(--spacing) * 88));
}
.demo-stack-tight {
display: grid;
gap: calc(var(--spacing) * 2);
}
</style>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/input.html"></import-element>
<div class="demo-narrow demo-stack-tight">
<ui-input type="password" aria-label="Password" placeholder="Password"></ui-input>
<ui-input type="number" aria-label="Quantity" value="2" min="1" max="10"></ui-input>
<ui-input type="search" aria-label="Search" placeholder="Search…"></ui-input>
<ui-input type="date" aria-label="Start date" value="2026-10-19"></ui-input>
<ui-input type="time" aria-label="Start time" value="09:30" step="900"></ui-input>
</div>
</body>
</html>With a label
Use ui-label for (or a native <label> around the element) to name the input. Clicking the label focuses it.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Input demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
<style>
.demo-narrow {
width: min(100%, calc(var(--spacing) * 88));
}
.demo-stack-tight {
display: grid;
gap: calc(var(--spacing) * 2);
}
</style>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/input.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/label.html"></import-element>
<div class="demo-narrow demo-stack-tight">
<ui-label for="input-name">Full name</ui-label>
<ui-input id="input-name" autocomplete="name"></ui-input>
</div>
</body>
</html>Disabled and read-only
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Input demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
<style>
.demo-narrow {
width: min(100%, calc(var(--spacing) * 88));
}
.demo-stack-tight {
display: grid;
gap: calc(var(--spacing) * 2);
}
</style>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/input.html"></import-element>
<div class="demo-narrow demo-stack-tight">
<ui-input aria-label="Disabled" placeholder="Disabled" disabled></ui-input>
<ui-input aria-label="API key" value="sk-live-4f9a" readonly></ui-input>
</div>
</body>
</html>Invalid
invalid sets aria-invalid on the inner input and draws a destructive border. Use it for errors you detect
yourself, such as a taken username. Native constraint errors are reported through validity instead.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Input demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
<style>
.demo-narrow {
width: min(100%, calc(var(--spacing) * 88));
}
</style>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/input.html"></import-element>
<div class="demo-narrow">
<ui-input aria-label="Username" value="admin" invalid></ui-input>
</div>
</body>
</html>Listening for input
input reaches the page from the inner field as you type, and change fires from the element when the value is
committed. Both run after value is updated.
You typed:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Input demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/input.html"></import-element>
<import-element from="./input-echo-demo.html"></import-element>
<input-echo-demo></input-echo-demo>
</body>
</html><custom-element name="input-echo-demo">
<script>
const echo = useState({ text: "" });
function typed(field) {
echo.text = field.value;
}
</script>
<style>
:host {
display: grid;
gap: calc(var(--spacing) * 2);
width: 100%;
max-width: calc(var(--spacing) * 80);
}
p {
margin: 0;
font-size: var(--text-sm);
color: var(--muted-foreground);
}
</style>
<ui-input aria-label="Message" placeholder="Type something" oninput="typed(this)"></ui-input>
<p>You typed: <slot name="#echo.text"></slot></p>
</custom-element>In a form
Constraint attributes (required, type="email", minlength, pattern, min, max) block submission with the
browser's own messages. Enter inside a field submits the form. Reset returns each input to its value attribute.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Input demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/input.html"></import-element>
<import-element from="./input-form-demo.html"></import-element>
<input-form-demo></input-form-demo>
</body>
</html><custom-element name="input-form-demo">
<script>
const result = useState({ text: "Submit the form to see its data." });
function submit(event) {
event.preventDefault();
result.text = [...new FormData(event.target)].map(([name, value]) => `${name}=${value}`).join("\n");
}
function reset() {
result.text = "The form was reset.";
}
</script>
<style>
:host {
display: grid;
gap: calc(var(--spacing) * 4);
width: 100%;
max-width: calc(var(--spacing) * 80);
}
form {
display: grid;
gap: calc(var(--spacing) * 3);
}
.actions {
display: flex;
gap: calc(var(--spacing) * 2);
}
output {
display: block;
padding: calc(var(--spacing) * 3);
border-radius: var(--radius-md);
background: var(--muted);
font-family: var(--font-mono);
font-size: var(--text-xs);
white-space: pre-wrap;
}
</style>
<form onsubmit="submit(event)" onreset="reset()">
<ui-input name="email" type="email" aria-label="Email" placeholder="Email" required></ui-input>
<ui-input name="handle" aria-label="Handle" value="realm" minlength="3" pattern="[a-z0-9_]+"></ui-input>
<ui-input name="seats" type="number" aria-label="Seats" value="5" min="1" max="50"></ui-input>
<div class="actions">
<ui-button type="submit">Create</ui-button>
<ui-button type="reset" variant="outline">Reset</ui-button>
</div>
</form>
<output><slot name="#result.text"></slot></output>
</custom-element>API
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
value |
string | "" |
Current value. The attribute is the initial and reset value. |
type |
"text" | "email" | "password" | "number" | "search" | "tel" | "url" | "date" | "time" | "datetime-local" | "month" | "week" | "color" |
"text" |
Native input type. |
placeholder |
string | Placeholder text. | |
autocomplete |
string | Native autofill hint. | |
inputmode |
string | Virtual keyboard hint. | |
pattern |
string | Regular expression the value must match. | |
min |
string | Minimum for number and date types. | |
max |
string | Maximum for number and date types. | |
step |
string | Step for number and date types. | |
minlength |
number | Minimum length. | |
maxlength |
number | Maximum length. | |
disabled |
boolean | false |
Disables the input. Also true inside a disabled <fieldset>. |
readonly |
boolean | false |
Makes the value read-only. |
required |
boolean | false |
The form is invalid while the value is empty. |
invalid |
boolean | false |
Sets aria-invalid="true" and the error style. Does not affect validity. |
aria-label |
string | Accessible name, passed to the inner input. Property name: label. |
Properties
| Property | Type | Description |
|---|---|---|
value |
string | Current value. Updated as the user types. Setting it updates the field and the submitted value. |
Every attribute above is also a property with the same name in camelCase (label for aria-label).
Events
| Event | Detail | Description |
|---|---|---|
input |
none | The inner input's native input event, which is composed and reaches the page. |
change |
none | Dispatched from the element (bubbling) when the inner input fires change. |
CSS parts
| Part | Description |
|---|---|
input |
The inner native <input>. |
Forms
ui-input is form-associated. It submits value under its name. Validity is mirrored from the inner input, so
required, type, pattern, minlength, maxlength, min, max and step all take part in checkValidity()
and reportValidity(), and the validation bubble points at the inner field. Enter submits the form for the
text-like types. form.reset() restores the value attribute, and a disabled <fieldset> disables the input.
Accessibility
The host delegates focus to the inner input. Name it with aria-label, ui-label for, or a native <label>
wrapped around the element. In the last two cases the label text is copied to the inner input's aria-label.