Card

Flexible content containers with optional headers, footers, hover effects, and collapsible functionality

Overview

Cards are flexible content containers that display information in a structured, visually appealing format. They're one of the most versatile UI components, perfect for showcasing products, articles, user profiles, or any grouped content.

Key Features

  • Flexible content structure with optional header, body, and footer
  • Hover effects with lift animation and shadow enhancement
  • Clickable cards that act as interactive links
  • Theme-aware styling with automatic light/dark mode support
  • Customisable shadows, borders, and spacing

When to Use Cards

  • Content Collections: Blog posts, products, team members, portfolio items
  • Dashboards: Statistics widgets, activity feeds, metric displays
  • Navigation: Feature highlights, service offerings, call-to-action blocks
  • Forms: Multi-step wizards, settings panels, grouped inputs

Basic Usage

// Option 1: Enhance existing HTML
<div id="my-card" class="card">
    <div class="card-header">Card Header</div>
    <div class="card-body">
        <h3 class="card-title">Card Title</h3>
        <p class="card-text">Card content goes here.</p>
    </div>
    <div class="card-footer">Card Footer</div>
</div>

const card = Domma.elements.card('#my-card', {
    hover: true,
    clickable: false
});

// Option 2: Generate HTML from options (NEW!)
const generatedCard = Domma.elements.card('#container', {
    title: 'Card Title',
    icon: 'info',
    content: '<p>Card content goes here.</p>',
    collapsible: true
});

Quick Start

Get started with a basic card in seconds. Cards work with pure CSS (no JavaScript required), but you can enhance them with hover effects using the Card component.

CSS-Only Card (No JavaScript)

<div class="card">
    <div class="card-body">
        <h3 class="card-title">Simple Card</h3>
        <p class="card-text">This card requires no JavaScript.</p>
    </div>
</div>
CSS-Only Card (No JavaScript)

Simple Card

This card requires no JavaScript. It uses only CSS classes for styling.

<div class="card">
    <div class="card-body">
        <h3 class="card-title">Simple Card</h3>
        <p class="card-text">This card requires no JavaScript. It uses only CSS classes for styling.</p>
    </div>
</div>

Card with Hover Effect (JavaScript)

// HTML
<div id="hover-card" class="card">
    <div class="card-body">
        <h3 class="card-title">Hover Me</h3>
        <p class="card-text">Move your cursor over this card.</p>
    </div>
</div>

// JavaScript
Domma.elements.card('#hover-card', { hover: true });
Card with Hover Effect (JavaScript)

Hover Me

Move your cursor over this card to see the lift effect.

<div id="quick-hover-card" class="card">
    <div class="card-body">
        <h3 class="card-title">Hover Me</h3>
        <p class="card-text">Move your cursor over this card to see the lift effect.</p>
    </div>
</div>
/**
 * Card with Hover Effect (JavaScript) - from public/showcase/elements/card.
 */
export function mount(root) {
    E.card(root.querySelector('#quick-hover-card'), {hover: true});
}

Step-by-Step Tutorial

Learn how to build an interactive product card from scratch, step by step.

Step 1: HTML Structure

Start with the basic card structure using semantic class names:

<div id="product-card" class="card">
    <div class="card-body">
        <h3 class="card-title">Product Name</h3>
        <p class="card-text">Brief description of the product.</p>
    </div>
</div>

Step 2: Add Header and Footer

Expand the structure with a header (for images or badges) and footer (for actions):

<div id="product-card" class="card">
    <div class="card-header">
        <span class="badge badge-primary">New</span>
    </div>
    <div class="card-body">
        <h3 class="card-title">Premium Headphones</h3>
        <p class="card-text">High-quality wireless headphones with noise cancellation.</p>
        <p class="card-text"><strong>£149.99</strong></p>
    </div>
    <div class="card-footer">
        <button class="btn btn-primary">Add to Cart</button>
    </div>
</div>

Step 3: Enable Hover Effect

Add the hover effect to make the card interactive:

const productCard = Domma.elements.card('#product-card', {
    hover: true  // Enables lift animation on hover
});

Step 4: Make It Clickable

Turn the entire card into a clickable link:

const productCard = Domma.elements.card('#product-card', {
    hover: true,
    clickable: true,
    onClick: (event) => {
        console.log('Card clicked!');
        window.location.href = '/product/premium-headphones';
    }
});

Step 5: Complete Working Demo

Here's the finished interactive product card:

Step 5: Complete Working Demo
New

Premium Headphones

High-quality wireless headphones with active noise cancellation and 30-hour battery life.

£149.99

<div id="tutorial-card" class="card">
    <div class="card-header">
        <span class="badge badge-primary">New</span>
    </div>
    <div class="card-body">
        <h3 class="card-title">Premium Headphones</h3>
        <p class="card-text mb-2">High-quality wireless headphones with active noise cancellation and
            30-hour battery life.</p>
        <p class="card-text"><strong>£149.99</strong></p>
    </div>
    <div class="card-footer">
        <button type="button" class="btn btn-primary btn-sm">Add to Cart</button>
    </div>
</div>
/**
 * Step 5: Complete Working Demo - from public/showcase/elements/card.
 */
export function mount(root) {
            // Tutorial example
    E.card(root.querySelector('#tutorial-card'), {
                hover: true,
                clickable: true,
                onClick: () => {
                    E.toast('Product added to cart!');
                }
            });
}

Examples & Variations

Example 1: Basic Card

Simple card with title and content:

Example 1: Basic Card

Basic Card

A simple card with just a title and some text content. No header or footer required.

<div class="card">
    <div class="card-body">
        <h3 class="card-title">Basic Card</h3>
        <p class="card-text">A simple card with just a title and some text content. No header or footer
            required.</p>
    </div>
</div>

Example 2: Card with Hover Effect

Card that lifts on hover with enhanced shadow:

Example 2: Card with Hover Effect

Hover Card

Hover over this card to see the lift animation and enhanced shadow effect.

<div id="example-hover" class="card">
    <div class="card-body">
        <h3 class="card-title">Hover Card</h3>
        <p class="card-text">Hover over this card to see the lift animation and enhanced shadow
            effect.</p>
    </div>
</div>
/**
 * Example 2: Card with Hover Effect - from public/showcase/elements/card.
 */
export function mount(root) {
            // Examples section
    E.card(root.querySelector('#example-hover'), {hover: true});
}

Example 3: Clickable Card

Entire card acts as a button:

Example 3: Clickable Card

Clickable Card

Click anywhere on this card to trigger an action.

<div id="example-clickable" class="card">
    <div class="card-body">
        <h3 class="card-title">Clickable Card</h3>
        <p class="card-text">Click anywhere on this card to trigger an action.</p>
    </div>
</div>
<div class="demo-result mt-2" id="click-result"></div>
/**
 * Example 3: Clickable Card - from public/showcase/elements/card.
 */
export function mount(root) {
    E.card(root.querySelector('#example-clickable'), {
        hover: true,
        clickable: true,
        onClick: (e) => {
            const result = $(root).find('#click-result')[0];
            result.classList.remove('hidden');
            result.textContent = `Card clicked at ${D().format('HH:mm:ss')}`;
        }
    });
}

Example 4: Styled Card (Primary)

Card with primary colour border:

Example 4: Styled Card (Primary)

Featured

Premium Plan

Unlock all features with our premium plan.

<div class="card card-primary">
    <div class="card-header">
        <h3 class="m-0">Featured</h3>
    </div>
    <div class="card-body">
        <h3 class="card-title">Premium Plan</h3>
        <p class="card-text">Unlock all features with our premium plan.</p>
    </div>
</div>

Example 5: Profile Card

User profile with avatar and actions:

Example 5: Profile Card
JD

Jane Doe

Senior Developer

Passionate about building beautiful, accessible web experiences.

<div id="profile-card" class="card">
    <div class="card-body text-center">
        <div class="profile-avatar">
            JD
        </div>
        <h3 class="card-title mb-1">Jane Doe</h3>
        <p class="card-subtitle mb-3">Senior Developer</p>
        <p class="card-text">Passionate about building beautiful, accessible web experiences.</p>
    </div>
    <div class="card-footer">
        <button type="button" class="btn btn-secondary btn-sm">View Profile</button>
        <button type="button" class="btn btn-primary btn-sm">Message</button>
    </div>
</div>
/**
 * Example 5: Profile Card - from public/showcase/elements/card.
 */
export function mount(root) {
    E.card(root.querySelector('#profile-card'), {hover: true});
}

Example 6: Statistics Card

Dashboard metric with icon:

Example 6: Statistics Card

Total Sales

£24,580

↑ 12.5% from last month

<div class="card">
    <div class="card-body">
        <div style="display: flex; justify-content: space-between; align-items: flex-start;">
            <div>
                <p class="card-subtitle mb-1">Total Sales</p>
                <h2 class="m-0">£24,580</h2>
                <p class="card-text mt-2">
                    ↑ 12.5% from last month
                </p>
            </div>
            <div data-icon="database" class="text-primary opacity-60"></div>
        </div>
    </div>
</div>

Example 7: Horizontal Card

Card with side-by-side layout:

Example 7: Horizontal Card

Wireless Mouse

Ergonomic design with precision tracking and long battery life. Perfect for productivity.

<div class="card">
    <div class="card-body">
        <div class="card-image-placeholder" data-icon="box"></div>
        <div>
            <h3 class="card-title mt-0">Wireless Mouse</h3>
            <p class="card-text">Ergonomic design with precision tracking and long battery life. Perfect
                for productivity.</p>
            <button type="button" class="btn btn-primary btn-sm mt-2">Learn More</button>
        </div>
    </div>
</div>

Example 8: Grid of Cards

Multiple cards in a responsive grid:

Example 8: Grid of Cards

Feature One

Fast and reliable performance.

Feature Two

Secure data encryption.

Feature Three

24/7 customer support.

<div class="grid grid-cols-1">
    <div id="grid-card-1" class="card">
        <div class="card-body">
            <h3 class="card-title">Feature One</h3>
            <p class="card-text">Fast and reliable performance.</p>
        </div>
    </div>
    <div id="grid-card-2" class="card">
        <div class="card-body">
            <h3 class="card-title">Feature Two</h3>
            <p class="card-text">Secure data encryption.</p>
        </div>
    </div>
    <div id="grid-card-3" class="card">
        <div class="card-body">
            <h3 class="card-title">Feature Three</h3>
            <p class="card-text">24/7 customer support.</p>
        </div>
    </div>
</div>
/**
 * Example 8: Grid of Cards - from public/showcase/elements/card.
 */
export function mount(root) {
            // Grid cards with hover
    ['#grid-card-1', '#grid-card-2', '#grid-card-3'].forEach(id => {
                E.card(id, {hover: true});
            });
}

Example 9: Collapsible Card

Card with collapsible content. Click the header to toggle, state persists via localStorage:

Example 9: Collapsible Card

Click to Collapse

This content can be collapsed by clicking the header. The chevron icon indicates the current state.

The collapsed/expanded state is remembered via localStorage, so it persists across page reloads.

Try collapsing this card and refreshing the page to see it remember your choice!

<div id="collapsible-demo" class="card">
    <div class="card-header">
        <h3 class="m-0">Click to Collapse</h3>
    </div>
    <div class="card-body">
        <p class="card-text">This content can be collapsed by clicking the header. The chevron icon indicates
            the current state.</p>
        <p class="card-text">The collapsed/expanded state is remembered via localStorage, so it persists across
            page reloads.</p>
        <p class="card-text mb-0">Try collapsing this card and refreshing the page to see it remember your
            choice!</p>
    </div>
</div>
/**
 * Example 9: Collapsible Card - from public/showcase/elements/card.
 */
export function mount(root) {
          // Collapsible card demo
    E.card(root.querySelector('#collapsible-demo'), {
            collapsible: true,
            onCollapse: () => console.log('Card collapsed'),
            onExpand: () => console.log('Card expanded')
          });
}

Accent Cards

The .card-accent variant adds a prominent left border to highlight a card without changing its background colour. It is theme-aware - the border colour updates automatically when the theme changes. Five colour modifiers are available.

Example 1: Basic Accent Card

Left border using the primary theme colour (.card-accent):

Example 1: Basic Accent Card

Accent Card

A simple accent card with a left border in the primary theme colour. Switch themes to see the border update automatically.

<div class="card card-accent" style="max-width: 420px;">
    <div class="card-body">
        <h3 class="card-title">Accent Card</h3>
        <p class="card-text">A simple accent card with a left border in the primary theme colour.
            Switch themes to see the border update automatically.</p>
    </div>
</div>
<div class="card card-accent">
  <div class="card-body">
    <h3 class="card-title">Accent Card</h3>
    <p class="card-text">Content here</p>
  </div>
</div>

Example 2: Colour Variants

Four semantic colour modifiers map to the standard status design tokens:

Example 2: Colour Variants
Success

.card-accent-success

Danger

.card-accent-danger

Warning

.card-accent-warning

Info

.card-accent-info

<div class="grid grid-cols-2 gap-4">
    <div class="card card-accent-success">
        <div class="card-body">
            <h5 class="card-title mb-1">Success</h5>
            <p class="card-text mb-0 text-sm">
                <code>.card-accent-success</code>
            </p>
        </div>
    </div>
    <div class="card card-accent-danger">
        <div class="card-body">
            <h5 class="card-title mb-1">Danger</h5>
            <p class="card-text mb-0 text-sm">
                <code>.card-accent-danger</code>
            </p>
        </div>
    </div>
    <div class="card card-accent-warning">
        <div class="card-body">
            <h5 class="card-title mb-1">Warning</h5>
            <p class="card-text mb-0 text-sm">
                <code>.card-accent-warning</code>
            </p>
        </div>
    </div>
    <div class="card card-accent-info">
        <div class="card-body">
            <h5 class="card-title mb-1">Info</h5>
            <p class="card-text mb-0 text-sm">
                <code>.card-accent-info</code>
            </p>
        </div>
    </div>
</div>
<div class="card card-accent-success">...</div>
<div class="card card-accent-danger">...</div>
<div class="card card-accent-warning">...</div>
<div class="card card-accent-info">...</div>

Example 3: Accent + Hover

Combine .card-accent with .card-hover for an interactive card that lifts on hover while keeping the accent border:

Example 3: Accent + Hover

Hover over me

The accent border remains visible during the hover lift animation.

<div class="card card-accent card-hover" style="max-width: 420px;">
    <div class="card-body">
        <h3 class="card-title">Hover over me</h3>
        <p class="card-text">The accent border remains visible during the hover lift animation.</p>
    </div>
</div>
<div class="card card-accent card-hover">
  <div class="card-body">...</div>
</div>

Example 4: Full Card Structure

Accent works seamlessly with the full header/body/footer structure:

Example 4: Full Card Structure
Deployment Status

Build #1,042 completed successfully. All 148 tests passed.

<div class="card card-accent" style="max-width: 420px;">
    <div class="card-header">
        <h5 class="m-0">Deployment Status</h5>
    </div>
    <div class="card-body">
        <p class="card-text">Build <strong>#1,042</strong> completed successfully. All 148 tests
            passed.</p>
    </div>
    <div class="card-footer">
        <small class="text-muted">Deployed 2 minutes ago</small>
    </div>
</div>
<div class="card card-accent">
  <div class="card-header">Deployment Status</div>
  <div class="card-body">Build #1,042 passed all tests.</div>
  <div class="card-footer">Deployed 2 minutes ago</div>
</div>

Example 5: Practical - Notification Cards

A real-world pattern: status-aware notification cards using colour variants to convey urgency at a glance:

Example 5: Practical - Notification Cards
Critical: Database connection lost

The primary database is unreachable. Failover initiated.

Warning: High memory usage

Server memory at 87%. Consider scaling up.

Deploy succeeded

Version 2.4.1 is live across all regions.

<div style="display: flex; flex-direction: column; gap: 12px; max-width: 500px;">
    <div class="card card-accent-danger card-hover">
        <div class="card-body" style="display: flex; align-items: flex-start; gap: 12px;">
            <span data-icon="alert-circle" style="color: var(--dm-danger); flex-shrink: 0; margin-top: 2px;"></span>
            <div>
                <strong>Critical: Database connection lost</strong>
                <p class="card-text mb-0 text-sm">The primary database is unreachable. Failover
                    initiated.</p>
            </div>
        </div>
    </div>
    <div class="card card-accent-warning card-hover">
        <div class="card-body" style="display: flex; align-items: flex-start; gap: 12px;">
            <span data-icon="alert-triangle" style="color: var(--dm-warning); flex-shrink: 0; margin-top: 2px;"></span>
            <div>
                <strong>Warning: High memory usage</strong>
                <p class="card-text mb-0 text-sm">Server memory at 87%. Consider scaling up.</p>
            </div>
        </div>
    </div>
    <div class="card card-accent-success card-hover">
        <div class="card-body" style="display: flex; align-items: flex-start; gap: 12px;">
            <span data-icon="check-circle" style="color: var(--dm-success); flex-shrink: 0; margin-top: 2px;"></span>
            <div>
                <strong>Deploy succeeded</strong>
                <p class="card-text mb-0 text-sm">Version 2.4.1 is live across all regions.</p>
            </div>
        </div>
    </div>
</div>

Example 6: JS Generation

Pass color: 'accent' to generate an accent card via the JavaScript API. The existing card-${color} pattern means no extra code is needed:

Example 6: JS Generation
<div id="accent-js-card" style="max-width: 420px;"></div>
/**
 * Example 6: JS Generation - from public/showcase/elements/card.
 */
export function mount(root) {
    E.card(root.querySelector('#accent-js-card'), {
        title: 'Generated Accent Card',
        icon: 'star',
        color: 'accent',
        content: '<p>Generated via JavaScript with <code>color: "accent"</code>.</p>',
        collapsible: true
    });
}
// color: 'accent' produces .card-accent automatically
E.card('#accent-js-card', {
    title: 'Generated Accent Card',
    icon: 'star',
    color: 'accent',
    content: '<p>Generated via JavaScript with color: "accent".</p>',
    collapsible: true
});

HTML Generation (DataCard-style)

The Card component can now generate its HTML structure automatically from options, perfect for creating cards dynamically from data. This provides a "DataCard"-style API while maintaining backwards compatibility with existing HTML-based usage.

Example 1: Generated Card with Title and Content

Create a card entirely from JavaScript options:

Domma.elements.card('#generated-card', {
    title: 'Generated Card',
    content: '<p>This card was created entirely from JavaScript options.</p>',
    collapsible: true
});
Example 1: Generated Card with Title and Content
<div id="generated-card" class="card-max-w-500"></div>
/**
 * Example 1: Generated Card with Title and Content - from public/showcase/elements/card.
 */
export function mount(root) {
            // HTML Generation Examples
            // Example 1: Basic generated card
    E.card(root.querySelector('#generated-card'), {
                title: 'Generated Card',
                content: '<p>This card was created entirely from JavaScript options.</p>',
                collapsible: true
            });
}

Example 2: Card with Icon

Add an icon to the card title:

Domma.elements.card('#icon-card', {
    title: 'Settings',
    icon: 'settings',
    content: '<p>Configure your application settings here.</p>',
    collapsible: true
});
Example 2: Card with Icon
<div id="icon-card" class="card-max-w-500"></div>
/**
 * Example 2: Card with Icon - from public/showcase/elements/card.
 */
export function mount(root) {
            // Example 2: Card with icon
    E.card(root.querySelector('#icon-card'), {
                title: 'Settings',
                icon: 'settings',
                content: '<p>Configure your application settings here.</p>',
                collapsible: true
            });
}

Example 3: Colored Card Variant

Apply a color variant to the card:

Domma.elements.card('#colored-card', {
    title: 'Important Notice',
    icon: 'alert',
    color: 'primary',
    content: '<p>This card uses the primary color variant.</p>',
    collapsible: true
});
Example 3: Colored Card Variant
<div id="colored-card" class="card-max-w-500"></div>
/**
 * Example 3: Colored Card Variant - from public/showcase/elements/card.
 */
export function mount(root) {
            // Example 3: Colored card
    E.card(root.querySelector('#colored-card'), {
                title: 'Important Notice',
                icon: 'alert',
                color: 'primary',
                content: '<p>This card uses the primary color variant.</p>',
                collapsible: true
            });
}

Example 4: Initially Collapsed Card

Create a card that starts collapsed:

Domma.elements.card('#collapsed-card', {
    title: 'Click to Expand',
    icon: 'info',
    collapsed: true,
    collapsible: true,
    content: '<p>This card was initially collapsed.</p>'
});
Example 4: Initially Collapsed Card
<div id="collapsed-card" class="card-max-w-500"></div>
/**
 * Example 4: Initially Collapsed Card - from public/showcase/elements/card.
 */
export function mount(root) {
            // Example 4: Initially collapsed
    E.card(root.querySelector('#collapsed-card'), {
                title: 'Click to Expand',
                icon: 'info',
                collapsed: true,
                collapsible: true,
                content: '<p>This card was initially collapsed.</p>'
            });
}

Example 5: Programmatic Control

Control card state with JavaScript methods:

const card = Domma.elements.card('#controlled-card', {
    title: 'Controlled Card',
    icon: 'box',
    collapsible: true,
    content: '<p>Use the buttons to control this card.</p>'
});

// Control methods
$('#expand-btn').on('click', () => card.expand());
$('#collapse-btn').on('click', () => card.collapse());
$('#toggle-btn').on('click', () => card.toggle());
Example 5: Programmatic Control
<div id="controlled-card" class="card-max-w-500"></div>
<div class="flex gap-2 mt-3">
    <button type="button" id="expand-btn" class="btn btn-sm btn-primary">Expand</button>
    <button type="button" id="collapse-btn" class="btn btn-sm btn-secondary">Collapse</button>
    <button type="button" id="toggle-btn" class="btn btn-sm btn-outline">Toggle</button>
</div>
/**
 * Example 5: Programmatic Control - from public/showcase/elements/card.
 */
export function mount(root) {
            // Example 5: Programmatic control
    const controlledCard = E.card(root.querySelector('#controlled-card'), {
                title: 'Controlled Card',
                icon: 'box',
                collapsible: true,
                content: '<p>Use the buttons to control this card.</p>'
            });
    $(root).find('#expand-btn').on('click', () => controlledCard.expand());
    $(root).find('#collapse-btn').on('click', () => controlledCard.collapse());
    $(root).find('#toggle-btn').on('click', () => controlledCard.toggle());
}

Example 6: Dynamic Content Updates

Update card content dynamically using setContent():

const card = Domma.elements.card('#dynamic-card', {
    title: 'Dynamic Content',
    icon: 'refresh',
    collapsible: true,
    content: '<p>Click the button to update this content.</p>'
});

$('#update-btn').on('click', () => {
    const timestamp = new Date().toLocaleTimeString();
    card.setContent(`<p>Content updated at ${timestamp}</p>`);
    card.updateHeight(); // Adjust height after content change
});
Example 6: Dynamic Content Updates
<div id="dynamic-card" class="card-max-w-500"></div>
<button type="button" id="update-btn" class="btn btn-sm btn-primary mt-3">Update Content</button>
/**
 * Example 6: Dynamic Content Updates - from public/showcase/elements/card.
 */
export function mount(root) {
            // Example 6: Dynamic content
    const dynamicCard = E.card(root.querySelector('#dynamic-card'), {
                title: 'Dynamic Content',
                icon: 'refresh',
                collapsible: true,
                content: '<p>Click the button to update this content.</p>'
            });
    $(root).find('#update-btn').on('click', () => {
                const timestamp = D().format('HH:mm:ss');
                dynamicCard.setContent(`<p>Content updated at ${timestamp}</p>`);
                dynamicCard.updateHeight();
            });
}

Example 7: Loading Data into Generated Cards

Perfect for dashboard widgets that load data asynchronously:

// Create card with loading state
const statsCard = Domma.elements.card('#stats-card', {
    title: 'User Statistics',
    icon: 'database',
    collapsible: true,
    content: '<div class="text-center">Loading...</div>'
});

// Simulate async data load
setTimeout(() => {
    const content = `
        <div class="grid grid-cols-2 gap-3">
            <div><strong>Total Users:</strong> 1,234</div>
            <div><strong>Active:</strong> 987</div>
            <div><strong>New Today:</strong> 42</div>
            <div><strong>Growth:</strong> +12%</div>
        </div>
    `;
    statsCard.setContent(content);
    statsCard.updateHeight();
}, 1500);
Example 7: Loading Data into Generated Cards
<div id="stats-card" class="card-max-w-500"></div>
/**
 * Loading data into a generated card - the card shows a skeleton while the
 * (simulated) request runs, then its content is swapped in. The timer is
 * cleared on teardown, so a reset never writes into a card that has gone.
 */
export function mount(root) {
    const statsCard = E.card(root.querySelector('#stats-card'), {
        title: 'User statistics',
        icon: 'database',
        collapsible: true,
        content: '<div class="skeleton-lines" aria-busy="true"><div class="skeleton skeleton-text"></div><div class="skeleton skeleton-text"></div></div>'
    });

    const timer = setTimeout(() => {
        statsCard.setContent(`
            <div class="grid grid-cols-2 gap-3">
                <div><strong>Total users:</strong> 1,234</div>
                <div><strong>Active:</strong> 987</div>
                <div><strong>New today:</strong> 42</div>
                <div><strong>Growth:</strong> +12%</div>
            </div>`);
        statsCard.updateHeight();
    }, 1500);

    return () => clearTimeout(timer);
}

Backwards Compatibility

The enhancement is fully backwards compatible. Existing HTML-based card usage continues to work:

// Still works - enhances existing HTML
<div id="existing-card" class="card">
    <div class="card-body">Existing HTML</div>
</div>

Domma.elements.card('#existing-card', { hover: true });

The Card component automatically detects whether to generate HTML (when title or content options are provided) or enhance existing HTML.

Best Practices

Accessibility

  • Semantic Structure: Use proper heading hierarchy (h1-h6) within card content
  • Clickable Cards: If the entire card is clickable, add role="button" or wrap in an ``tag with meaningful aria-label

Performance

  • Lazy Initialisation: Only initialise hover/click effects on cards that need them
  • Event Delegation: For grids with many cards, consider single parent event listener instead of individual listeners
  • Destroy Instances: Call card.destroy() when removing cards from DOM to prevent memory leaks
  • CSS-First Approach: Use CSS classes (e.g., .card-hover) when JavaScript isn't needed
  • Debounce Hover: For expensive hover effects, consider debouncing the animation triggers

Common Gotchas

Issue Solution
Hover effect not working Ensure you're calling elements.card() with hover: true. CSS-only cards don't have hover effects unless you add .card-hover class manually.
Card stretching in grid Cards in CSS Grid will stretch by default. Add align-self: start or use `height:
                    100%` on cards for equal-height rows. |

| Click events not firing | Check z-index of child elements. Buttons/links inside cards may intercept clicks. Use event.stopPropagation() on child elements if needed. | | Theme colours not applying | Ensure domma-themes.css is loaded and a theme class (e.g., .dm-theme-charcoal-dark) is on . |

Tips & Tricks

  • Loading States: Combine cards with the Loader component for skeleton screens
  • Animations: Stagger card entrance animations for visual interest (use CSS animation-delay)
  • Equal Heights: Use CSS Grid or Flexbox on the parent to create equal-height card rows
  • Responsive Images: Use object-fit: cover for card header images to maintain aspect ratio
  • Truncation: Limit card text with CSS: display: -webkit-box; -webkit-line-clamp: 3; overflow: hidden;
  • Integration: Cards work great inside Modals, Tabs, and Accordion panels

API Reference

Constructor Options

Option Type Default Description
hover Boolean false Enable hover lift effect with enhanced shadow
clickable Boolean false Make the entire card clickable
onClick Function null Callback function when card is clicked (requires clickable: true)
collapsible Boolean false Enable collapsible functionality with header click to toggle
collapsed Boolean false Initial collapsed state (requires collapsible: true)
persistKey String null localStorage key for persisting collapsed state
onCollapse Function null Callback when card is collapsed
onExpand Function null Callback when card is expanded
title String null Card title (triggers HTML generation if provided)
icon String null Icon name to display next to title
content String '' Card body HTML content (triggers HTML generation if title or content provided)
color String null Color variant (e.g., 'primary', 'secondary', 'success')

Methods

Method Parameters Returns Description
expand() None this Expand a collapsed card
collapse() None this Collapse an expanded card
toggle() None this Toggle between collapsed and expanded states
isCollapsed() None Boolean Returns true if card is currently collapsed
setContent(content) String (HTML) this Update the card body content (HTML is sanitized)
getBody() None HTMLElement Get the card body element
updateHeight() None this Recalculate card height after dynamic content changes
destroy() None void Removes event listeners and cleans up the card instance

Events

Event Parameters Description
onClick event: MouseEvent Fired when a clickable card is clicked

CSS Classes

Class Description
.card Base card container with background, border, and shadow
.card-header Optional top section with subtle background
.card-body Main content area with padding
.card-footer Optional bottom section with subtle background
.card-title Primary heading within card body
.card-title-icon Icon displayed next to card title
.card-subtitle Secondary heading with muted colour
.card-text Body text with appropriate line-height
.card-hover CSS-only hover effect (alternative to JavaScript)
.card-primary Card with primary colour border and themed header
.card-accent Left border highlight using the primary theme colour (--dm-primary)
.card-accent-success Left border in the success colour (--dm-success)
.card-accent-danger Left border in the danger colour (--dm-danger)
.card-accent-warning Left border in the warning colour (--dm-warning)
.card-accent-info Left border in the info colour (--dm-info)
.card-collapsible Enables collapsible functionality with smooth transitions
.card-collapsed Applied when card is in collapsed state

Hybrid CSS + JavaScript

Cards work beautifully with pure CSS, but you can enhance them with JavaScript for dynamic content loading, filtering, sorting, animations, and interactive features using Domma's declarative configuration.

Dynamic Content Loading

$.setup({
    '#product-grid': {
        events: {
            ready: async (e, $container) => {
                // Load products from API
                const products = await Domma.http.get('/api/products');

                // Render cards
                const html = _.map(products, product => `
                    <div class="card card-hover">
                        <img src="${product.image}" alt="${product.name}" class="card-img-top">
                        <div class="card-body">
                            <h4 class="card-title">${product.name}</h4>
                            <p class="card-text">${_.truncate(product.description, { length: 100 })}</p>
                            <div class="card-footer">
                                <span class="price">$${product.price}</span>
                                <button class="btn btn-primary btn-sm"
                                        data-id="${product.id}">
                                    Add to Cart
                                </button>
                            </div>
                        </div>
                    </div>
                `).join('');

                $container.html(html);
            },

            'click [data-id]': (e) => {
                const productId = $(e.currentTarget).attr('data-id');
                // Add to cart logic
                Domma.elements.toast.success('Added to cart!');
            }
        }
    }
});

Filterable Card Grid

$.setup({
    '#blog-grid': {
        initial: {
            data: { posts: [], filteredPosts: [], activeCategory: 'all' }
        },
        events: {
            ready: async (e, $el) => {
                const posts = await Domma.http.get('/api/posts');
                $el.data('posts', posts);
                $el.data('filteredPosts', posts);
                renderCards($el, posts);
            },

            'click [data-category]': (e, $el) => {
                const category = $(e.currentTarget).attr('data-category');
                const allPosts = $el.data('posts');

                // Filter posts
                const filtered = category === 'all'
                    ? allPosts
                    : _.filter(allPosts, { category });

                $el.data('activeCategory', category);
                $el.data('filteredPosts', filtered);

                // Update UI
                $('.category-filter').removeClass('active');
                $(e.currentTarget).addClass('active');

                // Render with animation
                $('.cards-container').fadeOut(200, function() {
                    renderCards($el, filtered);
                    $(this).fadeIn(200);
                });
            }
        }
    }
});

function renderCards($container, posts) {
    const html = _.map(posts, post => `
        <div class="card">
            <div class="card-body">
                <h4 class="card-title">${post.title}</h4>
                <p class="card-text">${_.truncate(post.excerpt, { length: 120 })}</p>
                <a href="/post/${post.slug}" class="btn btn-outline">Read More</a>
            </div>
        </div>
    `).join('');

    $container.find('.cards-container').html(html);
}

Sortable Cards

$.setup({
    '#team-grid': {
        initial: {
            data: { members: [] }
        },
        events: {
            ready: async (e, $el) => {
                const members = await Domma.http.get('/api/team');
                $el.data('members', members);
                renderTeam($el, members);
            },

            'change #sort-by': (e, $el) => {
                const sortBy = $(e.target).val();
                const members = $el.data('members');

                // Sort with utils
                const sorted = sortBy === 'name'
                    ? _.sortBy(members, 'name')
                    : _.orderBy(members, ['role', 'name'], ['asc', 'asc']);

                renderTeam($el, sorted);
            }
        }
    }
});

function renderTeam($container, members) {
    const html = _.map(members, member => `
        <div class="card text-center">
            <img src="${member.avatar}" alt="${member.name}"
                 class="card-img-top" class="team-member-avatar">
            <div class="card-body">
                <h4 class="card-title">${member.name}</h4>
                <p class="card-text">${member.role}</p>
            </div>
        </div>
    `).join('');

    $container.find('.team-container').html(html);
}

Interactive Card Actions

$.setup({
    '.product-card': {
        component: 'card',
        options: { hover: true, clickable: true },
        events: {
            click: (e, $card) => {
                const productId = $card.attr('data-product');
                window.location.href = `/product/${productId}`;
            },

            'click .btn-favorite': (e) => {
                e.stopPropagation(); // Don't trigger card click

                const $btn = $(e.currentTarget);
                const $card = $btn.closest('.card');
                const productId = $card.attr('data-product');

                // Toggle favorite
                $btn.toggleClass('active');
                const isFavorite = $btn.hasClass('active');

                Domma.http.post('/api/favorites', { productId, favorite: isFavorite })
                    .then(() => {
                        Domma.elements.toast.success(
                            isFavorite ? 'Added to favorites' : 'Removed from favorites'
                        );
                    });
            },

            'mouseenter': (e, $card) => {
                $card.find('.quick-actions').fadeIn(200);
            },

            'mouseleave': (e, $card) => {
                $card.find('.quick-actions').fadeOut(200);
            }
        }
    }
});

Benefits of Hybrid Approach

  • Performance - CSS handles layout instantly, JS loads dynamic content
  • Flexibility - Static cards + dynamic data = powerful combination
  • Interactivity - Filtering, sorting, actions enhance UX
  • Maintainability - Centralized logic with $.setup()

CSS-Only vs CSS+JS Hybrid

CSS-Only (Static)

<div class="card">
    <img src="image.jpg"
         class="card-img-top"
         alt="Product">
    <div class="card-body">
        <h4 class="card-title">
            Static Product
        </h4>
        <p class="card-text">
            Fixed content
        </p>
        <a href="#" class="btn">
            View
        </a>
    </div>
</div>

<!-- No dynamic content -->

CSS+JS Hybrid (Dynamic)

<div id="products">
    <!-- Cards loaded via JS -->
</div>

<script>
$.setup({
    '#products': {
        events: {
            ready: async (e, $el) => {
                const products = await
                    Domma.http.get('/api/products');

                const html = _.map(products, p => `
                    <div class="card">
                        ...
                    </div>
                `).join('');

                $el.html(html);
            }
        }
    }
});
</script>

CSS Customisation

Override these CSS variables to customise Card appearance and match your design system.

Variable Default Controls
--dm-card-bg var(--dm-surface) Card background colour
--dm-card-border var(--dm-border) Card border colour
--dm-card-shadow var(--dm-shadow) Card shadow intensity
--dm-radius-lg 0.5rem Card corner radius
--dm-surface var(--dm-white) General surface colour (inherited)
--dm-primary var(--dm-blue-600) Primary colour for .card-primary and .card-accent
--dm-success theme-defined Accent border colour for .card-accent-success
--dm-danger theme-defined Accent border colour for .card-accent-danger
--dm-warning theme-defined Accent border colour for .card-accent-warning
--dm-info theme-defined Accent border colour for .card-accent-info

Example Override

:root {
    /* Dark card theme */
    --dm-card-bg: #1e293b;
    --dm-card-border: #334155;
    --dm-card-shadow: 0 10px 15px rgba(0,0,0,0.3);

    /* Or flat design */
    --dm-card-shadow: none;
    --dm-radius-lg: 0;
}

Full CSS Customisation Cheat-Sheet →

Related Elements

Cards work great with these complementary components:

Modal

Display cards within modals for detailed views or forms.

Tabs

Organise cards into tabbed sections for categorised content.

Accordion

Use cards as accordion items for collapsible content.

Loader

Add loading states to cards for async content.