OTTODesign System

ComponentsForm

Auth field

The auth field is a variation of the text field. It allows users to mask and reveal the input and is primarily used for passwords.

Configurator

LiveAuth field: type, error, counter, password visibility, texts and state
Mindestens 8 Zeichen.
HTML
<oc-auth-field-v1><input type="password" id="af-cfg" minlength="8" aria-describedby="af-cfg-hint" aria-required="true"><label for="af-cfg">Passwort</label><div slot="hint" id="af-cfg-hint">Mindestens 8 Zeichen.</div></oc-auth-field-v1>

Usage

Anatomy

Mindestens 8 Zeichen.
1 2 3 4 5 6
  1. Container
  2. Label
  3. Input (masked)
  4. Visibility toggle
  5. Hint
  6. Counter
LiveAnatomy
HTML
<div class="anatomy" style="display:block;max-width:360px;margin:28px auto">
<oc-auth-field-v1 show-counter><input type="password" id="af-an" minlength="8" maxlength="20" value="otto1" aria-describedby="af-an-hint" aria-required="true" autocomplete="new-password"><label for="af-an">Passwort</label><div slot="hint" id="af-an-hint">Mindestens 8 Zeichen.</div></oc-auth-field-v1>
<span class="anatomy-pin" style="left:-16px;top:30%">1</span>
<span class="anatomy-pin" style="left:6%;top:-22px">2</span>
<span class="anatomy-pin" style="left:19%;top:32%">3</span>
<span class="anatomy-pin" style="left:calc(100% + 16px);top:32%">4</span>
<span class="anatomy-pin" style="left:-16px;top:88%">5</span>
<span class="anatomy-pin" style="left:calc(100% + 16px);top:88%">6</span>
<ol class="anatomy-key"><li data-n="1">Container</li><li data-n="2">Label</li><li data-n="3">Input (masked)</li><li data-n="4">Visibility toggle</li><li data-n="5">Hint</li><li data-n="6">Counter</li></ol>

Variants

There are two variants of the auth field:

  • default
  • error

Default

Mindestens 8 Zeichen.

Error

Bitte trage ein gültiges Passwort ein.
Mindestens 8 Zeichen.
Livevariants
HTML
<p class="demo-label">Default</p>
<oc-auth-field-v1 show-counter><input type="password" id="af-var-1" minlength="8" aria-describedby="af-var-1-hint" aria-required="true" autocomplete="new-password"><label for="af-var-1">Passwort</label><div slot="hint" id="af-var-1-hint">Mindestens 8 Zeichen.</div></oc-auth-field-v1>
<p class="demo-label">Error</p>
<oc-auth-field-v1 variant="error" show-counter><input type="password" id="af-var-2" minlength="8" aria-describedby="af-var-2-error af-var-2-hint" aria-required="true" aria-invalid="true" autocomplete="new-password"><label for="af-var-2">Passwort</label><div slot="error" id="af-var-2-error">Bitte trage ein gültiges Passwort ein.</div><div slot="hint" id="af-var-2-hint">Mindestens 8 Zeichen.</div></oc-auth-field-v1>

Hint, counter and error

The auth field comes with an optional hint. The hint helps the user filling out the auth field correctly and provides further context. The auth field can also include a counter which can be used for counting the letters and setting minimum or maximum values. The error is shown additionally above the hint if the user made a wrong input. It helps the user understanding the mistake and fixing the input.

Hint

Mindestens 8 Zeichen.

Hint and counter (type to see it count)

Mindestens 8 Zeichen.

Error and hint

Bitte trage ein gültiges Passwort ein.
Mindestens 8 Zeichen.
Livehint, counter and error
HTML
<oc-auth-field-v1><input type="password" id="af-hcv-1" minlength="8" aria-describedby="af-hcv-1-hint" aria-required="true" autocomplete="new-password"><label for="af-hcv-1">Passwort</label><div slot="hint" id="af-hcv-1-hint">Mindestens 8 Zeichen.</div></oc-auth-field-v1>
<oc-auth-field-v1 show-counter><input type="password" id="af-hcv-2" minlength="8" value="otto" aria-describedby="af-hcv-2-hint" aria-required="true" autocomplete="new-password"><label for="af-hcv-2">Passwort</label><div slot="hint" id="af-hcv-2-hint">Mindestens 8 Zeichen.</div></oc-auth-field-v1>
<oc-auth-field-v1 variant="error"><input type="password" id="af-hcv-3" minlength="8" aria-describedby="af-hcv-3-error af-hcv-3-hint" aria-required="true" aria-invalid="true" autocomplete="new-password"><label for="af-hcv-3">Passwort</label><div slot="error" id="af-hcv-3-error">Bitte trage ein gültiges Passwort ein.</div><div slot="hint" id="af-hcv-3-hint">Mindestens 8 Zeichen.</div></oc-auth-field-v1>

Behavior

States

Enabled (hover, focus, type)

Hinweis

Filled

Hinweis
LiveStates
HTML
<p class="demo-label">Enabled (hover, focus, type)</p>
<oc-auth-field-v1><input type="password" id="af-st-1" aria-describedby="af-st-1-hint" aria-required="true" autocomplete="current-password"><label for="af-st-1">Passwort</label><div slot="hint" id="af-st-1-hint">Hinweis</div></oc-auth-field-v1>
<p class="demo-label">Filled</p>
<oc-auth-field-v1><input type="password" id="af-st-2" value="geheim" aria-describedby="af-st-2-hint" aria-required="true" autocomplete="current-password"><label for="af-st-2">Passwort</label><div slot="hint" id="af-st-2-hint">Hinweis</div></oc-auth-field-v1>

Interaction

When the user fills in the input, the label moves to the top left and the input is displayed in the container. The user can reveal or mask the password input by tapping the visibility toggle. The input should be masked by default.

Empty: the label sits inside the field

Mindestens 8 Zeichen.

→ filling out input: the label moves to the border

Mindestens 8 Zeichen.
Livedefault and filled
HTML
<p class="demo-label">Empty: the label sits inside the field</p>
<oc-auth-field-v1><input type="password" id="af-in-1" aria-describedby="af-in-1-hint" aria-required="true" autocomplete="new-password"><label for="af-in-1">Passwort</label><div slot="hint" id="af-in-1-hint">Mindestens 8 Zeichen.</div></oc-auth-field-v1>
<p class="demo-label">→ filling out input: the label moves to the border</p>
<oc-auth-field-v1><input type="password" id="af-in-2" value="otto1" aria-describedby="af-in-2-hint" aria-required="true" autocomplete="new-password"><label for="af-in-2">Passwort</label><div slot="hint" id="af-in-2-hint">Mindestens 8 Zeichen.</div></oc-auth-field-v1>

Hidden (tap "Anzeigen")

Mindestens 8 Zeichen.

↕ Visible (tap "Ausblenden")

Mindestens 8 Zeichen.
Liveshow and hide password
HTML
<oc-auth-field-v1><input type="password" id="af-vis-1" value="Sommer2026" aria-describedby="af-vis-1-hint" aria-required="true" autocomplete="new-password"><label for="af-vis-1">Passwort</label><div slot="hint" id="af-vis-1-hint">Mindestens 8 Zeichen.</div></oc-auth-field-v1>
<oc-auth-field-v1 password-visible><input type="password" id="af-vis-2" value="Sommer2026" aria-describedby="af-vis-2-hint" aria-required="true" autocomplete="new-password"><label for="af-vis-2">Passwort</label><div slot="hint" id="af-vis-2-hint">Mindestens 8 Zeichen.</div></oc-auth-field-v1>

Fitting

You can adjust the width of the auth field according to your needs or set it to fill-parent. The height of the container is fixed to 48px. The height of the auth field is set to fit-content and changes when the hint and error are visible.

Custom width

Hinweis

Fills the parent

Hinweis
Livefitting
HTML
<div class="narrow" style="width:100%;margin:0 auto"><oc-auth-field-v1><input type="email" id="af-fit-1" aria-describedby="af-fit-1-hint" aria-required="true" autocomplete="email" spellcheck="false"><label for="af-fit-1">E-Mail-Adresse</label><div slot="hint" id="af-fit-1-hint">Hinweis</div></oc-auth-field-v1></div>
<oc-auth-field-v1><input type="email" id="af-fit-2" aria-describedby="af-fit-2-hint" aria-required="true" autocomplete="email" spellcheck="false"><label for="af-fit-2">E-Mail-Adresse</label><div slot="hint" id="af-fit-2-hint">Hinweis</div></oc-auth-field-v1>

Placement

You can place auth fields and text fields side by side. When placing them horizontally, make sure there is an 8px gap between them. Vertically, there should be a 24px gap.

Side by side, 8px gap

Mindestens 8 Zeichen.
Gib dein Passwort noch einmal ein.
Livehorizontal placement
HTML
<p class="demo-label">Side by side, 8px gap</p>
<oc-form-group-v1 orientation="horizontal" flex-behavior="grow" gap="var(--oc-base-dimension-8)" oc-aria-label="Passwort festlegen">
<oc-auth-field-v1><input type="password" id="af-h-1" aria-describedby="af-h-1-hint" aria-required="true" autocomplete="new-password"><label for="af-h-1">Passwort</label><div slot="hint" id="af-h-1-hint">Mindestens 8 Zeichen.</div></oc-auth-field-v1>
<oc-auth-field-v1><input type="password" id="af-h-2" aria-describedby="af-h-2-hint" aria-required="true" autocomplete="new-password"><label for="af-h-2">Passwort wiederholen</label><div slot="hint" id="af-h-2-hint">Gib dein Passwort noch einmal ein.</div></oc-auth-field-v1>
</oc-form-group-v1>

Stacked, 24px gap

Wir schicken dir eine Bestätigung.
Mindestens 8 Zeichen.
Gib dein Passwort noch einmal ein.
Livevertical placement
HTML
<p class="demo-label">Stacked, 24px gap</p>
<oc-form-group-v1 orientation="vertical" gap="var(--oc-base-dimension-24)" oc-aria-label="Konto anlegen">
<oc-auth-field-v1><input type="email" id="af-v-1" aria-describedby="af-v-1-hint" aria-required="true" autocomplete="email" spellcheck="false"><label for="af-v-1">E-Mail-Adresse</label><div slot="hint" id="af-v-1-hint">Wir schicken dir eine Bestätigung.</div></oc-auth-field-v1>
<oc-auth-field-v1><input type="password" id="af-v-2" aria-describedby="af-v-2-hint" aria-required="true" autocomplete="new-password"><label for="af-v-2">Passwort</label><div slot="hint" id="af-v-2-hint">Mindestens 8 Zeichen.</div></oc-auth-field-v1>
<oc-auth-field-v1><input type="password" id="af-v-3" aria-describedby="af-v-3-hint" aria-required="true" autocomplete="new-password"><label for="af-v-3">Passwort wiederholen</label><div slot="hint" id="af-v-3-hint">Gib dein Passwort noch einmal ein.</div></oc-auth-field-v1>
</oc-form-group-v1>

Best practices

DoOnly place related auth fields and text fields horizontally next to each other.
Don'tPlace unrelated auth fields and text Fields horizontally next to each other.

Content guidelines

DoUse short, precise labels to describe the required input.
Don'tUse overly descriptive labels to describe the required input.
DoProvide clear an useful error to help the user fix the issue.
Don'tUse generic errors, such as "Die Eingabe ist fehlerhaft".

Accessibility

For information on accessibility, refer to the technical documentation.

Status

Implementation

Live demo

Anmelden

Schön, dass du wieder da bist.

Passwort vergessen?
Anmelden

Neu bei OTTO?

Kundenkonto erstellen
LiveReal OTTO components, rendered by the OTTO component runtime
HTML
<div style="max-width:400px;margin:0 auto;display:grid;gap:24px">
<div style="display:grid;gap:4px">
  <p class="oc-headline-100">Anmelden</p>
  <p class="oc-copy-100 oc-text-color-secondary">Schön, dass du wieder da bist.</p>
<oc-form-group-v1 orientation="vertical" gap="var(--oc-base-dimension-24)" oc-aria-label="Anmeldedaten">
  <oc-auth-field-v1><input type="email" id="af-demo-1" value="lara.lavendel@example.de" aria-required="true" autocomplete="email" spellcheck="false"><label for="af-demo-1">E-Mail-Adresse</label></oc-auth-field-v1>
  <oc-auth-field-v1><input type="password" id="af-demo-2" aria-required="true" autocomplete="current-password"><label for="af-demo-2">Passwort</label></oc-auth-field-v1>
</oc-form-group-v1>
  <oc-checkbox-v2 width-behavior="fit" size="50"><input type="checkbox" checked aria-label="oc-auto"><label slot="label">Angemeldet bleiben</label></oc-checkbox-v2>
  <oc-link-v2 size="75"><a href="#">Passwort vergessen?</a></oc-link-v2>
<oc-button-v1 variant="primary">Anmelden</oc-button-v1>
<oc-divider-v1></oc-divider-v1>
<div style="display:grid;gap:12px">
  <p class="oc-copy-100"><b>Neu bei OTTO?</b></p>
  <oc-button-v1 variant="secondary">Kundenkonto erstellen</oc-button-v1>

Code

Version Tag Status API
v1 <oc-auth-field-v1> Stable, allowed for generation AuthFieldV1

Overview (v1)

Auth field

The auth field component is an alternative to the auth field component to support password managers. This component is designed to be used in a login form or any other form where credentials are required.

Note: Not all password managers are able to handle shadow DOM, so this component is a workaround to support them.

Default variation
HTML
<oc-auth-field-v1>
<input
  type="text"
  aria-describedby="hintXYZ"
  maxlength="250"
  id="inputXYZ"
  aria-required="true"
  spellcheck="false"
  autocorrect="off"
/>
<label for="inputXYZ">Label</label>
</oc-auth-field-v1>
Configuration

The auth field component offers styling variants such as default and error. This component is configurable, allowing you to tailor its features and appearance to your specific needs. To explore all the available options and adjust the component, use the component configurator and see the changes affect the component in real-time.

Usage guidelines

Before integrating the auth field component into your project, make sure you have correctly installed the OTTO components package. Look through the variations page for examples of possible component variations. Here, you can discover both common and specific variations that address different use cases.

Info

See the Auth field UX documentation for detailed user experience guidelines.

Layout considerations

This component has visual overflow. It extends beyond its bounding box and is clipped by parent containers with overflow: hidden. Ensure the parent container has sufficient padding to accommodate the component's full visual area.

Accessibility

The auth field component comes with a set of built-in accessibility features to ensure a seamless experience for all users.

Use aria-label

To make the auth field recognizable for screen readers, use the default slot to provide a clear and descriptive label. Use the aria-label attribute on the label to provide additional context if the visible label is not sufficient. See the general accessibility documentation for guidance on using oc-aria-label, including how it works with link and masked link behavior.

Invalidate the auth field

If the auth field is invalid, the aria-invalid attribute must be set to true on the input element. You should also provide an error message to the user by using the error slot and reference it in the aria-describedby attribute on the input element.

Further reading

Configuration (v1)

Auth field configuration

Configure the auth field component with the controls below and see the changes live in the preview canvas. Click the Show code button within the preview canvas to see the source code for the current component configuration.

HTML
<oc-auth-field-v1>
<input
  type="text"
  aria-describedby="hintXYZ"
  maxlength="250"
  id="inputXYZ"
  aria-required="true"
  spellcheck="false"
  autocorrect="off"
/>
<label for="inputXYZ">Label</label>
</oc-auth-field-v1>

Interactive configurator (Storybook controls); every option is listed in the API section of this file.

API v1

Auth Field v1 API

API: <oc-auth-field-v1> (AuthFieldV1)

The auth field component provides a single-line input field for user text input. It is mainly used for login forms or registration forms. The component consists of an input field and a label. The input field can be of type text, email, or password. The component can be styled in a default or error state.

Attributes / properties
Attribute Type Default Required Description
variant "default" | "error" "default" no Sets the main styling and behavior of the auth field.
password-visible boolean false no Allows to programmatically synchronize the toggle state of the password visibility for multiple password fields, e.g. for password confirmation. As a result, both toggle buttons show and hide the password value of both password fields simultaneously. This attribute is only available for auth field components with type="password".
show-counter boolean false no Indicates whether a counter for minlength or maxlength is shown.

Note: minlength and maxlength attributes need to be set on the input element.
without-optional-label boolean false no Excludes the automatic "(optional)" label suffix even if the feature toggle is active.
Slots
Slot Required Description
default yes The input and label elements for the auth field. Both elements need to be present and should be linked by the for attribute of the label element.

Note: Order is important. The input element needs to be before the label element.
hint no Hint for the input field.

The hint needs to be referenced by the aria-describedby attribute of the input element.
error no The error message for the input field.

The error needs to be referenced by the aria-describedby attribute of the input element.

aria-invalid should set to true on the input field.
Events
Event Detail type Description
oc-property-change OcAuthFieldV1Events["oc-property-change"] Whenever a property value changes, this event triggers. Use this event to track all property changes within the component.

Refer to the Events documentation for more information.
oc-mount { component: string; } Fired when the component is mounted to the DOM. The event is fired when the onMount hook of the component is called by the runtime.
oc-unmount { component: string; } Fired when the component is unmounted from the DOM. The event is fired when the function returned by the onMount hook of the component is called by the runtime.

Variations (v1)

Variations

Listed below are the most common variations of the auth field component as well as specific component variations for different use cases.

You can explore all available options using the component configurator, adjust the component to your needs, and see the changes live in a preview canvas.

Default

Story: components-auth-field-variations--default · tags: components

The default configuration uses the default variant and the default slot to render the input field and the label.

Args: defaultSlot=(see snippet)

HTML
<oc-auth-field-v1>
<input
  type="text"
  aria-describedby="hintXYZ"
  maxlength="250"
  id="inputXYZ"
  aria-required="true"
  spellcheck="false"
  autocorrect="off"
/>
<label for="inputXYZ">Label</label>
</oc-auth-field-v1>
Error

Story: components-auth-field-variations--invalid · tags: components

The error variant uses the error slot to render the error message, and the hint slot to render a hint.

Args: variant=error, defaultSlot=(see snippet), errorSlot=<div slot="error" id="errorXYZ">Your input is not valid.</div>, hintSlot=<div slot="hint" id="hintXYZ">Some hint</div>

Your input is not valid.
Some hint
HTML
<oc-auth-field-v1 variant="error">
<div slot="error" id="errorXYZ">Your input is not valid.</div>
<div slot="hint" id="hintXYZ">Some hint</div>
<input
  type="email"
  aria-describedby="errorXYZ hintXYZ"
  maxlength="250"
  id="inputXYZ"
  aria-required="true"
  aria-invalid="true"
  autocomplete=""
/>
<label for="inputXYZ">E-Mail</label>
</oc-auth-field-v1>
Max length

Story: components-auth-field-variations--max-length · tags: components

Variation that uses a text input field in the default slot, limiting the number of characters that can be entered.

Args: show-counter=true, defaultSlot=(see snippet)

HTML
<oc-auth-field-v1 show-counter>
<input
  type="text"
  aria-describedby="hintXYZ"
  maxlength="250"
  id="inputXYZ"
  aria-required="true"
  spellcheck="false"
  autocorrect="off"
/>
<label for="inputXYZ">Label</label>
</oc-auth-field-v1>
Password

Story: components-auth-field-variations--password · tags: components

Variation that uses a password input field in the default slot, with a hint in the hint slot.

Args: defaultSlot=(see snippet), hintSlot=<div slot="hint" id="hintXYZ">Some hint</div>

Some hint
HTML
<oc-auth-field-v1>
<div slot="hint" id="hintXYZ">Some hint</div>
<input
  type="password"
  aria-describedby="hintXYZ"
  minlength="8"
  id="inputXYZ"
  aria-required="true"
  autocomplete=""
/>
<label for="inputXYZ">Password</label>
</oc-auth-field-v1>
Demo: login form

Story: components-auth-field-variations--demo-login · tags: components

A demo showcasing how to use the oc-auth-field-v1 component in a login form.

<form novalidate>
  <oc-auth-field-v1>
    <input
      name="email"
      type="email"
      aria-describedby="errorEmail hintEmail"
      maxlength="250"
      id="inputEmail"
      aria-required="true"
      spellcheck="false"
      autocorrect="off"
      aria-invalid="false"
    />
    <label for="inputEmail">Username</label>
    <div slot="error" id="errorEmail"></div>
    <div slot="hint" id="hintEmail">Enter username</div>
  </oc-auth-field-v1>

  <oc-auth-field-v1>
    <input
      name="password"
      type="password"
      aria-describedby="errorPassword hintPassword"
      minlength="8"
      id="inputPassword"
      aria-required="true"
      aria-invalid="false"
      autocomplete="current-password"
    />
    <label for="inputPassword">Password</label>
    <div slot="error" id="errorPassword"></div>
    <div slot="hint" id="hintPassword">Enter password</div>
  </oc-auth-field-v1>

  <!-- Workaround for Safari < 18. -->
  <!-- Fixed with Safari 18: https://bugs.webkit.org/show_bug.cgi?id=279595 -->
  <input type="hidden" />

  <div class="buttons">
    <oc-button-v1 type="reset" variant="secondary">Reset</oc-button-v1>
    <oc-button-v1 type="submit">Submit</oc-button-v1>
  </div>
</form>

<script>
  (() => {
    const [form] = document.getElementsByTagName("form");
    const [resetButton] = document.getElementsByTagName("oc-button-v1");
    const [ocEmail, ocPassword] = document.getElementsByTagName("oc-auth-field-v1");
    const inputEmail = document.getElementById("inputEmail");
    const inputPassword = document.getElementById("inputPassword");

    resetButton.addEventListener("click", () => {
      inputEmail.ariaInvalid = "false";
      ocEmail.variant = "default";
      ocEmail.querySelector("[slot=error]").textContent = "";

      inputPassword.ariaInvalid = "false";
      ocPassword.variant = "default";
      ocPassword.querySelector("[slot=error]").textContent = "";
    });

    const validateEmail = () => {
      if (!inputEmail.value) {
        inputEmail.ariaInvalid = "true";
        ocEmail.querySelector("[slot=error]").textContent =
          "Please enter your email address.";
      } else if (inputEmail.validity.typeMismatch) {
        inputEmail.ariaInvalid = "true";
        ocEmail.querySelector("[slot=error]").textContent =
          "Please enter a valid email address.";
      } else {
        inputEmail.ariaInvalid = "false";
        ocEmail.querySelector("[slot=error]").textContent = "";
      }
      ocEmail.variant = inputEmail.ariaInvalid === "true" ? "error" : "default";
    };

    const validatePassword = () => {
      if (!inputPassword.value) {
        inputPassword.ariaInvalid = "true";
        ocPassword.querySelector("[slot=error]").textContent = "Please enter your password.";
      } else if (inputPassword.value.length < 8) {
        inputPassword.ariaInvalid = "true";
        ocPassword.querySelector("[slot=error]").textContent =
          "Password must be at least 8 characters long.";
      } else {
        inputPassword.ariaInvalid = "false";
        ocPassword.querySelector("[slot=error]").textContent = "";
      }

      ocPassword.variant = inputPassword.ariaInvalid === "true" ? "error" : "default";
    };

    form.addEventListener("submit", (ev) => {
      validateEmail();
      validatePassword();

      // focus the first invalid field
      if (inputEmail.ariaInvalid === "true") {
        inputEmail.focus();
      } else if (inputPassword.ariaInvalid === "true") {
        inputPassword.focus();
      }

      if (inputEmail.ariaInvalid !== "true" && inputPassword.ariaInvalid !== "true") {
        let formData = new FormData(form);
        formData.set("password", "********"); // just in case
        let data = JSON.stringify(Object.fromEntries(formData));
        alert("sending " + data);
      }
      ev.preventDefault();
    });
  })();
</script>

${safariObscureHint}

<style>
  form {
    display: flex;
    flex-direction: column;
    gap: 24px;
  }

  .buttons {
    display: flex;
    gap: 16px;
  }
</style>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Demo: login form",
  parameters: {
    controls: {
      disabled: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html`
      <form novalidate>
        <oc-auth-field-v1>
          <input
            name="email"
            type="email"
            aria-describedby="errorEmail hintEmail"
            maxlength="250"
            id="inputEmail"
            aria-required="true"
            spellcheck="false"
            autocorrect="off"
            aria-invalid="false"
          />
          <label for="inputEmail">Username</label>
          <div slot="error" id="errorEmail"></div>
          <div slot="hint" id="hintEmail">Enter username</div>
        </oc-auth-field-v1>

        <oc-auth-field-v1>
          <input
            name="password"
            type="password"
            aria-describedby="errorPassword hintPassword"
            minlength="8"
            id="inputPassword"
            aria-required="true"
            aria-invalid="false"
            autocomplete="current-password"
          />
          <label for="inputPassword">Password</label>
          <div slot="error" id="errorPassword"></div>
          <div slot="hint" id="hintPassword">Enter password</div>
        </oc-auth-field-v1>

        <!-- Workaround for Safari < 18. -->
        <!-- Fixed with Safari 18: https://bugs.webkit.org/show_bug.cgi?id=279595 -->
        <input type="hidden" />

        <div class="buttons">
          <oc-button-v1 type="reset" variant="secondary">Reset</oc-button-v1>
          <oc-button-v1 type="submit">Submit</oc-button-v1>
        </div>
      </form>

      <script>
        (() => {
          const [form] = document.getElementsByTagName("form");
          const [resetButton] = document.getElementsByTagName("oc-button-v1");
          const [ocEmail, ocPassword] = document.getElementsByTagName("oc-auth-field-v1");
          const inputEmail = document.getElementById("inputEmail");
          const inputPassword = document.getElementById("inputPassword");

          resetButton.addEventListener("click", () => {
            inputEmail.ariaInvalid = "false";
            ocEmail.variant = "default";
            ocEmail.querySelector("[slot=error]").textContent = "";

            inputPassword.ariaInvalid = "false";
            ocPassword.variant = "default";
            ocPassword.querySelector("[slot=error]").textContent = "";
          });

          const validateEmail = () => {
            if (!inputEmail.value) {
              inputEmail.ariaInvalid = "true";
              ocEmail.querySelector("[slot=error]").textContent =
                "Please enter your email address.";
            } else if (inputEmail.validity.typeMismatch) {
              inputEmail.ariaInvalid = "true";
              ocEmail.querySelector("[slot=error]").textContent =
                "Please enter a valid email address.";
            } else {
              inputEmail.ariaInvalid = "false";
              ocEmail.querySelector("[slot=error]").textContent = "";
            }
            ocEmail.variant = inputEmail.ariaInvalid === "true" ? "error" : "default";
          };

          const validatePassword = () => {
            if (!inputPassword.value) {
              inputPassword.ariaInvalid = "true";
              ocPassword.querySelector("[slot=error]").textContent = "Please enter your password.";
            } else if (inputPassword.value.length < 8) {
              inputPassword.ariaInvalid = "true";
              ocPassword.querySelector("[slot=error]").textContent =
                "Password must be at least 8 characters long.";
            } else {
              inputPassword.ariaInvalid = "false";
              ocPassword.querySelector("[slot=error]").textContent = "";
            }

            ocPassword.variant = inputPassword.ariaInvalid === "true" ? "error" : "default";
          };

          form.addEventListener("submit", (ev) => {
            validateEmail();
            validatePassword();

            // focus the first invalid field
            if (inputEmail.ariaInvalid === "true") {
              inputEmail.focus();
            } else if (inputPassword.ariaInvalid === "true") {
              inputPassword.focus();
            }

            if (inputEmail.ariaInvalid !== "true" && inputPassword.ariaInvalid !== "true") {
              let formData = new FormData(form);
              formData.set("password", "********"); // just in case
              let data = JSON.stringify(Object.fromEntries(formData));
              alert("sending " + data);
            }
            ev.preventDefault();
          });
        })();
      </script>

      ${safariObscureHint}

      <style>
        form {
          display: flex;
          flex-direction: column;
          gap: 24px;
        }

        .buttons {
          display: flex;
          gap: 16px;
        }
      </style>
    `;
  }
}
Demo: password repeat

Story: components-auth-field-variations--demo-password-repeat · tags: components

A demo showcasing how to use the oc-auth-field-v1 component in a registration form with a password repeat field.

<form novalidate>
  <oc-auth-field-v1>
    <input
      name="password"
      type="password"
      aria-describedby="errorPassword1 hintPassword1"
      minlength="8"
      id="inputPassword1"
      aria-required="true"
      aria-invalid="false"
      autocomplete="new-password"
    />
    <label for="inputPassword1">Password</label>
    <div slot="error" id="errorPassword1"></div>
    <div slot="hint" id="hintPassword1">Enter password</div>
  </oc-auth-field-v1>

  <oc-auth-field-v1>
    <input
      name="password"
      type="password"
      aria-describedby="errorPassword2 hintPassword2"
      minlength="8"
      id="inputPassword2"
      aria-required="true"
      aria-invalid="false"
      autocomplete="new-password"
    />
    <label for="inputPassword2">Repeat</label>
    <div slot="error" id="errorPassword2"></div>
    <div slot="hint" id="hintPassword2">Repeat password</div>
  </oc-auth-field-v1>
</form>

${safariObscureHint}

<script>
  (() => {
    const [ocPassword1, ocPassword2] = document.getElementsByTagName("oc-auth-field-v1");

    ocPassword1.addEventListener("oc-property-change", (ev) => {
      if (ev.detail.passwordVisible) {
        ocPassword2.passwordVisible = ev.detail.passwordVisible.current;
      }
    });

    ocPassword2.addEventListener("oc-property-change", (ev) => {
      if (ev.detail.passwordVisible) {
        ocPassword1.passwordVisible = ev.detail.passwordVisible.current;
      }
    });
  })();
</script>

<style>
  form {
    display: flex;
    flex-direction: column;
    gap: 24px;
  }

  .buttons {
    display: flex;
    gap: 16px;
  }
</style>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Demo: password repeat",
  parameters: {
    controls: {
      disabled: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html`
      <form novalidate>
        <oc-auth-field-v1>
          <input
            name="password"
            type="password"
            aria-describedby="errorPassword1 hintPassword1"
            minlength="8"
            id="inputPassword1"
            aria-required="true"
            aria-invalid="false"
            autocomplete="new-password"
          />
          <label for="inputPassword1">Password</label>
          <div slot="error" id="errorPassword1"></div>
          <div slot="hint" id="hintPassword1">Enter password</div>
        </oc-auth-field-v1>

        <oc-auth-field-v1>
          <input
            name="password"
            type="password"
            aria-describedby="errorPassword2 hintPassword2"
            minlength="8"
            id="inputPassword2"
            aria-required="true"
            aria-invalid="false"
            autocomplete="new-password"
          />
          <label for="inputPassword2">Repeat</label>
          <div slot="error" id="errorPassword2"></div>
          <div slot="hint" id="hintPassword2">Repeat password</div>
        </oc-auth-field-v1>
      </form>

      ${safariObscureHint}

      <script>
        (() => {
          const [ocPassword1, ocPassword2] = document.getElementsByTagName("oc-auth-field-v1");

          ocPassword1.addEventListener("oc-property-change", (ev) => {
            if (ev.detail.passwordVisible) {
              ocPassword2.passwordVisible = ev.detail.passwordVisible.current;
            }
          });

          ocPassword2.addEventListener("oc-property-change", (ev) => {
            if (ev.detail.passwordVisible) {
              ocPassword1.passwordVisible = ev.detail.passwordVisible.current;
            }
          });
        })();
      </script>

      <style>
        form {
          display: flex;
          flex-direction: column;
          gap: 24px;
        }

        .buttons {
          display: flex;
          gap: 16px;
        }
      </style>
    `;
  }
}

Interaction tests (AuthFieldV1.interactions.stories.ts)

Interaction test stories (automated tests, not usage patterns).

Label Should Be On The Top

Story: components-auth-field-interaction-tests--label-should-be-on-the-top · tags: components, auth-field, v1, play-fn

Enter username
HTML
<oc-auth-field-v1>
<input
  name="email"
  type="email"
  aria-describedby="hintEmail"
  maxlength="250"
  id="inputEmail"
  aria-required="true"
  spellcheck="false"
  autocorrect="off"
/>
<label for="inputEmail">Username</label>
<div slot="hint" id="hintEmail">Enter username</div>
</oc-auth-field-v1>
Story source (TypeScript, verbatim from Storybook)
{
  render: () => html`
    <oc-auth-field-v1>
      <input
        name="email"
        type="email"
        aria-describedby="hintEmail"
        maxlength="250"
        id="inputEmail"
        aria-required="true"
        spellcheck="false"
        autocorrect="off"
      />
      <label for="inputEmail">Username</label>
      <div slot="hint" id="hintEmail">Enter username</div>
    </oc-auth-field-v1>
  `,
  play: async ({
    canvasElement
  }) => {
    const input = canvasElement.querySelector("input")!;
    const label = canvasElement.querySelector("label")!;
    await expect(label.getBoundingClientRect().toJSON(), "should initially be not on the top").toMatchObject({
      bottom: 52,
      height: 24,
      left: 28,
      top: 28,
      x: 28,
      y: 28
    });
    await userEvent.type(input, "foo@bar.de");
    await waitFor(() => expect(label.getBoundingClientRect().toJSON(), "should be on the top").toMatchObject({
      bottom: 24,
      height: 16,
      left: 28,
      top: 8,
      x: 28,
      y: 8
    }));
  }
}

Password Toggle Should Show Password

Story: components-auth-field-interaction-tests--password-toggle-should-show-password · tags: components, auth-field, v1, play-fn

Enter password
HTML
<oc-auth-field-v1>
<input
  name="password"
  type="password"
  aria-describedby="hintPassword"
  maxlength="250"
  id="inputPassword"
  aria-required="true"
/>
<label for="inputPassword">Password</label>
<div slot="hint" id="hintPassword">Enter password</div>
</oc-auth-field-v1>
Story source (TypeScript, verbatim from Storybook)
{
  render: () => html`
    <oc-auth-field-v1>
      <input
        name="password"
        type="password"
        aria-describedby="hintPassword"
        maxlength="250"
        id="inputPassword"
        aria-required="true"
      />
      <label for="inputPassword">Password</label>
      <div slot="hint" id="hintPassword">Enter password</div>
    </oc-auth-field-v1>
  `,
  play: async ({
    canvasElement
  }) => {
    const input = canvasElement.querySelector("input")!;
    const passwordToggle = canvasElement.querySelector("oc-auth-field-v1")!.shadowRoot!.querySelector(".auth-field__password-toggle")! as HTMLElement;
    await expect(passwordToggle).toBeVisible();
    await expect(input.getAttribute("type"), "password should be visible").toBe("password");
    await userEvent.type(input, "MyTestPassword123");
    await click(passwordToggle);
    await expect(input.getAttribute("type"), "password should be visible").toBe("text");
  }
}

Hint And Error Should Have Correct Colors

Story: components-auth-field-interaction-tests--hint-and-error-should-have-correct-colors · tags: components, auth-field, v1, play-fn

Your input is not valid.
Some hint
HTML
<oc-auth-field-v1 variant="error">
<input
  type="email"
  aria-describedby="errorXYZ hintXYZ"
  maxlength="250"
  id="inputXYZ"
  aria-required="true"
  aria-invalid="true"
  autocomplete=""
/>
<label for="inputXYZ">E-Mail</label>
<div slot="error" id="errorXYZ">Your input is not valid.</div>
<div slot="hint" id="hintXYZ">Some hint</div>
</oc-auth-field-v1>
Story source (TypeScript, verbatim from Storybook)
{
  render: () => html`
    <oc-auth-field-v1 variant="error">
      <input
        type="email"
        aria-describedby="errorXYZ hintXYZ"
        maxlength="250"
        id="inputXYZ"
        aria-required="true"
        aria-invalid="true"
        autocomplete=""
      />
      <label for="inputXYZ">E-Mail</label>
      <div slot="error" id="errorXYZ">Your input is not valid.</div>
      <div slot="hint" id="hintXYZ">Some hint</div>
    </oc-auth-field-v1>
  `,
  play: async ({
    canvasElement
  }) => {
    const hint = canvasElement.querySelector("[slot=hint]")!;
    const error = canvasElement.querySelector("[slot=error]")!;
    await expect(getComputedStyle(hint).color).toBe("rgb(109, 109, 109)");
    await expect(getComputedStyle(error).color).toBe("rgb(179, 0, 35)");
  }
}

Should Submit Form On Enter

Story: components-auth-field-interaction-tests--should-submit-form-on-enter · tags: components, auth-field, v1, play-fn

Enter username

Enter password

HTML
<form>
<oc-auth-field-v1>
  <input
    name="email"
    type="email"
    aria-describedby="hintEmail"
    maxlength="250"
    id="inputEmail"
    aria-required="true"
    spellcheck="false"
    autocorrect="off"
  />
  <label for="inputEmail">Username</label>
  <div slot="hint" id="hintEmail">Enter username</div>
</oc-auth-field-v1>
<br />
<oc-auth-field-v1>
  <input
    name="password"
    type="password"
    aria-describedby="hintPassword"
    maxlength="250"
    id="inputPassword"
    aria-required="true"
  />
  <label for="inputPassword">Password</label>
  <div slot="hint" id="hintPassword">Enter password</div>
</oc-auth-field-v1>
<br />
<button type="submit">Submit</button>
</form>
Story source (TypeScript, verbatim from Storybook)
{
  render: () => html`
    <form>
      <oc-auth-field-v1>
        <input
          name="email"
          type="email"
          aria-describedby="hintEmail"
          maxlength="250"
          id="inputEmail"
          aria-required="true"
          spellcheck="false"
          autocorrect="off"
        />
        <label for="inputEmail">Username</label>
        <div slot="hint" id="hintEmail">Enter username</div>
      </oc-auth-field-v1>

      <br />

      <oc-auth-field-v1>
        <input
          name="password"
          type="password"
          aria-describedby="hintPassword"
          maxlength="250"
          id="inputPassword"
          aria-required="true"
        />
        <label for="inputPassword">Password</label>
        <div slot="hint" id="hintPassword">Enter password</div>
      </oc-auth-field-v1>

      <br />

      <button type="submit">Submit</button>
    </form>
  `,
  play: async ({
    canvasElement
  }) => {
    const form = canvasElement.querySelector("form")!;
    const button = canvasElement.querySelector("button")!;
    const [emailInput, passwordInput] = Array.from(canvasElement.querySelectorAll("input"));
    let formData;
    form.addEventListener("submit", event => {
      event.preventDefault();
      formData = Object.fromEntries(new FormData(form).entries());
    });
    await userEvent.type(emailInput, "foo@bar.de");
    await userEvent.type(passwordInput, "MyTestPassword123");
    await click(button);
    await expect(formData).toStrictEqual({
      email: "foo@bar.de",
      password: "MyTestPassword123"
    });
  }
}