CSS3 & Responsive Design

Cards, Modals & Common UI Patterns

30 min Lesson 50 of 60

Why UI Patterns Matter

Modern web interfaces are built from a toolkit of reusable visual patterns: cards that group related content, modals that demand focused attention, tooltips that provide contextual help, accordions that organize dense information, and progress indicators that communicate status. These patterns appear on virtually every website and application you use. Understanding how to build them with pure HTML and CSS -- without relying on a framework or JavaScript library -- gives you complete control over design, performance, and accessibility.

In this lesson, you will master the anatomy and construction of the most important CSS UI patterns. You will build cards with hover effects, image overlays, and responsive layouts; create modals using both the CSS :target technique and the native <dialog> element; style toast notifications, tooltips, accordions, progress bars, avatar components, and skeleton loading screens. Each pattern is built with semantic HTML, styled with CSS, and designed with accessibility in mind.

Card Anatomy

A card is a self-contained content unit that groups related information into a single visual block. The typical card has five distinct zones: an image area at the top, a header with a title, a body with descriptive text, optional metadata or tags, and a footer with action buttons or links. Not every card needs all five zones -- you should include only the parts that serve your content.

Basic Card Structure

<!-- HTML Structure -->
<article class="card">
    <div class="card__image">
        <img src="/images/project.jpg" alt="Dashboard project screenshot" loading="lazy">
    </div>
    <div class="card__header">
        <h3 class="card__title">Dashboard Redesign</h3>
        <span class="card__meta">Published Jan 15, 2025</span>
    </div>
    <div class="card__body">
        <p>A complete redesign of the analytics dashboard with improved data visualization and responsive layouts.</p>
    </div>
    <div class="card__footer">
        <a href="#" class="card__action">View Project</a>
        <a href="#" class="card__action card__action--secondary">Source Code</a>
    </div>
</article>

/* CSS */
.card {
    background: var(--bg-white);
    border: 1px solid var(--border-light);
    border-radius: 8px;
    overflow: hidden;
    display: flex;
    flex-direction: column;
}

.card__image img {
    width: 100%;
    height: 200px;
    object-fit: cover;
    display: block;
}

.card__header {
    padding: 1.25rem 1.25rem 0;
}

.card__title {
    margin: 0 0 0.25rem;
    font-size: 1.15rem;
    color: var(--text-dark);
}

.card__meta {
    font-size: 0.8rem;
    color: var(--text-light);
}

.card__body {
    padding: 0.75rem 1.25rem;
    flex: 1;
    color: var(--text-dark);
    font-size: 0.95rem;
    line-height: 1.6;
}

.card__footer {
    padding: 0 1.25rem 1.25rem;
    display: flex;
    gap: 0.75rem;
}

.card__action {
    display: inline-block;
    padding: 0.5rem 1rem;
    text-decoration: none;
    font-size: 0.85rem;
    font-weight: 600;
    border-radius: 4px;
    color: var(--bg-white);
    background-color: var(--primary);
    transition: background-color 0.2s;
}

.card__action:hover {
    opacity: 0.9;
}

.card__action--secondary {
    background-color: transparent;
    color: var(--primary);
    border: 1px solid var(--primary);
}

.card__action--secondary:hover {
    background-color: var(--primary-light);
}
Note: Using the <article> element for cards is semantically appropriate when each card represents a self-contained piece of content that could stand on its own (like a blog post, product, or project). If the card is purely decorative or is part of a larger content unit, a <div> is more appropriate. The flex-direction: column and flex: 1 on the body ensure the card stretches to fill equal height when cards are placed in a grid, pushing the footer to the bottom.

Card Hover Effects

Hover effects provide visual feedback that a card is interactive. Common effects include subtle elevation changes, border color shifts, and upward translation. The key is subtlety -- the effect should feel natural, not jarring. Always include a transition to smooth the animation and use :focus-within alongside :hover so keyboard users get the same visual feedback.

Card Hover and Focus Effects

/* Lift effect with shadow */
.card {
    transition: transform 0.25s ease, box-shadow 0.25s ease;
}

.card:hover,
.card:focus-within {
    transform: translateY(-4px);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}

/* Border color change */
.card--bordered {
    border: 2px solid var(--border-light);
    transition: border-color 0.2s;
}

.card--bordered:hover,
.card--bordered:focus-within {
    border-color: var(--primary);
}

/* Image zoom on hover */
.card__image {
    overflow: hidden;
}

.card__image img {
    transition: transform 0.3s ease;
}

.card:hover .card__image img,
.card:focus-within .card__image img {
    transform: scale(1.05);
}

/* Combined: lift + image zoom + shadow */
.card--interactive {
    transition: transform 0.3s ease, box-shadow 0.3s ease;
}

.card--interactive:hover,
.card--interactive:focus-within {
    transform: translateY(-6px);
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.1);
}

.card--interactive:hover .card__image img {
    transform: scale(1.08);
}

Card Layouts with Flexbox and Grid

Cards are almost never displayed alone -- they appear in grids. CSS Grid is ideal for card layouts because it handles equal-height columns automatically and lets you create responsive layouts with minimal code. The auto-fill and minmax() combination creates a fully responsive grid without any media queries.

Responsive Card Grid Layouts

/* Auto-responsive grid: no media queries needed */
.card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 1.5rem;
    padding: 1.5rem;
}

/* Fixed 3-column grid */
.card-grid--three {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 1.5rem;
}

/* Flexbox alternative for wrapping cards */
.card-row {
    display: flex;
    flex-wrap: wrap;
    gap: 1.5rem;
}

.card-row .card {
    flex: 1 1 300px;
    max-width: calc(33.333% - 1rem);
}

/* Two-column layout for larger cards */
.card-grid--two {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
    gap: 2rem;
}

/* Featured card: spans two columns */
.card-grid .card--featured {
    grid-column: span 2;
}

@media (max-width: 768px) {
    .card-row .card {
        max-width: 100%;
    }
    .card-grid .card--featured {
        grid-column: span 1;
    }
}

Horizontal Cards

Horizontal cards display the image side by side with the content instead of stacking vertically. This layout works well for blog post previews, search results, and list-style content. The technique uses Flexbox with flex-direction: row and gives the image a fixed width while the content area fills the remaining space.

Horizontal Card Layout

<!-- HTML Structure -->
<article class="card card--horizontal">
    <div class="card__image">
        <img src="/images/post.jpg" alt="Blog post cover">
    </div>
    <div class="card__content">
        <h3 class="card__title">Understanding CSS Grid</h3>
        <p class="card__body">A deep dive into CSS Grid layout with practical examples and common patterns.</p>
        <div class="card__footer">
            <span class="card__meta">5 min read</span>
            <a href="#">Read More</a>
        </div>
    </div>
</article>

/* CSS */
.card--horizontal {
    flex-direction: row;
}

.card--horizontal .card__image {
    flex: 0 0 240px;
}

.card--horizontal .card__image img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.card--horizontal .card__content {
    flex: 1;
    display: flex;
    flex-direction: column;
    padding: 1.25rem;
}

.card--horizontal .card__body {
    flex: 1;
    padding: 0;
}

.card--horizontal .card__footer {
    padding: 0;
    margin-top: 0.75rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

/* Stack vertically on small screens */
@media (max-width: 600px) {
    .card--horizontal {
        flex-direction: column;
    }
    .card--horizontal .card__image {
        flex: none;
        height: 200px;
    }
}

Cards with Ribbons, Badges, and Overlays

Visual accents like ribbons, badges, and image overlays draw attention to specific cards. Ribbons typically appear diagonally across a corner, badges sit in a corner as small labels, and overlays cover the image with text or gradient effects. All of these are achieved with CSS positioning and pseudo-elements.

Card Badges and Overlays

/* Corner badge */
.card--with-badge {
    position: relative;
}

.card__badge {
    position: absolute;
    top: 1rem;
    right: 1rem;
    padding: 0.25rem 0.75rem;
    background-color: var(--primary);
    color: white;
    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
    border-radius: 999px;
    z-index: 1;
}

/* Image overlay with gradient */
.card__image--overlay {
    position: relative;
}

.card__image--overlay::after {
    content: "";
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 60%;
    background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
    pointer-events: none;
}

.card__image--overlay .card__overlay-text {
    position: absolute;
    bottom: 1rem;
    left: 1rem;
    right: 1rem;
    color: white;
    z-index: 1;
    font-weight: 600;
}

/* Ribbon effect */
.card__ribbon {
    position: absolute;
    top: 0;
    left: 0;
    background-color: var(--primary);
    color: white;
    padding: 0.35rem 2rem;
    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
    transform: rotate(-45deg) translate(-30%, -10%);
    transform-origin: center;
    z-index: 1;
}

/* Full image overlay on hover */
.card__image--hover-overlay {
    position: relative;
}

.card__image--hover-overlay::before {
    content: "View Details";
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: rgba(0, 0, 0, 0.6);
    color: white;
    font-weight: 600;
    font-size: 1rem;
    opacity: 0;
    transition: opacity 0.3s;
    z-index: 1;
}

.card:hover .card__image--hover-overlay::before {
    opacity: 1;
}

Modal / Dialog Overlay

A modal (also called a dialog) is a window that overlays the main page content and demands user interaction before they can return to the page. Modals require three CSS components: a semi-transparent backdrop that covers the entire viewport, a centered content box, and smooth open/close transitions. There are two approaches: a pure CSS technique using the :target pseudo-class, and the modern HTML <dialog> element with its ::backdrop pseudo-element.

CSS-Only Modal with :target

The :target pseudo-class matches an element whose id corresponds to the URL fragment identifier (the part after #). By linking to a modal's id, you can show and hide it without JavaScript. The user clicks a link with href="#modal-id" to open it, and a close link with href="#" or href="#!" to dismiss it.

CSS-Only Modal Using :target

<!-- Trigger link -->
<a href="#demo-modal">Open Modal</a>

<!-- Modal structure -->
<div class="modal" id="demo-modal">
    <div class="modal__backdrop"></div>
    <div class="modal__content" role="dialog" aria-labelledby="modal-title">
        <h2 id="modal-title">Confirm Action</h2>
        <p>Are you sure you want to proceed?</p>
        <div class="modal__actions">
            <a href="#!" class="modal__close">Cancel</a>
            <a href="#!" class="modal__confirm">Confirm</a>
        </div>
    </div>
</div>

/* CSS */
.modal {
    position: fixed;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1000;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.3s, visibility 0.3s;
}

/* Show when targeted */
.modal:target {
    opacity: 1;
    visibility: visible;
}

.modal__backdrop {
    position: absolute;
    inset: 0;
    background-color: rgba(0, 0, 0, 0.5);
}

.modal__content {
    position: relative;
    background: var(--bg-white);
    border-radius: 8px;
    padding: 2rem;
    max-width: 480px;
    width: 90%;
    box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);
    transform: translateY(-20px);
    transition: transform 0.3s;
}

.modal:target .modal__content {
    transform: translateY(0);
}

.modal__actions {
    display: flex;
    gap: 0.75rem;
    justify-content: flex-end;
    margin-top: 1.5rem;
}

.modal__close,
.modal__confirm {
    padding: 0.5rem 1.25rem;
    border-radius: 4px;
    text-decoration: none;
    font-weight: 600;
    font-size: 0.9rem;
}

.modal__close {
    color: var(--text-dark);
    background-color: var(--bg-light);
}

.modal__confirm {
    color: white;
    background-color: var(--primary);
}
Important: The :target modal technique has limitations. It changes the URL hash, which affects browser history -- pressing the back button closes the modal instead of navigating to the previous page. It also cannot trap keyboard focus inside the modal, which is an accessibility requirement for true modal dialogs. For production applications, the native <dialog> element is strongly recommended because it handles focus trapping, Escape key dismissal, and backdrop interactions natively.

The Native dialog Element and ::backdrop

The HTML <dialog> element is the modern, accessible way to create modals. When opened with the showModal() method (via a small JavaScript call), it automatically creates a backdrop, traps keyboard focus inside the dialog, closes on the Escape key, and restores focus to the triggering element when closed. CSS controls the visual appearance, including the ::backdrop pseudo-element.

Styling the Native dialog Element

<!-- HTML -->
<button onclick="document.getElementById('my-dialog').showModal()">
    Open Dialog
</button>

<dialog id="my-dialog">
    <h2>Dialog Title</h2>
    <p>This is a native dialog with built-in accessibility features.</p>
    <form method="dialog">
        <button value="cancel">Cancel</button>
        <button value="confirm">Confirm</button>
    </form>
</dialog>

/* CSS */
dialog {
    border: none;
    border-radius: 12px;
    padding: 2rem;
    max-width: 500px;
    width: 90%;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
}

/* Style the backdrop */
dialog::backdrop {
    background-color: rgba(0, 0, 0, 0.5);
    backdrop-filter: blur(4px);
}

/* Open animation */
dialog[open] {
    animation: dialogFadeIn 0.3s ease forwards;
}

@keyframes dialogFadeIn {
    from {
        opacity: 0;
        transform: translateY(-20px) scale(0.95);
    }
    to {
        opacity: 1;
        transform: translateY(0) scale(1);
    }
}

dialog h2 {
    margin: 0 0 0.75rem;
    font-size: 1.25rem;
    color: var(--text-dark);
}

dialog p {
    color: var(--text-light);
    line-height: 1.6;
    margin-bottom: 1.5rem;
}

dialog form {
    display: flex;
    gap: 0.75rem;
    justify-content: flex-end;
}

dialog button {
    padding: 0.5rem 1.25rem;
    border: none;
    border-radius: 4px;
    font-weight: 600;
    font-size: 0.9rem;
    cursor: pointer;
}

dialog button[value="cancel"] {
    background-color: var(--bg-light);
    color: var(--text-dark);
}

dialog button[value="confirm"] {
    background-color: var(--primary);
    color: white;
}
Pro Tip: The <form method="dialog"> inside a <dialog> is a special form that closes the dialog when submitted, without making an HTTP request. The value attribute on the submit button is passed to the dialog's returnValue property, allowing you to determine which button the user clicked. This is the cleanest way to handle dialog buttons.

Toast / Notification Styling

Toasts are non-blocking notifications that appear temporarily at the edge of the screen -- typically the top-right or bottom-center. They inform the user about completed actions, errors, or updates without interrupting their workflow. Toasts slide or fade in, display for a few seconds, and then disappear. The CSS handles positioning, appearance, and entry/exit animations.

Toast Notification Component

/* Toast container: holds all toasts in a stack */
.toast-container {
    position: fixed;
    top: 1.5rem;
    right: 1.5rem;
    z-index: 2000;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    max-width: 380px;
    width: 100%;
    pointer-events: none;
}

.toast {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 1rem 1.25rem;
    background: var(--bg-white);
    border-radius: 8px;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
    border-left: 4px solid var(--text-light);
    pointer-events: auto;
    animation: toastSlideIn 0.3s ease forwards;
}

/* Variants */
.toast--success {
    border-left-color: #22c55e;
}

.toast--error {
    border-left-color: #ef4444;
}

.toast--warning {
    border-left-color: #f59e0b;
}

.toast--info {
    border-left-color: var(--primary);
}

.toast__icon {
    flex-shrink: 0;
    width: 1.25rem;
    height: 1.25rem;
    margin-top: 0.1rem;
}

.toast__content {
    flex: 1;
}

.toast__title {
    font-weight: 600;
    font-size: 0.9rem;
    color: var(--text-dark);
    margin-bottom: 0.25rem;
}

.toast__message {
    font-size: 0.85rem;
    color: var(--text-light);
    line-height: 1.4;
}

@keyframes toastSlideIn {
    from {
        transform: translateX(100%);
        opacity: 0;
    }
    to {
        transform: translateX(0);
        opacity: 1;
    }
}

CSS-Only Tooltip

Tooltips provide additional information when the user hovers over or focuses on an element. A pure CSS tooltip uses a custom data-tooltip attribute for the text content and the ::after pseudo-element to display it. The ::before pseudo-element creates a small triangular arrow pointing to the trigger element.

Pure CSS Tooltip

<!-- HTML: add data-tooltip to any element -->
<button class="tooltip" data-tooltip="Save your current progress">
    Save
</button>

/* CSS */
.tooltip {
    position: relative;
    cursor: pointer;
}

/* Tooltip bubble */
.tooltip::after {
    content: attr(data-tooltip);
    position: absolute;
    bottom: calc(100% + 8px);
    left: 50%;
    transform: translateX(-50%);
    padding: 0.5rem 0.85rem;
    background-color: var(--text-dark);
    color: white;
    font-size: 0.8rem;
    font-weight: 500;
    line-height: 1.4;
    white-space: nowrap;
    border-radius: 4px;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.2s, visibility 0.2s;
    pointer-events: none;
    z-index: 100;
}

/* Arrow */
.tooltip::before {
    content: "";
    position: absolute;
    bottom: calc(100% + 2px);
    left: 50%;
    transform: translateX(-50%);
    border: 6px solid transparent;
    border-top-color: var(--text-dark);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.2s, visibility 0.2s;
    pointer-events: none;
    z-index: 100;
}

/* Show on hover and focus */
.tooltip:hover::after,
.tooltip:hover::before,
.tooltip:focus-visible::after,
.tooltip:focus-visible::before {
    opacity: 1;
    visibility: visible;
}

/* Bottom tooltip variant */
.tooltip--bottom::after {
    bottom: auto;
    top: calc(100% + 8px);
}

.tooltip--bottom::before {
    bottom: auto;
    top: calc(100% + 2px);
    border-top-color: transparent;
    border-bottom-color: var(--text-dark);
}
Note: Pure CSS tooltips have accessibility limitations. The ::after pseudo-element content is not reliably announced by all screen readers, and touch devices have no hover state. For production tooltips, consider adding role="tooltip" and aria-describedby attributes with real DOM elements to ensure screen reader support. The CSS-only approach demonstrated here is suitable for progressive enhancement where the tooltip provides supplementary, non-essential information.

Accordion with details and summary

The HTML <details> and <summary> elements create a native disclosure widget that expands and collapses without any JavaScript. The browser handles the toggle behavior; CSS handles the visual presentation. This is the most accessible way to build accordion components because the keyboard interaction, ARIA semantics, and toggle state are all managed natively.

Styled Accordion with details/summary

<!-- HTML -->
<div class="accordion">
    <details class="accordion__item">
        <summary class="accordion__trigger">
            What is CSS?
        </summary>
        <div class="accordion__content">
            <p>CSS (Cascading Style Sheets) is a style sheet language used to describe the presentation of a document written in HTML or XML.</p>
        </div>
    </details>
    <details class="accordion__item">
        <summary class="accordion__trigger">
            How does specificity work?
        </summary>
        <div class="accordion__content">
            <p>CSS specificity determines which styles are applied when multiple rules target the same element. Inline styles have the highest specificity, followed by IDs, classes, and elements.</p>
        </div>
    </details>
</div>

/* CSS */
.accordion {
    max-width: 640px;
    border: 1px solid var(--border-light);
    border-radius: 8px;
    overflow: hidden;
}

.accordion__item {
    border-bottom: 1px solid var(--border-light);
}

.accordion__item:last-child {
    border-bottom: none;
}

.accordion__trigger {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 1rem 1.25rem;
    font-weight: 600;
    font-size: 1rem;
    color: var(--text-dark);
    cursor: pointer;
    list-style: none;
    transition: background-color 0.2s;
}

/* Remove default marker triangle */
.accordion__trigger::-webkit-details-marker {
    display: none;
}

.accordion__trigger::marker {
    display: none;
    content: "";
}

/* Custom arrow icon */
.accordion__trigger::after {
    content: "+";
    font-size: 1.25rem;
    font-weight: 400;
    color: var(--text-light);
    transition: transform 0.3s;
}

.accordion__item[open] .accordion__trigger::after {
    content: "−";
}

.accordion__trigger:hover {
    background-color: var(--bg-light);
}

.accordion__content {
    padding: 0 1.25rem 1.25rem;
    color: var(--text-light);
    line-height: 1.7;
}
Pro Tip: If you want only one accordion item open at a time (exclusive accordion behavior), you can use the name attribute on <details> elements. When multiple <details> elements share the same name value, the browser automatically closes the other items when one is opened. This feature is supported in modern browsers and requires no JavaScript: <details name="faq">.

Progress Bars

Progress bars communicate completion status. While the native <progress> element exists, its cross-browser styling is inconsistent. A custom CSS progress bar built with two nested <div> elements provides full visual control. The outer element is the track and the inner element is the fill bar, sized dynamically with a width percentage or CSS custom property.

Custom Progress Bar

<!-- HTML -->
<div class="progress" role="progressbar" aria-valuenow="65" aria-valuemin="0" aria-valuemax="100" aria-label="Profile completion">
    <div class="progress__fill" style="--progress: 65%">
        <span class="progress__label">65%</span>
    </div>
</div>

/* CSS */
.progress {
    width: 100%;
    height: 1.25rem;
    background-color: var(--bg-light);
    border-radius: 999px;
    overflow: hidden;
}

.progress__fill {
    height: 100%;
    width: var(--progress, 0%);
    background: linear-gradient(90deg, var(--primary), var(--primary-light));
    border-radius: 999px;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    transition: width 0.5s ease;
}

.progress__label {
    padding-right: 0.5rem;
    font-size: 0.7rem;
    font-weight: 700;
    color: white;
}

/* Thin variant */
.progress--thin {
    height: 6px;
}

.progress--thin .progress__label {
    display: none;
}

/* Color variants */
.progress--success .progress__fill {
    background: linear-gradient(90deg, #22c55e, #4ade80);
}

.progress--warning .progress__fill {
    background: linear-gradient(90deg, #f59e0b, #fbbf24);
}

.progress--danger .progress__fill {
    background: linear-gradient(90deg, #ef4444, #f87171);
}

/* Animated striped progress */
.progress--striped .progress__fill {
    background-image: linear-gradient(
        45deg,
        rgba(255, 255, 255, 0.2) 25%,
        transparent 25%,
        transparent 50%,
        rgba(255, 255, 255, 0.2) 50%,
        rgba(255, 255, 255, 0.2) 75%,
        transparent 75%
    );
    background-size: 1rem 1rem;
    animation: progressStripes 1s linear infinite;
}

@keyframes progressStripes {
    from { background-position: 1rem 0; }
    to { background-position: 0 0; }
}
Important: Always include ARIA attributes on custom progress bars. The role="progressbar", aria-valuenow, aria-valuemin, aria-valuemax, and aria-label attributes ensure screen readers can announce the progress status. Without these attributes, the progress bar is invisible to assistive technology users.

Avatar Components

Avatars are small circular or rounded images representing users. They appear in comments, user lists, navigation headers, and team sections. The CSS pattern uses border-radius: 50% for circular shapes, object-fit: cover to prevent image distortion, and optionally uses initials as a fallback when no image is available.

Avatar Component Variants

/* Basic circular avatar */
.avatar {
    width: 3rem;
    height: 3rem;
    border-radius: 50%;
    object-fit: cover;
    border: 2px solid var(--border-light);
}

/* Size variants */
.avatar--sm { width: 2rem; height: 2rem; }
.avatar--md { width: 3rem; height: 3rem; }
.avatar--lg { width: 4.5rem; height: 4.5rem; }
.avatar--xl { width: 6rem; height: 6rem; }

/* Initials fallback (no image) */
.avatar-initials {
    width: 3rem;
    height: 3rem;
    border-radius: 50%;
    background-color: var(--primary);
    color: white;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 1rem;
    text-transform: uppercase;
}

/* Avatar with status indicator */
.avatar-wrapper {
    position: relative;
    display: inline-block;
}

.avatar-status {
    position: absolute;
    bottom: 2px;
    right: 2px;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    border: 2px solid var(--bg-white);
}

.avatar-status--online { background-color: #22c55e; }
.avatar-status--offline { background-color: #94a3b8; }
.avatar-status--busy { background-color: #ef4444; }

/* Avatar group: overlapping avatars */
.avatar-group {
    display: flex;
}

.avatar-group .avatar {
    margin-left: -0.75rem;
    border: 2px solid var(--bg-white);
    transition: transform 0.2s;
}

.avatar-group .avatar:first-child {
    margin-left: 0;
}

.avatar-group .avatar:hover {
    transform: translateY(-4px);
    z-index: 1;
}

/* Avatar group overflow count */
.avatar-group__overflow {
    width: 3rem;
    height: 3rem;
    border-radius: 50%;
    background-color: var(--bg-light);
    color: var(--text-light);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.8rem;
    font-weight: 600;
    margin-left: -0.75rem;
    border: 2px solid var(--bg-white);
}

Skeleton Loading Screens

Skeleton screens are placeholder UI elements that mimic the shape of real content while data is loading. They improve perceived performance by giving users an immediate visual structure instead of a blank screen or spinner. The technique uses gray placeholder shapes with a shimmering animation that sweeps across the surface, signaling that content is loading.

Skeleton Loading Component

<!-- Skeleton card placeholder -->
<div class="skeleton-card" aria-hidden="true">
    <div class="skeleton skeleton--image"></div>
    <div class="skeleton-card__body">
        <div class="skeleton skeleton--title"></div>
        <div class="skeleton skeleton--text"></div>
        <div class="skeleton skeleton--text skeleton--short"></div>
    </div>
</div>

/* CSS */
.skeleton {
    background-color: #e2e8f0;
    border-radius: 4px;
    position: relative;
    overflow: hidden;
}

/* Shimmer animation */
.skeleton::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(
        90deg,
        transparent,
        rgba(255, 255, 255, 0.5),
        transparent
    );
    animation: shimmer 1.5s infinite;
}

@keyframes shimmer {
    0% {
        transform: translateX(-100%);
    }
    100% {
        transform: translateX(100%);
    }
}

/* Shape variants */
.skeleton--image {
    width: 100%;
    height: 200px;
    border-radius: 8px 8px 0 0;
}

.skeleton--title {
    width: 70%;
    height: 1.25rem;
    margin-bottom: 0.75rem;
}

.skeleton--text {
    width: 100%;
    height: 0.85rem;
    margin-bottom: 0.5rem;
}

.skeleton--short {
    width: 40%;
}

.skeleton--circle {
    width: 3rem;
    height: 3rem;
    border-radius: 50%;
}

/* Skeleton card layout */
.skeleton-card {
    background: var(--bg-white);
    border: 1px solid var(--border-light);
    border-radius: 8px;
    overflow: hidden;
}

.skeleton-card__body {
    padding: 1.25rem;
}

/* Skeleton for list items */
.skeleton-list-item {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 1rem;
    border-bottom: 1px solid var(--border-light);
}

.skeleton-list-item .skeleton--circle {
    flex-shrink: 0;
}

.skeleton-list-item__text {
    flex: 1;
}

/* Grid of skeleton cards */
.skeleton-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 1.5rem;
}
Pro Tip: Always add aria-hidden="true" to skeleton placeholders so screen readers skip them entirely. When the real content loads and replaces the skeleton, the screen reader will naturally announce the actual content. Additionally, consider adding a visually hidden <p> with role="status" and text like "Loading content..." near the skeleton area so screen reader users know something is loading.

Putting It All Together: A Practical UI Dashboard

Real-world interfaces combine multiple UI patterns on a single page. A dashboard might include a card grid for overview metrics, an avatar group for team members, progress bars for project status, toasts for notifications, and a modal for confirming actions. Understanding how these components compose together is just as important as knowing how to build each one individually. Keep your components modular: each pattern should be a standalone CSS class that works regardless of where it is placed in the DOM.

Composing Components: Stats Card with Progress

<!-- A card containing a stat and progress bar -->
<article class="card stat-card">
    <div class="stat-card__header">
        <span class="stat-card__label">Storage Used</span>
        <span class="stat-card__value">7.2 GB</span>
    </div>
    <div class="progress" role="progressbar" aria-valuenow="72" aria-valuemin="0" aria-valuemax="100">
        <div class="progress__fill" style="--progress: 72%"></div>
    </div>
    <p class="stat-card__detail">2.8 GB available of 10 GB</p>
</article>

/* CSS */
.stat-card {
    padding: 1.5rem;
}

.stat-card__header {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    margin-bottom: 1rem;
}

.stat-card__label {
    font-size: 0.85rem;
    color: var(--text-light);
    font-weight: 500;
}

.stat-card__value {
    font-size: 1.5rem;
    font-weight: 700;
    color: var(--text-dark);
}

.stat-card .progress {
    height: 8px;
    margin-bottom: 0.75rem;
}

.stat-card__detail {
    font-size: 0.8rem;
    color: var(--text-light);
    margin: 0;
}

Practice Exercise

Build a complete UI component library page that showcases all of the patterns covered in this lesson. Start by creating a responsive card grid with at least four cards, each containing an image, title, description, and action buttons. Add a hover lift effect with shadow and an image zoom transition to each card. Make one card a horizontal variant with the image on the left. Add a badge reading "New" to at least one card and an image gradient overlay with text on another. Next, build a modal that opens when the user clicks one of the card action buttons. Use the native <dialog> element with a styled ::backdrop and an open animation. Below the cards, create an FAQ section using <details> and <summary> elements styled as an accordion with at least four items sharing the same name attribute for exclusive open behavior. Add a row of three progress bars with different fill levels and color variants (success, warning, danger). Include an avatar group with five overlapping circular avatars and an overflow count badge. Finally, create a three-card skeleton loading state that matches the dimensions of your real cards, complete with shimmer animation. Ensure all interactive components have visible focus indicators, all images have alt text, the progress bars have proper ARIA attributes, and the skeleton placeholders are marked with aria-hidden="true".