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.

Previewindex.html
<!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.

Previewindex.html
<!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.

Previewindex.html
Full name
<!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

Previewindex.html
<!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.

Previewindex.html
<!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.

Previewindex.htmlinput-echo-demo.html

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.

Previewindex.htmlinput-form-demo.html
Create Reset
<!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.

Edit this page on GitHub · View as Markdown