Modern CSS View
A side-by-side viewer for legacy vs modern CSS. Pass in a list of comparisons; each row stacks a before snippet (the old way) next to an after snippet (the modern equivalent), with an optional note printed beneath that explains the win. A segmented toggle flips which side gets the spotlight — the opposite pane dims so the active treatment reads first.
Drop it into a docs page on @layer, content-visibility, :has(), @scope, container queries, native nesting, or any other "the old way / the new way" CSS story.
Modern CSS, three ways
Each row stacks the legacy approach next to the modern equivalent. Flip the toggle to spotlight one side.
/* unlayered — specificity wins */
button.button { padding: 10px 20px; }
.button { padding: 12px 24px; }
button { padding: 16px 28px; }
/* winner: button.button — highest specificity */@layer reset, framework, components, overrides;
@layer framework { .button { padding: 12px 24px; } }
@layer components { button.button { padding: 10px 20px; } }
@layer overrides { button { padding: 16px 28px; } }
/* winner: overrides — last layer wins, ignores specificity */Layers beat specificity, so source order in the cascade — not selector weight — picks the winner.
.card-grid > article {
/* every off-screen card still pays for layout + paint */
padding: 24px;
border-radius: 12px;
}.card-grid > article {
padding: 24px;
border-radius: 12px;
/* skip layout + paint until the card scrolls into view */
content-visibility: auto;
contain-intrinsic-size: 0 280px;
}Off-screen sections render lazily on scroll. Pair with contain-intrinsic-size so the scrollbar doesn't jump.
/* needs a JS class toggle to style the parent */
.card.has-cover-image {
padding-top: 0;
}
// onMount: card.classList.toggle("has-cover-image", !!img)/* purely declarative — no JS needed */
.card:has(> img.cover) {
padding-top: 0;
}:has() reads forward into descendants so the parent can react to its content without any imperative glue.
Installation
npx shadcn@latest add https://craftbits.dev/r/modern-css-view.jsonUsage
import { useState } from "react";
import {
ModernCssView,
type ModernCssViewComparison,
type ModernCssViewSide,
} from "@craft-bits/core";
const comparisons: ModernCssViewComparison[] = [
{
id: "layers",
title: "@layer cascade",
before: "button.button { padding: 10px 20px; }",
after: "@layer overrides { button { padding: 16px 28px; } }",
note: "Last layer wins regardless of selector specificity.",
},
];
export function Demo() {
const [side, setSide] = useState<ModernCssViewSide>("after");
return (
<ModernCssView
title="Modern CSS, three ways"
comparisons={comparisons}
side={side}
onSideChange={setSide}
/>
);
}Skip the side / onSideChange pair to run uncontrolled — the toggle owns its own state and defaultSide picks the initial pane.
Anatomy
- Header. Optional
title(rendered with thecb-labelstyle) anddescription. The segmented before/after toggle lives on the same row at the right. - Comparison row. A bordered card per
ModernCssViewComparison. The card carries its own smalltitle/subtitleline, two side-by-side code panes, and an optionalnoteunderneath. - Code pane. A
<pre><code>block with a uppercase mono header strip showing the pane label (defaults toLegacy/Modern) and a small accent dot when the pane is active. The opposite pane dims to ~50% opacity. - Toggle. Two-segment radio group rendered as a pill switch. Click either segment — or hit space when focused — to flip the active side.
Understanding the component
- Controlled and uncontrolled. Pass
sideplusonSideChangefor a controlled toggle; otherwise the component owns its own state anddefaultSide(defaults to"after") seeds the initial pane. - Tone driven by data attributes. The active pane carries
data-cb-tone="active"and the dimmed pane carriesdata-cb-tone="muted". The wrapper getsdata-cb-sideso consumers can style downstream chrome from the active side without prop drilling. - Motion. Each pane crossfades to its active or muted opacity with the
SPRINGS.smoothmotion token. Animations short-circuit underprefers-reduced-motion. - No syntax highlighter. Snippets render as plain monospace text. Drop in
CodeBlockor your own highlighter via custom rendering if you need shiki — the API is intentionally a plainstringso the panel stays lightweight.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
comparisons | ModernCssViewComparison[] | required | Ordered list of before/after rows. |
side | 'before' | 'after' | — | Controlled active side. Pair with onSideChange. |
defaultSide | 'before' | 'after' | 'after' | Initial side in uncontrolled mode. |
onSideChange | (side) => void | — | Called whenever the active side flips. |
title | ReactNode | — | Optional heading above the panel. |
description | ReactNode | — | Optional sub-headline under the title. |
hideToggle | boolean | false | Hide the segmented before/after toggle. |
headingAs | 'h2' | 'h3' | 'h4' | 'h3' | Tag for the title element. |
toggleLabels | { before: ReactNode; after: ReactNode } | sensible defaults | Override the segmented toggle copy. |
className | string | — | Merged onto the root via cn(). |
ModernCssViewComparison
| Field | Type | Description |
|---|---|---|
id | string | Stable identifier. |
title | ReactNode | Heading for the comparison row. |
subtitle | ReactNode | Optional one-line subtitle next to the title. |
beforeLabel | ReactNode | Override the before pane label. Defaults to 'Legacy'. |
afterLabel | ReactNode | Override the after pane label. Defaults to 'Modern'. |
before | string | Legacy code snippet rendered in the left pane. |
after | string | Modern code snippet rendered in the right pane. |
note | ReactNode | Optional one-line takeaway printed beneath the panes. |
Accessibility
- The wrapper is a
<section>withdata-cb-edu="modern-css-view"anddata-cb-sidemirroring the active side. - The segmented toggle is
role="radiogroup"with tworole="radio"buttons;aria-checkedis set on the active option so screen readers announce the current pane. - Each code pane uses semantic
<pre><code>so screen readers and copy operations preserve the snippet's whitespace. - The toggle has a
focus-visiblering and a subtleactive:scale-[0.98]press state. - Animations short-circuit under
prefers-reduced-motion.
Credits
- Extracted from:
terminal-dreams(src/components/frontend-design/perf-css/ui/ModernCSSView.tsx). The original was wired to thecss-perf-simulatorengine (a fixedLayerRule[]resolver, a 16-card content-visibility list with a measuredrenderMs, and a hard-codedCSS_IN_JS_MODESchip group) and styled with a co-locatedCSSPerfLab.module.csschrome. This rewrite strips the engine coupling and the project chrome — consumers pass an arbitrary list of{ id, title, before, after, note }comparisons so the view can teach@layer,content-visibility,:has(),@scope, container queries, native nesting, or anything else with a meaningful "before / after" story.