Source Code
<div class="cp-row">
<div class="cp-preview" data-theme="light">
<span class="cp-preview-label">Light</span>
<article class="cp-card">
<div class="cp-avatar">MK</div>
<h3>Maya Kessler</h3>
<p class="cp-role">Product Designer</p>
<p class="cp-bio">Designs interfaces for climate and energy startups. Based in Berlin.</p>
<div class="cp-tags">
<span class="cp-tag">Figma</span>
<span class="cp-tag">Design Systems</span>
</div>
<button class="cp-btn">View profile</button>
</article>
</div>
<div class="cp-preview" data-theme="dark">
<span class="cp-preview-label">Dark</span>
<article class="cp-card">
<div class="cp-avatar">MK</div>
<h3>Maya Kessler</h3>
<p class="cp-role">Product Designer</p>
<p class="cp-bio">Designs interfaces for climate and energy startups. Based in Berlin.</p>
<div class="cp-tags">
<span class="cp-tag">Figma</span>
<span class="cp-tag">Design Systems</span>
</div>
<button class="cp-btn">View profile</button>
</article>
</div>
</div>* { box-sizing: border-box; }
body { font-family: system-ui, -apple-system, sans-serif; background: #f1f5f9; margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; }
.cp-row { display: flex; gap: 24px; flex-wrap: wrap; justify-content: center; }
.cp-preview {
padding: 26px; border-radius: 18px; display: flex; flex-direction: column; align-items: center; gap: 14px;
--cp-page: #f8fafc; --cp-surface: #ffffff; --cp-text: #1e293b; --cp-muted: #64748b; --cp-border: #e2e8f0; --cp-accent: #4f46e5; --cp-tag-bg: #eef2ff;
background: var(--cp-page);
}
.cp-preview[data-theme="dark"] { --cp-page: #0f172a; --cp-surface: #1e293b; --cp-text: #f1f5f9; --cp-muted: #94a3b8; --cp-border: #334155; --cp-accent: #818cf8; --cp-tag-bg: #312e81; }
.cp-preview-label { font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.06em; color: var(--cp-muted); }
.cp-card { width: 240px; background: var(--cp-surface); border: 1px solid var(--cp-border); border-radius: 16px; padding: 22px; text-align: center; color: var(--cp-text); }
.cp-avatar { width: 52px; height: 52px; border-radius: 50%; background: var(--cp-accent); color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 14px; margin: 0 auto 12px; }
.cp-card h3 { margin: 0 0 2px; font-size: 15px; }
.cp-role { margin: 0 0 10px; font-size: 12px; color: var(--cp-muted); font-weight: 600; }
.cp-bio { margin: 0 0 14px; font-size: 12px; color: var(--cp-muted); line-height: 1.6; }
.cp-tags { display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; margin-bottom: 16px; }
.cp-tag { background: var(--cp-tag-bg); color: var(--cp-accent); font-size: 10.5px; font-weight: 700; padding: 3px 9px; border-radius: 999px; }
.cp-btn { width: 100%; background: var(--cp-accent); color: #fff; border: none; padding: 9px; border-radius: 9px; font-weight: 700; font-size: 12.5px; cursor: pointer; font-family: inherit; }
@media (max-width: 560px) {
.cp-row { flex-direction: column; align-items: center; }
}// This snippet is intentionally static: each preview wrapper carries its own
// fixed data-theme attribute ("light" or "dark"), and every color inside the
// card is a CSS custom property scoped to that wrapper. No JS toggle is
// needed to demonstrate both themes side by side -- the same card markup and
// stylesheet renders correctly under either attribute value simultaneously.Card Component Light/Dark Preview — Free HTML CSS JS Snippet
Card Component Light/Dark Preview · Cards · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Card Component Light/Dark Preview — Side-by-Side Scoped Theme Comparison

Reviewing a component's dark mode usually means flipping a global toggle back and forth and relying on memory to compare the two states. This snippet instead renders the identical card markup twice, each instance wrapped in its own preview container with a fixed data-theme attribute — "light" on one, "dark" on the other — so both variants are visible at the same time for direct, side-by-side comparison.
Scoped theming instead of a global switch
Each .cp-preview wrapper defines its own full set of CSS custom properties (surface, text, muted, border, accent, tag background), with the dark wrapper overriding all of them via a [data-theme="dark"] attribute selector scoped to that specific wrapper. Because the attribute lives on the wrapper rather than a shared ancestor like <html> or <body>, the two previews never interfere with each other — there is no global state to toggle, and no JavaScript is needed to produce the comparison at all.
Identical markup and CSS prove the theming actually works
Both cards use byte-for-byte the same HTML structure and the same .cp-card CSS rules. The only difference between them is which custom-property values are in scope where they render — which is exactly the property a token-based design system is supposed to guarantee: components shouldn't need theme-specific markup or theme-specific CSS classes, only theme-scoped variable values.
Practical for design review and QA
This pattern is directly useful for style-guide pages, design QA, and pull-request screenshots — anywhere a reviewer needs to confirm that a component reads correctly in both themes without manually toggling anything or trusting a memory of "how it looked a moment ago."
Step by step
How to Use
- 1Load the snippetClick "Card Component Light/Dark Preview" in the sidebar to load its HTML, CSS, and JS into the editor panels. The preview updates instantly.
- 2Edit the codeModify any panel — HTML, CSS, or JS. The preview refreshes as you type. Use Reset in each panel header to restore the original.
- 3Preview on devicesClick the Mobile (375px), Tablet (768px), or Desktop buttons in the preview header to check responsiveness.
- 4Export in your formatClick "HTML" to download a standalone file, "JSX" for a React component, "Tailwind" for a React + Tailwind CSS component, "Tailwind HTML" for a standalone HTML file with Tailwind CDN, "Vue" for a Vue 3 SFC with <template>/<script setup>/<style scoped>, or "Angular" for a standalone Angular .component.ts file. "Copy all" copies the full code to clipboard.
- 5Save your versionClick "Save as", type a name, and press Enter. Your snippet saves to IndexedDB and appears in the Saved tab.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
The goal is direct visual comparison — seeing both themes at once rather than switching between them and relying on memory. Each preview wrapper carries its own fixed data-theme attribute so both render permanently in their respective themes.
Each .cp-preview wrapper defines its own set of CSS custom properties, and a [data-theme="dark"] attribute selector scoped to that same wrapper overrides them. Since the attribute lives on the wrapper itself rather than a shared ancestor, the two previews never affect each other.
The card's own CSS rules never reference "light" or "dark" directly — they only reference custom properties via var(). This is what proves the same component markup and stylesheet correctly adapts to whichever theme context it happens to be rendered inside.
Yes — duplicate a .cp-preview block, give it a distinct data-theme value (e.g. "high-contrast"), and add a matching CSS override block redefining the same custom properties for that value.