OTTODesign System

Components

Button

Buttons are elements to trigger most UI interactions, such as submitting forms or opening sheets.

Buttons are used for most interactions in our UI.

Configurator

LiveButton: change the settings; the markup below follows.
In den Warenkorb
HTML
<oc-button-v1 variant="primary">In den Warenkorb</oc-button-v1>

Usage

Anatomy

Label 1 2 3 4
  1. Icon left (optional)
  2. Label
  3. Icon right (optional)
  4. Container
LiveAnnotations
HTML
<div class="anatomy" style="width:420px">
<oc-button-v1 variant="primary" icon-type-left="smiley-positive" icon-type-right="arrow-right">Label</oc-button-v1>
<span class="anatomy-pin" style="left:8%;top:0%">1</span>
<span class="anatomy-pin" style="left:50%;top:0%">2</span>
<span class="anatomy-pin" style="left:92%;top:0%">3</span>
<span class="anatomy-pin" style="left:100%;top:100%">4</span>
<ol class="anatomy-key"><li data-n="1">Icon left (optional)</li><li data-n="2">Label</li><li data-n="3">Icon right (optional)</li><li data-n="4">Container</li></ol>

Variants

The button come in two sizes: 50 and 100. Use button 100, the larger size, as the default. Only use button 50 in areas with limited space because it provides an suboptimal hitbox size.

The button is available in the following variants:

  • Primary: Use for the most important action on a page. Use only once per view.
  • Secondary: Use for less important actions.
  • Secondary-over-color: Use for less important actions on frame, stacked or colored backgrounds.
  • Tertiary: Has the least visual attention and functions like links but are styled as buttons.
  • Success: Use to indicate a positive outcome of an action. The success button is displayed as a variant because it has its own states.
  • Custom-color-soft: Use semantic design tokens with the prefix "$oc-semantic-color-background-soft-" for the background and use semantic design tokens with sufficient color contrast for the text. Please check with the design system team before doing so.
  • Custom-color-strong: Use semantic design tokens with the prefix "$oc-semantic-color-background-strong-" for the background and use semantic design tokens with sufficient color contrast for the text. Please check with the design system team before doing so.

primary, secondary

LabelLabel
LabelLabel

secondary-over-color, on frame

LabelLabel

tertiary

LabelLabel

success (from primary and from secondary)

LabelLabel
LabelLabel

custom-color-strong, custom-color-soft

LabelLabel
LabelLabel
Livevariants and sizes
HTML
<p class="demo-label">primary, secondary</p>
<oc-button-v1 variant="primary" icon-type-left="smiley-positive" icon-type-right="arrow-right">Label</oc-button-v1><oc-button-v1 variant="primary" size="50" icon-type-left="smiley-positive" icon-type-right="arrow-right" style="flex:none; flex:none">Label</oc-button-v1></div>
<oc-button-v1 variant="secondary" icon-type-left="smiley-positive" icon-type-right="arrow-right">Label</oc-button-v1><oc-button-v1 variant="secondary" size="50" icon-type-left="smiley-positive" icon-type-right="arrow-right" style="flex:none; flex:none">Label</oc-button-v1></div>
<p class="demo-label">secondary-over-color, on frame</p>
<oc-button-v1 variant="secondary-over-color" icon-type-left="smiley-positive" icon-type-right="arrow-right">Label</oc-button-v1><oc-button-v1 variant="secondary-over-color" size="50" icon-type-left="smiley-positive" icon-type-right="arrow-right" style="flex:none; flex:none">Label</oc-button-v1></div>
<p class="demo-label">tertiary</p>
<oc-button-v1 variant="tertiary" icon-type-left="smiley-positive" icon-type-right="arrow-right">Label</oc-button-v1><oc-button-v1 variant="tertiary" size="50" icon-type-left="smiley-positive" icon-type-right="arrow-right" style="flex:none; flex:none">Label</oc-button-v1></div>
<p class="demo-label">success (from primary and from secondary)</p>
<oc-button-v1 variant="primary" success icon-type-left="check" icon-type-right="arrow-right">Label</oc-button-v1><oc-button-v1 variant="primary" size="50" success icon-type-left="check" icon-type-right="arrow-right" style="flex:none">Label</oc-button-v1></div>
<oc-button-v1 variant="secondary" success icon-type-left="check" icon-type-right="arrow-right">Label</oc-button-v1><oc-button-v1 variant="secondary" size="50" success icon-type-left="check" icon-type-right="arrow-right" style="flex:none">Label</oc-button-v1></div>
<p class="demo-label">custom-color-strong, custom-color-soft</p>
<oc-button-v1 variant="custom-color-strong" icon-type-left="smiley-positive" icon-type-right="arrow-right" style="--background-color: var(--oc-semantic-color-background-strong-mint); --text-color: var(--oc-semantic-color-background-soft-mint)">Label</oc-button-v1><oc-button-v1 variant="custom-color-strong" size="50" icon-type-left="smiley-positive" icon-type-right="arrow-right" style="flex:none; --background-color: var(--oc-semantic-color-background-strong-mint); --text-color: var(--oc-semantic-color-background-soft-mint)">Label</oc-button-v1></div>
<oc-button-v1 variant="custom-color-soft" icon-type-left="smiley-positive" icon-type-right="arrow-right" style="--background-color: var(--oc-semantic-color-background-soft-mint); --text-color: var(--oc-semantic-color-text-default)">Label</oc-button-v1><oc-button-v1 variant="custom-color-soft" size="50" icon-type-left="smiley-positive" icon-type-right="arrow-right" style="flex:none; --background-color: var(--oc-semantic-color-background-soft-mint); --text-color: var(--oc-semantic-color-text-default)">Label</oc-button-v1></div>

Icons are optional and can be placed on either the left or right side of the button. The decorative icon on the left reduces cognitive load by symbolizing the button’s action. The icon on the right serve as an affordance for interaction, such as an arrow pointing forward for navigation.

Loading button

Buttons can be put into a loading state. This replaces the icons and the label with a centered spinner. This feature should be used for actions expected to complete asynchronously. During loading, the button has no interactive states and is not interactive.

Size 100: before and while loading

Label Label Label Label

Size 50: before and while loading

Label
Label
Label
Label
LiveLoading button
HTML
<p class="demo-label">Size 100: before and while loading</p>
<oc-button-v1 variant="primary" fit-content icon-type-left="smiley-positive" icon-type-right="arrow-right">Label</oc-button-v1>
<oc-button-v1 variant="primary" fit-content loading icon-type-left="smiley-positive" icon-type-right="arrow-right">Label</oc-button-v1>
<oc-button-v1 variant="secondary" fit-content icon-type-left="smiley-positive" icon-type-right="arrow-right">Label</oc-button-v1>
<oc-button-v1 variant="secondary" fit-content loading icon-type-left="smiley-positive" icon-type-right="arrow-right">Label</oc-button-v1>
<p class="demo-label">Size 50: before and while loading</p>
<div><oc-button-v1 variant="primary" size="50" icon-type-left="smiley-positive" icon-type-right="arrow-right" style="flex:none; flex:none">Label</oc-button-v1></div>
<div><oc-button-v1 variant="primary" size="50" loading icon-type-left="smiley-positive" icon-type-right="arrow-right" style="flex:none">Label</oc-button-v1></div>
<div><oc-button-v1 variant="secondary" size="50" icon-type-left="smiley-positive" icon-type-right="arrow-right" style="flex:none; flex:none">Label</oc-button-v1></div>
<div><oc-button-v1 variant="secondary" size="50" loading icon-type-left="smiley-positive" icon-type-right="arrow-right" style="flex:none">Label</oc-button-v1></div>

Behavior

States

Enabled (hover, press and tab to it)

Label Label
Label
Label

Disabled

Label Label
Label
Label
LiveStates
HTML
<p class="demo-label">Enabled (hover, press and tab to it)</p>
<oc-button-v1 variant="primary" fit-content icon-type-left="smiley-positive" icon-type-right="arrow-right">Label</oc-button-v1>
<oc-button-v1 variant="secondary" fit-content icon-type-left="smiley-positive" icon-type-right="arrow-right">Label</oc-button-v1>
<div class="on-frame" style="margin:0"><oc-button-v1 variant="secondary-over-color" fit-content icon-type-left="smiley-positive" icon-type-right="arrow-right">Label</oc-button-v1></div>
<oc-button-v1 variant="tertiary" fit-content icon-type-left="smiley-positive" icon-type-right="arrow-right">Label</oc-button-v1>
<p class="demo-label">Disabled</p>
<oc-button-v1 variant="primary" fit-content disabled icon-type-left="smiley-positive" icon-type-right="arrow-right">Label</oc-button-v1>
<oc-button-v1 variant="secondary" fit-content disabled icon-type-left="smiley-positive" icon-type-right="arrow-right">Label</oc-button-v1>
<div class="on-frame" style="margin:0"><oc-button-v1 variant="secondary-over-color" fit-content disabled icon-type-left="smiley-positive" icon-type-right="arrow-right">Label</oc-button-v1></div>
<oc-button-v1 variant="tertiary" fit-content disabled icon-type-left="smiley-positive" icon-type-right="arrow-right">Label</oc-button-v1>

Success states

Enabled (hover, press and tab to it)

Label

Disabled

Label
LiveSuccess button states
HTML
<p class="demo-label">Enabled (hover, press and tab to it)</p>
<oc-button-v1 variant="primary" fit-content success icon-type-left="check">Label</oc-button-v1>
<p class="demo-label">Disabled</p>
<oc-button-v1 variant="primary" fit-content success disabled icon-type-left="check">Label</oc-button-v1>

Success interaction

You can start a positive action with a primary, secondary or tertiary button. Once the positive action is completed, the button changes to the success variant. Make sure the size (100, 50) and fitting of the success button match the preceding button.

Only use the success variant in combination with a check icon, as there should be redundant cues for the state.

Folgen

Klick oder Tipp löst die positive Aktion aus

Folge ich

Der Button zeigt das positive Ergebnis

LiveSuccess button interaction
HTML
<oc-button-v1 variant="secondary" size="50" icon-type-left="plus" style="flex:none">Folgen</oc-button-v1>
<oc-icon-v1 type="arrow-long-down" style="color:#e5006b"></oc-icon-v1>
<oc-button-v1 variant="secondary" size="50" success icon-type-left="check" style="flex:none">Folge ich</oc-button-v1>
FolgenFolge ich
FolgenFolge ich
FolgenFolge ich
LiveSuccess button match
HTML
<oc-button-v1 variant="primary" fit-content icon-type-left="smiley-positive">Folgen</oc-button-v1><oc-icon-v1 type="arrow-long-right" style="color:#e5006b"></oc-icon-v1><oc-button-v1 variant="primary" fit-content success icon-type-left="check">Folge ich</oc-button-v1></div>
<oc-button-v1 variant="secondary" size="50" icon-type-left="plus" style="flex:none">Folgen</oc-button-v1><oc-icon-v1 type="arrow-long-right" style="color:#e5006b"></oc-icon-v1><oc-button-v1 variant="secondary" size="50" success icon-type-left="check" style="flex:none">Folge ich</oc-button-v1></div>
<oc-button-v1 variant="tertiary" size="50" icon-type-left="plus" style="flex:none">Folgen</oc-button-v1><oc-icon-v1 type="arrow-long-right" style="color:#e5006b"></oc-icon-v1><oc-button-v1 variant="tertiary" size="50" success icon-type-left="check" style="flex:none">Folge ich</oc-button-v1></div>

According to your needs, you can choose whether the positive action can be undone. If you do not want the positive action to be reversible, use the disabled state of the Success Button.

Reversible

Folge ich

Klick oder Tipp auf den Success-Button macht die Aktion rückgängig

Folgen

Not reversible

Bewertung gesendet

Das positive Ergebnis lässt sich nicht rückgängig machen

LiveSuccess button disabled
HTML
<p class="demo-label">Reversible</p>
<oc-button-v1 variant="secondary" size="50" success icon-type-left="check" style="flex:none">Folge ich</oc-button-v1>
<oc-icon-v1 type="arrow-long-down" style="color:#e5006b"></oc-icon-v1>
<oc-button-v1 variant="secondary" size="50" icon-type-left="plus" style="flex:none">Folgen</oc-button-v1>
<p class="demo-label">Not reversible</p>
<oc-button-v1 variant="secondary" size="50" success disabled icon-type-left="check" style="flex:none">Bewertung gesendet</oc-button-v1>

Fitting

The horizontal fitting of the button can behave in two different ways: fill-parent and fit-content. The default setting is fit-content. In contrast, the fill-parent button extends across the entire parent element, with the label centered. Generally, if the label exceeds the button's width, it will be truncated, which is best avoided.

The vertical fitting of the button is fit-content.

Size 100, fit-content

Label

Size 100, fill parent (default)

Label

Size 50, fit-content (default)

Label

Size 50, fill-parent

Label
Livefill-parent and fit-content
HTML
<p class="demo-label">Size 100, fit-content</p>
<oc-button-v1 variant="primary" fit-content icon-type-left="smiley-positive" icon-type-right="arrow-right">Label</oc-button-v1>
<p class="demo-label">Size 100, fill parent (default)</p>
<oc-button-v1 variant="primary" icon-type-left="smiley-positive" icon-type-right="arrow-right">Label</oc-button-v1>
<p class="demo-label">Size 50, fit-content (default)</p>
<oc-button-v1 variant="primary" size="50" icon-type-left="smiley-positive" icon-type-right="arrow-right" style="flex:none; flex:none">Label</oc-button-v1>
<p class="demo-label">Size 50, fill-parent</p>
<oc-button-v1 variant="primary" size="50" fill-parent icon-type-left="smiley-positive" icon-type-right="arrow-right">Label</oc-button-v1>

Placement

When placing multiple buttons horizontal or vertical to each other, make sure there is a gap of 8px in-between. When placing buttons horizontal to each other, the primary button should be on the right. When placing buttons vertical to each other, the primary button should be on top. Always make sure that there are at least 8 pixels in between buttons.

Abbrechen Speichern
Live8px horizontal gap
HTML
<oc-button-v1 variant="secondary" fit-content>Abbrechen</oc-button-v1>
<oc-button-v1 variant="primary" fit-content>Speichern</oc-button-v1>
Speichern Abbrechen
Live8px vertical gap
HTML
<oc-button-v1 variant="primary">Speichern</oc-button-v1>
<oc-button-v1 variant="secondary">Abbrechen</oc-button-v1>

Best practices

AbbrechenSpeichern
Zur KasseWeiter einkaufen
DoPlace one primary button per view.
AbbrechenSpeichern
Zur KasseWeiter einkaufen
Don'tPlace more than one primary button per view.
Rücksendung anmelden Bestellung ansehen
DoShow the full text on buttons. If necessary, they should be stacked when they cannot be placed side by side.
Rücksendung anmelden Bestellung ansehen
Don'tTruncate the button text.

Sneaker „Mojo“ aus dem neuen Fashion-Drop, jetzt mit Extra-Rabatt.

-12 %-20 % Extra
MerkenIn den Warenkorb
DoUse few buttons for only the relevant actions.

Sneaker „Mojo“ aus dem neuen Fashion-Drop, jetzt mit Extra-Rabatt.

-12 %-20 % Extra
TeilenVergleichen
MerkenIn den Warenkorb
Größe wählenBewertungen
Don'tDon't clutter your UI with too many buttons.
Speichern Abbrechen
DoUse the colors assigned by the design system.
Speichern Abbrechen
Don'tDon't use custom colors for buttons. The colors of different button variations have been designed to be consistent and accessible.

Rücksendung

Du hast 30 Tage Zeit, deine Artikel kostenlos zurückzuschicken. Melde die Rücksendung in deinem Kundenkonto an, druck das Etikett aus und gib das Paket bei Hermes ab. Sobald es bei uns ist, bekommst du dein Geld zurück.

Rücksendung anmeldenBestellungen ansehen
DoPlace buttons inside a block.

Rücksendung

Du hast 30 Tage Zeit, deine Artikel kostenlos zurückzuschicken. Melde die Rücksendung in deinem Kundenkonto an, druck das Etikett aus und gib das Paket bei Hermes ab. Sobald es bei uns ist, bekommst du dein Geld zurück. Bei Speditionsware holen wir die Ware bei dir ab: Vereinbare dafür einfach einen Termin. Artikel, die du im Laden gekauft hast, kannst du auch dort zurückgeben. Bitte leg den Lieferschein bei, damit wir die Rücksendung schnell zuordnen können.

Rücksendung anmelden
CautionButtons can be used as sticky elements with a shadow300.
Folgen
DoOnly use the secondary-over-color variant on frame, stacked or colored background.
Folgen
Don'tUse the secondary-over-color button on a white or canvas background.
Folgen Folge ich
DoChange the label to a positive outcome when using the success button.
Folgen Folgen
Don'tUse the same label for both the positive action and the positive outcome.
Folgen Folge ich
CautionMake sure to use the success button sparingly and only to display specific positive outcomes, such as "Marke folgen".
In den Warenkorb
DoUse decorative icons for the left icon and affordance icons for the right icon.
In den Warenkorb
Don'tUse decorative icons for the right icon and affordance icons for the left icon.

Content guidelines

Do1. Use simple, clear language.
  1. Keep the label concise, under 4 words and fewer than 20 characters, including spaces.

  2. Capitalize the first letter of the first word and proper nouns in the button label text.

  3. Use a verb that describes the action, such as "Zum Warenkorb hinzufügen".

  4. Contextualize the action the user is taking.

  5. Maintain consistent wording for the same button across different views.

  6. Avoid punctuation marks such as periods or exclamation points.

Don't1. Use ambiguous wording or overly creative content, as they may not be accessible.
  1. Include polite expressions like "please" and "thank you."

  2. Use non-specific phrases like "learn more" or "click here."

  3. Use emojis or exclamation points, as they aren’t appropriate for the functional nature of buttons.

  4. Write labels as nouns or adjectives, which can be unclear and disorienting.

Accessibility

When writing accessible button labels, it's crucial to ensure that it provides sufficient context about the button's behavior. If a label like "Weiter" or "Mehr anzeigen" is used, it can be confusing for screen reader users when read out of context. In such cases, it's necessary to provide an alternative text with deeper context, such as "Weiter zur Kasse" or "Mehr ähnliche Produkte anzeigen," using the aria-label attribute. This attribute helps to describe the button's function fully, considering the rest of the website content is not visible to the screen reader.

For information on accessibility, refer to the technical documentation.

Status

  • Icon Button: Rounded Buttons consist of a single icon on a white circle
  • Link: A link allows users to navigate through applications and websites.
  • Skip Link

Implementation

Live demo

Weißer Ledersneaker mit Gummisohle

Mojo

Ledersneaker „Court“, weiß

Glattleder, Gummisohle in Honigbraun

89,99 €

statt 112,99 €

-20 %

Lieferbar · in 2 bis 4 Werktagen bei dir

In den WarenkorbGemerkt

Kostenlose Rücksendung innerhalb von 30 Tagen

LiveReal OTTO components, rendered by the OTTO component runtime
HTML
<div class="on-frame" style="margin:0;padding:24px">
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(16rem,1fr));gap:16px;max-width:880px;margin:0 auto;align-items:start">
<div style="background:#fff;border-radius:16px;overflow:hidden"><img src="/previews/imagery/samples/otto-product-still/product-sneaker.webp" alt="Weißer Ledersneaker mit Gummisohle" style="display:block;margin:0;width:100%;aspect-ratio:1;object-fit:cover"></div>
<oc-block-v2><div style="display:grid;gap:16px">
  <div style="display:grid;gap:4px"><p class="oc-copy-75 oc-text-color-secondary">Mojo</p><p class="oc-headline-100">Ledersneaker „Court“, weiß</p><p class="oc-copy-75 oc-text-color-secondary">Glattleder, Gummisohle in Honigbraun</p></div>
<p class="oc-headline-200 oc-text-color-sale">89,99 €</p><p class="oc-copy-75 oc-text-color-secondary">statt <s>112,99 €</s></p><oc-tag-v1 variant="sale" size="50">-20 %</oc-tag-v1></div>
  <p class="oc-copy-75"><span class="oc-text-color-success" style="font-weight:700">Lieferbar</span> · in 2 bis 4 Werktagen bei dir</p>
  <div style="display:grid;gap:8px"><oc-button-v1 variant="primary" icon-type-left="basket">In den Warenkorb</oc-button-v1><oc-button-v1 variant="secondary" success icon-type-left="check">Gemerkt</oc-button-v1></div>
  <p class="oc-copy-75 oc-text-color-secondary">Kostenlose Rücksendung innerhalb von 30 Tagen</p>
</oc-block-v2>

Code

Version Tag Status API
v1 <oc-button-v1> Stable, allowed for generation ButtonV1

Overview (v1)

Button

The button component is used to trigger specific actions in your application. It supports various styles, sizes, and can incorporate icons. It can be configured to fit content, be disabled, and set with an ARIA label for accessibility.

Default variation
Label
HTML
<oc-button-v1 variant="primary" size="100">Label</oc-button-v1>
Configuration

The button component is available in the main styling variants primary, secondary, and tertiary. 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 button 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 Button UX documentation for detailed user experience guidelines.

Accessibility

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

Keyboard navigation
Shortcut Description
Space / Enter Activates the button

Important

If the button component or a normal HTML button with the type submit is used in a form, pressing Enter on any of the following form elements triggers the submit action immediately (implicit submit): oc-checkbox, oc-radio-button, oc-switch-v2, oc-selection-tile, and oc-text-field.

Use oc-aria-label

To make the button 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.

Multiple buttons

When multiple buttons with the same visible label are present, ensure each button is uniquely labeled so users understand their purpose.

See an example here.

Buttons with icons

The icon inside the button component has the state aria-hidden="true" , which hides the icon from assistive technologies and prevents it from being read out loud by screen readers.

Further reading

Configuration (v1)

Button configuration

Configure the button 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.

Label
HTML
<oc-button-v1 variant="primary" size="100">Label</oc-button-v1>

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

API v1

Button v1 API

API: <oc-button-v1> (ButtonV1)

The Button component is used to trigger specific actions in your application. It supports various styles, sizes, and can incorporate icons. It can be configured to fit content, be disabled, and set with an ARIA label for accessibility.

Attributes / properties
Attribute Type Default Required Description
variant "primary" | "secondary" | "secondary-on-color" | "secondary-over-color" | "tertiary" | "custom-color-strong" | "custom-color-soft" "primary" no Sets the main styling variant of the button.

Note: "secondary-on-color" is deprecated, use "secondary-over-color" instead.
type "submit" | "reset" | "button" undefined no Sets the type of the button. Can be one of: "button", "reset", "submit".
size "50" | "100" "100" no Sets the size of the button.
icon-type-left icon name (428 values; see Icon list in `storybook/components/icon/README.md`) undefined no Sets the icon for the left side of the button.
icon-type-right icon name (428 values; see Icon list in `storybook/components/icon/README.md`) undefined no Sets the icon for the right side of the button.
disabled boolean false no Indicates whether the button is disabled.
loading "" | "keep-native-events" | "true" false no Indicates whether the button is in a loading state. When enabled, displays a loading animation.

For buttons of type "submit", the loading state also stops the propagation of click events after the first click to prevent multiple form submissions. Set to "keep-native-events" to enable the loading state but keep native events for this case.

Note: The loading animation overwrites the icon on the right side set via icon-type-right.
success boolean false no Indicates whether the button is in a success state. This is a visual state that indicates a successful action. It does not change the functionality of the button.
fit-content boolean false no Indicates whether the button width should fit its content. This attribute only applies to size="100".
fill-parent boolean false no Indicates whether the button width should fill to its parent. This attribute only applies to size="50".
form string undefined no Sets the form id the button belongs to. Only available as an attribute.
formaction string undefined no Sets the action for form submission. Only available as an attribute. Only for type="submit".
formmethod string undefined no Sets the method for form submission. Only available as an attribute. Only for type="submit".
href string undefined no Deprecated: Use an a tag in the default slot instead.

Visit SEO optimization techniques for more information.

Sets the link target of the button.
base64-href string undefined no Sets the base64 encoded link target of the button. Use this attribute to prevent search engines from indexing the link target.
target "_blank" | "_self" | "_parent" | "_top" undefined no Sets the target attribute of the button.

Note: Only applies when href or base64Href is set.
rel string undefined no Sets the rel attribute of the button.

Note: Only applies when href or base64Href is set.
oc-aria-label string undefined no Sets the ARIA label of the button.
Slots
Slot Required Description
default yes Sets the text content of the button.
Events
Event Detail type Description
click PointerEvent Clicking the button or pressing Enter or Space while the button is focused triggers the click event.
oc-property-change OcButtonV1Events["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
--background-color undefined Sets a custom background color through a CSS variable.

Note: The preferred way of using colors is via design tokens instead of hex values.

Note: This CSS variable only applies when using variant="custom-color-strong" or variant="custom-color-soft".
--text-color undefined Sets a custom text color through a CSS variable.

Note: The preferred way of using colors is via design tokens instead of hex values.

Note: This CSS variable only applies when using variant="custom-color-strong" or variant="custom-color-soft".

Variations (v1)

Variations

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

The default configuration uses size=100 and the primary variant.

Label
HTML
<oc-button-v1 variant="primary" size="100">Label</oc-button-v1>
Primary 100 with icons left and right

Story: components-button-variations--button-100-primary-with-icons-left-right · tags: components, button, v1

The primary variant, with size=100, and icons on both the left and right sides.

Args: icon-type-left=arrow-left, icon-type-right=arrow-right

Label
HTML
<oc-button-v1 variant="primary" size="100" icon-type-left="arrow-left" icon-type-right="arrow-right">
Label
</oc-button-v1>
Primary 100 with fit-content and icon left

Story: components-button-variations--button-100-primary-with-icon-left-fit-content · tags: components, button, v1

The primary variant, with size=100 and fit-content=true, and an icon on the left side.

Args: icon-type-left=arrow-left, fit-content=true

Label
HTML
<oc-button-v1 variant="primary" size="100" icon-type-left="arrow-left" fit-content>Label</oc-button-v1>
Primary 100 disabled with icon right

Story: components-button-variations--button-100-primary-disabled-with-icon-right · tags: components, button, v1

The disabled primary variant, with size=100, and an icon on the right side.

Args: icon-type-right=arrow-right, disabled=true

Label
HTML
<oc-button-v1 variant="primary" size="100" icon-type-right="arrow-right" disabled>Label</oc-button-v1>
Primary 100 with loading state

Story: components-button-variations--button-100-with-loading-state · tags: components, button, v1

The primary variant, with size=100, and the setting for showing a loading spinner. Make sure to place the button as well as the placeholder / actual content in a common parent container. This way - together witha couple of aria roles -, a screen reader is able to identify the container as a status / live region that is being updated.

Args: size=100, variant=primary, defaultSlot=Label, loading=true

<div role="status" aria-live="assertive" aria-atomic="true">
  <!-- Content goes here -->
  <oc-button-v1 variant="primary" size="100" loading>Label</oc-button-v1>
</div>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Primary 100 with loading state",
  parameters: {
    // Disables Chromatic's snapshotting, as the spinner is animated und thus leads to diffs
    chromatic: {
      disableSnapshot: true,
      hideInChromatic: true
    }
  },
  args: {
    size: "100",
    variant: "primary",
    defaultSlot: "Label",
    loading: true
  },
  render(args) {
    const {
      defaultSlot,
      ...props
    } = args;
    return html` <div role="status" aria-live="assertive" aria-atomic="true">
      <!-- Content goes here -->
      <oc-button-v1 ${spread(props)}>${unsafeHTML(defaultSlot)}</oc-button-v1>
    </div>`;
  }
}
Primary 50 with icons left and right

Story: components-button-variations--button-50-primary-with-icons-left-right · tags: components, button, v1

The primary variant, with size=50, and icons on both the left and right sides.

Args: size=50, icon-type-left=arrow-left, icon-type-right=arrow-right

Label
HTML
<oc-button-v1 variant="primary" size="50" icon-type-left="arrow-left" icon-type-right="arrow-right">
Label
</oc-button-v1>
Primary 50 disabled with icon right

Story: components-button-variations--button-50-primary-disabled-with-icon-right · tags: components, button, v1

The disabled primary variant, with size=50, and an icon on the right side.

Args: size=50, icon-type-right=arrow-right, disabled=true

Label
HTML
<oc-button-v1 variant="primary" size="50" icon-type-right="arrow-right" disabled>Label</oc-button-v1>
Secondary 100

Story: components-button-variations--button-100-secondary · tags: components, button, v1

The secondary variant, with size=100.

Args: variant=secondary

Label
HTML
<oc-button-v1 variant="secondary" size="100">Label</oc-button-v1>
Secondary 50

Story: components-button-variations--button-50-secondary · tags: components, button, v1

The secondary variant, with size=50.

Args: size=50, variant=secondary

Label
HTML
<oc-button-v1 variant="secondary" size="50">Label</oc-button-v1>
Secondary-Over-Color 100

Story: components-button-variations--button-100-secondary-on-color · tags: components, button, v1

The secondary-over-color variant, with size=100.

Args: variant=secondary-over-color

Label
HTML
<oc-button-v1 variant="secondary-over-color" size="100">Label</oc-button-v1>
Tertiary 100 with icon left

Story: components-button-variations--button-100-tertiary-with-icon-left · tags: components, button, v1

The tertiary variant, with size=100, and an icon on the left side.

Args: variant=tertiary, icon-type-left=arrow-left

Label
HTML
<oc-button-v1 variant="tertiary" size="100" icon-type-left="arrow-left">Label</oc-button-v1>
Tertiary 50 with icon left

Story: components-button-variations--button-50-tertiary-with-icon-left · tags: components, button, v1

The tertiary variant, with size=50, and an icon on the left side.

Args: size=50, variant=tertiary, icon-type-left=arrow-left

Label
HTML
<oc-button-v1 variant="tertiary" size="50" icon-type-left="arrow-left">Label</oc-button-v1>

Story: components-button-variations--with-link-behavior · tags: components, button, v1

The button component as a link. Search engines will detect the href.

Args: defaultSlot=<!--The <a> tag should be placed inside the light DOM to improve SEO.--><a href='#'>Label</a>

<oc-button-v1 variant="primary" size="100">
  <!--The <a> tag should be placed inside the light DOM to improve SEO.--><a href='#'>Label</a>
</oc-button-v1>

Story: components-button-variations--with-masked-link-behavior · tags: components, button, v1

The button component as a masked link. Search engines will not detect the href.

Args: base64-href=Iw==

Label
HTML
<oc-button-v1 variant="primary" size="100" base64-href="Iw==">Label</oc-button-v1>

Story: components-button-variations--with-link-switch-behavior · tags: components, button, v1

The button component as a link with switch behavior. Search engines will detect the href, while users receive the Base64-encoded version.

Args: base64-href=Iz92YXJpYW50PWZvbw==, defaultSlot=<!--Search engines will detect the href, while users receive the Base64-encoded version.--><a href='#'>Label</a>

<oc-button-v1 variant="primary" size="100" base64-href="Iz92YXJpYW50PWZvbw==">
  <!--Search engines will detect the href, while users receive the Base64-encoded version.--><a href='#'>Label</a>
</oc-button-v1>
Demo: accessible multiple buttons

Story: components-button-variations--demo-accessible-multiple-buttons · tags: components, button, v1

Demonstration of how to make multiple buttons with the same label accessible.

  • Produkt 1 Zum Warenkorb hinzufügen
  • Produkt 2 Zum Warenkorb hinzufügen
  • Produkt 3 Zum Warenkorb hinzufügen
HTML
<ul>
<li class="oc-p-50">
  Produkt 1
  <oc-button-v1
    size="50"
    variant="primary"
    oc-aria-label="Produkt 1 zum Warenkorb hinzufügen"
    >Zum Warenkorb hinzufügen
  </oc-button-v1>
</li>
<li class="oc-p-50">
  Produkt 2
  <oc-button-v1
    size="50"
    variant="primary"
    oc-aria-label="Produkt 2 zum Warenkorb hinzufügen"
    >Zum Warenkorb hinzufügen
  </oc-button-v1>
</li>
<li class="oc-p-50">
  Produkt 3
  <oc-button-v1
    size="50"
    variant="primary"
    oc-aria-label="Produkt 2 zum Warenkorb hinzufügen"
    >Zum Warenkorb hinzufügen
  </oc-button-v1>
</li>
</ul>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Demo: accessible multiple buttons",
  parameters: {
    controls: {
      disabled: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html`
      <ul>
        <li class="oc-p-50">
          Produkt 1
          <oc-button-v1
            size="50"
            variant="primary"
            oc-aria-label="Produkt 1 zum Warenkorb hinzufügen"
            >Zum Warenkorb hinzufügen
          </oc-button-v1>
        </li>
        <li class="oc-p-50">
          Produkt 2
          <oc-button-v1
            size="50"
            variant="primary"
            oc-aria-label="Produkt 2 zum Warenkorb hinzufügen"
            >Zum Warenkorb hinzufügen
          </oc-button-v1>
        </li>
        <li class="oc-p-50">
          Produkt 3
          <oc-button-v1
            size="50"
            variant="primary"
            oc-aria-label="Produkt 2 zum Warenkorb hinzufügen"
            >Zum Warenkorb hinzufügen
          </oc-button-v1>
        </li>
      </ul>
    `;
  }
}
Demo: secondary button to success

Story: components-button-variations--demo-secondary-button-to-success · tags: components, button, v1

Demonstration of how to change a secondary button to a success button.

<oc-button-v1 id="follow" size="100" variant="secondary" fit-content oc-aria-label="Folgen">
  Folgen
</oc-button-v1>

<script>
  (() => {
    const followButton = document.getElementById("follow");
    followButton.addEventListener("click", followAction);

    function followAction() {
      followButton.success = true;
      followButton.iconTypeLeft = "check";
      followButton.innerHTML = "Gefolgt";
      followButton.ocAriaLabel = "Gefolgt";

      followButton.removeEventListener("click", followAction);
      followButton.addEventListener("click", unfollowAction);
    }

    function unfollowAction() {
      followButton.success = false;
      followButton.iconTypeLeft = undefined;
      followButton.innerHTML = "Folgen";
      followButton.ocAriaLabel = "Folgen";

      followButton.removeEventListener("click", unfollowAction);
      followButton.addEventListener("click", followAction);
    }
  })();
</script>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Demo: secondary button to success",
  parameters: {
    controls: {
      disabled: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html`
      <oc-button-v1 id="follow" size="100" variant="secondary" fit-content oc-aria-label="Folgen">
        Folgen
      </oc-button-v1>

      <script>
        (() => {
          const followButton = document.getElementById("follow");
          followButton.addEventListener("click", followAction);

          function followAction() {
            followButton.success = true;
            followButton.iconTypeLeft = "check";
            followButton.innerHTML = "Gefolgt";
            followButton.ocAriaLabel = "Gefolgt";

            followButton.removeEventListener("click", followAction);
            followButton.addEventListener("click", unfollowAction);
          }

          function unfollowAction() {
            followButton.success = false;
            followButton.iconTypeLeft = undefined;
            followButton.innerHTML = "Folgen";
            followButton.ocAriaLabel = "Folgen";

            followButton.removeEventListener("click", unfollowAction);
            followButton.addEventListener("click", followAction);
          }
        })();
      </script>
    `;
  }
}
Demo: using native button element

Story: components-button-variations--demo-using-native-button-element · tags: components, button, v1

Args: defaultSlot=<button>Label</button>

HTML
<oc-button-v1 variant="primary" size="100"><button>Label</button></oc-button-v1>
Custom color strong

Story: components-button-variations--custom-color-strong · tags: components, button, v1

Demonstration of how to use the custom-color-strong variant with custom background and text colors.

Args: variant=custom-color-strong, icon-type-left=smiley-positive, icon-type-right=arrow-right, --background-color=var(--oc-semantic-color-background-strong-pink), --text-color=var(--oc-semantic-color-background-soft-pink)

<oc-button-v1 class="${className}" variant="custom-color-strong" size="100" icon-type-left="smiley-positive" icon-type-right="arrow-right" style="--background-color: var(--oc-semantic-color-background-strong-pink); --text-color: var(--oc-semantic-color-background-soft-pink)">Label</oc-button-v1>
${css}
Story source (TypeScript, verbatim from Storybook)
{
  name: "Custom color strong",
  args: {
    variant: "custom-color-strong",
    "icon-type-left": "smiley-positive",
    "icon-type-right": "arrow-right",
    "--background-color": "var(--oc-semantic-color-background-strong-pink)",
    "--text-color": "var(--oc-semantic-color-background-soft-pink)"
  },
  render(args) {
    const {
      defaultSlot,
      ...props
    } = args;
    const {
      css,
      className
    } = cssVariablesExample(props);
    return html`
      <oc-button-v1 class="${className}" ${spread(props)}>${unsafeHTML(defaultSlot)}</oc-button-v1>
      ${css}
    `;
  }
}
Custom color soft

Story: components-button-variations--custom-color-soft · tags: components, button, v1

Demonstration of how to use the custom-color-soft variant with custom background and text colors.

Args: variant=custom-color-soft, icon-type-left=smiley-positive, icon-type-right=arrow-right, --background-color=var(--oc-semantic-color-background-soft-pink), --text-color=var(--oc-semantic-color-background-strong-pink)

<oc-button-v1 class="${className}" variant="custom-color-soft" size="100" icon-type-left="smiley-positive" icon-type-right="arrow-right" style="--background-color: var(--oc-semantic-color-background-soft-pink); --text-color: var(--oc-semantic-color-background-strong-pink)">Label</oc-button-v1>
${css}
Story source (TypeScript, verbatim from Storybook)
{
  name: "Custom color soft",
  args: {
    variant: "custom-color-soft",
    "icon-type-left": "smiley-positive",
    "icon-type-right": "arrow-right",
    "--background-color": "var(--oc-semantic-color-background-soft-pink)",
    "--text-color": "var(--oc-semantic-color-background-strong-pink)"
  },
  render(args) {
    const {
      defaultSlot,
      ...props
    } = args;
    const {
      css,
      className
    } = cssVariablesExample(props);
    return html`
      <oc-button-v1 class="${className}" ${spread(props)}>${unsafeHTML(defaultSlot)}</oc-button-v1>
      ${css}
    `;
  }
}
Demo: custom colors

Story: components-button-variations--demo-custom-colors · tags: components, button, v1

Comprehensive demo showcasing multiple buttons with custom background and text colors using design tokens. This demo demonstrates the flexibility of the custom-color-strong and custom-color-soft variants.

Label
Label
Label
Label
Label
Label
HTML
<style>
.demo-custom-colors {
  display: flex;
  flex-direction: column;
  gap: 2rem;
  padding: 2rem;
}
.demo-row {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
  align-items: center;
}
.demo-item {
  width: 100%;
  max-width: 328px;
}
</style>
    <oc-button-v1
      variant="custom-color-strong"
      icon-type-left="smiley-positive"
      icon-type-right="arrow-right"
      style="--background-color: var(--oc-semantic-color-background-strong-mint);"
    >
      Label
    </oc-button-v1>
    <oc-button-v1
      variant="custom-color-strong"
      icon-type-left="smiley-positive"
      icon-type-right="arrow-right"
      style="--background-color: var(--oc-semantic-color-background-strong-pink); --text-color: var(--oc-semantic-color-background-soft-pink)"
    >
      Label
    </oc-button-v1>
    <oc-button-v1
      variant="custom-color-strong"
      icon-type-left="smiley-positive"
      icon-type-right="arrow-right"
      style="--background-color: #522EB7"
    >
      Label
    </oc-button-v1>
    <oc-button-v1
      variant="custom-color-soft"
      icon-type-left="smiley-positive"
      icon-type-right="arrow-right"
      style="--background-color: var(--oc-semantic-color-background-soft-mint)"
    >
      Label
    </oc-button-v1>
    <oc-button-v1
      variant="custom-color-soft"
      icon-type-left="smiley-positive"
      icon-type-right="arrow-right"
      style="--background-color: var(--oc-semantic-color-background-soft-pink); --text-color: var(--oc-semantic-color-background-strong-pink)"
    >
      Label
    </oc-button-v1>
    <oc-button-v1
      variant="custom-color-soft"
      icon-type-left="smiley-positive"
      icon-type-right="arrow-right"
      style="--background-color: #E5D5FE; --text-color: #522EB7"
    >
      Label
    </oc-button-v1>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Demo: custom colors",
  parameters: {
    controls: {
      disabled: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html`
      <style>
        .demo-custom-colors {
          display: flex;
          flex-direction: column;
          gap: 2rem;
          padding: 2rem;
        }

        .demo-row {
          display: flex;
          gap: 1rem;
          flex-wrap: wrap;
          align-items: center;
        }

        .demo-item {
          width: 100%;
          max-width: 328px;
        }
      </style>

      <div class="demo-custom-colors">
        <div class="demo-row">
          <div class="demo-item">
            <oc-button-v1
              variant="custom-color-strong"
              icon-type-left="smiley-positive"
              icon-type-right="arrow-right"
              style="--background-color: var(--oc-semantic-color-background-strong-mint);"
            >
              Label
            </oc-button-v1>
          </div>

          <div class="demo-item">
            <oc-button-v1
              variant="custom-color-strong"
              icon-type-left="smiley-positive"
              icon-type-right="arrow-right"
              style="--background-color: var(--oc-semantic-color-background-strong-pink); --text-color: var(--oc-semantic-color-background-soft-pink)"
            >
              Label
            </oc-button-v1>
          </div>

          <div class="demo-item">
            <oc-button-v1
              variant="custom-color-strong"
              icon-type-left="smiley-positive"
              icon-type-right="arrow-right"
              style="--background-color: #522EB7"
            >
              Label
            </oc-button-v1>
          </div>
        </div>

        <div class="demo-row" style="margin-top: 1.5rem">
          <div class="demo-item">
            <oc-button-v1
              variant="custom-color-soft"
              icon-type-left="smiley-positive"
              icon-type-right="arrow-right"
              style="--background-color: var(--oc-semantic-color-background-soft-mint)"
            >
              Label
            </oc-button-v1>
          </div>

          <div class="demo-item">
            <oc-button-v1
              variant="custom-color-soft"
              icon-type-left="smiley-positive"
              icon-type-right="arrow-right"
              style="--background-color: var(--oc-semantic-color-background-soft-pink); --text-color: var(--oc-semantic-color-background-strong-pink)"
            >
              Label
            </oc-button-v1>
          </div>

          <div class="demo-item">
            <oc-button-v1
              variant="custom-color-soft"
              icon-type-left="smiley-positive"
              icon-type-right="arrow-right"
              style="--background-color: #E5D5FE; --text-color: #522EB7"
            >
              Label
            </oc-button-v1>
          </div>
        </div>
      </div>
    `;
  }
}

Interaction tests (ButtonV1.form.interactions.stories.ts)

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

Form Association

Story: components-button-interaction-tests--form-association · tags: play-fn

no form referenced form
within form
HTML
<div id="form-association">
<oc-button-v1 type="submit">no form</oc-button-v1>
<oc-button-v1 type="submit" form="form-association-form" formaction="/bar"
  >referenced form
</oc-button-v1>
<form id="form-association-form" method="get" action="/foo">
  <oc-button-v1 type="submit" formmethod="post">within form</oc-button-v1>
</form>
Story source (TypeScript, verbatim from Storybook)
{
  render() {
    return html` <div id="form-association">
      <oc-button-v1 type="submit">no form</oc-button-v1>
      <oc-button-v1 type="submit" form="form-association-form" formaction="/bar"
        >referenced form
      </oc-button-v1>
      <form id="form-association-form" method="get" action="/foo">
        <oc-button-v1 type="submit" formmethod="post">within form</oc-button-v1>
      </form>
    </div>`;
  },
  async play({
    canvasElement
  }) {
    const form = canvasElement.getElementsByTagName("form").item(0)!;
    const [button1, button2, button3] = Array.from(canvasElement.getElementsByTagName("oc-button-v1"));

    // @ts-expect-error -- internally defined property
    await expect(button1.internals.form).toBe(null);
    // @ts-expect-error -- internally defined property
    await expect(button2.internals.form).toBe(form);
    // @ts-expect-error -- internally defined property
    await expect(button3.internals.form).toBe(form);
  }
}

Should Not Submit When Disabled

Story: components-button-interaction-tests--should-not-submit-when-disabled · tags: play-fn

never submit me never submit me
HTML
<form id="form1" method="get" action="/foo">
<label>
  <input type="text" name="user" />
</label>
<oc-button-v1 type="submit" disabled>never submit me</oc-button-v1>
<oc-button-v1 type="submit" disabled formaction="/trigger-me"
  >never submit me
</oc-button-v1>
<button type="submit"></button>
</form>
Story source (TypeScript, verbatim from Storybook)
{
  render() {
    return html`
      <form id="form1" method="get" action="/foo">
        <label>
          <input type="text" name="user" />
        </label>
        <oc-button-v1 type="submit" disabled>never submit me</oc-button-v1>
        <oc-button-v1 type="submit" disabled formaction="/trigger-me"
          >never submit me
        </oc-button-v1>
        <button type="submit"></button>
      </form>
    `;
  },
  async play({
    canvasElement
  }) {
    const [button1] = Array.from(canvasElement.getElementsByTagName("oc-button-v1"));
    const input = canvasElement.getElementsByTagName("input").item(0)!;
    const form = canvasElement.querySelector("form") as HTMLFormElement;
    let numberOfSubmitEvents = 0;
    form.addEventListener("submit", ev => {
      ev.preventDefault();
      numberOfSubmitEvents += 1;
    });
    await userEvent.type(input, "{Enter}");
    await userEvent.type(button1, "{Enter}");
    await expect(numberOfSubmitEvents, "never submit disabled button").toBe(0);
  }
}

Should Not Submit From Textarea

Story: components-button-interaction-tests--should-not-submit-from-textarea · tags: play-fn

never submit me
HTML
<form id="form1">
<label>
  <textarea name="user"> </textarea>
</label>
<oc-button-v1 type="submit">never submit me</oc-button-v1>
</form>
Story source (TypeScript, verbatim from Storybook)
{
  render() {
    return html`
      <form id="form1">
        <label>
          <textarea name="user"> </textarea>
        </label>
        <oc-button-v1 type="submit">never submit me</oc-button-v1>
      </form>
    `;
  },
  async play({
    canvasElement
  }) {
    const input = canvasElement.getElementsByTagName("textarea").item(0)!;
    const form = canvasElement.querySelector("form") as HTMLFormElement;
    let numberOfSubmitEvents = 0;
    form.addEventListener("submit", ev => {
      ev.preventDefault();
      numberOfSubmitEvents += 1;
    });
    await userEvent.type(input, "{Enter}");
    await expect(numberOfSubmitEvents, "never submit disabled button").toBe(0);
  }
}

Should Work With Two Buttons

Story: components-button-interaction-tests--should-work-with-two-buttons · tags: play-fn

do not submit me submit
HTML
<form id="form1" method="get" action="/foo">
<label>
  <input type="text" name="user" />
</label>
<oc-button-v1 type="submit">do not submit me</oc-button-v1>
<oc-button-v1 type="submit" formaction="/trigger-me">submit</oc-button-v1>
</form>
Story source (TypeScript, verbatim from Storybook)
{
  render() {
    return html`
      <form id="form1" method="get" action="/foo">
        <label>
          <input type="text" name="user" />
        </label>
        <oc-button-v1 type="submit">do not submit me</oc-button-v1>
        <oc-button-v1 type="submit" formaction="/trigger-me">submit</oc-button-v1>
      </form>
    `;
  },
  async play({
    canvasElement
  }) {
    const button2 = canvasElement.getElementsByTagName("oc-button-v1").item(1)!;
    const form = canvasElement.querySelector("form") as HTMLFormElement;
    let submitter: HTMLElement | null = null;
    let numberOfSubmitEvents = 0;
    form.addEventListener("submit", ev => {
      ev.preventDefault();
      submitter = ev.submitter;
      numberOfSubmitEvents += 1;
    });
    await userEvent.type(button2, "{Enter}");
    await expect(numberOfSubmitEvents, "submit once").toBe(1);
    await expect(
    // @ts-expect-error -- internally defined property
    submitter?.formAction.endsWith("/trigger-me"), "should triggered second button only").toBeTruthy();
    await userEvent.click(button2);
    await expect(numberOfSubmitEvents, "second submit").toBe(2);
    await expect(
    // @ts-expect-error -- internally defined property
    submitter?.formAction.endsWith("/trigger-me"), "should triggered second button only").toBeTruthy();
  }
}

Should Trigger Form Action

Story: components-button-interaction-tests--should-trigger-form-action · tags: play-fn

submit submit
HTML
<form id="form1" method="get" action="/foo">
<label>
  <input type="text" name="user" />
</label>
<oc-button-v1 type="submit" formaction="/trigger-me">submit</oc-button-v1>
<input type="submit" formaction="/do-not-trigger-me" />
<button type="submit" formaction="/do-not-trigger-me"></button>
<oc-button-v1 type="submit" formaction="/do-not-trigger-me">submit</oc-button-v1>
</form>
Story source (TypeScript, verbatim from Storybook)
{
  render() {
    return html`
      <form id="form1" method="get" action="/foo">
        <label>
          <input type="text" name="user" />
        </label>
        <oc-button-v1 type="submit" formaction="/trigger-me">submit</oc-button-v1>
        <input type="submit" formaction="/do-not-trigger-me" />
        <button type="submit" formaction="/do-not-trigger-me"></button>
        <oc-button-v1 type="submit" formaction="/do-not-trigger-me">submit</oc-button-v1>
      </form>
    `;
  },
  async play({
    canvasElement
  }) {
    const input = canvasElement.getElementsByTagName("input").item(0)!;
    const form = canvasElement.querySelector("form") as HTMLFormElement;
    let submitter: HTMLElement | null = null;
    let numberOfSubmitEvents = 0;
    form.addEventListener("submit", ev => {
      ev.preventDefault();
      submitter = ev.submitter;
      numberOfSubmitEvents += 1;
    });
    await userEvent.type(input, "{Enter}");
    await expect(numberOfSubmitEvents, "submit once").toBe(1);
    await expect(
    // @ts-expect-error -- internally defined property
    submitter?.formAction.endsWith("/trigger-me"), "should have form action '/trigger-me'").toBeTruthy();
  }
}

Should Not Be Triggered By Non Form Elements

Story: components-button-interaction-tests--should-not-be-triggered-by-non-form-elements · tags: play-fn

Foo submit
HTML
<form id="form1" method="get" action="/foo">
<label>
  <input type="text" name="user" />
</label>
<a href="#">Foo</a>
<oc-button-v1 type="submit" formaction="/trigger-me">submit</oc-button-v1>
</form>
Story source (TypeScript, verbatim from Storybook)
{
  render() {
    return html`
      <form id="form1" method="get" action="/foo">
        <label>
          <input type="text" name="user" />
        </label>
        <a href="#">Foo</a>
        <oc-button-v1 type="submit" formaction="/trigger-me">submit</oc-button-v1>
      </form>
    `;
  },
  async play({
    canvasElement
  }) {
    const link = canvasElement.getElementsByTagName("a").item(0)!;
    const form = canvasElement.querySelector("form") as HTMLFormElement;
    link.addEventListener("click", ev => ev.preventDefault());
    let numberOfSubmitEvents = 0;
    form.addEventListener("submit", ev => {
      ev.preventDefault();
      numberOfSubmitEvents += 1;
    });
    await userEvent.type(link, "{Enter}");
    await expect(numberOfSubmitEvents, "should not trigger submit").toBe(0);
  }
}

Should Submit Like Native Buttons

Story: components-button-interaction-tests--should-submit-like-native-buttons · tags: play-fn

Should submit the form but not by oc-button-v1
HTML
<form id="form1">
<label>
  {Enter}
  <input type="text" name="name" />
</label>
<oc-button-v1 type="submit" data-should-submit></oc-button-v1>
<oc-button-v1 type="submit" data-should-not-submit></oc-button-v1>
</form>
<form id="form2">
<label>
  {Enter}
  <input type="text" name="name" />
</label>
<input type="submit" data-should-submit />
<oc-button-v1 type="submit" data-should-not-submit></oc-button-v1>
</form>
<form id="form3">
<label>
  {Enter}
  <input type="text" name="name" />
</label>
Should submit the form but not by oc-button-v1
</form>
<form id="form4">
<input type="submit" data-should-submit />
<oc-button-v1 type="submit" data-should-not-submit></oc-button-v1>
<label>
  {Enter}
  <input type="text" name="name" form="form4" />
</label>
</form>
<form id="form5">
<label>
  {Enter}
  <input type="text" name="name" />
</label>
<oc-button-v1 type="submit" disabled data-should-not-submit></oc-button-v1>
<oc-button-v1 type="submit" data-should-not-submit></oc-button-v1>
</form>
<form id="form6">
<label>
  {Enter}
  <input type="text" name="name" />
</label>
<input type="submit" disabled data-should-not-submit />
<oc-button-v1 type="submit" data-should-not-submit></oc-button-v1>
</form>
<form id="form7">
<label>
  {Enter}
  <input type="text" name="name" />
</label>
</form>
<oc-button-v1 type="submit" form="form7" data-should-submit></oc-button-v1>
<oc-button-v1 type="submit" form="form7" data-should-not-submit></oc-button-v1>
<button type="submit" form="form7" data-should-not-submit></button>
Story source (TypeScript, verbatim from Storybook)
{
  render() {
    return html`
      <form id="form1">
        <label>
          {Enter}
          <input type="text" name="name" />
        </label>
        <oc-button-v1 type="submit" data-should-submit></oc-button-v1>
        <oc-button-v1 type="submit" data-should-not-submit></oc-button-v1>
      </form>

      <form id="form2">
        <label>
          {Enter}
          <input type="text" name="name" />
        </label>
        <input type="submit" data-should-submit />
        <oc-button-v1 type="submit" data-should-not-submit></oc-button-v1>
      </form>

      <form id="form3">
        <label>
          {Enter}
          <input type="text" name="name" />
        </label>
        Should submit the form but not by oc-button-v1
      </form>

      <form id="form4">
        <input type="submit" data-should-submit />
        <oc-button-v1 type="submit" data-should-not-submit></oc-button-v1>
        <label>
          {Enter}
          <input type="text" name="name" form="form4" />
        </label>
      </form>

      <form id="form5">
        <label>
          {Enter}
          <input type="text" name="name" />
        </label>
        <oc-button-v1 type="submit" disabled data-should-not-submit></oc-button-v1>
        <oc-button-v1 type="submit" data-should-not-submit></oc-button-v1>
      </form>

      <form id="form6">
        <label>
          {Enter}
          <input type="text" name="name" />
        </label>
        <input type="submit" disabled data-should-not-submit />
        <oc-button-v1 type="submit" data-should-not-submit></oc-button-v1>
      </form>

      <form id="form7">
        <label>
          {Enter}
          <input type="text" name="name" />
        </label>
      </form>

      <oc-button-v1 type="submit" form="form7" data-should-submit></oc-button-v1>
      <oc-button-v1 type="submit" form="form7" data-should-not-submit></oc-button-v1>
      <button type="submit" form="form7" data-should-not-submit></button>
    `;
  },
  async play({
    canvasElement
  }) {
    const inputs = Array.from(canvasElement.querySelectorAll(`input[name="name"]`)) as HTMLInputElement[];
    const submits: {
      form: string;
      submitter: HTMLElement | null;
    }[] = [];
    canvasElement.querySelectorAll("form").forEach(form => {
      form.addEventListener("submit", ev => {
        ev.preventDefault();
        submits.push({
          form: form.id,
          submitter: ev.submitter
        });
      });
    });

    // eslint-disable-next-line no-restricted-syntax
    for (const input of inputs) {
      // eslint-disable-next-line no-await-in-loop
      await userEvent.type(input, "{Enter}");
    }
    await expect(submits.length).toBe(5);
    await expect(submits[0].form).toBe("form1");
    await expect(submits[0].submitter?.dataset.shouldNotSubmit).toBeUndefined();
    await expect(submits[1].form).toBe("form2");
    await expect(submits[1].submitter?.dataset.shouldNotSubmit).toBeUndefined();
    await expect(submits[2].form).toBe("form3");
    await expect(submits[2].submitter).toBe(undefined);
    await expect(submits[3].form).toBe("form4");
    await expect(submits[3].submitter?.dataset.shouldSubmit).toBeDefined();
    await expect(submits[4].form).toBe("form7");
    await expect(submits[4].submitter?.dataset.shouldNotSubmit).toBeUndefined();
  }
}

Interaction tests (ButtonV1.interactions.stories.ts)

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

Trigger Form Submit When Button Type Is Set

Story: components-button-interaction-tests--trigger-form-submit-when-button-type-is-set · tags: play-fn

Args: variant=primary, size=100, type=submit

Submit Form
HTML
<form onsubmit="return false;">
<label for="username">Username</label>
<input id="username" />
<oc-button-v1 variant="primary" size="100" type="submit">Submit Form</oc-button-v1>
</form>
Story source (TypeScript, verbatim from Storybook)
{
  args: {
    variant: "primary",
    size: "100",
    type: "submit"
  },
  render(args) {
    return html` <form onsubmit="return false;">
      <label for="username">Username</label>
      <input id="username" />
      <oc-button-v1 ${spread(args)}>Submit Form</oc-button-v1>
    </form>`;
  },
  play: async ({
    canvasElement
  }) => {
    const innerButton = getByShadowRole(canvasElement, "button");
    const form = canvasElement.querySelector("form");
    let formSubmitted = false;
    if (form) {
      form.addEventListener("submit", event => {
        formSubmitted = true;
        event.preventDefault();
      });
    }
    await expect(formSubmitted).toBeFalsy();
    await click(innerButton);
    await expect(formSubmitted).toBeTruthy();
  }
}

Do Not Trigger Form Submit When Button Type Is Not Set

Story: components-button-interaction-tests--do-not-trigger-form-submit-when-button-type-is-not-set · tags: play-fn

Args: variant=primary, size=100

Submit Form
HTML
<form onsubmit="return false;">
<label for="username">Username</label>
<input id="username" />
<oc-button-v1 variant="primary" size="100">Submit Form</oc-button-v1>
</form>
Story source (TypeScript, verbatim from Storybook)
{
  args: {
    variant: "primary",
    size: "100"
  },
  render(args) {
    return html` <form onsubmit="return false;">
      <label for="username">Username</label>
      <input id="username" />
      <oc-button-v1 ${spread(args)}>Submit Form</oc-button-v1>
    </form>`;
  },
  play: async ({
    canvasElement
  }) => {
    const innerButton = getByShadowRole(canvasElement, "button");
    const form = canvasElement.querySelector("form");
    let formSubmitted = false;
    if (form) {
      form.addEventListener("submit", event => {
        formSubmitted = true;
        event.preventDefault();
      });
    }
    await expect(formSubmitted).toBeFalsy();
    await click(innerButton);
    await expect(formSubmitted).toBeFalsy();
  }
}

Trigger Form Reset When Button Type Is Set

Story: components-button-interaction-tests--trigger-form-reset-when-button-type-is-set · tags: play-fn

Args: variant=primary, size=100, type=reset

Reset Form
HTML
<form onsubmit="return false;" onreset="return false;">
<label for="username">Username</label>
<input id="username" />
<oc-button-v1 variant="primary" size="100" type="reset">Reset Form</oc-button-v1>
</form>
Story source (TypeScript, verbatim from Storybook)
{
  args: {
    variant: "primary",
    size: "100",
    type: "reset"
  },
  render(args) {
    return html` <form onsubmit="return false;" onreset="return false;">
      <label for="username">Username</label>
      <input id="username" />
      <oc-button-v1 ${spread(args)}>Reset Form</oc-button-v1>
    </form>`;
  },
  play: async ({
    canvasElement
  }) => {
    const innerButton = getByShadowRole(canvasElement, "button");
    const form = canvasElement.querySelector("form");
    let formResetted = false;
    if (form) {
      form.addEventListener("reset", event => {
        formResetted = true;
        event.preventDefault();
      });
    }
    await expect(formResetted).toBeFalsy();
    await click(innerButton);
    await expect(formResetted).toBeTruthy();
  }
}

Should Show Loading On Click

Story: components-button-interaction-tests--should-show-loading-on-click · tags: play-fn

Args: variant=primary, size=100, type=button

Button
HTML
<oc-button-v1 variant="primary" size="100" type="button">Button</oc-button-v1>
Story source (TypeScript, verbatim from Storybook)
{
  args: {
    variant: "primary",
    size: "100",
    type: "button"
  },
  parameters: {
    // Disables Chromatic's snapshotting, as the spinner is animated und thus leads to diffs
    chromatic: {
      disableSnapshot: true
    }
  },
  render(args) {
    return html` <oc-button-v1 ${spread(args)}>Button</oc-button-v1>`;
  },
  play: async ({
    canvasElement
  }) => {
    // given
    const ocButton = canvasElement.getElementsByTagName("oc-button-v1").item(0)!;
    const button = getByShadowRole(canvasElement, "button");
    button.addEventListener("click", () => {
      ocButton.setAttribute("loading", "");
    });

    // there should be no spinner before clicking the button
    const noSpinner = Array.from(button.childNodes).find(node => node.nodeName === "OC-SPINNER-V1");
    await expect(noSpinner, "no spinner should be rendered before clicking the button").toBeFalsy();

    // when
    await click(button);

    // then
    // wait for some time because of the default debounce (?)
    await waitFor("spinner to arrive", async () => {
      const spinner = Array.from(button.childNodes).find(node => node.nodeName === "OC-SPINNER-V1");
      await expect(spinner, "spinner should be rendered after clicking the button").toBeTruthy();
    });
  }
}

Should Not Trigger Click When Loading Is True

Story: components-button-interaction-tests--should-not-trigger-click-when-loading-is-true · tags: play-fn

Button
HTML
<oc-button-v1 type="submit">Button</oc-button-v1>
Story source (TypeScript, verbatim from Storybook)
{
  render() {
    return html` <oc-button-v1 type="submit">Button</oc-button-v1>`;
  },
  play: async ({
    canvasElement
  }) => {
    const ocButton = canvasElement.getElementsByTagName("oc-button-v1").item(0)!;
    const button = getByShadowRole(canvasElement, "button");
    let clickCount = 0;
    let documentClickCount = 0;
    document.addEventListener("click", () => {
      documentClickCount++;
    });
    ocButton.addEventListener("click", () => {
      clickCount++;
    });

    // when
    await click(button);

    // then
    await expect(clickCount, "direct click count should be 1 after first click").toBe(1);
    await expect(documentClickCount, "document click count should be 1 after first click").toBe(1);

    // when loading is set to true
    ocButton.setAttribute("loading", "");

    // give it time to settle
    await waitFor("spinner to arrive", async () => {
      const spinner = Array.from(button.childNodes).find(node => node.nodeName === "OC-SPINNER-V1");
      await expect(spinner, "spinner should be rendered after clicking the button").toBeTruthy();
    });
    await click(button);

    // then the click event should not be triggered again
    await expect(clickCount, "direct click count should increase when loading is true").toBe(2);
    // TODO slop: the document click count should not increase because the click event should not propagate, but currently it does for the first click after setting loading to true. This is because the bouncer logic is only applied on subsequent clicks, but not on the first click after setting loading to true. This is a known issue and will be fixed in the next iteration.
    await expect(documentClickCount, "document click count should not increase when loading is true, but does for the first click after setting loading to true").toBe(2);
    await click(button);

    // then the click event should not be triggered again
    await expect(clickCount, "direct click count should increase when loading is true").toBe(3);
    await expect(documentClickCount, "document click count should not increase when loading is true, and now works for the second click after setting loading to true").toBe(2);

    // and the spinner should be rendered inside the button
    const spinner = Array.from(button.childNodes).find(node => node.nodeName === "OC-SPINNER-V1");
    await expect(spinner, "spinner should be rendered when loading is true").toBeTruthy();
  }
}

Should Handle Base 64 Href

Story: components-button-interaction-tests--should-handle-base-64-href · tags: play-fn

Link
HTML
<oc-button-v1 base64-href="L2Zvbw==">Link</oc-button-v1>
Story source (TypeScript, verbatim from Storybook)
{
  render() {
    return html` <oc-button-v1 base64-href="L2Zvbw==">Link</oc-button-v1>`;
  },
  async play({
    canvasElement
  }) {
    const ocButton = canvasElement.getElementsByTagName("oc-button-v1").item(0)!;
    ocButton.focus();
    let linkElement: HTMLAnchorElement;
    await waitFor("render link element inside shadow root", async () => {
      // instead of the button element there should now be an anchor element
      const buttonElement = ocButton.shadowRoot!.querySelector("button");
      linkElement = ocButton.shadowRoot!.querySelector("a")!;
      await expect(buttonElement, "no button should be rendered").toBeNull();
      await expect(linkElement.tagName, "anchor should be rendered").toBe("A");
    });

    // anchor element inside the shadow root should have the correct href attribute
    await expect(linkElement!.getAttribute("href")).toBe(`/foo`);

    // anchor element inside the shadow root should have the same fully href as the host
    await expect(ocButton.href).toBe(`${window.location.origin}/foo`);
    await expect(linkElement!.href).toBe(`${window.location.origin}/foo`);
  }
}