OTTODesign System

Code

Card

Storybook group: Components · Sidebar path: Components/Card · Extracted 28.09.2026

Version Tag Status API
v3 <oc-card-v3> Stable, allowed for generation CardV3
v2 <oc-card-v2> Deprecated, NOT allowed for generation CardV2

Only the latest version (v3) is allowed for generation. Older versions are kept for reference and are deprecated.

Overview (v3)

Source: ./src/components/card/v3/Overview.mdx

Card

The card component is 🔥.

Default variation

Story Default:

<oc-card-v3><div>Card Content</div></oc-card-v3>

Configuration

The card component is available in the main styling variants .. 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 card 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.

Accessibility

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

Configuration (v3)

Source: ./src/components/card/v3/Configuration.mdx

Card configuration

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

Story Default:

<oc-card-v3><div>Card Content</div></oc-card-v3>

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

Migration (v3)

Source: ./src/components/card/v3/Migration.mdx

Migration from Card v2 to v3

The oc-card component has been updated from oc-card-v2 to oc-card-v3. This migration guide provides step-by-step instructions to update your project to the latest version.

Skip to:

What's new

Simplified API with native elements

The component now uses native HTML elements (e.g. <a>, <button>, <div>) in the default slot, providing:

  • Improved accessibility with native semantics
  • Simpler API with fewer custom attributes
  • Better form integration
Automatic accessibility labeling

The aria-label="oc-auto" pattern is supported on the slotted element. It is especially useful when the visible label for the card lives in a slot="content" element rather than inside the interactive element itself:

<oc-card-v3>
  <a href="/product" aria-label="oc-auto"></a>
  <div slot="content">
    <h3>Product Title</h3>
    <p>$49.99</p>
    <oc-button-v2>Add to cart</oc-button-v2>
  </div>
</oc-card-v3>

The accessible name of the <a> is automatically derived from the text content of the content slot.

Content slot for complex layouts

The new content slot allows for cards with multiple interactive elements, such as a card that is clickable but also contains separate action buttons.

Explicit interaction patterns

Instead of using attributes like as-button or href, v3 makes the interaction pattern explicit through the HTML structure:

  • Link: <oc-card-v3><a href="...">content</a></oc-card-v3>
  • Button: <oc-card-v3><button>content</button></oc-card-v3>
  • Masked link: <oc-card-v3><div role="link" tabindex="0" data-masked-ref="...">content</div></oc-card-v3>

This makes the component's behavior clearer and more maintainable.

API changes

Removed attributes

The following attributes have been removed and their functionality moved to the slotted element:

v2 Attribute v3 Equivalent Notes
as-button Use a <button> tag in the default slot Button behavior now explicit via native button
background-color Use --background-color CSS variable Moved to CSS variable
base64-href Use data-masked-ref attribute on <a> or <div> Moved to slotted element
href Use an <a> tag with href in the default slot Moved to native anchor element
oc-aria-label Use aria-label on the slotted element Renamed to standard attribute
rel Set rel attribute on the slotted <a> element Moved to native anchor element
target Set target attribute on the slotted <a> element Moved to native anchor element
Removed slots
v2 Slot v3 Equivalent Notes
primary-action Use <a> or <button> in default Interactive element moved to default slot
default default or content Default now contains the interactive element or wrapper
New slots
Slot Description
default Contains the interactive element (<a>, <button>, or <div> for masked links)
content Extra content slot for cards with multiple interactive elements
Unchanged attributes
Attribute Notes
elevation-level Works the same in v3
inverted Works the same in v3
size Works the same in v3
CSS Variables
Variable Notes
--background-color Works the same in v3

How to migrate

The main change in v3 is the simplified API that uses the default slot for the interactive element (link or button) instead of managing it through attributes or named slots.

Migrate a basic non-interactive card
<!-- From: -->
<oc-card-v2>My Card</oc-card-v2>
<!-- To: -->
<oc-card-v3>
  <div>My Card</div>
</oc-card-v3>

Note: v3 requires content to be provided through a slotted element. A non-interactive <div> wrapper is used here — the card will render without hover/active styles or pointer cursors, indicating it is not interactive.

<!-- From: -->
<oc-card-v2>
  <a slot="primary-action" href="/product" aria-label="View product"></a>
  Card content
</oc-card-v2>
<!-- To: -->
<oc-card-v3>
  <a href="/product">Card content</a>
</oc-card-v3>
Migrate a card with button behavior
<!-- From: -->
<oc-card-v2 as-button oc-aria-label="Open card">Card content</oc-card-v2>
<!-- To: -->
<oc-card-v3>
  <button>Card content</button>
</oc-card-v3>
<!-- From: -->
<oc-card-v2 base64-href="Iw==" oc-aria-label="Card">Card content</oc-card-v2>
<!-- To: -->
<oc-card-v3>
  <div role="link" tabindex="0" data-masked-ref="Iw==">Card content</div>
</oc-card-v3>

Note: The accessible name for role="link" is computed from the element's text content, so no explicit aria-label is needed when the content is descriptive text. For icon-only or non-descriptive content, add aria-label="…" explicitly on the <div>.

<!-- From: -->
<oc-card-v2 base64-href="Iw==">
  <a slot="primary-action" href="/seo-url" aria-label="Card"></a>
  Card content
</oc-card-v2>
<!-- To: -->
<oc-card-v3>
  <a href="/seo-url" data-masked-ref="Iw==">Card content</a>
</oc-card-v3>

Note: Moving the card content inside the <a> element means the browser computes the accessible name directly from the text content, so the explicit aria-label from v2 is no longer needed. For icon-only or non-descriptive content, add aria-label="…" on the <a> explicitly.

Migrate a card with custom background color
<!-- From: -->
<oc-card-v2 background-color="#f0f0f0">Card content</oc-card-v2>
<!-- To: -->
<oc-card-v3 style="--background-color: #f0f0f0">
  <div>Card content</div>
</oc-card-v3>
Migrate a card with image content
<!-- From: -->
<oc-card-v2>
  <a slot="primary-action" href="/product" aria-label="View product"></a>
  <oc-placeholder-v1 variant="image" width="180px" height="240px"></oc-placeholder-v1>
</oc-card-v2>
<!-- To: -->
<oc-card-v3>
  <a href="/product">
    <oc-placeholder-v1 variant="image" width="180px" height="240px"></oc-placeholder-v1>
  </a>
</oc-card-v3>
Migrate a card with multiple interactive elements

For cards that need multiple interactive elements (e.g., a primary link and separate action buttons), use the content slot:

<!-- From: -->
<oc-card-v2>
  <a slot="primary-action" href="/product" aria-label="View product"></a>
  <h3>Product Title</h3>
  <p>Description</p>
  <oc-button-v2>Add to cart</oc-button-v2>
</oc-card-v2>
<!-- To: -->
<oc-card-v3>
  <a href="/product" aria-label="oc-auto"></a>
  <div slot="content">
    <h3>Product Title</h3>
    <p>Description</p>
    <oc-button-v2>Add to cart</oc-button-v2>
  </div>
</oc-card-v3>

Note: <div slot="content"> must be a direct child of <oc-card-v3>, not nested inside the interactive element. The default slot (the <a> tag here) and the content slot must be siblings.

API v2 (v2, deprecated, not for generation)

Source: ./src/components/card/v2/CardV2.API.g.mdx

Card v2 API

API: <oc-card-v2> (CardV2)

The Card component provides a container for content with properties for setting base64 encoded URL, ARIA label, and emitting a click event when interacted with. It can also behave as a button and trigger an action instead of navigating to a new page.

Attributes / properties
Attribute Type Default Required Description
size "50" | "100" no The size of the card.
inverted boolean false no Indicates whether the card styles should be inverted.
elevation-level "canvas" | "stacked" | "above" | "sticky" "canvas" no Sets the elevation level of the card. The elevation level determines the shadow and depth of the card.

- "canvas": no shadow, card appears flat, to be used on frame. Note: Has no effect when background-color or inverted has been set. - "stacked": shadow, card appears stacked, to be used on canvas. Note: Has no effect when background-color or inverted has been set. - "above": shadow, card appears above other levels. - "sticky": shadow, card appears above all other levels and is sticky.
background-color string undefined no Sets the background color of the card.
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 card.
base64-href string undefined no Sets the base64 encoded link target of the card. 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 card.

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

Note: Only applies when href or base64Href is set.
as-button boolean false no Indicates whether the card behaves as a button or as a link when clicked.
oc-aria-label string undefined no Sets the ARIA label of the card.
Slots
Slot Required Description
default yes Sets the content of the card component.
primary-action no Add an empty tag to change the default behavior of the card component.

- empty a tag: the card component behaves as a link (SEO relevant) - empty button tag: the card component behaves as a button - empty div tag: for link masking when there are multiple interactive elements
Events
Event Detail type Description
click PointerEvent Clicking the card or pressing Enter or Space while the card is focused triggers the click event. Use this event in case the as-button attribute is set to true.
oc-property-change OcCardV2Events["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.

API v3

Source: ./src/components/card/v3/CardV3.API.g.mdx

Card v3 API

API: <oc-card-v3> (CardV3)

The card component is a versatile container that can be used to display content and actions on a single topic.

Attributes / properties
Attribute Type Default Required Description
size "50" | "100" "100" no The size of the card.
inverted boolean false no Sets the card styles to be inverted.
elevation-level "canvas" | "stacked" | "above" | "sticky" "canvas" no Sets the elevation level of the card. The elevation level determines the shadow and depth of the card. - canvas: no shadow, card appears flat, to be used on frame.

Note: Has no effect, when background-color or inverted has been set. - stacked: shadow, card appears stacked, to be used on canvas.

Note: Has no effect, when background-color or inverted has been set. - above: shadow, card appears above other levels. - sticky: shadow, card appears above all other levels and is sticky.
Slots
Slot Required Description
default yes Accepts one of the following elements: - <input type="checkbox"> for multi-selection - <input type="radio"> for single-selection - <button role="switch"> for switch behavior - <button> for button behavior - <a href="#"> for link behavior - <div data-masked-ref="..."> for masked link behavior - <a data-masked-ref="..."> for link switching - aria-current on <button> or <a> for current-marked behavior

If a non-interactive element is used, the card will not have any interactive styles or behaviors applied to it.

For more information on interactive components, refer to the interactive component slots documentation.
content yes Extra content for the card component. Needed for cards with checked state or multiple interaction elements. Otherwise, just use the default slot.
Events
Event Detail type Description
oc-property-change OcCardV3Events["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 the custom background color

Variations (v3)

Source: ./src/components/card/v3/Variations.mdx

Variations

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

The default configuration uses no additional attributes and is not interactive.

<oc-card-v3><div>Card Content</div></oc-card-v3>

Inverted

Story: components-card-variations--inverted · tags: components

Variation with inverted sets a default background color and the text color to white.

Args: inverted=true

<oc-card-v3 inverted><div>Card Content</div></oc-card-v3>

With button behavior

Story: components-card-variations--with-button-behavior · tags: components

Variation using a button to provide button functionality and trigger an action instead of navigating to a new page.

Args: defaultSlot=<button>Card Content</button>

<oc-card-v3><button>Card Content</button></oc-card-v3>

With checked behavior

Story: components-card-variations--with-checked-behavior · tags: components

The card component can be marked as checked using a checkbox, a radio button, a switch button, or a current-marked link in the default slot. See marking content as checked for guidance on choosing the right pattern.

Args: defaultSlot=not used in that story, contentSlot=not used in that story

<h2>Checkbox</h2>
<div class="row">
  <oc-card-v3 >
    <input type="checkbox" aria-label="oc-auto" />
    <label slot="content">Selectable</label>
  </oc-card-v3>
  <oc-card-v3 >
    <input type="checkbox" aria-label="oc-auto" checked />
    <label slot="content">Selectable</label>
  </oc-card-v3>
  <oc-card-v3 >
    <input type="checkbox" aria-label="oc-auto" />
    <label slot="content">Selectable</label>
  </oc-card-v3>
</div>

<h2>Radio Button</h2>
<div class="row">
  <oc-card-v3 >
    <input type="radio" name="card-selection-demo" aria-label="oc-auto" />
    <label slot="content">Selectable</label>
  </oc-card-v3>
  <oc-card-v3 >
    <input type="radio" name="card-selection-demo" aria-label="oc-auto" checked />
    <label slot="content">Selectable</label>
  </oc-card-v3>
  <oc-card-v3 >
    <input type="radio" name="card-selection-demo" aria-label="oc-auto" />
    <label slot="content">Selectable</label>
  </oc-card-v3>
</div>

<h2>Switch Button</h2>
<div class="row">
  <oc-card-v3 >
    <button role="switch" aria-checked="false">Toggleable</button>
  </oc-card-v3>
  <oc-card-v3 >
    <button role="switch" aria-checked="true">Toggleable</button>
  </oc-card-v3>
  <oc-card-v3 >
    <button role="switch" aria-checked="false">Toggleable</button>
  </oc-card-v3>
</div>

<h2>Current-Marked Link</h2>
<div class="row">
  <oc-card-v3 >
    <a href="#">Link</a>
  </oc-card-v3>
  <oc-card-v3 >
    <a href="#" aria-current="page">Link</a>
  </oc-card-v3>
  <oc-card-v3 >
    <a href="#">Link</a>
  </oc-card-v3>
</div>

<style>
  .row {
    display: flex;
    gap: 16px;
    margin-bottom: 24px;
    height: 120px;
  }

  .row > oc-card-v3 {
    flex: 0 0 120px;
  }
</style>
Story source (TypeScript, verbatim from Storybook)
{
  name: "With checked behavior",
  args: {
    defaultSlot: "not used in that story",
    contentSlot: "not used in that story"
  },
  // eslint-disable-next-line @typescript-eslint/no-unused-vars -- defaultSlot/contentSlot are not used, this story renders fixed content
  render({
    defaultSlot,
    contentSlot,
    ...props
  }) {
    return html`
      <h2>Checkbox</h2>
      <div class="row">
        <oc-card-v3 ${spread(props)}>
          <input type="checkbox" aria-label="oc-auto" />
          <label slot="content">Selectable</label>
        </oc-card-v3>
        <oc-card-v3 ${spread(props)}>
          <input type="checkbox" aria-label="oc-auto" checked />
          <label slot="content">Selectable</label>
        </oc-card-v3>
        <oc-card-v3 ${spread(props)}>
          <input type="checkbox" aria-label="oc-auto" />
          <label slot="content">Selectable</label>
        </oc-card-v3>
      </div>

      <h2>Radio Button</h2>
      <div class="row">
        <oc-card-v3 ${spread(props)}>
          <input type="radio" name="card-selection-demo" aria-label="oc-auto" />
          <label slot="content">Selectable</label>
        </oc-card-v3>
        <oc-card-v3 ${spread(props)}>
          <input type="radio" name="card-selection-demo" aria-label="oc-auto" checked />
          <label slot="content">Selectable</label>
        </oc-card-v3>
        <oc-card-v3 ${spread(props)}>
          <input type="radio" name="card-selection-demo" aria-label="oc-auto" />
          <label slot="content">Selectable</label>
        </oc-card-v3>
      </div>

      <h2>Switch Button</h2>
      <div class="row">
        <oc-card-v3 ${spread(props)}>
          <button role="switch" aria-checked="false">Toggleable</button>
        </oc-card-v3>
        <oc-card-v3 ${spread(props)}>
          <button role="switch" aria-checked="true">Toggleable</button>
        </oc-card-v3>
        <oc-card-v3 ${spread(props)}>
          <button role="switch" aria-checked="false">Toggleable</button>
        </oc-card-v3>
      </div>

      <h2>Current-Marked Link</h2>
      <div class="row">
        <oc-card-v3 ${spread(props)}>
          <a href="#">Link</a>
        </oc-card-v3>
        <oc-card-v3 ${spread(props)}>
          <a href="#" aria-current="page">Link</a>
        </oc-card-v3>
        <oc-card-v3 ${spread(props)}>
          <a href="#">Link</a>
        </oc-card-v3>
      </div>

      <style>
        .row {
          display: flex;
          gap: 16px;
          margin-bottom: 24px;
          height: 120px;
        }

        .row > oc-card-v3 {
          flex: 0 0 120px;
        }
      </style>
    `;
  }
}

Story: components-card-variations--with-link-behavior · tags: components

The card component with a link as primary action. Search engines will detect the href.

Args: defaultSlot=<a href='#'>Card Content</a>

<oc-card-v3><a href='#'>Card Content</a></oc-card-v3>

Story: components-card-variations--with-masked-link-behavior · tags: components

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

Args: defaultSlot=<div role="link" tabindex="0" data-masked-ref='Iw=='>Card Content</div>

<oc-card-v3><div role="link" tabindex="0" data-masked-ref='Iw=='>Card Content</div></oc-card-v3>

Story: components-card-variations--with-link-switch-behavior · tags: components

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

Args: defaultSlot=<a href='/foo' data-masked-ref='L2Jhcg=='>Card Content</a>

<oc-card-v3><a href='/foo' data-masked-ref='L2Jhcg=='>Card Content</a></oc-card-v3>

Story: components-card-variations--link-with-image-content · tags: components

The image variant using the oc-placeholder-v1 component to provide image content and an a tag in the primary-action slot to provide navigation functionality.

Args: defaultSlot=(see snippet)

<oc-card-v3>
  <a href='#' aria-label="Card with image content">
      <oc-placeholder-v1 variant="image" width="180px" height="240px"></oc-placeholder-v1>
    </a>
</oc-card-v3>

Story: components-card-variations--link-with-elevation · tags: components

Variation using the new elevation-level property to provide a shadow effect and an a tag in the default slot to provide navigation functionality.

Args: defaultSlot=<a href='#'>Card with shadow</a>, elevation-level=above

<!-- Wrapper to set the width and the height-->
<div style="width: 180px; height: 240px; display: grid">
  <!-- The component-->
  <oc-card-v3 elevation-level="above"
    ><a href='#'>Card with shadow</a>${unsafeHTML(contentSlot)}
  </oc-card-v3>
</div>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Link with elevation",
  args: {
    defaultSlot: `<a href='#'>Card with shadow</a>`,
    "elevation-level": "above"
  },
  render({
    defaultSlot,
    contentSlot,
    ...props
  }) {
    return html`
      <!-- Wrapper to set the width and the height-->
      <div style="width: 180px; height: 240px; display: grid">
        <!-- The component-->
        <oc-card-v3 ${spread(props)}
          >${unsafeHTML(defaultSlot)}${unsafeHTML(contentSlot)}
        </oc-card-v3>
      </div>
    `;
  }
}

Demo: Different sizes

Story: components-card-variations--demo-different-sizes · tags: components

Variation using the new elevation-level property.

Args: defaultSlot=<a href='#' aria-label='Card with shadow'></a>

<div style="display: flex; gap: 16px; flex-wrap: wrap;">
  <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
    <p>size 100</p>
    <!-- Wrapper to set the width and the height-->
    <div style="width: 180px; height: 240px; display: grid">
      <!-- The component-->
      <oc-card-v3  size="100"
        ><a href='#' aria-label='Card with shadow'></a>${unsafeHTML(contentSlot)}
      </oc-card-v3>
    </div>
  </div>

  <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
    <p>size 50</p>
    <!-- Wrapper to set the width and the height-->
    <div style="width: 180px; height: 240px; display: grid; ">
      <!-- The component-->
      <oc-card-v3  size="50"
        ><a href='#' aria-label='Card with shadow'></a><a href='#' aria-label='Card with shadow'></a>
      </oc-card-v3>
    </div>
  </div>
</div>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Demo: Different sizes",
  args: {
    defaultSlot: `<a href='#' aria-label='Card with shadow'></a>`
  },
  render({
    defaultSlot,
    contentSlot,
    ...props
  }) {
    return html`
      <div style="display: flex; gap: 16px; flex-wrap: wrap;">
        <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
          <p>size 100</p>
          <!-- Wrapper to set the width and the height-->
          <div style="width: 180px; height: 240px; display: grid">
            <!-- The component-->
            <oc-card-v3 ${spread(props)} size="100"
              >${unsafeHTML(defaultSlot)}${unsafeHTML(contentSlot)}
            </oc-card-v3>
          </div>
        </div>

        <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
          <p>size 50</p>
          <!-- Wrapper to set the width and the height-->
          <div style="width: 180px; height: 240px; display: grid; ">
            <!-- The component-->
            <oc-card-v3 ${spread(props)} size="50"
              >${unsafeHTML(defaultSlot)}${unsafeHTML(defaultSlot)}
            </oc-card-v3>
          </div>
        </div>
      </div>
    `;
  }
}

Demo: Inverted card

Story: components-card-variations--demo-inverted-card · tags: components

Variation using the new elevation-level property.

Args: inverted=true, defaultSlot=(see snippet)

<!-- Wrapper to set the width and the height-->
<div style="width: 296px; display: grid;">
  <!-- The component-->
  <oc-card-v3 inverted size="100"
    >
    <a href='#' style="padding: 16px;" >
      <h1 class="oc-headline-100">Headline</h1>
      <p class="oc-copy-100">
        Dies ist ein Typoblindtext. An ihm kann man sehen, ob alle Buchstaben da sind und wie sie
        aussehen. Manchmal benutzt man Worte wie Hamburgefonts, Rafgenduks oder Handgloves
      </p>
    </a>
    ${unsafeHTML(contentSlot)}
  </oc-card-v3>
</div>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Demo: Inverted card",
  args: {
    inverted: true,
    defaultSlot: `
    <a href='#' style="padding: 16px;" >
      <h1 class="oc-headline-100">Headline</h1>
      <p class="oc-copy-100">
        Dies ist ein Typoblindtext. An ihm kann man sehen, ob alle Buchstaben da sind und wie sie
        aussehen. Manchmal benutzt man Worte wie Hamburgefonts, Rafgenduks oder Handgloves
      </p>
    </a>
    `
  },
  render({
    defaultSlot,
    contentSlot,
    ...props
  }) {
    return html`
      <!-- Wrapper to set the width and the height-->
      <div style="width: 296px; display: grid;">
        <!-- The component-->
        <oc-card-v3 ${spread(props)} size="100"
          >${unsafeHTML(defaultSlot)}${unsafeHTML(contentSlot)}
        </oc-card-v3>
      </div>
    `;
  }
}

Demo: Different elevations

Story: components-card-variations--demo-different-elevations · tags: components

Variation using the new elevation-level property.

Args: defaultSlot=<a href='#'>Card Content</a>

<div style="display: flex; gap: 16px; flex-wrap: wrap;">
  <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
    <p>canvas</p>
    <!-- Wrapper to set the width and the height-->
    <div style="width: 180px; height: 240px; display: grid">
      <!-- The component-->
      <oc-card-v3  elevation-level="canvas"
        ><a href='#'>Card Content</a>${unsafeHTML(contentSlot)}
      </oc-card-v3>
    </div>
  </div>

  <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
    <p>stacked</p>
    <!-- Wrapper to set the width and the height-->
    <div
      style="width: 180px; height: 240px; display: grid; background-color: #fff; padding: 4px"
    >
      <!-- The component-->
      <oc-card-v3  elevation-level="stacked"
        ><a href='#'>Card Content</a>${unsafeHTML(contentSlot)}
      </oc-card-v3>
    </div>
  </div>

  <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
    <p>above</p>
    <!-- Wrapper to set the width and the height-->
    <div style="width: 180px; height: 240px; display: grid">
      <!-- The component-->
      <oc-card-v3  elevation-level="above"
        ><a href='#'>Card Content</a>${unsafeHTML(contentSlot)}
      </oc-card-v3>
    </div>
  </div>

  <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
    <p>sticky</p>
    <!-- Wrapper to set the width and the height-->
    <div style="width: 180px; height: 240px; display: grid">
      <!-- The component-->
      <oc-card-v3  elevation-level="sticky"
        ><a href='#'>Card Content</a>${unsafeHTML(contentSlot)}
      </oc-card-v3>
    </div>
  </div>
</div>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Demo: Different elevations",
  args: {
    defaultSlot: `<a href='#'>Card Content</a>`
  },
  render({
    defaultSlot,
    contentSlot,
    ...props
  }) {
    return html`
      <div style="display: flex; gap: 16px; flex-wrap: wrap;">
        <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
          <p>canvas</p>
          <!-- Wrapper to set the width and the height-->
          <div style="width: 180px; height: 240px; display: grid">
            <!-- The component-->
            <oc-card-v3 ${spread(props)} elevation-level="canvas"
              >${unsafeHTML(defaultSlot)}${unsafeHTML(contentSlot)}
            </oc-card-v3>
          </div>
        </div>

        <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
          <p>stacked</p>
          <!-- Wrapper to set the width and the height-->
          <div
            style="width: 180px; height: 240px; display: grid; background-color: #fff; padding: 4px"
          >
            <!-- The component-->
            <oc-card-v3 ${spread(props)} elevation-level="stacked"
              >${unsafeHTML(defaultSlot)}${unsafeHTML(contentSlot)}
            </oc-card-v3>
          </div>
        </div>

        <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
          <p>above</p>
          <!-- Wrapper to set the width and the height-->
          <div style="width: 180px; height: 240px; display: grid">
            <!-- The component-->
            <oc-card-v3 ${spread(props)} elevation-level="above"
              >${unsafeHTML(defaultSlot)}${unsafeHTML(contentSlot)}
            </oc-card-v3>
          </div>
        </div>

        <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
          <p>sticky</p>
          <!-- Wrapper to set the width and the height-->
          <div style="width: 180px; height: 240px; display: grid">
            <!-- The component-->
            <oc-card-v3 ${spread(props)} elevation-level="sticky"
              >${unsafeHTML(defaultSlot)}${unsafeHTML(contentSlot)}
            </oc-card-v3>
          </div>
        </div>
      </div>
    `;
  }
}

Demo: Reco A11y

Story: components-card-variations--demo-reco-a-11-y · tags: components

Args: defaultSlot=<a id="a42" href="#" aria-label="oc-auto"></a>, contentSlot=(see snippet)

<!-- Wrapper to set the width-->
<div style="width: 180px;">
  <!-- The component-->
  <oc-card-v3 class="${className}" >
    <a id="a42" href="#" aria-label="oc-auto"></a>
<div slot="content">
  <oc-checkbox-v2 label-placement="left" class="oc-m-75">
    <input type="checkbox" aria-label="oc-auto" />
    <label slot="label">Produkt vergleichen</label>
  </oc-checkbox-v2>
  
  <div style="position: relative">
    <oc-placeholder-v1 variant="image" width="100%"></oc-placeholder-v1>
    
    <button
      class="oc-secondary-action oc-p-25"
      style="position: absolute; top: 16px; left: 16px"
      variant="sold-out"
    >
      Unser Tipp
    </button>
    <oc-icon-button-v3
      style="position: absolute; top: 16px; right: 16px"
      class="oc-secondary-action"
      icon="inplace-zoom"
      oc-aria-label="Mehr davon"
      elevation-level="canvas"
    ></oc-icon-button-v3>
    <oc-tag-v1 style="position: absolute; bottom: 16px; left: 16px" variant="sold-out"
    >Fast ausverkauft
    </oc-tag-v1
    >
  </div>
  
  <button class="oc-secondary-action oc-mt-50 oc-m-25 oc-p-50">
    <span class="oc-copy-75">gesponsert</span>
    <oc-icon-v1 type="info-hint" size="50"></oc-icon-v1>
  </button>
    
  <!-- mark as aria-hidden to avoid double reading-->
  <div id="product1" class="oc-mt-50 oc-mx-75" aria-hidden="true">
    <div class="oc-copy-75">BRAND</div>
    <div class="oc-copy-100">Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</div>
  </div>

  <div 
    style="display: flex; align-items: center"
    class="oc-mx-75 oc-mt-100 oc-gap-25"
  >
    <oc-interactive-overlay-v1 class="oc-secondary-action oc-p-25"><oc-energy-label-v1  variant="A"></oc-energy-label-v1></oc-interactive-overlay-v1>
    <oc-link-v2 variant="secondary" class="oc-secondary-action"><a href="/foo">Produktdatenblatt</a></oc-link-v2>
  </div>
  
  <div  class="oc-copy-75 oc-mx-75 oc-mt-100">4 / 5 Sterne</div>
  
  <oc-link-v2
    variant="primary"
    class="oc-secondary-action oc-mx-75 oc-mt-100"
  ><a href="/foo2">(4) Testurteile</a></oc-link-v2>

  <section class="oc-mx-75 oc-mt-100" aria-label="Preis">
    <div class="oc-copy-100">UVP <s>XX,XX €</s></div>
    <div class="oc-headline-50 oc-mt-25">Preisaktion</div>
    <div class="oc-headline-50 oc-mt-25">XX,XX €</div>
    <div class="oc-copy-75 oc-mt-25">(XX,XX € / YYYml)</div>
    <div class="oc-mt-25">
      <oc-tag-v1 variant="sale">-12%</oc-tag-v1>
      <oc-tag-v1 variant="sale" class="oc-secondary-action" show-info-icon>-20% Extra</oc-tag-v1>
    </div>
  </section>

  <button class="oc-secondary-action oc-mt-50 oc-m-25 oc-p-50">
    <span class="oc-copy-75">Nachhaltigkeit</span>
  </button>
  
  <div class="oc-mx-75 oc-mt-100">lieferbar - am nächsten Tag bei dir</div>

  <div class="oc-mx-75 oc-mt-50">
    <oc-logo-v1
      logo="otto-up"
      style="height: 16px"
      fill-parent
      oc-aria-label="Otto UP"
    ></oc-logo-v1>
  </div>

  <div 
    role="radiogroup"
    style="display: flex"
    aria-label="Farben"
    class="oc-mx-75 oc-mt-50 oc-mb-100 oc-gap-50"
  >
    <oc-selection-tile-v2
      variant="color"
      color="#222222"
    >
      <input type="radio" name="color" value="schwarz" aria-label="schwarz" />
    </oc-selection-tile-v2>
    <oc-selection-tile-v2
      variant="color"
      color="#008FFD"
    >
      <input type="radio" name="color" value="blau" aria-label="blau" />
    </oc-selection-tile-v2>
    <oc-selection-tile-v2
      variant="color"
      color="#50CC7F"
    >
      <input type="radio" name="color" value="gruen" aria-label="grün" />
    </oc-selection-tile-v2>
    <oc-selection-tile-v2
      variant="color"
      color="#FF006C"
    >
      <input type="radio" name="color" value="rot" aria-label="rot" />
    </oc-selection-tile-v2>
    <oc-selection-tile-v2
      variant="color"
      color="#777777"
    >
      <input type="radio" name="color" value="grau" aria-label="grau" />
    </oc-selection-tile-v2>
  </div>
</div>

  </oc-card-v3>
</div>
${css}
Story source (TypeScript, verbatim from Storybook)
{
  name: "Demo: Reco A11y",
  args: {
    defaultSlot: `<a id="a42" href="#" aria-label="oc-auto"></a>`,
    contentSlot: `
<div slot="content">
  <oc-checkbox-v2 label-placement="left" class="oc-m-75">
    <input type="checkbox" aria-label="oc-auto" />
    <label slot="label">Produkt vergleichen</label>
  </oc-checkbox-v2>
  
  <div style="position: relative">
    <oc-placeholder-v1 variant="image" width="100%"></oc-placeholder-v1>
    
    <button
      class="oc-secondary-action oc-p-25"
      style="position: absolute; top: 16px; left: 16px"
      variant="sold-out"
    >
      Unser Tipp
    </button>
    <oc-icon-button-v3
      style="position: absolute; top: 16px; right: 16px"
      class="oc-secondary-action"
      icon="inplace-zoom"
      oc-aria-label="Mehr davon"
      elevation-level="canvas"
    ></oc-icon-button-v3>
    <oc-tag-v1 style="position: absolute; bottom: 16px; left: 16px" variant="sold-out"
    >Fast ausverkauft
    </oc-tag-v1
    >
  </div>
  
  <button class="oc-secondary-action oc-mt-50 oc-m-25 oc-p-50">
    <span class="oc-copy-75">gesponsert</span>
    <oc-icon-v1 type="info-hint" size="50"></oc-icon-v1>
  </button>
    
  <!-- mark as aria-hidden to avoid double reading-->
  <div id="product1" class="oc-mt-50 oc-mx-75" aria-hidden="true">
    <div class="oc-copy-75">BRAND</div>
    <div class="oc-copy-100">Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</div>
  </div>

  <div 
    style="display: flex; align-items: center"
    class="oc-mx-75 oc-mt-100 oc-gap-25"
  >
    <oc-interactive-overlay-v1 class="oc-secondary-action oc-p-25"><oc-energy-label-v1  variant="A"></oc-energy-label-v1></oc-interactive-overlay-v1>
    <oc-link-v2 variant="secondary" class="oc-secondary-action"><a href="/foo">Produktdatenblatt</a></oc-link-v2>
  </div>
  
  <div  class="oc-copy-75 oc-mx-75 oc-mt-100">4 / 5 Sterne</div>
  
  <oc-link-v2
    variant="primary"
    class="oc-secondary-action oc-mx-75 oc-mt-100"
  ><a href="/foo2">(4) Testurteile</a></oc-link-v2>

  <section class="oc-mx-75 oc-mt-100" aria-label="Preis">
    <div class="oc-copy-100">UVP <s>XX,XX €</s></div>
    <div class="oc-headline-50 oc-mt-25">Preisaktion</div>
    <div class="oc-headline-50 oc-mt-25">XX,XX €</div>
    <div class="oc-copy-75 oc-mt-25">(XX,XX € / YYYml)</div>
    <div class="oc-mt-25">
      <oc-tag-v1 variant="sale">-12%</oc-tag-v1>
      <oc-tag-v1 variant="sale" class="oc-secondary-action" show-info-icon>-20% Extra</oc-tag-v1>
    </div>
  </section>

  <button class="oc-secondary-action oc-mt-50 oc-m-25 oc-p-50">
    <span class="oc-copy-75">Nachhaltigkeit</span>
  </button>
  
  <div class="oc-mx-75 oc-mt-100">lieferbar - am nächsten Tag bei dir</div>

  <div class="oc-mx-75 oc-mt-50">
    <oc-logo-v1
      logo="otto-up"
      style="height: 16px"
      fill-parent
      oc-aria-label="Otto UP"
    ></oc-logo-v1>
  </div>

  <div 
    role="radiogroup"
    style="display: flex"
    aria-label="Farben"
    class="oc-mx-75 oc-mt-50 oc-mb-100 oc-gap-50"
  >
    <oc-selection-tile-v2
      variant="color"
      color="#222222"
    >
      <input type="radio" name="color" value="schwarz" aria-label="schwarz" />
    </oc-selection-tile-v2>
    <oc-selection-tile-v2
      variant="color"
      color="#008FFD"
    >
      <input type="radio" name="color" value="blau" aria-label="blau" />
    </oc-selection-tile-v2>
    <oc-selection-tile-v2
      variant="color"
      color="#50CC7F"
    >
      <input type="radio" name="color" value="gruen" aria-label="grün" />
    </oc-selection-tile-v2>
    <oc-selection-tile-v2
      variant="color"
      color="#FF006C"
    >
      <input type="radio" name="color" value="rot" aria-label="rot" />
    </oc-selection-tile-v2>
    <oc-selection-tile-v2
      variant="color"
      color="#777777"
    >
      <input type="radio" name="color" value="grau" aria-label="grau" />
    </oc-selection-tile-v2>
  </div>
</div>
`
  },
  render({
    defaultSlot,
    contentSlot,
    ...props
  }) {
    const {
      css,
      className
    } = cssVariablesExample(props);
    return html`
      <!-- Wrapper to set the width-->
      <div style="width: 180px;">
        <!-- The component-->
        <oc-card-v3 class="${className}" ${spread(props)}>
          ${unsafeHTML(defaultSlot)}${unsafeHTML(contentSlot)}
        </oc-card-v3>
      </div>
      ${css}
    `;
  }
}

Interaction tests (CardV3.interactions.stories.ts)

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

Should Have Pointer Events Set

Story: components-card-interaction-tests--should-have-pointer-events-set · tags:

Should Have No Pointer Events Set

Story: components-card-interaction-tests--should-have-no-pointer-events-set · tags:

V2 (v2, deprecated, not for generation)

Source: ./src/components/card/v2/Overview.mdx

Card

The card component provides a container for content with properties for setting the link URL, base64 encoded URL, ARIA label, and emitting a click event when interacted with. It can also behave as a button and trigger an action instead of navigating to a new page.

Default variation

Story Default:

<oc-card-v2></oc-card-v2>

Configuration

The card 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 card 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 Card UX documentation for detailed user experience guidelines.

Accessibility

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

Multiple interactions

The card component can have multiple interactions:

  • The primary-action is the main action that's triggered when the user clicks on the card.
  • The secondary-action is an additional action that's triggered when the user clicks on a specific element within the card.
  • Use the CSS class oc-secondary-action to style the secondary action.
<oc-card-v2>
  <!-- Place the <a> tag in the light DOM to improve SEO.-->
  <!-- Use aria-label if the <a> tag needs to be empty. -->
  <a slot="primary-action" href="#" aria-label="Card"></a>
  <oc-button-v1 class="oc-secondary-action">Secondary action</oc-button-v1>
</oc-card-v2>
Use oc-aria-label

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

See the Link behavior and masked links section in the accessibility documentation for how to apply oc-aria-label in these scenarios.

Further reading

V2/Configuration (v2, deprecated, not for generation)

Source: ./src/components/card/v2/Configuration.mdx

Card configuration

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

Story Default:

<oc-card-v2></oc-card-v2>

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

V2/Interaction tests (CardV2.interactions.stories.ts)

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

Should Handle Base 64 Href

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

<!-- Wrapper to set the width and the height-->
<div style="width: 180px; height: 240px; display: grid">
  <!-- The component-->
  <oc-card-v2 base64-href="L2Zvbw==" oc-aria-label="link to test"></oc-card-v2>
</div>
Story source (TypeScript, verbatim from Storybook)
{
  render() {
    return html`<!-- Wrapper to set the width and the height-->
      <div style="width: 180px; height: 240px; display: grid">
        <!-- The component-->
        <oc-card-v2 base64-href="L2Zvbw==" oc-aria-label="link to test"></oc-card-v2>
      </div>`;
  },
  async play({
    canvasElement
  }) {
    const ocCard = canvasElement.getElementsByTagName("oc-card-v2").item(0)!;
    ocCard.focus();
    await tick();

    // instead of the div element there should now be an anchor element
    const linkElement = deepQuerySelector<HTMLAnchorElement>(ocCard, ".card__interactive")!;
    await expect(linkElement.tagName).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(ocCard.href).toBe(`${window.location.origin}/foo`);
    await expect(linkElement.href).toBe(`${window.location.origin}/foo`);
  }
}

V2/Variations (v2, deprecated, not for generation)

Source: ./src/components/card/v2/Variations.mdx

Variations

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

The default configuration uses no additional attributes and is not interactive.

<oc-card-v2></oc-card-v2>

Inverted

Story: components-card-v2-variations--inverted · tags: components

Variation with inverted sets a default background color and the text color to white.

Args: inverted=true

<oc-card-v2 inverted></oc-card-v2>

With button behavior

Story: components-card-v2-variations--with-button-behavior · tags: components

Variation using the as-button attribute to provide button functionality and trigger an action instead of navigating to a new page.

Args: as-button=true, oc-aria-label=Card

<oc-card-v2 as-button oc-aria-label="Card"></oc-card-v2>

Story: components-card-v2-variations--with-link-behavior · tags: components

The card component with a link as primary action. Search engines will detect the href.

Args: primaryActionSlot=(see snippet)

<oc-card-v2>
  <!--The <a> tag should be placed inside the light DOM to improve SEO.--><a slot="primary-action" href='#' aria-label='Card'></a>
</oc-card-v2>

Story: components-card-v2-variations--with-masked-link-behavior · tags: components

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

Args: base64-href=Iw==, oc-aria-label=Card

<oc-card-v2 base64-href="Iw==" oc-aria-label="Card"></oc-card-v2>

Story: components-card-v2-variations--with-link-switch-behavior · tags: components

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

Args: base64-href=Iz92YXJpYW50PWZvbw==, primaryActionSlot=(see snippet)

<oc-card-v2 base64-href="Iz92YXJpYW50PWZvbw==">
  <!--Search engines will detect the href, while users receive the Base64-encoded version.--><a href='#' slot="primary-action" aria-label='Card'></a>
</oc-card-v2>

Story: components-card-v2-variations--link-with-image-content · tags: components

The image variant using the oc-placeholder-v1 component to provide image content and an a tag in the primary-action slot to provide navigation functionality.

Args: primaryActionSlot=<a slot="primary-action" href='#' aria-label='Placeholder'></a>, defaultSlot=<oc-placeholder-v1 variant="image" width="180px" height="240px"></oc-placeholder-v1>

<oc-card-v2>
  <a slot="primary-action" href='#' aria-label='Placeholder'></a>
  <oc-placeholder-v1 variant="image" width="180px" height="240px"></oc-placeholder-v1>
</oc-card-v2>

Story: components-card-v2-variations--link-with-elevation · tags: components

Variation using the new elevation-level property to provide a shadow effect and an a tag in the primary-action slot to provide navigation functionality.

Args: primaryActionSlot=<a slot="primary-action" href='#' aria-label='Card with shadow'></a>, elevation-level=above

<!-- Wrapper to set the width and the height-->
<div style="width: 180px; height: 240px; display: grid">
  <!-- The component-->
  <oc-card-v2 elevation-level="above"
    ><a slot="primary-action" href='#' aria-label='Card with shadow'></a>${unsafeHTML(defaultSlot)}
  </oc-card-v2>
</div>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Link with elevation",
  args: {
    primaryActionSlot: `<a slot="primary-action" href='#' aria-label='Card with shadow'></a>`,
    "elevation-level": "above"
  },
  render({
    defaultSlot,
    primaryActionSlot,
    ...props
  }) {
    return html`
      <!-- Wrapper to set the width and the height-->
      <div style="width: 180px; height: 240px; display: grid">
        <!-- The component-->
        <oc-card-v2 ${spread(props)}
          >${unsafeHTML(primaryActionSlot)}${unsafeHTML(defaultSlot)}
        </oc-card-v2>
      </div>
    `;
  }
}

Demo: Different sizes

Story: components-card-v2-variations--demo-different-sizes · tags: components

Variation using the new elevation-level property.

Args: primaryActionSlot=<a slot="primary-action" href='#' aria-label='Card with shadow'></a>

<div style="display: flex; gap: 16px; flex-wrap: wrap;">
  <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
    <p>size 100</p>
    <!-- Wrapper to set the width and the height-->
    <div style="width: 180px; height: 240px; display: grid">
      <!-- The component-->
      <oc-card-v2  size="100"
        ><a slot="primary-action" href='#' aria-label='Card with shadow'></a>${unsafeHTML(defaultSlot)}
      </oc-card-v2>
    </div>
  </div>

  <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
    <p>size 50</p>
    <!-- Wrapper to set the width and the height-->
    <div style="width: 180px; height: 240px; display: grid; ">
      <!-- The component-->
      <oc-card-v2  size="50"
        ><a slot="primary-action" href='#' aria-label='Card with shadow'></a>${unsafeHTML(defaultSlot)}
      </oc-card-v2>
    </div>
  </div>
</div>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Demo: Different sizes",
  args: {
    primaryActionSlot: `<a slot="primary-action" href='#' aria-label='Card with shadow'></a>`
  },
  render({
    defaultSlot,
    primaryActionSlot,
    ...props
  }) {
    return html`
      <div style="display: flex; gap: 16px; flex-wrap: wrap;">
        <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
          <p>size 100</p>
          <!-- Wrapper to set the width and the height-->
          <div style="width: 180px; height: 240px; display: grid">
            <!-- The component-->
            <oc-card-v2 ${spread(props)} size="100"
              >${unsafeHTML(primaryActionSlot)}${unsafeHTML(defaultSlot)}
            </oc-card-v2>
          </div>
        </div>

        <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
          <p>size 50</p>
          <!-- Wrapper to set the width and the height-->
          <div style="width: 180px; height: 240px; display: grid; ">
            <!-- The component-->
            <oc-card-v2 ${spread(props)} size="50"
              >${unsafeHTML(primaryActionSlot)}${unsafeHTML(defaultSlot)}
            </oc-card-v2>
          </div>
        </div>
      </div>
    `;
  }
}

Demo: Inverted card

Story: components-card-v2-variations--demo-inverted-card · tags: components

Variation using the new elevation-level property.

Args: primaryActionSlot=<a slot="primary-action" href='#' aria-label='Card with shadow'></a>, inverted=true, defaultSlot=(see snippet)

<!-- Wrapper to set the width and the height-->
<div style="width: 296px; height: 126px; display: grid;">
  <!-- The component-->
  <oc-card-v2 inverted size="100"
    ><a slot="primary-action" href='#' aria-label='Card with shadow'></a>
    <div style="padding: 16px;">
      <h1 class="oc-headline-100">Headline</h1>
      <p class="oc-copy-100">
        Dies ist ein Typoblindtext. An ihm kann man sehen, ob alle Buchstaben da sind und wie sie
        aussehen. Manchmal benutzt man Worte wie Hamburgefonts, Rafgenduks oder Handgloves
      </p>
    </div>
    
  </oc-card-v2>
</div>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Demo: Inverted card",
  args: {
    primaryActionSlot: `<a slot="primary-action" href='#' aria-label='Card with shadow'></a>`,
    inverted: true,
    defaultSlot: `
    <div style="padding: 16px;">
      <h1 class="oc-headline-100">Headline</h1>
      <p class="oc-copy-100">
        Dies ist ein Typoblindtext. An ihm kann man sehen, ob alle Buchstaben da sind und wie sie
        aussehen. Manchmal benutzt man Worte wie Hamburgefonts, Rafgenduks oder Handgloves
      </p>
    </div>
    `
  },
  render({
    defaultSlot,
    primaryActionSlot,
    ...props
  }) {
    return html`
      <!-- Wrapper to set the width and the height-->
      <div style="width: 296px; height: 126px; display: grid;">
        <!-- The component-->
        <oc-card-v2 ${spread(props)} size="100"
          >${unsafeHTML(primaryActionSlot)}${unsafeHTML(defaultSlot)}
        </oc-card-v2>
      </div>
    `;
  }
}

Demo: Different elevations

Story: components-card-v2-variations--demo-different-elevations · tags: components

Variation using the new elevation-level property.

Args: primaryActionSlot=<a slot="primary-action" href='#' aria-label='Card with shadow'></a>

<div style="display: flex; gap: 16px; flex-wrap: wrap;">
  <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
    <p>canvas</p>
    <!-- Wrapper to set the width and the height-->
    <div style="width: 180px; height: 240px; display: grid">
      <!-- The component-->
      <oc-card-v2  elevation-level="canvas"
        ><a slot="primary-action" href='#' aria-label='Card with shadow'></a>${unsafeHTML(defaultSlot)}
      </oc-card-v2>
    </div>
  </div>

  <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
    <p>stacked</p>
    <!-- Wrapper to set the width and the height-->
    <div
      style="width: 180px; height: 240px; display: grid; background-color: #fff; padding: 4px"
    >
      <!-- The component-->
      <oc-card-v2  elevation-level="stacked"
        ><a slot="primary-action" href='#' aria-label='Card with shadow'></a>${unsafeHTML(defaultSlot)}
      </oc-card-v2>
    </div>
  </div>

  <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
    <p>above</p>
    <!-- Wrapper to set the width and the height-->
    <div style="width: 180px; height: 240px; display: grid">
      <!-- The component-->
      <oc-card-v2  elevation-level="above"
        ><a slot="primary-action" href='#' aria-label='Card with shadow'></a>${unsafeHTML(defaultSlot)}
      </oc-card-v2>
    </div>
  </div>

  <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
    <p>sticky</p>
    <!-- Wrapper to set the width and the height-->
    <div style="width: 180px; height: 240px; display: grid">
      <!-- The component-->
      <oc-card-v2  elevation-level="sticky"
        ><a slot="primary-action" href='#' aria-label='Card with shadow'></a>${unsafeHTML(defaultSlot)}
      </oc-card-v2>
    </div>
  </div>
</div>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Demo: Different elevations",
  args: {
    primaryActionSlot: `<a slot="primary-action" href='#' aria-label='Card with shadow'></a>`
  },
  render({
    defaultSlot,
    primaryActionSlot,
    ...props
  }) {
    return html`
      <div style="display: flex; gap: 16px; flex-wrap: wrap;">
        <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
          <p>canvas</p>
          <!-- Wrapper to set the width and the height-->
          <div style="width: 180px; height: 240px; display: grid">
            <!-- The component-->
            <oc-card-v2 ${spread(props)} elevation-level="canvas"
              >${unsafeHTML(primaryActionSlot)}${unsafeHTML(defaultSlot)}
            </oc-card-v2>
          </div>
        </div>

        <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
          <p>stacked</p>
          <!-- Wrapper to set the width and the height-->
          <div
            style="width: 180px; height: 240px; display: grid; background-color: #fff; padding: 4px"
          >
            <!-- The component-->
            <oc-card-v2 ${spread(props)} elevation-level="stacked"
              >${unsafeHTML(primaryActionSlot)}${unsafeHTML(defaultSlot)}
            </oc-card-v2>
          </div>
        </div>

        <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
          <p>above</p>
          <!-- Wrapper to set the width and the height-->
          <div style="width: 180px; height: 240px; display: grid">
            <!-- The component-->
            <oc-card-v2 ${spread(props)} elevation-level="above"
              >${unsafeHTML(primaryActionSlot)}${unsafeHTML(defaultSlot)}
            </oc-card-v2>
          </div>
        </div>

        <div style="display: flex; flex-direction: column; gap: 16px; align-items: center;">
          <p>sticky</p>
          <!-- Wrapper to set the width and the height-->
          <div style="width: 180px; height: 240px; display: grid">
            <!-- The component-->
            <oc-card-v2 ${spread(props)} elevation-level="sticky"
              >${unsafeHTML(primaryActionSlot)}${unsafeHTML(defaultSlot)}
            </oc-card-v2>
          </div>
        </div>
      </div>
    `;
  }
}

Demo: Reco A11y

Story: components-card-v2-variations--demo-reco-a-11-y · tags: components

Args: primaryActionSlot=<a id="a42" href="#" slot="primary-action" aria-labelledby="product1"></a>, defaultSlot=(see snippet)

<oc-card-v2>
  <a id="a42" href="#" slot="primary-action" aria-labelledby="product1"></a>
  <oc-checkbox-v2 label-placement="left" class="oc-secondary-action oc-m-75">
      <input type="checkbox" aria-label="oc-auto" />
      <label slot="label">Produkt vergleichen</label>
    </oc-checkbox-v2>
    
    <div style="position: relative">
      <oc-placeholder-v1 variant="image" width="100%"></oc-placeholder-v1>
      <button
        class="oc-secondary-action oc-p-25"
        style="position: absolute; top: 16px; left: 16px"
        variant="sold-out"
      >
        Unser Tipp
      </button>
      <oc-icon-button-v3
        style="position: absolute; top: 16px; right: 16px"
        class="oc-secondary-action"
        icon="inplace-zoom"
        oc-aria-label="Mehr davon"
        elevation-level="canvas"
      ></oc-icon-button-v3>
      <oc-tag-v1 style="position: absolute; bottom: 16px; left: 16px" variant="sold-out"
      >Fast ausverkauft
      </oc-tag-v1
      >
    </div>
    
    <button class="oc-secondary-action oc-mt-50 oc-m-25 oc-p-50">
      <span class="oc-copy-75">gesponsert</span>
      <oc-icon-v1 type="info-hint" size="50"></oc-icon-v1>
    </button>
      
    <!-- mark as aria-hidden to avoid double reading-->
    <div id="product1" class="oc-mt-50 oc-mx-75" aria-hidden="true">
      <div class="oc-copy-75">BRAND</div>
      <div class="oc-copy-100">Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</div>
    </div>
  
    <div
      style="display: flex; align-items: center"
      class="oc-mx-75 oc-mt-100 oc-gap-25"
    >
      <oc-interactive-overlay-v1 class="oc-secondary-action oc-p-25"><oc-energy-label-v1  variant="A"></oc-energy-label-v1></oc-interactive-overlay-v1>
      <oc-link-v2 variant="secondary" class="oc-secondary-action"><a href="/foo">Produktdatenblatt</a></oc-link-v2>
    </div>
    
    <div class="oc-copy-75 oc-mx-75 oc-mt-100">4 / 5 Sterne</div>
    
    <oc-link-v2
      variant="primary"
      class="oc-secondary-action oc-mx-75 oc-mt-100"
    ><a href="/foo2">(4) Testurteile</a></oc-link-v2>
  
    <section class="oc-mx-75 oc-mt-100" aria-label="Preis">
      <div class="oc-copy-100">UVP <s>XX,XX €</s></div>
      <div class="oc-headline-50 oc-mt-25">Preisaktion</div>
      <div class="oc-headline-50 oc-mt-25">XX,XX €</div>
      <div class="oc-copy-75 oc-mt-25">(XX,XX € / YYYml)</div>
      <div class="oc-mt-25">
        <oc-tag-v1 variant="sale">-12%</oc-tag-v1>
        <oc-tag-v1 variant="sale" class="oc-secondary-action" show-info-icon>-20% Extra</oc-tag-v1>
      </div>
    </section>
  
    <button class="oc-secondary-action oc-mt-50 oc-m-25 oc-p-50">
      <span class="oc-copy-75">Nachhaltigkeit</span>
    </button>
    
    <div class="oc-mx-75 oc-mt-100">lieferbar - am nächsten Tag bei dir</div>
  
    <div class="oc-mx-75 oc-mt-50">
      <oc-logo-v1
        logo="otto-up"
        style="height: 16px"
        fill-parent=""
        oc-aria-label="Otto UP"
      ></oc-logo-v1>
    </div>
  
    <ul
      style="display: flex"
      aria-label="Farben"
      class="oc-mx-75 oc-mt-50 oc-mb-100 oc-gap-50"
    >
      <li>
        <oc-selection-tile-v2
          variant="color"
          color="#222222"
          class="oc-secondary-action"
        >
          <input type="radio" name="color" aria-label="schwarz" />
        </oc-selection-tile-v2>
      </li>
      <li>
        <oc-selection-tile-v2
          variant="color"
          color="#008FFD"
          class="oc-secondary-action"
        >
          <input type="radio" name="color" aria-label="blau" />
        </oc-selection-tile-v2>
      </li>
      <li>
        <oc-selection-tile-v2
          variant="color"
          color="#50CC7F"
          class="oc-secondary-action"
        >
          <input type="radio" name="color" aria-label="grün" />
        </oc-selection-tile-v2>
      </li>
      <li>
        <oc-selection-tile-v2
          variant="color"
          color="#FF006C"
          class="oc-secondary-action"
        >
          <input type="radio" name="color" aria-label="rot" />
        </oc-selection-tile-v2>
      </li>
      <li>
        <oc-selection-tile-v2
          variant="color"
          color="#777777"
          class="oc-secondary-action"
        >
          <input type="radio" name="color" aria-label="grau" />
        </oc-selection-tile-v2>
      </li>
    </ul>
</oc-card-v2>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Demo: Reco A11y",
  args: {
    primaryActionSlot: `<a id="a42" href="#" slot="primary-action" aria-labelledby="product1"></a>`,
    defaultSlot: `
  <oc-checkbox-v2 label-placement="left" class="oc-secondary-action oc-m-75">
    <input type="checkbox" aria-label="oc-auto" />
    <label slot="label">Produkt vergleichen</label>
  </oc-checkbox-v2>
  
  <div style="position: relative">
    <oc-placeholder-v1 variant="image" width="100%"></oc-placeholder-v1>
    <button
      class="oc-secondary-action oc-p-25"
      style="position: absolute; top: 16px; left: 16px"
      variant="sold-out"
    >
      Unser Tipp
    </button>
    <oc-icon-button-v3
      style="position: absolute; top: 16px; right: 16px"
      class="oc-secondary-action"
      icon="inplace-zoom"
      oc-aria-label="Mehr davon"
      elevation-level="canvas"
    ></oc-icon-button-v3>
    <oc-tag-v1 style="position: absolute; bottom: 16px; left: 16px" variant="sold-out"
    >Fast ausverkauft
    </oc-tag-v1
    >
  </div>
  
  <button class="oc-secondary-action oc-mt-50 oc-m-25 oc-p-50">
    <span class="oc-copy-75">gesponsert</span>
    <oc-icon-v1 type="info-hint" size="50"></oc-icon-v1>
  </button>
    
  <!-- mark as aria-hidden to avoid double reading-->
  <div id="product1" class="oc-mt-50 oc-mx-75" aria-hidden="true">
    <div class="oc-copy-75">BRAND</div>
    <div class="oc-copy-100">Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</div>
  </div>

  <div
    style="display: flex; align-items: center"
    class="oc-mx-75 oc-mt-100 oc-gap-25"
  >
    <oc-interactive-overlay-v1 class="oc-secondary-action oc-p-25"><oc-energy-label-v1  variant="A"></oc-energy-label-v1></oc-interactive-overlay-v1>
    <oc-link-v2 variant="secondary" class="oc-secondary-action"><a href="/foo">Produktdatenblatt</a></oc-link-v2>
  </div>
  
  <div class="oc-copy-75 oc-mx-75 oc-mt-100">4 / 5 Sterne</div>
  
  <oc-link-v2
    variant="primary"
    class="oc-secondary-action oc-mx-75 oc-mt-100"
  ><a href="/foo2">(4) Testurteile</a></oc-link-v2>

  <section class="oc-mx-75 oc-mt-100" aria-label="Preis">
    <div class="oc-copy-100">UVP <s>XX,XX €</s></div>
    <div class="oc-headline-50 oc-mt-25">Preisaktion</div>
    <div class="oc-headline-50 oc-mt-25">XX,XX €</div>
    <div class="oc-copy-75 oc-mt-25">(XX,XX € / YYYml)</div>
    <div class="oc-mt-25">
      <oc-tag-v1 variant="sale">-12%</oc-tag-v1>
      <oc-tag-v1 variant="sale" class="oc-secondary-action" show-info-icon>-20% Extra</oc-tag-v1>
    </div>
  </section>

  <button class="oc-secondary-action oc-mt-50 oc-m-25 oc-p-50">
    <span class="oc-copy-75">Nachhaltigkeit</span>
  </button>
  
  <div class="oc-mx-75 oc-mt-100">lieferbar - am nächsten Tag bei dir</div>

  <div class="oc-mx-75 oc-mt-50">
    <oc-logo-v1
      logo="otto-up"
      style="height: 16px"
      fill-parent=""
      oc-aria-label="Otto UP"
    ></oc-logo-v1>
  </div>

  <ul
    style="display: flex"
    aria-label="Farben"
    class="oc-mx-75 oc-mt-50 oc-mb-100 oc-gap-50"
  >
    <li>
      <oc-selection-tile-v2
        variant="color"
        color="#222222"
        class="oc-secondary-action"
      >
        <input type="radio" name="color" aria-label="schwarz" />
      </oc-selection-tile-v2>
    </li>
    <li>
      <oc-selection-tile-v2
        variant="color"
        color="#008FFD"
        class="oc-secondary-action"
      >
        <input type="radio" name="color" aria-label="blau" />
      </oc-selection-tile-v2>
    </li>
    <li>
      <oc-selection-tile-v2
        variant="color"
        color="#50CC7F"
        class="oc-secondary-action"
      >
        <input type="radio" name="color" aria-label="grün" />
      </oc-selection-tile-v2>
    </li>
    <li>
      <oc-selection-tile-v2
        variant="color"
        color="#FF006C"
        class="oc-secondary-action"
      >
        <input type="radio" name="color" aria-label="rot" />
      </oc-selection-tile-v2>
    </li>
    <li>
      <oc-selection-tile-v2
        variant="color"
        color="#777777"
        class="oc-secondary-action"
      >
        <input type="radio" name="color" aria-label="grau" />
      </oc-selection-tile-v2>
    </li>
  </ul>

`
  }
}