Card
A versatile and feature-rich card container component with purposeful variants, color themes, elevation control, and interactive states. Perfect for grouping related content, creating structured layouts, and building modern UI patterns. Built with accessibility in mind and fully customizable through CSS custom properties.
Features
- 🎨 4 Variants: solid, flat, glass, frost
- 🌈 6 Color Themes: primary, secondary, info, success, warning, error
- 📏 5 Padding Sizes: none, sm, md, lg, xl
- 📊 6 Elevation Levels: 0-5 for precise shadow control
- 🖼️ 5 Slots: media, header, content, footer, actions
- 📱 Horizontal orientation for side-by-side media layouts
- 🎯 Interactive States: interactive, disabled, loading
- ♿ Fully Accessible: WCAG 2.1 Level AA compliant with keyboard navigation
- 🔧 Customizable: CSS custom properties for complete control
- ⚡ Custom Events: Emits
activatewith event details (trigger,originalEvent)
Source Code
View Source Code
import { defineComponent, handle, html, onMount, onSlotChange, ref, signal, watch } from '@vielzeug/craftit';
import type { ElevationLevel, PaddingSize, ThemeColor } from '../../types';
import { frostVariantMixin, reducedMotionMixin, surfaceMixins } from '../../styles';
const INTERACTIVE_DESCENDANT_SELECTOR =
'button, a[href], input, select, textarea, summary, [role="button"], [role="link"], [contenteditable=""], [contenteditable="true"]';
function slotHasMeaningfulContent(slot: HTMLSlotElement | null | undefined): boolean {
if (!slot) return false;
return slot.assignedNodes({ flatten: true }).some((node) => {
if (node.nodeType === Node.ELEMENT_NODE) return true;
if (node.nodeType === Node.TEXT_NODE) return !!node.textContent?.trim();
return false;
});
}
function isNestedInteractiveTarget(host: HTMLElement, event: Event): boolean {
for (const node of event.composedPath()) {
if (!(node instanceof HTMLElement)) continue;
if (node === host) return false;
if (node.matches(INTERACTIVE_DESCENDANT_SELECTOR) || !!node.closest(INTERACTIVE_DESCENDANT_SELECTOR)) {
return true;
}
}
return false;
}
import componentStyles from './card.css?inline';
/** Card component properties */
export type BitCardEvents = {
activate: { originalEvent: MouseEvent | KeyboardEvent; trigger: 'pointer' | 'keyboard' };
};
export type BitCardProps = {
/** Theme color */
color?: ThemeColor;
/** Disable interaction */
disabled?: boolean;
/** Shadow elevation level (0-5) */
elevation?: `${ElevationLevel}`;
/** Make the card interactive (role=button, keyboard nav, emits activate) */
interactive?: boolean;
/** Show a loading progress bar */
loading?: boolean;
/** Card orientation */
orientation?: 'horizontal';
/** Internal padding size */
padding?: PaddingSize;
/** Visual style variant */
variant?: 'solid' | 'flat' | 'glass' | 'frost';
};
/**
* A versatile card container with semantic slots for media, header, body, footer, and actions.
*
* @element bit-card
*
* @attr {string} variant - Visual variant: 'solid' | 'flat' | 'glass' | 'frost'
* @attr {string} color - Theme color: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'
* @attr {string} padding - Internal padding: 'none' | 'sm' | 'md' | 'lg' | 'xl'
* @attr {string} elevation - Shadow elevation: '0' | '1' | '2' | '3' | '4' | '5'
* @attr {string} orientation - Card layout: 'horizontal'
* @attr {boolean} interactive - Enable pointer/keyboard activation
* @attr {boolean} disabled - Disable card interaction
* @attr {boolean} loading - Show loading progress bar
*
* @fires activate - Emitted when an interactive card is activated
*
* @slot media - Media content displayed at top/left
* @slot header - Card header (title, subtitle)
* @slot - Main card content
* @slot footer - Card footer content
* @slot actions - Action buttons or links
*
* @cssprop --card-bg - Background color
* @cssprop --card-color - Text color
* @cssprop --card-border - Border width
* @cssprop --card-border-color - Border color
* @cssprop --card-radius - Border radius
* @cssprop --card-padding - Internal padding
* @cssprop --card-shadow - Box shadow
* @cssprop --card-hover-shadow - Shadow on hover
*
* @example
* ```html
* <bit-card elevation="2"><h3 slot="header">Title</h3><p>Content</p></bit-card>
* <bit-card interactive color="primary"><h3 slot="header">Click me</h3></bit-card>
* <bit-card variant="frost" color="secondary">Frosted card</bit-card>
* ```
*/
export const CARD_TAG = defineComponent<BitCardProps, BitCardEvents>({
props: {
color: { default: undefined },
disabled: { default: false, type: Boolean },
elevation: { default: undefined },
interactive: { default: false, type: Boolean },
loading: { default: false, type: Boolean },
orientation: { default: undefined },
padding: { default: undefined },
variant: { default: undefined },
},
setup({ emit, host, props }) {
const mediaSlot = ref<HTMLSlotElement>();
const headerSlot = ref<HTMLSlotElement>();
const contentSlot = ref<HTMLSlotElement>();
const footerSlot = ref<HTMLSlotElement>();
const actionsSlot = ref<HTMLSlotElement>();
const hasMedia = signal(false);
const hasHeader = signal(false);
const hasContent = signal(false);
const hasFooter = signal(false);
const hasActions = signal(false);
function updateSlotState() {
hasMedia.value = slotHasMeaningfulContent(mediaSlot.value);
hasHeader.value = slotHasMeaningfulContent(headerSlot.value);
hasContent.value = slotHasMeaningfulContent(contentSlot.value);
hasFooter.value = slotHasMeaningfulContent(footerSlot.value);
hasActions.value = slotHasMeaningfulContent(actionsSlot.value);
}
onMount(() => {
onSlotChange('media', updateSlotState);
onSlotChange('header', updateSlotState);
onSlotChange('default', updateSlotState);
onSlotChange('footer', updateSlotState);
onSlotChange('actions', updateSlotState);
});
watch(
[props.interactive, props.disabled, props.loading, props.variant, props.color, props.padding, props.orientation],
() => {
if (props.interactive.value) {
host.setAttribute('role', 'button');
host.setAttribute('tabindex', props.disabled.value ? '-1' : '0');
host.setAttribute('aria-disabled', String(props.disabled.value));
} else {
host.removeAttribute('role');
host.removeAttribute('tabindex');
host.removeAttribute('aria-disabled');
}
host.setAttribute('aria-busy', props.loading.value ? 'true' : 'false');
},
{ immediate: true },
);
const handleClick = (e: MouseEvent) => {
if (!props.interactive.value || props.disabled.value) return;
if (isNestedInteractiveTarget(host, e)) return;
emit('activate', { originalEvent: e, trigger: 'pointer' });
};
const handleKeydown = (e: KeyboardEvent) => {
if (!props.interactive.value || props.disabled.value) return;
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
emit('activate', { originalEvent: e, trigger: 'keyboard' });
}
};
handle(host, 'click', handleClick);
handle(host, 'keydown', handleKeydown);
return html`
<div class="card" part="card">
<div class="loading-bar" part="loading-bar"></div>
<div class="card-media" part="media" ?hidden="${() => !hasMedia.value}">
<slot ref=${mediaSlot} name="media"></slot>
</div>
<div class="card-body" part="body">
<div class="card-header" part="header" ?hidden="${() => !hasHeader.value}">
<slot ref=${headerSlot} name="header"></slot>
</div>
<div class="card-content" part="content" ?hidden="${() => !hasContent.value}">
<slot ref=${contentSlot}></slot>
</div>
<div class="card-footer" part="footer" ?hidden="${() => !hasFooter.value}">
<slot ref=${footerSlot} name="footer"></slot>
</div>
<div class="card-actions" part="actions" ?hidden="${() => !hasActions.value}">
<slot ref=${actionsSlot} name="actions"></slot>
</div>
</div>
</div>
`;
},
styles: [...surfaceMixins, frostVariantMixin('.card'), reducedMotionMixin, componentStyles],
tag: 'bit-card',
});Basic Usage
<bit-card>
<img slot="media" src="/hero.jpg" alt="Card hero image" />
<bit-text slot="header" variant="heading" size="md">Card Title</bit-text>
<bit-text>This is the card content. It can contain any HTML elements.</bit-text>
<bit-text slot="footer" size="sm" variant="caption">Additional information</bit-text>
<div slot="actions">
<bit-button size="sm">Primary</bit-button>
<bit-button size="sm" variant="ghost">Secondary</bit-button>
</div>
</bit-card>
<script type="module">
import '@vielzeug/buildit/card';
import '@vielzeug/buildit/button';
import '@vielzeug/buildit/text';
</script>Visual Options
Variants
Four named variants cover the full range from solid to translucent:
- Default (no
variant) - Canvas background with gentle border. Picks upcoloras a tinted backdrop. solid- Filled with the theme color; best for prominent cards with acolorattribute.flat- Subtle backdrop tint with a semi-transparent border; low visual weight.glass- Glassmorphism with backdrop blur and inset shadow; great for overlays.frost- Frosted glass with stronger blur and color-tinted transparency.
Elevation Control
Use the elevation prop (0-5) to control shadow depth. Works with any variant.
<!-- No shadow -->
<bit-card elevation="0">Flat appearance</bit-card>
<!-- High elevation -->
<bit-card elevation="4">High shadow</bit-card>Frost Variant
Modern frost effect with backdrop blur that adapts based on color:
- Without color: Subtle canvas-based frost overlay
- With color: Frosted glass effect with colored tint
Best Used With
Frost variant works best when placed over colorful backgrounds or images to showcase the blur and transparency effects.
Padding Sizes
Control the internal spacing of the card.
Color Themes
Apply semantic color themes to cards for different contexts.
Elevation Levels
Control shadow depth with explicit elevation levels (0-5).
Orientation
Set orientation="horizontal" for a side-by-side media + content layout. The vertical layout is the default and requires no attribute.
Media & Actions Slots
Media Slot
Add images, videos, or custom content at the top of the card (or left side in horizontal orientation).
Actions Slot
Separate slot for action buttons with automatic layout.
States
Disabled State
Prevent interaction and show visual feedback.
Loading State
Show an animated loading indicator while content is being fetched.
Interactive Cards
Set interactive to enable hover/active states, keyboard activation (Enter/Space), and typed activate events.
Practical Examples
User Profile Card
Product Card
Status Card
Stats Card
Horizontal Product List
Perfect for compact layouts and list views:
API Reference
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
variant | 'solid' | 'flat' | 'glass' | 'frost' | — | Visual style variant |
color | 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error' | — | Color theme for the card |
padding | 'none' | 'sm' | 'md' | 'lg' | 'xl' | — | Internal padding size |
elevation | '0' | '1' | '2' | '3' | '4' | '5' | — | Shadow elevation level (0=none, 5=maximum) |
orientation | 'horizontal' | — | Side-by-side media + content layout |
interactive | boolean | false | Enable hover/active states and activation |
disabled | boolean | false | Disable card interaction |
loading | boolean | false | Show animated loading bar at the top |
Slots
| Slot | Description |
|---|---|
| (default) | Main content area of the card |
media | Media section (images/video) at the top |
header | Header section below media |
footer | Footer section at the bottom of the card |
actions | Action buttons section (typically in footer) |
Events
| Event | Detail | Description |
|---|---|---|
click | — | Native browser click (always available) |
activate | { trigger: 'pointer' | 'keyboard', originalEvent: MouseEvent | KeyboardEvent } | Emitted when an interactive card is activated |
CSS Custom Properties
| Property | Default | Description |
|---|---|---|
--card-bg | var(--color-canvas) | Background color |
--card-color | var(--color-contrast-900) | Text color |
--card-border | var(--border) | Border width |
--card-border-color | var(--color-contrast-300) | Border color |
--card-radius | var(--rounded-lg) | Border radius |
--card-padding | var(--size-4) | Internal padding |
--card-shadow | var(--shadow-sm) | Box shadow |
--card-hover-shadow | var(--shadow-md) | Hover state shadow |
Customization
Custom Styling
<bit-card
style="
--card-bg: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--card-color: white;
--card-radius: var(--rounded-2xl);
--card-padding: var(--size-8);
">
<bit-text slot="header" variant="heading" size="md">Custom Styled Card</bit-text>
<bit-text>This card has custom colors and spacing.</bit-text>
</bit-card>Custom Shadow
<bit-card
style="
--card-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
--card-hover-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
"
interactive>
<bit-text slot="header" variant="heading" size="md">Enhanced Shadow</bit-text>
<bit-text>Hover to see the custom shadow effect.</bit-text>
</bit-card>Accessibility
The card component follows WCAG 2.1 Level AA standards.
bit-card
✅ Keyboard Navigation
Enter/Spaceactivate the card wheninteractiveis set.Tabmoves focus to the card.- Disabled cards have
tabindex="-1"and cannot receive focus.
✅ Screen Readers
role="button"is applied wheninteractiveis set;aria-disabledreflects the disabled state.aria-busyreflects the loading state.- Proper content hierarchy with semantic slots for screen reader users.
✅ Semantic Structure
- Uses semantic HTML for proper content organization.
- Compliant with WCAG 2.1 Level AA color contrast requirements.
Best Practices
- Use semantic headings in the
headerslot to maintain proper document structure. - Add meaningful
alttext for images in themediaslot. - Make a card
interactiveonly when the whole card acts as a single action. - If you need inner actions, place buttons/links in the
actionsslot; nested interactive elements do not trigger card activation. - Maintain color contrast when using custom
--card-bgoverrides. - Use
disabledinstead of removing interactive cards from the DOM. - Use the
loadingstate to provide visual feedback during async operations.