| 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.
Migrate a card with link behavior
<!-- 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>
Migrate a card with masked link (base64-href)
<!-- 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>.
Migrate a card with link switch behavior
<!-- 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. |
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 behaviorIf 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>
`;
}
}
With link behavior
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>
With masked link behavior
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>
With link switch behavior
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>
Link with image content
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>
Link with elevation
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-actionis the main action that's triggered when the user clicks on the card. - The
secondary-actionis an additional action that's triggered when the user clicks on a specific element within the card. - Use the CSS class
oc-secondary-actionto 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.
Button or masked link
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>
With link behavior
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>
With masked link behavior
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>
With link switch behavior
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>
Link with image content
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>
Link with elevation
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>
`
}
}