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.

Preview

Modern CSS, three ways

Each row stacks the legacy approach next to the modern equivalent. Flip the toggle to spotlight one side.

@layer cascadeCascade order over specificity
Legacy
/* unlayered — specificity wins */
button.button { padding: 10px 20px; }
.button       { padding: 12px 24px; }
button        { padding: 16px 28px; }
/* winner: button.button — highest specificity */
Modern
@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.

content-visibility: autoSkip layout for off-screen sections
Legacy
.card-grid > article {
  /* every off-screen card still pays for layout + paint */
  padding: 24px;
  border-radius: 12px;
}
Modern
.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.

:has() parent selectorStyle a parent based on its children
Legacy
/* needs a JS class toggle to style the parent */
.card.has-cover-image {
  padding-top: 0;
}
// onMount: card.classList.toggle("has-cover-image", !!img)
Modern
/* 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.

Customize
Layout

Installation

npx shadcn@latest add https://craftbits.dev/r/modern-css-view.json

Usage

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 the cb-label style) and description. 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 small title / subtitle line, two side-by-side code panes, and an optional note underneath.
  • Code pane. A <pre><code> block with a uppercase mono header strip showing the pane label (defaults to Legacy / 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

  1. Controlled and uncontrolled. Pass side plus onSideChange for a controlled toggle; otherwise the component owns its own state and defaultSide (defaults to "after") seeds the initial pane.
  2. Tone driven by data attributes. The active pane carries data-cb-tone="active" and the dimmed pane carries data-cb-tone="muted". The wrapper gets data-cb-side so consumers can style downstream chrome from the active side without prop drilling.
  3. Motion. Each pane crossfades to its active or muted opacity with the SPRINGS.smooth motion token. Animations short-circuit under prefers-reduced-motion.
  4. No syntax highlighter. Snippets render as plain monospace text. Drop in CodeBlock or your own highlighter via custom rendering if you need shiki — the API is intentionally a plain string so the panel stays lightweight.

Props

PropTypeDefaultDescription
comparisonsModernCssViewComparison[]requiredOrdered 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) => voidCalled whenever the active side flips.
titleReactNodeOptional heading above the panel.
descriptionReactNodeOptional sub-headline under the title.
hideTogglebooleanfalseHide the segmented before/after toggle.
headingAs'h2' | 'h3' | 'h4''h3'Tag for the title element.
toggleLabels{ before: ReactNode; after: ReactNode }sensible defaultsOverride the segmented toggle copy.
classNamestringMerged onto the root via cn().

ModernCssViewComparison

FieldTypeDescription
idstringStable identifier.
titleReactNodeHeading for the comparison row.
subtitleReactNodeOptional one-line subtitle next to the title.
beforeLabelReactNodeOverride the before pane label. Defaults to 'Legacy'.
afterLabelReactNodeOverride the after pane label. Defaults to 'Modern'.
beforestringLegacy code snippet rendered in the left pane.
afterstringModern code snippet rendered in the right pane.
noteReactNodeOptional one-line takeaway printed beneath the panes.

Accessibility

  • The wrapper is a <section> with data-cb-edu="modern-css-view" and data-cb-side mirroring the active side.
  • The segmented toggle is role="radiogroup" with two role="radio" buttons; aria-checked is 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-visible ring and a subtle active: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 the css-perf-simulator engine (a fixed LayerRule[] resolver, a 16-card content-visibility list with a measured renderMs, and a hard-coded CSS_IN_JS_MODES chip group) and styled with a co-located CSSPerfLab.module.css chrome. 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.