OTTODesign System

Components

Card

A card is an interactive container that contains content that is related to a single subject or topic.

Configurator

LiveCard: size, elevation, colours, content and selected state

Neu im Shop

Wohnzimmer

HTML
<oc-card-v3 elevation-level="stacked"><a href="#" aria-label="oc-auto"></a><div slot="content" class="oc-p-50" style="display:grid;gap:16px"><div class="oc-px-50 oc-pt-50"><p class="oc-copy-100">Neu im Shop</p><p class="oc-headline-100">Wohnzimmer</p></div><img class="oc-br-100" src="/references/images/otto/otto-shop-teaser-wohnzimmer.jpg" alt="" style="display:block;margin:0;width:100%;aspect-ratio:4/3;object-fit:cover"></div></oc-card-v3>

Usage

In comparison to blocks, cards contain content, which leads away from the page (offsite content). Mostly the content of a card is different from the content of the page. Cards can serve as entry points into deeper levels of detail or navigation.

Card on the page

Neu im Shop

Wohnzimmer

Leads to its own page

Wohnzimmer

Sofa, Couchtisch, Leuchten: Hier findest du alles, was dein Wohnzimmer gemütlich macht. Lass dich von unseren Ideen inspirieren und bestell bequem nach Hause.

Livecard serving as entry point
HTML
<div class="on-frame" style="display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1.1fr);gap:24px;align-items:center">
<p class="demo-label">Card on the page</p>
<oc-card-v3><a href="#" aria-label="oc-auto"></a><div slot="content" class="oc-p-50" style="display:grid;gap:16px"><div class="oc-px-50 oc-pt-50"><p class="oc-copy-100">Neu im Shop</p><p class="oc-headline-100">Wohnzimmer</p></div><img class="oc-br-100" src="/references/images/otto/otto-shop-teaser-wohnzimmer.jpg" alt="" style="display:block;margin:0;width:100%;aspect-ratio:4/3;object-fit:cover"></div></oc-card-v3>
<oc-icon-v1 type="arrow-long-right"></oc-icon-v1>
<p class="demo-label">Leads to its own page</p>
<oc-block-v2><div style="display:grid;gap:16px"><img src="/references/images/otto/otto-shop-teaser-wohnzimmer.jpg" alt="" style="display:block;margin:0;width:100%;aspect-ratio:4/3;object-fit:cover"><p class="oc-headline-200">Wohnzimmer</p><p class="oc-copy-100">Sofa, Couchtisch, Leuchten: Hier findest du alles, was dein Wohnzimmer gemütlich macht. Lass dich von unseren Ideen inspirieren und bestell bequem nach Hause.</p></div></oc-block-v2>

Cards are always interactive. You have two options on how to make a card interactive:

  • fully interactive: The whole card is interactive and can contain only one link target. You can increase the affordance by adding an action signifier. But remember that the fully interactive card comes with states.
  • partially interactive: The card itself is static but has to contain at least one interactive component, e.g. button, links or stacked cards. Mostly the interactive components are placed on the bottom of the card. Note that this variant has no states itself.

Note: Interactive cards with interactive components inside are possible, but require a lot of special attention to details because of target sizes, that should be big enough to not interfere with each other.

fully: the whole card is one target

Neu im Shop

Wohnzimmer

partially: only the buttons are targets

Neu im Shop

Wohnzimmer

MerkenAnsehen
Liveinteractive areas of fully vs. partially interactive card
HTML
<p class="demo-label">fully: the whole card is one target</p>
<oc-card-v3><a href="#" aria-label="oc-auto"></a><div slot="content" class="oc-p-50" style="display:grid;gap:16px"><div class="oc-px-50 oc-pt-50"><p class="oc-copy-100">Neu im Shop</p><p class="oc-headline-100">Wohnzimmer</p></div><img class="oc-br-100" src="/references/images/otto/otto-shop-teaser-wohnzimmer.jpg" alt="" style="display:block;margin:0;width:100%;aspect-ratio:4/3;object-fit:cover"></div></oc-card-v3>
<p class="demo-label">partially: only the buttons are targets</p>
<oc-card-v3><div class="oc-p-50" style="display:grid;gap:16px"><div class="oc-px-50 oc-pt-50"><p class="oc-copy-100">Neu im Shop</p><p class="oc-headline-100">Wohnzimmer</p></div><img class="oc-br-100" src="/references/images/otto/otto-shop-teaser-wohnzimmer.jpg" alt="" style="display:block;margin:0;width:100%;aspect-ratio:4/3;object-fit:cover"><div class="demo-row tight gap-8" style="max-width:none"><oc-button-v1 variant="secondary" size="50">Merken</oc-button-v1><oc-button-v1 variant="primary" size="50">Ansehen</oc-button-v1></div></div></oc-card-v3>

Variants

Cards are available in two sizes: 50 and 100. The card sizes differ in their border radius. A larger border radius results in a higher affordance, so card 100 has a higher affordance than card 50. You can use size 50 for smaller cards and size 100 for larger cards. There is also the inverted card, which has a colored background and takes care of its content text being inverted too.

Card 50

Card 100

Card 50, inverted

Card 100, inverted

Livecard 50, card 100 and inverted
HTML
<div class="on-frame" style="display:grid;grid-template-columns:2fr 3fr;gap:32px 48px;align-items:center">
<p class="demo-label">Card 50</p><oc-card-v3 size="50"><a href="#" aria-label="Card 50"></a><div slot="content" style="height:120px"></div></oc-card-v3></div>
<p class="demo-label">Card 100</p><oc-card-v3><a href="#" aria-label="Card 100"></a><div slot="content" style="height:190px"></div></oc-card-v3></div>
<p class="demo-label">Card 50, inverted</p><oc-card-v3 size="50" inverted><a href="#" aria-label="Card 50 inverted"></a><div slot="content" style="height:120px"></div></oc-card-v3></div>
<p class="demo-label">Card 100, inverted</p><oc-card-v3 inverted><a href="#" aria-label="Card 100 inverted"></a><div slot="content" style="height:190px"></div></oc-card-v3></div>

Behavior

States

Enabled

Hover (shown)

Pressed (shown)

Selected

Focus (shown)

LiveStates
HTML
<div class="on-frame" style="display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:24px 20px">
<oc-card-v3><a href="#" aria-label="Enabled"></a><div slot="content" style="aspect-ratio:1"></div></oc-card-v3>
<oc-card-v3 style="background-image:linear-gradient(var(--oc-semantic-color-overlay-hover),var(--oc-semantic-color-overlay-hover))"><a href="#" aria-label="Hover"></a><div slot="content" style="aspect-ratio:1"></div></oc-card-v3>
<oc-card-v3 has-action-child-active><a href="#" aria-label="Pressed"></a><div slot="content" style="aspect-ratio:1"></div></oc-card-v3>
<oc-card-v3><a href="#" aria-current="page" aria-label="Selected"></a><div slot="content" style="aspect-ratio:1"></div></oc-card-v3>
<oc-card-v3 style="outline:2px solid var(--oc-semantic-focus-outline-color);outline-offset:2px"><a href="#" aria-label="Focus"></a><div slot="content" style="aspect-ratio:1"></div></oc-card-v3>
<oc-card-v3 inverted><a href="#" aria-label="Enabled inverted"></a><div slot="content" style="aspect-ratio:1"></div></oc-card-v3>
<oc-card-v3 inverted style="background-image:linear-gradient(var(--oc-semantic-color-overlay-hover),var(--oc-semantic-color-overlay-hover))"><a href="#" aria-label="Hover inverted"></a><div slot="content" style="aspect-ratio:1"></div></oc-card-v3>
<oc-card-v3 inverted has-action-child-active><a href="#" aria-label="Pressed inverted"></a><div slot="content" style="aspect-ratio:1"></div></oc-card-v3>
<oc-card-v3 inverted><a href="#" aria-current="page" aria-label="Selected inverted"></a><div slot="content" style="aspect-ratio:1"></div></oc-card-v3>
<oc-card-v3 inverted style="outline:2px solid var(--oc-semantic-focus-outline-color);outline-offset:2px"><a href="#" aria-label="Focus inverted"></a><div slot="content" style="aspect-ratio:1"></div></oc-card-v3>

For information on states, refer to foundations.

Fitting

Cards can have a flexible layout and dimensions based on its content and context as you can see in the following examples:

fill-parent, 4:3

fixed-width, 1:1

fit-content height

Deine Lieferung

Dein Paket kommt morgen zwischen 10 und 14 Uhr.

ÄndernVerfolgen
LiveFitting
HTML
<div class="on-frame" style="display:grid;grid-template-columns:1.2fr 0.8fr 1.5fr;gap:24px;align-items:start">
<p class="demo-label">fill-parent, 4:3</p><oc-card-v3><a href="#" aria-label="Wohnzimmer"></a><div slot="content" class="oc-p-50"><img class="oc-br-100" src="/references/images/otto/otto-shop-teaser-wohnzimmer.jpg" alt="" style="display:block;margin:0;width:100%;aspect-ratio:4/3;object-fit:cover"></div></oc-card-v3></div>
<p class="demo-label">fixed-width, 1:1</p><oc-card-v3 style="width:140px"><a href="#" aria-label="Mojo Drop"></a><div slot="content" class="oc-p-50"><img class="oc-br-100" src="/references/images/otto/otto-fashion-drop-mojo-04.jpg" alt="" style="display:block;margin:0;width:100%;aspect-ratio:1;object-fit:cover"></div></oc-card-v3></div>
<p class="demo-label">fit-content height</p><oc-card-v3><div class="oc-p-100" style="display:grid;gap:12px"><p class="oc-headline-100">Deine Lieferung</p><p class="oc-copy-100">Dein Paket kommt morgen zwischen 10 und 14 Uhr.</p><div class="demo-row tight gap-8" style="max-width:none"><oc-button-v1 variant="secondary" size="50">Ändern</oc-button-v1><oc-button-v1 variant="primary" size="50">Verfolgen</oc-button-v1></div></div></oc-card-v3></div>

Placement

If you want to arrange cards vertically, the recommended spacing is 40px. Some cases, like the module assembly service, generate the recommended spacing results from a bottom spacing (24px) of the upper card and top spacing (16px) of the lower card.

If you want to arrange cards horizontally the recommended spacing is 16px. For smaller cards you can also use smaller distances.

Versand

Ab 29 € liefern wir kostenlos.

Rückgabe

30 Tage kostenlos zurückschicken.

40px vertical gap

Live40px vertical gap
HTML
<div class="on-frame"><div class="narrow" style="margin:0 auto;display:grid;gap:40px">
<oc-card-v3><a href="#" aria-label="Versand"></a><div slot="content" class="oc-p-100" style="min-height:96px"><p class="oc-headline-100">Versand</p><p class="oc-copy-100">Ab 29 € liefern wir kostenlos.</p></div></oc-card-v3>
<oc-card-v3><a href="#" aria-label="Rückgabe"></a><div slot="content" class="oc-p-100" style="min-height:96px"><p class="oc-headline-100">Rückgabe</p><p class="oc-copy-100">30 Tage kostenlos zurückschicken.</p></div></oc-card-v3>
<p class="demo-label" style="text-align:center;margin-top:12px">40px vertical gap</p></div>

16px horizontal gap

Live16px horizontal gap
HTML
<div class="on-frame"><div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
<oc-card-v3><a href="#" aria-label="Wohnzimmer"></a><div slot="content" class="oc-p-50"><img class="oc-br-100" src="/references/images/otto/otto-shop-teaser-wohnzimmer.jpg" alt="" style="display:block;margin:0;width:100%;aspect-ratio:4/3;object-fit:cover"></div></oc-card-v3>
<oc-card-v3><a href="#" aria-label="Kinderzimmer"></a><div slot="content" class="oc-p-50"><img class="oc-br-100" src="/references/images/otto/otto-shop-teaser-kinderzimmer.jpg" alt="" style="display:block;margin:0;width:100%;aspect-ratio:4/3;object-fit:cover"></div></oc-card-v3>
<p class="demo-label" style="text-align:center;margin-top:12px">16px horizontal gap</p></div>
Elevation

Refer to the elevation guidelines. Cards are placed on a frame (level 0) as an element of the canvas level (level 1). Cards can also be placed on top of the canvas level (cards or blocks) as stacked cards (level 1.5). Therefore, stacked cards have a different background color (stacked background). Stacked cards can also have a custom background color.

card

frame

stacked card

canvas (block)

Livecard vs. stacked card
HTML
<div style="display:grid;grid-template-columns:1fr 1fr;margin:-32px">
<div class="oc-background-color-frame" style="padding:40px 32px;display:grid;gap:24px"><oc-card-v3><a href="#" aria-label="Card"></a><div slot="content" class="oc-p-100" style="aspect-ratio:4/3;display:grid;place-items:center"><p class="oc-copy-100">card</p></div></oc-card-v3><p class="demo-label" style="text-align:center">frame</p></div>
<div style="padding:40px 32px;display:grid;gap:24px"><oc-card-v3 elevation-level="stacked"><a href="#" aria-label="Stacked card"></a><div slot="content" class="oc-p-100" style="aspect-ratio:4/3;display:grid;place-items:center"><p class="oc-copy-100">stacked card</p></div></oc-card-v3><p class="demo-label" style="text-align:center">canvas (block)</p></div>

Content Guidelines

The Card component comes without content. All types of content may be included in cards, like these:

  • image
  • headline
  • text
  • action signifiers (for interactive cards)
  • actions (for partially interactive cards)

But keep in mind that you should not mix content from different topics within a single card.

Neue Bücher im Sortiment

Krimis, Kochbücher, Kinderbücher: Stöbere durch unsere Neuheiten und lass dir dein nächstes Lieblingsbuch nach Hause liefern.

MerkenEntdecken
LiveCard with example content
HTML
<div class="on-frame"><div style="max-width:340px;margin:0 auto">
<oc-card-v3><div class="oc-p-50" style="display:grid;gap:16px"><img class="oc-br-100" src="/references/images/otto/otto-buecher-sortiment.jpg" alt="" style="display:block;margin:0;width:100%;aspect-ratio:4/3;object-fit:cover"><div class="oc-px-50" style="display:grid;gap:8px"><p class="oc-headline-100">Neue Bücher im Sortiment</p><p class="oc-copy-100">Krimis, Kochbücher, Kinderbücher: Stöbere durch unsere Neuheiten und lass dir dein nächstes Lieblingsbuch nach Hause liefern.</p></div><div class="oc-px-50 oc-pb-50 demo-row tight gap-8" style="max-width:none"><oc-button-v1 variant="secondary" size="50">Merken</oc-button-v1><oc-button-v1 variant="primary" size="50">Entdecken</oc-button-v1></div></div></oc-card-v3>
</div>

Border radiuses

If you place images inside the card, make sure the border matches the border radius of the card. There is a thumb rule that helps you determine the correct nested border radius:

card-border-radius - card-padding = content-border-radius

If the result is less than or equal to 0, it won't work. From that point on, use the minimum border radius of 4px for interactive content.

DoIf you are using a card 100 with a padding of 4px your image should have a border radius of 12 px.

16px (card border radius) - 4 px (card padding) = 12px (content border radius)

Card 16px · padding 4px · image 12px

LiveDo
HTML
<div class="on-frame"><div class="group" style="max-width:280px;margin:0 auto"><p class="demo-label">Card 16px · padding 4px · image 12px</p>
<oc-card-v3><a href="#" aria-label="Wohnzimmer"></a><div slot="content" class="oc-p-25"><img class="oc-br-150" src="/references/images/otto/otto-shop-teaser-wohnzimmer.jpg" alt="" style="display:block;margin:0;width:100%;aspect-ratio:4/3;object-fit:cover"></div></oc-card-v3>
</div>
DoIf you are using a card 100 with a padding of 8px your image should have a border radius of 8 px.

16px (card border radius) - 8 px (card padding) = 8px (content border radius)

Card 16px · padding 8px · image 8px

LiveDo
HTML
<div class="on-frame"><div class="group" style="max-width:280px;margin:0 auto"><p class="demo-label">Card 16px · padding 8px · image 8px</p>
<oc-card-v3><a href="#" aria-label="Wohnzimmer"></a><div slot="content" class="oc-p-50"><img class="oc-br-100" src="/references/images/otto/otto-shop-teaser-wohnzimmer.jpg" alt="" style="display:block;margin:0;width:100%;aspect-ratio:4/3;object-fit:cover"></div></oc-card-v3>
</div>

padding 8px · image 16px (too blunt)

padding 8px · image 4px (too sharp)

Don'tUse border radiuses that are too sharp or too blunt for content.
CautionIf you use a card 100 with padding set to 16 px, the result would be 0 px. However, due to the interactive element rule, the minimum border radius is 4 px. You may still use slightly rounded corners for the content in this case.

16px (card border radius) - 16 px (card padding) = 0px (content border radius)

Card 16px · padding 16px · image 4px (minimum)

LiveCaution
HTML
<div class="on-frame"><div class="group" style="max-width:280px;margin:0 auto"><p class="demo-label">Card 16px · padding 16px · image 4px (minimum)</p>
<oc-card-v3><a href="#" aria-label="Wohnzimmer"></a><div slot="content" class="oc-p-100"><img class="oc-br-50" src="/references/images/otto/otto-shop-teaser-wohnzimmer.jpg" alt="" style="display:block;margin:0;width:100%;aspect-ratio:4/3;object-fit:cover"></div></oc-card-v3>
</div>

Best Practices

Neu im Shop

Kinderzimmer

DoUse an interactive card with or without action siginifier.

Versand und Lieferung

Deine Bestellung ist in der Regel in 2 bis 4 Werktagen bei dir. Ab 29 € Bestellwert liefern wir kostenlos. Große Artikel wie Möbel bringt dir unsere Spedition bis in die Wohnung, den Termin stimmen wir vorher mit dir ab.

Don'tUse a static card without interactive components.
DoOnly use cards with a filled background.
Don'tUse cards with an Outline.
DoUse same card size inside a feature.
Don'tMix different card dimensions inside a feature.
DoUse the provided semantic tokens "color/background-card" when using the over-color variant.
Don'tUse custom colors.
CautionAvoid placing over-color cards on colored blocks or colored backgrounds.

MOJO

Sweatshirt mit Print

59,99 €

CautionAvoid placing content which belongs to the card outside of the card.

Neu im Shop

Wohnzimmer

target

DoUse cards with a single link target.

Neu im Shop

Wohnzimmer

In den Warenkorb

target 1: the card

target 2: the button

CautionUse cards with overlapping link targets very cautious, expecially if the targets are rather small.

Neu im Shop

Wohnbereiche

Alle Wohnideen

target 1: the stacked cards

target 2: the button

CautionWhen using a card as a interactive cluster, try to clearly separate link targets

Accessibility

For information on accessibility, refer to the technical documentation.

Status

Implementation

Live demo

Das könnte dir gefallen

Alle ansehen
-20 %

Sneaker »Court«, Leder, Weiß

79,99 € 99,99 €

Lieferung in 2 bis 4 Werktagen

Sessel »Lounge«, Bouclé, Natur

449,99 €

Lieferung in 1 bis 2 Wochen

Kopfhörer »Studio«, kabellos, Schwarz

98,99 €

Lieferung in 2 bis 4 Werktagen

Wohnideen für dich

Mehr Inspiration

Neu im Shop

Wohnzimmer

Bis zu -30 %

Kinderzimmer

Fashion Drop

Mojo

LiveReal OTTO components, rendered by the OTTO component runtime
HTML
<div class="on-frame demo-stack" style="gap:32px">
<div style="display:flex;justify-content:space-between;align-items:baseline;gap:12px"><p class="oc-headline-200" style="margin:0">Das könnte dir gefallen</p><oc-link-v2><a href="#">Alle ansehen</a></oc-link-v2></div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(12rem,1fr));gap:16px">
<oc-card-v3><a href="#" aria-label="oc-auto"></a><div slot="content" class="oc-p-50" style="display:grid;gap:12px"><div style="position:relative"><img class="oc-br-100" src="/previews/imagery/samples/otto-product-still/product-sneaker.webp" alt="" style="display:block;margin:0;width:100%;aspect-ratio:1;object-fit:cover"><oc-tag-v1 variant="sale" size="50" style="position:absolute;top:8px;left:8px">-20 %</oc-tag-v1></div><div class="oc-px-50 oc-pb-50" style="display:grid;gap:4px"><p class="oc-copy-100" style="margin:0">Sneaker »Court«, Leder, Weiß</p><p class="oc-headline-100 oc-text-color-sale" style="margin:0">79,99 € <span class="oc-copy-75 oc-text-color-secondary" style="text-decoration:line-through">99,99 €</span></p><p class="oc-copy-75 oc-text-color-secondary" style="margin:0">Lieferung in 2 bis 4 Werktagen</p></div></div></oc-card-v3>
<oc-card-v3><a href="#" aria-label="oc-auto"></a><div slot="content" class="oc-p-50" style="display:grid;gap:12px"><div style="position:relative"><img class="oc-br-100" src="/previews/imagery/samples/otto-product-still/product-armchair.webp" alt="" style="display:block;margin:0;width:100%;aspect-ratio:1;object-fit:cover"></div><div class="oc-px-50 oc-pb-50" style="display:grid;gap:4px"><p class="oc-copy-100" style="margin:0">Sessel »Lounge«, Bouclé, Natur</p><p class="oc-headline-100" style="margin:0">449,99 €</p><p class="oc-copy-75 oc-text-color-secondary" style="margin:0">Lieferung in 1 bis 2 Wochen</p></div></div></oc-card-v3>
<oc-card-v3><a href="#" aria-label="oc-auto"></a><div slot="content" class="oc-p-50" style="display:grid;gap:12px"><div style="position:relative"><img class="oc-br-100" src="/previews/imagery/samples/otto-product-still/product-headphones.webp" alt="" style="display:block;margin:0;width:100%;aspect-ratio:1;object-fit:cover"></div><div class="oc-px-50 oc-pb-50" style="display:grid;gap:4px"><p class="oc-copy-100" style="margin:0">Kopfhörer »Studio«, kabellos, Schwarz</p><p class="oc-headline-100" style="margin:0">98,99 €</p><p class="oc-copy-75 oc-text-color-secondary" style="margin:0">Lieferung in 2 bis 4 Werktagen</p></div></div></oc-card-v3>
<div style="display:flex;justify-content:space-between;align-items:baseline;gap:12px"><p class="oc-headline-200" style="margin:0">Wohnideen für dich</p><oc-link-v2><a href="#">Mehr Inspiration</a></oc-link-v2></div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(12rem,1fr));gap:16px">
  <oc-card-v3><a href="#" aria-label="oc-auto"></a><div slot="content" class="oc-p-50" style="display:grid;gap:16px"><div class="oc-px-50 oc-pt-50"><p class="oc-copy-100" style="margin:0">Neu im Shop</p><p class="oc-headline-100" style="margin:0">Wohnzimmer</p></div><img class="oc-br-100" src="/previews/imagery/samples/otto-lifestyle-photo/lifestyle-living-room.webp" alt="" style="display:block;margin:0;width:100%;aspect-ratio:4/3;object-fit:cover"></div></oc-card-v3>
  <oc-card-v3><a href="#" aria-label="oc-auto"></a><div slot="content" class="oc-p-50" style="display:grid;gap:16px"><div class="oc-px-50 oc-pt-50"><p class="oc-copy-100" style="margin:0">Bis zu -30 %</p><p class="oc-headline-100" style="margin:0">Kinderzimmer</p></div><img class="oc-br-100" src="/previews/imagery/samples/otto-lifestyle-photo/lifestyle-kids-room.webp" alt="" style="display:block;margin:0;width:100%;aspect-ratio:4/3;object-fit:cover"></div></oc-card-v3>
  <oc-card-v3 inverted><a href="#" aria-label="oc-auto"></a><div slot="content" class="oc-p-50" style="display:grid;gap:16px"><div class="oc-px-50 oc-pt-50"><p class="oc-copy-100" style="margin:0">Fashion Drop</p><p class="oc-headline-100" style="margin:0">Mojo</p></div><img class="oc-br-100" src="/references/images/otto/otto-fashion-drop-mojo-06.jpg" alt="" style="display:block;margin:0;width:100%;aspect-ratio:4/3;object-fit:cover"></div></oc-card-v3>

Code

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)

Card

The card component is 🔥.

Default variation
Card Content
HTML
<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)

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.

Card Content
HTML
<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)

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:

Product Title

$49.99

Add to cart
HTML
<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>
</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)

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

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)

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.

Card Content
HTML
<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

Card Content
HTML
<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>

HTML
<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

Checkbox

Radio Button

Switch Button

Current-Marked Link

HTML
<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>
<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>
<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>
<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>
<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>

HTML
<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>

Card Content
HTML
<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>

HTML
<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)

HTML
<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)

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
<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)

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.

<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)

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>

`
  }
}