browser-style

Content Card

Content Card is a generic card-component that can be extended into a variety of card types, such as articles, products, videos, and more. It can output structured markup for SEO and rich snippets. It consists of two areas: Media Area and Content Area.

Media Area

This area contains visual elements like images or videos.

Content Area

This area contains the main information of the card, including text, metadata, and actions.

Popover

A generic popover can be attached to any card type.


Card Types

Here are the card types supported by the component, with examples in data.json. Each card type can optionally include Schema.org structured data markup when useSchema: true (default setting).

1. Article Card

A standard card for blog posts, news, or articles.

2. Product Card

Designed to showcase a product for e-commerce.

3. Video Card

A card focused on presenting a video.

4. Author/Profile Card

A simple card to feature a person.

5. Event Card

Used to announce and provide details about an event.

6. Poll Card

An interactive card to engage users with a question.

7. Quote Card

A simple, visual card for displaying quotes.

8. News Card

Similar to Article Card but specifically for news content with NewsArticle schema.

9. Recipe Card

A specialized card for food recipes with Recipe schema.

10. Event Card

Designed for events, conferences, and gatherings with Event schema.

11. Business Card

A professional card for local businesses with LocalBusiness schema.

12. Poll Card

An interactive card for user polls and surveys.

13. FAQ Card

A card for frequently asked questions with FAQPage schema.

14. Timeline Card

A card for displaying chronological events with EventSeries schema.


Schema.org Integration

Overview

All card types support optional Schema.org structured data markup, which enhances SEO and enables rich snippets in search results. Schema markup is enabled by default (useSchema: true) but can be disabled per card or globally.

Disabling Schema Markup

// Disable for a specific card
const cardSettings = { useSchema: false };
card.dataset = { data: cardData, settings: cardSettings };

// Disable via HTML attribute
<article-card settings='{"useSchema": false}'></article-card>

Schema Benefits

Schema Properties by Card Type

Article/News Cards

Recipe Cards

Event Cards

Business Cards

Product Cards


Component Architecture

BaseCard Class

The BaseCard class serves as the foundation for all card types, providing:

Key Features

Usage Examples

// Setting data programmatically
const articleCard = document.createElement('article-card');
articleCard.dataset = {
  data: articleData,
  settings: { 
    useSchema: true,
    styles: { 'cc-headline': 'class="custom-headline"' }
  }
};

// Using settings attribute
<recipe-card settings='{"useSchema": false}'></recipe-card>

// Accessing card properties
const settings = articleCard.settings;
const data = articleCard.data;


Data Structure

Common Data Properties

All card types share a common data structure with the following top-level properties:

{
  "id": "unique-identifier",           // Unique card identifier
  "type": "article|news|recipe|...",   // Card type for component selection
  "media": { ... },                    // Media configuration (optional)
  "ribbon": { ... },                   // Ribbon overlay (optional)
  "sticker": { ... },                  // Sticker/badge overlay (optional)
  "content": { ... },                  // Core content data
  "authors": [...],                    // Author information (optional)
  "engagement": { ... },               // Engagement metrics (optional)
  "tags": [...],                       // Tag/category links (optional)
  "links": [...],                      // Navigation links (optional)
  "actions": [...],                    // Action buttons (optional)
  // Type-specific properties (recipe, event, business, poll, etc.)
}

Media Configuration

"media": {
  "sources": [
    {
      "type": "image|video",
      "src": "path/to/media",
      "alt": "Description",
      "srcset": "responsive-sizes", // Optional
      "width": 800,                 // Optional
      "height": 600,                // Optional
      "loading": "lazy|eager",      // Optional
      "poster": "path/to/poster"    // For videos
    }
  ],
  "caption": "Media description"      // Optional figcaption
}

Overlay Elements

// Ribbon - larger banner overlay
"ribbon": {
  "text": "FEATURED",
  "style": "featured|live|popular",
  "color": "#ff0000"                  // Optional custom color
}

// Sticker - smaller badge overlay  
"sticker": {
  "text": "NEW",
  "style": "badge|pill",
  "position": "top-left|top-right|bottom-left|bottom-right"
}

Content Structure

"content": {
  "category": "Category Name",         // Content categorization
  "headline": "Main Title",           // Primary heading
  "headlineTag": "h2",                // HTML heading level (default: h2)
  "subheadline": "Secondary Title",   // Optional subtitle
  "summary": "Brief description",     // Short description text
  "text": "Detailed content",          // Full content (can be HTML or array)
  "published": {
    "datetime": "2025-07-16T10:00:00Z",
    "formatted": "July 16, 2025"
  },
  "modified": {                       // Optional update information
    "datetime": "2025-07-16T12:00:00Z", 
    "formatted": "(Updated July 16, 2025)"
  },
  "readingTime": "5 min read"         // Time estimate
}

Author Information

"authors": [
  {
    "name": "John Doe",
    "role": "Senior Developer",        // Optional job title/role
    "avatar": "path/to/avatar.jpg",    // Optional profile image
    "url": "https://johndoe.com",      // Optional profile link
    "contacts": [                      // Optional contact methods
      {
        "type": "email|twitter|linkedin",
        "url": "mailto:john@example.com",
        "label": "Email John"
      }
    ]
  }
]

Engagement Metrics

"engagement": {
  "reactions": [
    {
      "type": "like|dislike|love",
      "icon": "thumbs_up",
      "count": 150,
      "value": 4.5,                   // For ratings (optional)
      "max": 5,                       // Maximum rating value (optional)
      "min": 0,                       // Minimum rating value (optional)
      "active": false,                // User's current state (optional)
      "ariaLabel": "Like this post"
    }
  ],
  "commentCount": 25,
  "shareCount": 80,
  "viewCount": 1500
}
"links": [
  {
    "url": "https://example.com",
    "text": "Read More",
    "icon": "external-link",          // Optional icon
    "hideText": false,                // Show/hide text (default: false)
    "isWrapper": false                // Make entire card clickable (default: false)
  }
]

"actions": [
  {
    "text": "Save Article",
    "icon": "bookmark",               // Optional icon
    "url": "https://save.com",        // For link actions
    "ariaLabel": "Save this article", // Accessibility label
    "popover": {                      // For popover actions
      "content": "<h3>Saved!</h3><p>Article saved to your reading list.</p>"
    },
    "attributes": {                   // Additional HTML attributes
      "type": "submit",
      "data-action": "save"
    }
  }
]

Type-Specific Properties

Recipe Data

"recipe": {
  "prepTime": "PT15M",               // ISO 8601 duration or readable format
  "cookTime": "PT30M", 
  "servings": "4",
  "instructions": [                  // Step-by-step instructions
    "Preheat oven to 350°F",
    "Mix ingredients in a bowl",
    "Bake for 30 minutes"
  ]
}

Event Data

"event": {
  "startDate": "2025-10-25T09:00:00",
  "endDate": "2025-10-26T17:00:00",
  "location": {
    "name": "Convention Center",
    "address": "123 Main St, City, State 12345"
  },
  "organizer": {
    "name": "Event Organization"
  },
  "offers": [
    {
      "name": "Early Bird",
      "price": "299",
      "currency": "USD"
    }
  ],
  "status": "Scheduled",             // EventStatus
  "attendanceMode": "OfflineEventAttendanceMode"
}

Business Data

"business": {
  "address": {
    "streetAddress": "123 Main Street",
    "addressLocality": "San Francisco", 
    "addressRegion": "CA",
    "postalCode": "94105",
    "addressCountry": "US"
  },
  "geo": {
    "latitude": "37.7749",
    "longitude": "-122.4194",
    "mapProvider": {
      "type": "openstreetmap",
      "name": "OpenStreetMap",
      "url": "https://www.openstreetmap.org/export/embed.html?bbox={lng1},{lat1},{lng2},{lat2}",
      "latOffset": 0.003,
      "lngOffset": 0.005,
      "zoom": 15
    }
  },
  "telephone": "+1-415-555-0123",
  "email": "hello@business.com",
  "website": "https://business.com",
  "sameAs": [                        // Social media profiles
    "https://facebook.com/business",
    "https://twitter.com/business"
  ],
  "foundingDate": "2018-03-15",
  "openingHours": [
    {
      "schema": "Mo-Fr 06:00-20:00",  // Schema.org format
      "display": "Monday - Friday: 6:00 AM - 8:00 PM"
    }
  ]
}

Poll Data

"poll": {
  "endpoint": "./api/polls/poll-1.json",
  "allowMultiple": false,            // Allow multiple selections
  "showResults": "afterVote",        // When to show results
  "totalVotes": 5420,
  "labels": {                        // Customizable UI labels
    "vote": "Vote",
    "submitVote": "Submit Vote", 
    "results": "Results",
    "totalVotes": "Total votes:",
    "votes": "votes"
  }
}

Below is the proposed semantic HTML markup for each component part.

Media Area

<figure class="ic-media">
    <!-- Image -->
    <img 
        class="ic-media-image" 
        src="..." 
        srcset="..." 
        alt="..." 
        width="..." 
        height="..." 
        loading="lazy"
    >

    <!-- Video -->
    <video 
        class="ic-media-video" 
        src="..." 
        poster="..."
        controls 
        autoplay 
        muted 
        loop
    >
        <track kind="captions" src="..." srclang="en" label="English">
    </video>

    <!-- 
      Overlays 
      Using role="status" helps assistive technologies identify these as status indicators.
    -->
    <div class="ic-ribbon" role="status">FEATURED</div>
    <span class="ic-sticker" role="status">NEW</span>

    <!-- Caption -->
    <figcaption class="ic-media-caption">This is a caption.</figcaption>
</figure>

Note on Gallery Usage: The <figure> element is flexible and can contain multiple images or videos to create a small gallery. The figcaption would then describe the entire gallery.

<figure class="ic-media">
    <img class="ic-media-image" src="gallery-1.jpg" alt="...">
    <img class="ic-media-image" src="gallery-2.jpg" alt="...">
    <figcaption class="ic-media-caption">A gallery of items.</figcaption>
</figure>

Content Area

<article class="ic-content">
    <!-- Core Content -->
    <strong class="ic-category">Technology</strong>
    <!-- The heading tag below is dynamic (h2, h3, etc.) based on content.headlineTag -->
    <h2 class="ic-headline">Card Headline</h2>
    <h3 class="ic-subheadline">Card Subheadline</h3>
    <p class="ic-summary">A brief summary of the content.</p>
    <div class="ic-text">
        <p>More detailed text can go here, including <strong>HTML</strong> elements.</p>
    </div>

    <!-- Metadata & Engagement -->
    <div class="ic-meta">
        <time class="ic-published" datetime="2023-10-01T12:00:00Z">October 1, 2023</time>
        <span class="ic-reading-time">5 min read</span>
    </div>
    <div class="ic-engagement">
        <button class="ic-reaction" aria-pressed="false">
            <span class="ic-reaction-icon">👍</span> 100
        </button>
        <span class="ic-comments">25 comments</span>
        <span class="ic-views">1.5k views</span>
    </div>

    <!-- 
      Attribution & Grouping 
      The <address> tag is used here because it semantically represents the contact information 
      for the author(s) of the parent <article> element.
    -->
    <address class="ic-authors">
        <div class="ic-author">
            <a href="/authors/john-doe" class="ic-author-link">
                <img class="ic-avatar" src="..." alt="John Doe" width="40" height="40">
                <span class="ic-author-name">John Doe</span>
            </a>
            <span class="ic-author-role">Author</span>
        </div>
        <!-- Repeat .ic-author div for multiple authors -->
    </address>
    <ul class="ic-tags">
        <li><a href="/tags/tech" class="ic-tag">Technology</a></li>
    </ul>

    <!-- Product Information -->
    <section class="ic-product">
        <h4 class="ic-product-name">Premium Course</h4>
        <div class="ic-product-price">
            <span>USD 49.99</span>
            <del class="ic-product-price-original">USD 69.99</del>
            <span class="ic-product-discount">Save 28%</span>
        </div>
        <div class="ic-product-rating" role="img" aria-label="Rating: 4.7 out of 5 stars">
            <span aria-hidden="true">★★★★☆</span>
            <span>(4.7/5 from 356 ratings)</span>
        </div>
        <span class="ic-product-availability">In Stock</span>
    </section>

    <!-- Actions -->
    <div class="ic-actions">
        <a href="..." class="ic-action-primary">
            <span class="ic-action-icon">▶</span> Read more
        </a>
        <button type="button" class="ic-action-secondary">
            <span class="ic-action-icon">...</span> Save
        </button>
    </div>
</article>

<!-- Popover Structure -->
<div id="popover-id" popover class="ic-popover">
    <button type="button" popovertarget="popover-id" popovertargetaction="hide" class="ic-popover-close">✕</button>
    <!-- Popover content is rendered here based on type (video, article, etc.) -->
    <div class="ic-popover-article">...</div>
</div>

Customization and Styling

Settings Configuration

Each card can be customized through the settings object:

const settings = {
  // Schema.org structured data
  useSchema: true,                    // Enable/disable schema markup (default: true)
  
  // Custom styling
  styles: {
    'cc-headline': 'class="custom-headline text-lg font-bold"',
    'cc-summary': 'class="text-gray-600"',
    'cc-content': 'class="p-4"'
  },
  
  // Responsive image configuration
  srcsetBreakpoints: [280, 500, 720, 1080, 1440], // Custom breakpoints
  layoutIndex: 0,                     // Layout position for responsive sizing
  layoutSrcsets: [...],               // Pre-calculated srcset configurations
  layoutSrcset: "...",                // Computed srcset for current layout
  
  // Component-specific settings
  componentSettings: {
    // Card-type specific configurations
  }
};

CSS Class Structure

The component uses a consistent CSS class naming convention:

/* Base component classes */
.cc                     /* Base card container */
.cc-content             /* Content area wrapper */
.cc-media               /* Media area wrapper */

/* Content elements */
.cc-category            /* Content category/tagline */
.cc-headline            /* Primary heading */
.cc-subheadline         /* Secondary heading */
.cc-summary             /* Brief description */
.cc-text                /* Detailed content */

/* Metadata and engagement */
.cc-meta                /* Publication metadata wrapper */
.cc-published           /* Publication date */
.cc-reading-time        /* Reading/prep time */
.cc-engagement          /* Engagement metrics wrapper */
.cc-reaction            /* Reaction buttons */
.cc-comments            /* Comment count */
.cc-views               /* View count */

/* Attribution */
.cc-authors             /* Authors wrapper */
.cc-author              /* Individual author */
.cc-avatar              /* Author avatar image */
.cc-author-name         /* Author name */
.cc-author-role         /* Author role/title */
.cc-tags                /* Tags wrapper */
.cc-tag                 /* Individual tag */

/* Actions and links */
.cc-actions             /* Actions wrapper */
.cc-action-primary      /* Primary action button */
.cc-action-secondary    /* Secondary action button */
.cc-links               /* Links wrapper */

/* Media elements */
.cc-media-image         /* Media images */
.cc-media-video         /* Media videos */
.cc-media-caption       /* Media captions */
.cc-ribbon              /* Ribbon overlays */
.cc-sticker             /* Sticker/badge overlays */

/* Type-specific classes */
.cc-recipe-meta         /* Recipe metadata */
.cc-recipe-ingredients  /* Recipe ingredients */
.cc-recipe-instructions /* Recipe instructions */
.cc-event-location      /* Event location */
.cc-event-organizer     /* Event organizer */
.cc-business-map        /* Business map embed */
.cc-faq                 /* FAQ wrapper */
.cc-faq-item            /* FAQ item */
.cc-faq-title           /* FAQ question */
.cc-faq-panel           /* FAQ answer */
.cc-timeline            /* Timeline wrapper */
.cc-timeline-item       /* Timeline item */
.cc-timeline-headline   /* Timeline event title */
.cc-timeline-text       /* Timeline event description */
.cc-poll-form           /* Poll form */
.cc-poll-option         /* Poll option */
.cc-poll-results        /* Poll results */

Style Injection

The getStyle() utility function allows dynamic style injection:

// In card render methods
${getStyle('cc-headline', settings)}
// Outputs: class="cc-headline" or custom styles from settings.styles['cc-headline']

// Custom styles example
settings.styles['cc-headline'] = 'class="text-2xl font-bold text-blue-600"';

Responsive Behavior

Cards are designed to be responsive by default:

Theme Integration

Cards can integrate with various CSS frameworks and design systems:

Tailwind CSS Example

const tailwindSettings = {
  styles: {
    'cc-content': 'class="p-6 bg-white rounded-lg shadow-md"',
    'cc-headline': 'class="text-xl font-bold text-gray-900 mb-2"',
    'cc-summary': 'class="text-gray-600 leading-relaxed"',
    'cc-actions': 'class="flex gap-2 mt-4"',
    'cc-action-primary': 'class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"'
  }
};

CSS Custom Properties

.cc {
  --cc-border-radius: 8px;
  --cc-padding: 1rem;
  --cc-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  --cc-primary-color: #3b82f6;
  --cc-text-color: #374151;
}

.cc-content {
  padding: var(--cc-padding);
  border-radius: var(--cc-border-radius);
  box-shadow: var(--cc-shadow);
  color: var(--cc-text-color);
}

Implementation Examples

Basic Usage

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="./index.css">
</head>
<body>
  <!-- Article Card -->
  <article-card id="article-1"></article-card>
  
  <!-- Recipe Card with custom settings -->
  <recipe-card 
    id="recipe-1" 
    settings='{"useSchema": true, "styles": {"cc-headline": "class=\"large-title\""}}'>
  </recipe-card>
  
  <script type="module">
    import './cards/ArticleCard.js';
    import './cards/RecipeCard.js';
    
    // Load and set data
    fetch('./data.json')
      .then(response => response.json())
      .then(data => {
        const articleCard = document.getElementById('article-1');
        const articleData = data.find(item => item.id === 'article-1');
        articleCard.dataset = { data: articleData };
        
        const recipeCard = document.getElementById('recipe-1');
        const recipeData = data.find(item => item.id === 'recipe-1');
        recipeCard.dataset = { data: recipeData };
      });
  </script>
</body>
</html>

Dynamic Card Creation

import { ArticleCard } from './cards/ArticleCard.js';
import { RecipeCard } from './cards/RecipeCard.js';
import { BusinessCard } from './cards/BusinessCard.js';

class CardRenderer {
  static cardTypes = {
    'article': ArticleCard,
    'recipe': RecipeCard,
    'business': BusinessCard,
    // ... other card types
  };

  static createCard(data, settings = {}) {
    const CardClass = this.cardTypes[data.type];
    if (!CardClass) {
      console.warn(`Unknown card type: ${data.type}`);
      return null;
    }

    const card = new CardClass();
    card.dataset = { data, settings };
    return card;
  }

  static renderCards(container, cardsData, globalSettings = {}) {
    const fragment = document.createDocumentFragment();
    
    cardsData.forEach(cardData => {
      const card = this.createCard(cardData, globalSettings);
      if (card) {
        fragment.appendChild(card);
      }
    });
    
    container.appendChild(fragment);
  }
}

// Usage
const container = document.getElementById('cards-container');
const cardsData = await fetch('./data.json').then(r => r.json());
const settings = { 
  useSchema: true,
  styles: {
    'cc-content': 'class="card-content"'
  }
};

CardRenderer.renderCards(container, cardsData, settings);

Custom Card Type

import { BaseCard } from '../base/BaseCard.js';
import { getStyle, renderActions, renderHeader, renderMedia } from '../base/utils.js';

export class CustomCard extends BaseCard {
  constructor() {
    super();
  }

  render() {
    const renderContext = this._setSchema('Thing'); // Set appropriate schema
    if (!renderContext) return '';
    
    const { settings, useSchema, content, headlineTag } = renderContext;
    const { customData = {} } = this.data;

    return `
      ${this.data.media ? renderMedia(this.data.media, this.data.ribbon, this.data.sticker, useSchema, settings) : ''}
      <div ${getStyle('cc-content', settings)}>
        ${renderHeader(content, settings)}
        ${content.headline ? `<${headlineTag} ${getStyle('cc-headline', settings)} ${useSchema ? 'itemprop="name"' : ''}>${content.headline}</${headlineTag}>` : ''}
        
        <!-- Custom content rendering -->
        ${customData.specialField ? `<div ${getStyle('cc-custom-field', settings)}>${customData.specialField}</div>` : ''}
        
        ${renderActions(this.data.actions, useSchema, settings)}
      </div>
    `;
  }
}

// Register the custom element
customElements.define('custom-card', CustomCard);

API Reference

BaseCard Methods

dataset (getter/setter)

Sets or gets the complete card data and settings.

card.dataset = { data: cardData, settings: cardSettings };
const { data, settings } = card.dataset;

data (getter)

Returns the card’s data object.

const cardData = card.data;

settings (getter)

Returns the resolved settings object with defaults applied.

const cardSettings = card.settings;

getStyle(componentName)

Returns the style string for a given component class name.

const headlineStyle = card.getStyle('cc-headline');
// Returns: 'class="cc-headline"' or custom styles

Utility Functions

The utils.js module provides common rendering functions:

renderMedia(media, ribbon, sticker, useSchema, settings)

Renders the media area with images, videos, and overlays.

renderHeader(content, settings)

Renders the category/tagline header section.

renderAuthors(authors, useSchema, settings)

Renders author information with avatars and contact links.

renderEngagement(engagement, useSchema, settings)

Renders engagement metrics (likes, comments, shares, views).

renderTags(tags, settings)

Renders tag links.

Renders navigation links.

renderActions(actions, useSchema, settings)

Renders action buttons with popover support.

getStyle(className, settings)

Returns appropriate style attributes for a given class name.


Browser Support


Performance Considerations


Accessibility Features


Backend Implementation

For complete Umbraco CMS implementation details including document types, compositions, property editors, and deployment strategies, see the dedicated Umbraco Implementation Guide. ````