OTTODesign System

ComponentsForm

Form group

The form group groups multiple form elements, such as text fields, radio buttons or checkboxes, together in a container and provides a shared aria label, hint or error for them.

Configurator

LiveForm group: orientation, sizing, gap, hint and error message for all fields
VornameNachname
HTML
<oc-form-group-v1 orientation="horizontal" flex-behavior="grow" gap="var(--oc-base-dimension-8)" hint="Bitte gib deinen Namen so an wie im Personalausweis." oc-aria-label="Name"><oc-text-field-v1 autocomplete="given-name" style="width:10rem;min-width:0">Vorname</oc-text-field-v1><oc-text-field-v1 autocomplete="family-name" style="width:10rem;min-width:0">Nachname</oc-text-field-v1></oc-form-group-v1>

Usage

Variants

You can use form group with the following form components:

Radio buttons

Checkboxes

Text field and dropdown

Telefonnummer

Text areas

Das gefällt mir Das gefällt mir nicht
Livevariants
HTML
<p class="demo-label">Radio buttons</p>
<oc-form-group-v1 orientation="horizontal" flex-behavior="shrink" hint="Hinweis" oc-aria-label="Versandart" style="max-width:20rem">
<oc-radio-button-v2 width-behavior="fit"><input type="radio" name="fg-var-rb" aria-label="oc-auto"><label slot="label">Option 1</label></oc-radio-button-v2>
<oc-radio-button-v2 width-behavior="fit"><input type="radio" name="fg-var-rb" aria-label="oc-auto"><label slot="label">Option 2</label></oc-radio-button-v2>
<oc-radio-button-v2 width-behavior="fit"><input type="radio" name="fg-var-rb" aria-label="oc-auto"><label slot="label">Option 3</label></oc-radio-button-v2>
<oc-radio-button-v2 width-behavior="fit"><input type="radio" name="fg-var-rb" aria-label="oc-auto"><label slot="label">Option 4</label></oc-radio-button-v2>
</oc-form-group-v1>
<p class="demo-label">Checkboxes</p>
<oc-form-group-v1 orientation="horizontal" flex-behavior="shrink" hint="Hinweis" oc-aria-label="Farbe" style="max-width:20rem">
<oc-checkbox-v2 width-behavior="fit"><input type="checkbox" aria-label="oc-auto"><label slot="label">Option 1</label></oc-checkbox-v2>
<oc-checkbox-v2 width-behavior="fit"><input type="checkbox" aria-label="oc-auto"><label slot="label">Option 2</label></oc-checkbox-v2>
<oc-checkbox-v2 width-behavior="fit"><input type="checkbox" aria-label="oc-auto"><label slot="label">Option 3</label></oc-checkbox-v2>
<oc-checkbox-v2 width-behavior="fit"><input type="checkbox" aria-label="oc-auto"><label slot="label">Option 4</label></oc-checkbox-v2>
</oc-form-group-v1>
<p class="demo-label">Text field and dropdown</p>
<oc-form-group-v1 orientation="horizontal" flex-behavior="grow" gap="var(--oc-base-dimension-8)" hint="Hinweis" oc-aria-label="Telefon">
<oc-text-field-v1 type="tel">Telefonnummer</oc-text-field-v1>
<oc-dropdown-v1 label="Art" empty-option><option>Mobil</option><option>Festnetz</option></oc-dropdown-v1>
</oc-form-group-v1>
<p class="demo-label">Text areas</p>
<oc-form-group-v1 orientation="horizontal" flex-behavior="grow" gap="var(--oc-base-dimension-8)" hint="Hinweis" oc-aria-label="Bewertung">
<oc-text-area-v1>Das gefällt mir</oc-text-area-v1>
<oc-text-area-v1>Das gefällt mir nicht</oc-text-area-v1>
</oc-form-group-v1>

The components of the cluster form can also be combined within the form group. For example, a text field can be combined with a dropdown.

Telefonnummer
Livecombined components
HTML
<oc-form-group-v1 orientation="horizontal" flex-behavior="grow" gap="var(--oc-base-dimension-8)" hint="Hinweis" oc-aria-label="Telefon">
<oc-text-field-v1 type="tel">Telefonnummer</oc-text-field-v1>
<oc-dropdown-v1 label="Art" empty-option><option>Mobil</option><option>Festnetz</option></oc-dropdown-v1>
</oc-form-group-v1>

You can optionally extend the form group with a error or a hint. The hint helps the user filling out the form group correctly and provides further context. 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

Vorname Nachname

Error and hint

Vorname Nachname
Liveerror and hint
HTML
<p class="demo-label">Hint</p>
<oc-form-group-v1 orientation="horizontal" flex-behavior="grow" gap="var(--oc-base-dimension-8)" hint="Bitte gib deinen Namen an, wie im Personalausweis." oc-aria-label="Name">
<oc-text-field-v1>Vorname</oc-text-field-v1>
<oc-text-field-v1>Nachname</oc-text-field-v1>
</oc-form-group-v1>
<p class="demo-label">Error and hint</p>
<oc-form-group-v1 orientation="horizontal" flex-behavior="grow" gap="var(--oc-base-dimension-8)" hint="Bitte gib deinen Namen an, wie im Personalausweis." validation-message="Der eingegebene Text ist kein Name. Bitte korrigiere deine Eingabe." oc-aria-label="Name">
<oc-text-field-v1>Vorname</oc-text-field-v1>
<oc-text-field-v1>Nachname</oc-text-field-v1>
</oc-form-group-v1>

Behavior

Fitting

The components within the form group can either grow or shrink. For grow, the components will expand to fill the available space. Meanwhile, for shrink, the components will reduce their size to fit.

flex-behavior grow

Vorname Nachname

flex-behavior shrink

Vorname Nachname
Livegrow and shrink
HTML
<p class="demo-label">flex-behavior grow</p>
<oc-form-group-v1 orientation="horizontal" flex-behavior="grow" gap="var(--oc-base-dimension-8)" hint="Hinweis" oc-aria-label="Name">
<oc-text-field-v1>Vorname</oc-text-field-v1>
<oc-text-field-v1>Nachname</oc-text-field-v1>
</oc-form-group-v1>
<p class="demo-label">flex-behavior shrink</p>
<oc-form-group-v1 orientation="horizontal" flex-behavior="shrink" gap="var(--oc-base-dimension-8)" hint="Hinweis" oc-aria-label="Name">
<oc-text-field-v1>Vorname</oc-text-field-v1>
<oc-text-field-v1>Nachname</oc-text-field-v1>
</oc-form-group-v1>

Placement

You can arrange the components within the form group either vertically or horizontally. Horizontal placement is particularly useful as it allows the hint and/or error to stretch across the full width. The gap can be customized. For text field for example you should at least use 8 px or for checkbox you should use at least 24px.

Horizontal, 24px gap

Vertical, 24px gap

Liveplacement
HTML
<p class="demo-label">Horizontal, 24px gap</p>
<oc-form-group-v1 orientation="horizontal" flex-behavior="shrink" gap="var(--oc-base-dimension-24)" hint="Hinweis" oc-aria-label="Versandart">
<oc-radio-button-v2 width-behavior="fit"><input type="radio" name="fg-place" aria-label="oc-auto"><label slot="label">Option 1</label></oc-radio-button-v2>
<oc-radio-button-v2 width-behavior="fit"><input type="radio" name="fg-place" aria-label="oc-auto"><label slot="label">Option 2</label></oc-radio-button-v2>
</oc-form-group-v1>
<p class="demo-label">Vertical, 24px gap</p>
<oc-form-group-v1 orientation="vertical" gap="var(--oc-base-dimension-24)" hint="Hinweis" oc-aria-label="Farbe">
<oc-checkbox-v2><input type="checkbox" aria-label="oc-auto"><label slot="label">Option 1</label></oc-checkbox-v2>
<oc-checkbox-v2><input type="checkbox" aria-label="oc-auto"><label slot="label">Option 2</label></oc-checkbox-v2>
</oc-form-group-v1>

Best practices

Vorname Nachname
DoCombine related components within the form ground. You can also combine different but related components within the form group.
Vorname Nachname
Don'tUse related components individually which should be combined in a form group.

Name

Vorname Nachname

Geburtstag

DoDivide a form into different form groups or components depending on their content.
Vorname Nachname
Don'tCombine components of a complete form into one form group.
Telefonnummer
DoOnly use components that belong to the form cluster and are intended for use within the form group.
Mehr erfahren
Don'tUse components which are not of the form cluster and not intended for use within the form group.

Accessibility

You can create one shared aria-label for the different components of the form group. This label is used by screen readers and should describe the form group's overall content or purpose. For example, for a form group that allows you to configure your birthday, you should create an Aria label such as "Geburtstag". The advantage is that the screen reader will not only read out the individual components, such as "Tag", "Monat" and "Jahr", but also summarise them as "Geburtstag".

Geburtstag

1
  1. oc-aria-label="Geburtstag" names the whole group for screen readers
Livearia-label for form group
HTML
<div class="anatomy" style="display:block;width:max-content;margin:28px auto">
<p class="demo-title">Geburtstag</p>
<oc-form-group-v1 orientation="horizontal" flex-behavior="shrink" gap="var(--oc-base-dimension-8)" hint="Hinweis" oc-aria-label="Geburtstag">
<oc-dropdown-v1 label="Tag" empty-option style="width:6rem"><option>1</option><option>2</option><option>3</option></oc-dropdown-v1>
<oc-dropdown-v1 label="Monat" empty-option style="width:10rem"><option>Januar</option><option>Februar</option><option>März</option></oc-dropdown-v1>
<oc-dropdown-v1 label="Jahr" empty-option style="width:7rem"><option>1990</option><option>1991</option><option>1992</option></oc-dropdown-v1>
</oc-form-group-v1>
<span class="anatomy-pin" style="left:50%;top:calc(100% + 16px)">1</span>
<ol class="anatomy-key"><li data-n="1">oc-aria-label="Geburtstag" names the whole group for screen readers</li></ol>

For information on accessibility, refer to the technical documentation.

Status

Implementation

Live demo

Neue Lieferadresse

Wir liefern an Haus- und Packstation-Adressen in Deutschland.

Vorname Nachname Straße Nr. PLZ Ort
Abbrechen Adresse speichern
LiveReal OTTO components, rendered by the OTTO component runtime
HTML
<div style="max-width:520px;margin:0 auto;display:grid;gap:24px">
<div style="display:grid;gap:4px">
  <p class="oc-headline-100">Neue Lieferadresse</p>
  <p class="oc-copy-100 oc-text-color-secondary">Wir liefern an Haus- und Packstation-Adressen in Deutschland.</p>
<oc-form-group-v1 orientation="horizontal" flex-behavior="grow" gap="var(--oc-base-dimension-8)" oc-aria-label="Name">
  <oc-text-field-v1 value="Lara" autocomplete="given-name">Vorname</oc-text-field-v1>
  <oc-text-field-v1 value="Lavendel" autocomplete="family-name">Nachname</oc-text-field-v1>
</oc-form-group-v1>
<oc-form-group-v1 orientation="horizontal" flex-behavior="grow" gap="var(--oc-base-dimension-8)" oc-aria-label="Straße und Hausnummer">
  <oc-text-field-v1 value="Werner-Otto-Straße" autocomplete="address-line1">Straße</oc-text-field-v1>
  <oc-text-field-v1 value="1–7" style="flex:0 1 7rem;min-width:0">Nr.</oc-text-field-v1>
</oc-form-group-v1>
<oc-form-group-v1 orientation="horizontal" flex-behavior="grow" gap="var(--oc-base-dimension-8)" validation-message="Die Postleitzahl passt nicht zum Ort. Bitte prüfe deine Eingabe." oc-aria-label="Postleitzahl und Ort">
  <oc-text-field-v1 type="integer" value="22197" maxlength="5" hide-counter autocomplete="postal-code" style="flex:0 1 8rem;min-width:0">PLZ</oc-text-field-v1>
  <oc-text-field-v1 value="Hamburg" autocomplete="address-level2">Ort</oc-text-field-v1>
</oc-form-group-v1>
<oc-checkbox-v2><input type="checkbox" checked aria-label="oc-auto"><label slot="label">Als Standard-Lieferadresse speichern</label></oc-checkbox-v2>
  <oc-button-v1 variant="secondary">Abbrechen</oc-button-v1>
  <oc-button-v1 variant="primary">Adresse speichern</oc-button-v1>

Code

Version Tag Status API
v1 <oc-form-group-v1> Stable, allowed for generation FormGroupV1

Overview (v1)

Form group

The form group component groups multiple form elements in a container and provides a label it. It supports flexible layout options, validation messages, hint messages, and ARIA labels for accessibility.

Default variation
<oc-form-group-v1 class="${className}" oc-aria-label="Form Group" orientation="horizontal" flex-behavior="grow" style="--gap: var(--oc-base-dimension-8)"
  ><oc-text-field-v1>Text Field 1</oc-text-field-v1><oc-text-field-v1>Text Field 2</oc-text-field-v1></oc-form-group-v1
>${css}
Configuration

The form group 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 form group 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 Form group UX documentation for detailed user experience guidelines.

Accessibility

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

Use oc-aria-label

To make the form group recognizable for screen readers, use the oc-aria-label attribute to provide clear and descriptive context information. See the general accessibility documentation for guidance on using oc-aria-label, including how it works with link and masked link behavior.

Aria Description

The content of the form group can have additional information by using the hint attribute. This helps users understand the purpose of the form group.

Validation

The form group can have a validation state by using the validation-message attribute. The validation messages should be clear and concise. They are automatically associated with the form elements and set the validation state of those elements.

Configuration (v1)

Form group configuration

Configure the Form group 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.

<oc-form-group-v1 class="${className}" oc-aria-label="Form Group" orientation="horizontal" flex-behavior="grow" style="--gap: var(--oc-base-dimension-8)"
  ><oc-text-field-v1>Text Field 1</oc-text-field-v1><oc-text-field-v1>Text Field 2</oc-text-field-v1></oc-form-group-v1
>${css}

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

API v1

Form Group v1 API

API: <oc-form-group-v1> (FormGroupV1)

The Form Group component groups multiple form elements in a container and provides a label it. It supports flexible layout options, validation messages, hint messages, and ARIA labels for accessibility.

Attributes / properties
Attribute Type Default Required Description
orientation "horizontal" | "vertical" undefined no Defines the orientation of the form group, determining the layout of its child elements.
hint string undefined no Sets and displays a hint message for the form group, providing details for all child elements.
validation-message string undefined no Sets and displays an error message for the form group, marking all child elements as invalid.
flex-behavior "shrink" | "grow" undefined no Determines whether the form group should shrink or grow its child elements.

If set to grow, the child elements will expand to fill the available space.
If set to shrink, the child elements will reduce in size to fit.

This setting works best with a horizontal orientation.
gap string undefined no Sets the gap between child elements in the form group.

Also, can be set by using the --gap CSS variable.
oc-aria-label string no Sets the ARIA label of the form group. This label is used by screen readers and should describe the overall content or purpose of the form group.
Slots
Slot Required Description
default yes Sets the content of the form group. Example:
<oc-text-field-v1>Text Field 1</oc-text-field-v1>
<oc-text-field-v1>Text Field 2</oc-text-field-v1>
Events
Event Detail type Description
oc-property-change OcFormGroupV1Events["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.
CSS custom properties
Custom property Default Description
--gap Sets the gap between child elements in the form group.
--column-gap Sets the gap between columns in the form group.
--row-gap Sets the gap between rows in the form group.

Variations (v1)

Variations

Listed below are the most common variations of the form group 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-form-group-variations--default · tags: components

The default configuration of the form group uses two text field components.

Args: --gap=var(--oc-base-dimension-8)

<oc-form-group-v1 class="${className}" oc-aria-label="Form Group" orientation="horizontal" flex-behavior="grow" style="--gap: var(--oc-base-dimension-8)"
  ><oc-text-field-v1>Text Field 1</oc-text-field-v1><oc-text-field-v1>Text Field 2</oc-text-field-v1></oc-form-group-v1
>${css}
Story source (TypeScript, verbatim from Storybook)
{
  name: "Default",
  args: {
    "--gap": "var(--oc-base-dimension-8)"
  },
  render(args) {
    const {
      defaultSlot,
      ...props
    } = args;
    const {
      css,
      className
    } = cssVariablesExample(args);
    return html`<oc-form-group-v1 class="${className}" ${spread(props)}
        >${unsafeHTML(defaultSlot)}</oc-form-group-v1
      >${css}`;
  }
}
With hint

Story: components-form-group-variations--hint · tags: components

Variation with the hint attribute set, providing additional information about the form group.

Args: hint=Example hint., --gap=var(--oc-base-dimension-8)

Text Field 1Text Field 2
HTML
<oc-form-group-v1 oc-aria-label="Form Group" orientation="horizontal" flex-behavior="grow" hint="Example hint." style="--gap: var(--oc-base-dimension-8)">
<oc-text-field-v1>Text Field 1</oc-text-field-v1><oc-text-field-v1>Text Field 2</oc-text-field-v1>
</oc-form-group-v1>
Story source (TypeScript, verbatim from Storybook)
{
  name: "With hint",
  args: {
    hint: "Example hint.",
    "--gap": "var(--oc-base-dimension-8)"
  },
  render: Default.render
}
With validation message

Story: components-form-group-variations--validation · tags: components

Variation with the validation-message attribute set, providing form validation feedback to the user.

Args: validation-message=Invalid input., --gap=var(--oc-base-dimension-8)

Text Field 1Text Field 2
HTML
<oc-form-group-v1 oc-aria-label="Form Group" orientation="horizontal" flex-behavior="grow" validation-message="Invalid input." style="--gap: var(--oc-base-dimension-8)">
<oc-text-field-v1>Text Field 1</oc-text-field-v1><oc-text-field-v1>Text Field 2</oc-text-field-v1>
</oc-form-group-v1>
Story source (TypeScript, verbatim from Storybook)
{
  name: "With validation message",
  args: {
    "validation-message": "Invalid input.",
    "--gap": "var(--oc-base-dimension-8)"
  },
  render: Default.render
}
Without layout

Story: components-form-group-variations--without-layout · tags: components

Variation with undefined orientation and flex-behavior attributes, showing the form group without layout.

Args: orientation=null, flex-behavior=null, --gap=var(--oc-base-dimension-8)

Text Field 1Text Field 2
HTML
<oc-form-group-v1 oc-aria-label="Form Group" style="--gap: var(--oc-base-dimension-8)">
<oc-text-field-v1>Text Field 1</oc-text-field-v1><oc-text-field-v1>Text Field 2</oc-text-field-v1>
</oc-form-group-v1>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Without layout",
  args: {
    orientation: undefined,
    "flex-behavior": undefined,
    "--gap": "var(--oc-base-dimension-8)"
  },
  render: Default.render
}
Demo: Text Field

Story: components-form-group-variations--demo-text-field · tags: components

A demo showcasing how to group multiple text fields in a form group with horizontal orientation and grow flex-behavior.

Args: defaultSlot=(see snippet), hint=Example hint., oc-aria-label=Text Field, --gap=var(--oc-base-dimension-8)

Straße Hausnummer
HTML
<oc-form-group-v1 oc-aria-label="Text Field" orientation="horizontal" flex-behavior="grow" hint="Example hint." style="--gap: var(--oc-base-dimension-8)">
<oc-text-field-v1 style="flex: 8 0 auto">Straße</oc-text-field-v1>
<oc-text-field-v1 style="flex: 1 0 4rem">Hausnummer</oc-text-field-v1>
</oc-form-group-v1>
Demo: Text Area

Story: components-form-group-variations--demo-text-area · tags: components

A demo showcasing how to group multiple text areas in a form group with horizontal orientation and grow flex-behavior.

Args: defaultSlot=<oc-text-area-v1>Content 1</oc-text-area-v1> <oc-text-area-v1>Content 2</oc-text-area-v1>, hint=Example hint., oc-aria-label=Text Area, --gap=var(--oc-base-dimension-8)

Content 1 Content 2
HTML
<oc-form-group-v1 oc-aria-label="Text Area" orientation="horizontal" flex-behavior="grow" hint="Example hint." style="--gap: var(--oc-base-dimension-8)">
<oc-text-area-v1>Content 1</oc-text-area-v1>
<oc-text-area-v1>Content 2</oc-text-area-v1>
</oc-form-group-v1>
Demo: Checkbox

Story: components-form-group-variations--demo-checkbox · tags: components

A demo showcasing how to group multiple checkboxes in a form group with horizontal orientation and shrink flex-behavior.

Args: defaultSlot=(see snippet), flex-behavior=shrink, hint=Example hint., oc-aria-label=Checkbox

Option 1 Option 2 Option 3 Option 4 Option 5
HTML
<oc-form-group-v1 oc-aria-label="Checkbox" orientation="horizontal" flex-behavior="shrink" hint="Example hint.">
<oc-checkbox-v1 name="options" value="option1">Option 1</oc-checkbox-v1>
<oc-checkbox-v1 name="options" value="option2">Option 2</oc-checkbox-v1>
<oc-checkbox-v1 name="options" value="option3">Option 3</oc-checkbox-v1>
<oc-checkbox-v1 name="options" value="option4">Option 4</oc-checkbox-v1>
<oc-checkbox-v1 name="options" value="option5">Option 5</oc-checkbox-v1>
<oc-checkbox-v2>
  <input type="checkbox" name="options" value="option6" aria-label="oc-auto"/>
  <label slot="label">Option 6</label>
</oc-checkbox-v2>
<oc-checkbox-v2>
  <input type="checkbox" name="options" value="option7" aria-label="oc-auto"/>
  <label slot="label">Option 7</label>
</oc-checkbox-v2>
<oc-checkbox-v2>
  <input type="checkbox" name="options" value="option8" aria-label="oc-auto"/>
  <label slot="label">Option 8</label>
</oc-checkbox-v2>
<oc-checkbox-v2>
  <input type="checkbox" name="options" value="option9" aria-label="oc-auto"/>
  <label slot="label">Option 9</label>
</oc-checkbox-v2>
<oc-checkbox-v2>
  <input type="checkbox" name="options" value="option10" aria-label="oc-auto"/>
  <label slot="label">Option 10</label>
</oc-checkbox-v2>
</oc-form-group-v1>
Demo: Radio Button

Story: components-form-group-variations--demo-radio-button · tags: components

A demo showcasing how to group multiple radio buttons in a form group with horizontal orientation and grow flex-behavior.

Args: defaultSlot=(see snippet), flex-behavior=grow, hint=Example hint., oc-aria-label=Radio Button

Option 1 Option 2 Option 3 Option 4 Option 5 Option 6
HTML
<oc-form-group-v1 oc-aria-label="Radio Button" orientation="horizontal" flex-behavior="grow" hint="Example hint.">
<oc-radio-button-v1 name="options" value="option1">Option 1</oc-radio-button-v1>
<oc-radio-button-v1 name="options" value="option2">Option 2</oc-radio-button-v1>
<oc-radio-button-v1 name="options" value="option3">Option 3</oc-radio-button-v1>
<oc-radio-button-v1 name="options" value="option4">Option 4</oc-radio-button-v1>
<oc-radio-button-v1 name="options" value="option5">Option 5</oc-radio-button-v1>
<oc-radio-button-v1 name="options" value="option6">Option 6</oc-radio-button-v1>
<oc-radio-button-v2>
  <input type="radio" name="options" value="option6" aria-label="oc-auto"/>
  <label slot="label">Option 6</label>
</oc-radio-button-v2>
<oc-radio-button-v2>
  <input type="radio" name="options" value="option7" aria-label="oc-auto"/>
  <label slot="label">Option 7</label>
</oc-radio-button-v2>
<oc-radio-button-v2>
  <input type="radio" name="options" value="option8" aria-label="oc-auto"/>
  <label slot="label">Option 8</label>
</oc-radio-button-v2>
<oc-radio-button-v2>
  <input type="radio" name="options" value="option9" aria-label="oc-auto"/>
  <label slot="label">Option 9</label>
</oc-radio-button-v2>
<oc-radio-button-v2>
  <input type="radio" name="options" value="option10" aria-label="oc-auto"/>
  <label slot="label">Option 10</label>
</oc-radio-button-v2>
</oc-form-group-v1>
Demo: Dropdown

Story: components-form-group-variations--demo-dropdown · tags: components

A demo showcasing how to group multiple dropdowns in a form group with horizontal orientation and grow flex-behavior.

Args: defaultSlot=(see snippet), hint=Example hint., oc-aria-label=Dropdown, --gap=var(--oc-base-dimension-8)

HTML
<oc-form-group-v1 oc-aria-label="Dropdown" orientation="horizontal" flex-behavior="grow" hint="Example hint." style="--gap: var(--oc-base-dimension-8)">
<oc-dropdown-v1 label='Options 1'><option>Option 1</option><option>Option 2</option></oc-dropdown-v1>
<oc-dropdown-v1 label='Options 2'><option>Option 1</option><option>Option 2</option></oc-dropdown-v1>
</oc-form-group-v1>
Demo: Form

Story: components-form-group-variations--form-demo · tags: components

A demo form showcasing all form elements with different hints.

<form novalidate>
  <oc-text-field-v1 class="oc-mt-150" hint="Example hint." name="field"
    >text field
  </oc-text-field-v1>
  <oc-text-area-v1 class="oc-mt-150" hint="Example hint." name="area"
    >text area</oc-text-area-v1
  >

  <oc-dropdown-v1
    class="oc-mt-150"
    hint="Example hint."
    label="Options"
    name="dropdown"
    options='[{"label":"Option 1","value":"1"},{"label":"Option 2","value":"2"},{"label":"Option 3","value":"3"}]'
    >dropdown
  </oc-dropdown-v1>

  <oc-form-group-v1
    class="demo-class oc-mt-150"
    hint="Text fields hint."
    orientation="horizontal"
    flex-behavior="grow"
  >
    <oc-text-field-v1 name="field1">text field 1</oc-text-field-v1>
    <oc-text-field-v1 name="field2">text field 2</oc-text-field-v1>
  </oc-form-group-v1>

  <oc-form-group-v1
    class="demo-class oc-mt-150"
    hint="Text areas hint."
    orientation="horizontal"
    flex-behavior="grow"
  >
    <oc-text-area-v1 name="area1">text area 1</oc-text-area-v1>
    <oc-text-area-v1 name="area2">text area 2</oc-text-area-v1>
  </oc-form-group-v1>

  <oc-form-group-v1
    class="demo-class oc-mt-150"
    hint="Dropdowns hint."
    orientation="horizontal"
    flex-behavior="grow"
  >
    <oc-dropdown-v1
      label="Options"
      name="dropdown1"
      options='[{"label":"Option 1","value":"1"},{"label":"Option 2","value":"2"},{"label":"Option 3","value":"3"}]'
      >dropdown 1
    </oc-dropdown-v1>
    <oc-dropdown-v1
      label="Options"
      name="dropdown2"
      options='[{"label":"Option 1","value":"1"},{"label":"Option 2","value":"2"},{"label":"Option 3","value":"3"}]'
      >dropdown 2
    </oc-dropdown-v1>
  </oc-form-group-v1>

  <oc-form-group-v1
    class="oc-mt-150"
    hint="Checkboxes hint."
    orientation="horizontal"
    flex-behavior="shrink"
  >
    <oc-checkbox-v1 name="checkbox" value="checked1">checkbox 1</oc-checkbox-v1>
    <oc-checkbox-v1 name="checkbox" value="checked2">checkbox 2</oc-checkbox-v1>
  </oc-form-group-v1>

  <oc-form-group-v1
    class="oc-mt-150"
    hint="Radio buttons hint."
    orientation="horizontal"
    flex-behavior="shrink"
  >
    <oc-radio-button-v1 name="radio" value="checked1">radio button 1</oc-radio-button-v1>
    <oc-radio-button-v1 name="radio" value="checked2">radio button 2</oc-radio-button-v1>
  </oc-form-group-v1>

  <div class="oc-mt-150">
    <oc-button-v1 type="reset" fit-content variant="secondary">Reset</oc-button-v1>
    <oc-button-v1 fit-content id="mark-invalid">Mark Invalid</oc-button-v1>
    <oc-button-v1 type="submit" fit-content>Submit</oc-button-v1>
  </div>
</form>

<style>
  .demo-class {
    --gap: var(--oc-base-dimension-8);
  }
</style>

<script>
  (() => {
    const [textField] = document.getElementsByTagName("oc-text-field-v1");
    const [textArea] = document.getElementsByTagName("oc-text-area-v1");
    const [dropdown] = document.getElementsByTagName("oc-dropdown-v1");
    const formGroups = document.getElementsByTagName("oc-form-group-v1");

    const [form] = document.getElementsByTagName("form");
    const [reset, markInvalid] = document.getElementsByTagName("oc-button-v1");

    markInvalid.addEventListener("click", () => {
      textField.validationMessage = "This field is invalid";
      textArea.validationMessage = "This field is invalid";
      dropdown.validationMessage = "This field is invalid";
      for (const formGroup of formGroups) {
        formGroup.validationMessage = "These fields are invalid";
      }
    });

    reset.addEventListener("click", () => {
      textField.validationMessage = undefined;
      textArea.validationMessage = undefined;
      dropdown.validationMessage = undefined;
      for (const formGroup of formGroups) {
        formGroup.validationMessage = undefined;
      }
    });

    form.addEventListener("submit", (ev) => {
      let formData = new FormData(form);
      let data = "";
      formData.forEach((value, key) => (data += key + "=" + value + "\\n"));
      console.log(data);
      alert("sending:\\n" + data);
      ev.preventDefault();
    });
  })();
</script>
Story source (TypeScript, verbatim from Storybook)
{
  parameters: {
    controls: {
      disabled: true
    }
  },
  name: "Demo: Form",
  render() {
    return html`
      <form novalidate>
        <oc-text-field-v1 class="oc-mt-150" hint="Example hint." name="field"
          >text field
        </oc-text-field-v1>
        <oc-text-area-v1 class="oc-mt-150" hint="Example hint." name="area"
          >text area</oc-text-area-v1
        >

        <oc-dropdown-v1
          class="oc-mt-150"
          hint="Example hint."
          label="Options"
          name="dropdown"
          options='[{"label":"Option 1","value":"1"},{"label":"Option 2","value":"2"},{"label":"Option 3","value":"3"}]'
          >dropdown
        </oc-dropdown-v1>

        <oc-form-group-v1
          class="demo-class oc-mt-150"
          hint="Text fields hint."
          orientation="horizontal"
          flex-behavior="grow"
        >
          <oc-text-field-v1 name="field1">text field 1</oc-text-field-v1>
          <oc-text-field-v1 name="field2">text field 2</oc-text-field-v1>
        </oc-form-group-v1>

        <oc-form-group-v1
          class="demo-class oc-mt-150"
          hint="Text areas hint."
          orientation="horizontal"
          flex-behavior="grow"
        >
          <oc-text-area-v1 name="area1">text area 1</oc-text-area-v1>
          <oc-text-area-v1 name="area2">text area 2</oc-text-area-v1>
        </oc-form-group-v1>

        <oc-form-group-v1
          class="demo-class oc-mt-150"
          hint="Dropdowns hint."
          orientation="horizontal"
          flex-behavior="grow"
        >
          <oc-dropdown-v1
            label="Options"
            name="dropdown1"
            options='[{"label":"Option 1","value":"1"},{"label":"Option 2","value":"2"},{"label":"Option 3","value":"3"}]'
            >dropdown 1
          </oc-dropdown-v1>
          <oc-dropdown-v1
            label="Options"
            name="dropdown2"
            options='[{"label":"Option 1","value":"1"},{"label":"Option 2","value":"2"},{"label":"Option 3","value":"3"}]'
            >dropdown 2
          </oc-dropdown-v1>
        </oc-form-group-v1>

        <oc-form-group-v1
          class="oc-mt-150"
          hint="Checkboxes hint."
          orientation="horizontal"
          flex-behavior="shrink"
        >
          <oc-checkbox-v1 name="checkbox" value="checked1">checkbox 1</oc-checkbox-v1>
          <oc-checkbox-v1 name="checkbox" value="checked2">checkbox 2</oc-checkbox-v1>
        </oc-form-group-v1>

        <oc-form-group-v1
          class="oc-mt-150"
          hint="Radio buttons hint."
          orientation="horizontal"
          flex-behavior="shrink"
        >
          <oc-radio-button-v1 name="radio" value="checked1">radio button 1</oc-radio-button-v1>
          <oc-radio-button-v1 name="radio" value="checked2">radio button 2</oc-radio-button-v1>
        </oc-form-group-v1>

        <div class="oc-mt-150">
          <oc-button-v1 type="reset" fit-content variant="secondary">Reset</oc-button-v1>
          <oc-button-v1 fit-content id="mark-invalid">Mark Invalid</oc-button-v1>
          <oc-button-v1 type="submit" fit-content>Submit</oc-button-v1>
        </div>
      </form>

      <style>
        .demo-class {
          --gap: var(--oc-base-dimension-8);
        }
      </style>

      <script>
        (() => {
          const [textField] = document.getElementsByTagName("oc-text-field-v1");
          const [textArea] = document.getElementsByTagName("oc-text-area-v1");
          const [dropdown] = document.getElementsByTagName("oc-dropdown-v1");
          const formGroups = document.getElementsByTagName("oc-form-group-v1");

          const [form] = document.getElementsByTagName("form");
          const [reset, markInvalid] = document.getElementsByTagName("oc-button-v1");

          markInvalid.addEventListener("click", () => {
            textField.validationMessage = "This field is invalid";
            textArea.validationMessage = "This field is invalid";
            dropdown.validationMessage = "This field is invalid";
            for (const formGroup of formGroups) {
              formGroup.validationMessage = "These fields are invalid";
            }
          });

          reset.addEventListener("click", () => {
            textField.validationMessage = undefined;
            textArea.validationMessage = undefined;
            dropdown.validationMessage = undefined;
            for (const formGroup of formGroups) {
              formGroup.validationMessage = undefined;
            }
          });

          form.addEventListener("submit", (ev) => {
            let formData = new FormData(form);
            let data = "";
            formData.forEach((value, key) => (data += key + "=" + value + "\\n"));
            console.log(data);
            alert("sending:\\n" + data);
            ev.preventDefault();
          });
        })();
      </script>
    `;
  }
}

Interaction tests (FormGroupV1.interactions.stories.ts)

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

Should Pass Details To Children

Story: components-form-group-interaction-tests--should-pass-details-to-children · tags: play-fn

text field 1 text field 2 text area 1 text area 2 dropdown 1 dropdown 2 checkbox 1 checkbox 2 radio button 1 radio button 2
HTML
<oc-form-group-v1
class="oc-mt-150"
hint="my hint"
orientation="horizontal"
flex-behavior="grow"
>
<oc-text-field-v1 name="field1">text field 1</oc-text-field-v1>
<oc-text-field-v1 name="field2">text field 2</oc-text-field-v1>
</oc-form-group-v1>
<oc-form-group-v1
class="oc-mt-150"
hint="my hint"
orientation="horizontal"
flex-behavior="grow"
>
<oc-text-area-v1 name="area1">text area 1</oc-text-area-v1>
<oc-text-area-v1 name="area2">text area 2</oc-text-area-v1>
</oc-form-group-v1>
<oc-form-group-v1
class="oc-mt-150"
hint="my hint"
orientation="horizontal"
flex-behavior="grow"
>
<oc-dropdown-v1
  label="Options"
  name="dropdown1"
  options='[{"label":"Option 1","value":"1"},{"label":"Option 2","value":"2"},{"label":"Option 3","value":"3"}]'
  >dropdown 1
</oc-dropdown-v1>
<oc-dropdown-v1
  label="Options"
  name="dropdown2"
  options='[{"label":"Option 1","value":"1"},{"label":"Option 2","value":"2"},{"label":"Option 3","value":"3"}]'
  >dropdown 2
</oc-dropdown-v1>
</oc-form-group-v1>
<oc-form-group-v1
class="oc-mt-150"
hint="my hint"
orientation="horizontal"
flex-behavior="shrink"
>
<oc-checkbox-v1 name="checkbox" value="checked1">checkbox 1</oc-checkbox-v1>
<oc-checkbox-v1 name="checkbox" value="checked2">checkbox 2</oc-checkbox-v1>
</oc-form-group-v1>
<oc-form-group-v1
class="oc-mt-150"
hint="my hint"
orientation="horizontal"
flex-behavior="shrink"
>
<oc-radio-button-v1 name="radio" value="checked1">radio button 1</oc-radio-button-v1>
<oc-radio-button-v1 name="radio" value="checked2">radio button 2</oc-radio-button-v1>
</oc-form-group-v1>
Story source (TypeScript, verbatim from Storybook)
{
  render: () => {
    return html`
      <oc-form-group-v1
        class="oc-mt-150"
        hint="my hint"
        orientation="horizontal"
        flex-behavior="grow"
      >
        <oc-text-field-v1 name="field1">text field 1</oc-text-field-v1>
        <oc-text-field-v1 name="field2">text field 2</oc-text-field-v1>
      </oc-form-group-v1>

      <oc-form-group-v1
        class="oc-mt-150"
        hint="my hint"
        orientation="horizontal"
        flex-behavior="grow"
      >
        <oc-text-area-v1 name="area1">text area 1</oc-text-area-v1>
        <oc-text-area-v1 name="area2">text area 2</oc-text-area-v1>
      </oc-form-group-v1>

      <oc-form-group-v1
        class="oc-mt-150"
        hint="my hint"
        orientation="horizontal"
        flex-behavior="grow"
      >
        <oc-dropdown-v1
          label="Options"
          name="dropdown1"
          options='[{"label":"Option 1","value":"1"},{"label":"Option 2","value":"2"},{"label":"Option 3","value":"3"}]'
          >dropdown 1
        </oc-dropdown-v1>
        <oc-dropdown-v1
          label="Options"
          name="dropdown2"
          options='[{"label":"Option 1","value":"1"},{"label":"Option 2","value":"2"},{"label":"Option 3","value":"3"}]'
          >dropdown 2
        </oc-dropdown-v1>
      </oc-form-group-v1>

      <oc-form-group-v1
        class="oc-mt-150"
        hint="my hint"
        orientation="horizontal"
        flex-behavior="shrink"
      >
        <oc-checkbox-v1 name="checkbox" value="checked1">checkbox 1</oc-checkbox-v1>
        <oc-checkbox-v1 name="checkbox" value="checked2">checkbox 2</oc-checkbox-v1>
      </oc-form-group-v1>

      <oc-form-group-v1
        class="oc-mt-150"
        hint="my hint"
        orientation="horizontal"
        flex-behavior="shrink"
      >
        <oc-radio-button-v1 name="radio" value="checked1">radio button 1</oc-radio-button-v1>
        <oc-radio-button-v1 name="radio" value="checked2">radio button 2</oc-radio-button-v1>
      </oc-form-group-v1>
    `;
  },
  play: async ({
    canvasElement
  }) => {
    const textFields = Array.from(canvasElement.getElementsByTagName("oc-text-field-v1"));
    const textAreas = Array.from(canvasElement.getElementsByTagName("oc-text-area-v1"));
    const dropdowns = Array.from(canvasElement.getElementsByTagName("oc-dropdown-v1"));
    const checkboxes = Array.from(canvasElement.getElementsByTagName("oc-checkbox-v1"));
    const radioButtons = Array.from(canvasElement.getElementsByTagName("oc-radio-button-v1"));
    await waitFor(() => [...textFields, ...textAreas, ...dropdowns].flatMap(element => [
    // @ts-expect-error -- internally defined property
    expect(element.hideDetails, "should not show details").toBeTruthy(), expect(element.hint, "should have a hint").toBe("my hint")]));
    await waitFor(() => [...textFields, ...textAreas, ...dropdowns, ...checkboxes, ...radioButtons].map(element =>
    // @ts-expect-error -- internally defined property
    expect(element.validationMessage, "should not have validation message").toBeUndefined()));
    Array.from(canvasElement.getElementsByTagName("oc-form-group-v1")).forEach(formGroup => {
      formGroup.setAttribute("validation-message", "This is a validation message");
      formGroup.setAttribute("hint", "This is a hint");
    });
    await tick();
    await waitFor(() => [...textFields, ...textAreas, ...dropdowns].flatMap(element => [
    // @ts-expect-error -- internally defined property
    expect(element.hideDetails, "still should not show details").toBeTruthy(), expect(element.hint, "should have new hint").toBe("This is a hint")]));
    await waitFor(() => [...textFields, ...textAreas, ...dropdowns, ...checkboxes, ...radioButtons].map(element =>
    // @ts-expect-error -- internally defined property
    expect(element.validationMessage, "should have correct validation message").toBe("This is a validation message")));
  }
}

Should Handle Async Data

Story: components-form-group-interaction-tests--should-handle-async-data · tags: play-fn

text field 1
HTML
<oc-form-group-v1 orientation="horizontal" flex-behavior="grow">
<oc-text-field-v1 name="field1">text field 1</oc-text-field-v1>
</oc-form-group-v1>
Story source (TypeScript, verbatim from Storybook)
{
  render: () => {
    return html`
      <oc-form-group-v1 orientation="horizontal" flex-behavior="grow">
        <oc-text-field-v1 name="field1">text field 1</oc-text-field-v1>
      </oc-form-group-v1>
    `;
  },
  play: async ({
    canvasElement
  }) => {
    const formGroup = canvasElement.getElementsByTagName("oc-form-group-v1")[0];
    const textField1 = canvasElement.getElementsByTagName("oc-text-field-v1")[0];
    formGroup.hint = "This is a hint";
    await tick();
    await waitFor(() => [
    // @ts-expect-error -- internally defined property
    expect(textField1.hideDetails, "textField1.hideDetail").toBeTruthy(), expect(textField1.validationMessage, "textField1.validationMessage").toBeUndefined(), expect(textField1.hint, "textField1.hint").toBe("This is a hint")]);
    await tick();
    const textField2 = document.createElement("oc-text-field-v1");
    textField2.innerText = "text field 2";
    formGroup.appendChild(textField2);
    await waitFor(() => [
    // @ts-expect-error -- internally defined property
    expect(textField2.hideDetails, "textField2.hideDetails").toBeTruthy(), expect(textField2.validationMessage, "textField2.validationMessage").toBeUndefined(), expect(textField2.hint, "textField2.hint").toBe("This is a hint")]);
    formGroup.validationMessage = "This is a validation message";
    await tick();
    const textField3 = document.createElement("oc-text-field-v1");
    textField3.innerText = "text field 3";
    formGroup.appendChild(textField3);
    await waitFor(() => [
    // @ts-expect-error -- internally defined property
    expect(textField1.hideDetails, "textField1.hideDetails").toBeTruthy(), expect(textField1.validationMessage, "textField1.validationMessage").toBe("This is a validation message"), expect(textField1.hint, "textField1.hint").toBe("This is a hint"),
    // @ts-expect-error -- internally defined property
    expect(textField2.hideDetails, "textField2.hideDetails").toBeTruthy(), expect(textField2.validationMessage, "textField2.validationMessage").toBe("This is a validation message"), expect(textField2.hint, "textField2.hint").toBe("This is a hint"),
    // @ts-expect-error -- internally defined property
    expect(textField3.hideDetails, "textField3.hideDetails").toBeTruthy(), expect(textField3.validationMessage, "textField3.validationMessage").toBe("This is a validation message"), expect(textField3.hint, "textField3.hint").toBe("This is a hint")]);
  }
}