Field

ui-field stacks a label, a control, an optional ui-field-description and an error message with consistent spacing. It is a layout element with the group role. The control inside keeps its own form behaviour. Its parts go in the default slot and show in source order. ui-field-group stacks several fields, and ui-field-set groups fields under a ui-field-legend.

Pair the label with the control through ui-label for, which also gives the control its accessible name.

Previewindex.html
Email We only use it to send receipts.
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Field 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/field.html"></import-element>
  <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">
    <ui-field>
      <ui-label for="field-email">Email</ui-label>
      <ui-input id="field-email" type="email" placeholder="m@example.com"></ui-input>
      <ui-field-description>We only use it to send receipts.</ui-field-description>
    </ui-field>
  </div>
</body>
</html>

Examples

Error message

error shows a message below the field in a role="alert" paragraph, and invalid colours the label. Set invalid on the control as well so its border and aria-invalid match.

Previewindex.html
Username
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Field 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/field.html"></import-element>
  <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">
    <ui-field invalid error="Choose a username with at least 3 characters.">
      <ui-label for="field-username">Username</ui-label>
      <ui-input id="field-username" value="ab" invalid></ui-input>
    </ui-field>
  </div>
</body>
</html>

Horizontal

orientation="horizontal" puts the label and control on one row, which suits switches and checkboxes.

Previewindex.html
Marketing emails
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Field 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/field.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/label.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/switch.html"></import-element>

  <div class="demo-narrow">
    <ui-field orientation="horizontal">
      <ui-label for="field-marketing">Marketing emails</ui-label>
      <ui-switch id="field-marketing"></ui-switch>
    </ui-field>
  </div>
</body>
</html>

Horizontal with a description

Wrap the label and description in ui-field-content so they stay together in a column beside the control. Source order decides which side the control is on.

Previewindex.html
Sync across devices Your settings follow you to every device you sign in on.
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Field 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-medium {
      width: min(100%, calc(var(--spacing) * 120));
    }
  </style>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/checkbox.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/field.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/label.html"></import-element>

  <div class="demo-medium">
    <ui-field orientation="horizontal">
      <ui-checkbox id="field-sync"></ui-checkbox>
      <ui-field-content>
        <ui-label for="field-sync">Sync across devices</ui-label>
        <ui-field-description>Your settings follow you to every device you sign in on.</ui-field-description>
      </ui-field-content>
    </ui-field>
  </div>
</body>
</html>

Field group

ui-field-group stacks fields with a larger gap than the parts inside a field.

Previewindex.html
First name Last name
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Field 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/field.html"></import-element>
  <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">
    <ui-field-group>
      <ui-field>
        <ui-label for="field-first-name">First name</ui-label>
        <ui-input id="field-first-name" autocomplete="given-name"></ui-input>
      </ui-field>
      <ui-field>
        <ui-label for="field-last-name">Last name</ui-label>
        <ui-input id="field-last-name" autocomplete="family-name"></ui-input>
      </ui-field>
    </ui-field-group>
  </div>
</body>
</html>

Field set

ui-field-set takes a ui-field-legend, an optional ui-field-description and the fields. The legend names the group.

Previewindex.html
Shipping address Where should we send the order? Street City
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Field 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-medium {
      width: min(100%, calc(var(--spacing) * 120));
    }
  </style>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/field.html"></import-element>
  <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-medium">
    <ui-field-set>
      <ui-field-legend>Shipping address</ui-field-legend>
      <ui-field-description>Where should we send the order?</ui-field-description>
      <ui-field-group>
        <ui-field>
          <ui-label for="field-street">Street</ui-label>
          <ui-input id="field-street" autocomplete="street-address"></ui-input>
        </ui-field>
        <ui-field>
          <ui-label for="field-city">City</ui-label>
          <ui-input id="field-city" autocomplete="address-level2"></ui-input>
        </ui-field>
      </ui-field-group>
    </ui-field-set>
  </div>
</body>
</html>

Legend as a label

variant="label" on ui-field-legend uses label-sized text, for a set of checkboxes or radios.

Previewindex.html
Notify me about Mentions Replies
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Field 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/checkbox.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/field.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/label.html"></import-element>

  <div class="demo-narrow">
    <ui-field-set>
      <ui-field-legend variant="label">Notify me about</ui-field-legend>
      <ui-field-group>
        <ui-field orientation="horizontal">
          <ui-checkbox id="field-notify-mentions" checked></ui-checkbox>
          <ui-label for="field-notify-mentions">Mentions</ui-label>
        </ui-field>
        <ui-field orientation="horizontal">
          <ui-checkbox id="field-notify-replies"></ui-checkbox>
          <ui-label for="field-notify-replies">Replies</ui-label>
        </ui-field>
      </ui-field-group>
    </ui-field-set>
  </div>
</body>
</html>

In a form

Fields do not change what a form submits. The controls inside do. This form validates on submit and shows the errors through error and invalid.

Previewindex.htmlfield-form-demo.html
Profile This is shown on your public page. Name Email Used for sign-in only. Public profile Save profile
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Field 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/field.html"></import-element>
  <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>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/switch.html"></import-element>

  <import-element from="./field-form-demo.html"></import-element>
  <field-form-demo></field-form-demo>
</body>
</html>
<custom-element name="field-form-demo">
  <script>
    const signup = useState({
      nameError: "",
      emailError: "",
      text: "Submit the form to see its data.",
      get nameInvalid() {
        return this.nameError !== "";
      },
      get emailInvalid() {
        return this.emailError !== "";
      },
    });

    function submit(event) {
      event.preventDefault();
      const data = new FormData(event.target);
      signup.nameError = data.get("name") ? "" : "Enter your name.";
      signup.emailError = String(data.get("email")).includes("@") ? "" : "Enter a valid email address.";
      if (signup.nameError || signup.emailError) {
        signup.text = "Fix the errors above.";
        return;
      }
      signup.text = [...data].map(([name, value]) => `${name}=${value}`).join("\n");
    }
  </script>

  <style>
    :host {
      display: grid;
      gap: calc(var(--spacing) * 4);
      width: 100%;
      max-width: calc(var(--spacing) * 96);
    }
    form {
      display: grid;
      gap: calc(var(--spacing) * 6);
    }
    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)" novalidate>
    <ui-field-set>
      <ui-field-legend>Profile</ui-field-legend>
      <ui-field-description>This is shown on your public page.</ui-field-description>
      <ui-field _invalid="#signup.nameInvalid" _error="#signup.nameError">
        <ui-label for="field-form-name">Name</ui-label>
        <ui-input id="field-form-name" name="name" _invalid="#signup.nameInvalid"></ui-input>
      </ui-field>
      <ui-field _invalid="#signup.emailInvalid" _error="#signup.emailError">
        <ui-label for="field-form-email">Email</ui-label>
        <ui-input id="field-form-email" name="email" type="email" _invalid="#signup.emailInvalid"></ui-input>
        <ui-field-description>Used for sign-in only.</ui-field-description>
      </ui-field>
      <ui-field orientation="horizontal">
        <ui-label for="field-form-public">Public profile</ui-label>
        <ui-switch id="field-form-public" name="public" checked></ui-switch>
      </ui-field>
    </ui-field-set>
    <ui-button type="submit">Save profile</ui-button>
  </form>
  <output><slot name="#signup.text"></slot></output>
</custom-element>

API

Parts

Part Slot Renders
ui-field One field: label, control, description and error, stacked (or in a row when horizontal). Role group.
ui-label (default) The label of a ui-field. A plain child. See Label.
ui-field-content (default) A column that keeps a label and description together beside the control in a horizontal field.
ui-field-description (default) Help text in muted var(--text-sm). Goes in ui-field, ui-field-content or ui-field-set.
ui-field-group (default) A column of fields with a gap of calc(var(--spacing) * 7).
ui-field-set A group of fields under a legend. Role group.
ui-field-legend legend The heading of a ui-field-set. Assigns itself to the legend slot, so you do not set slot.

Parts show in source order. Every part's default slot takes any content.

ui-field attributes

Attribute Type Default Description
orientation "vertical" | "horizontal" "vertical" Stacks the parts, or puts them on one row.
invalid boolean false Colours the label with --destructive.
error string Error message shown below the field. Hidden when empty.

ui-field-legend attributes

Attribute Type Default Description
variant "label" Uses var(--text-sm) instead of var(--text-base).

The other parts have no attributes.

CSS parts

Element Part Description
ui-field error The error paragraph (role="alert").

CSS states

Element State Description
ui-field :state(attr-invalid) Set while invalid is present.
ui-field :state(attr-orientation-is-horizontal) Set for the horizontal layout.

Accessibility

ui-field has role="group". Its error paragraph has role="alert", so a message that appears is announced. The field does not link its label, description or error to the control. Use ui-label for (or aria-label on the control) for the name. ui-field-set has role="group" and takes its accessible name from its ui-field-legend.

Edit this page on GitHub · View as Markdown