Mode-Aware Hero Components and Astro CSS Scoping Fix

Key breakthrough: discovered :global() wrapper needed for data-mode selectors in Astro's scoped CSS.

Claude
Claude Claude

Changelog - 2025-12-04 (#3)

Dark Matter Site: Mode-Aware Hero Components and Astro CSS Scoping Fix

Overview

Refactored hero section variants (v5, v6, v7) to support all three visual modes (light, dark, vibrant) with dynamic switching. The key breakthrough was discovering that Astro's CSS scoping prevents [data-mode] selectors from working without the :global() wrapper.


Changes by Area

1. Mode-Aware Hero Components

Reorganized hero variants into two folders:

src/layouts/sections/narratives/
├── vibrant/           # Original vibrant-only designs (preserved)
│   ├── WhyNowWhyUs-v5.astro
│   ├── WhyNowWhyUs-v6.astro
│   └── WhyNowWhyUs-v7.astro
└── needs-modes/       # Mode-aware versions with light/dark/vibrant support
    ├── WhyNowWhyUs-v5.astro
    └── WhyNowWhyUs-v6.astro

Mode Support Features:

  • Light mode: Clean white background, subtle purple accents, dark text
  • Dark mode: Deep void background, moderate glows, light text
  • Vibrant mode: Glowing purple borders, text shadows, animated effects, gradient backgrounds

2. Astro CSS Scoping Fix (The Breakthrough)

Problem: Mode-specific CSS selectors weren't applying. Components rendered flat with no borders/glows even when data-mode="vibrant" was set.

Root Cause: Astro scopes <style> blocks by adding unique attributes to selectors. When writing:

[data-mode="vibrant"] .card-inner { ... }

Astro transforms it to:

[data-mode="vibrant"][data-astro-cid-xyz] .card-inner[data-astro-cid-xyz] { ... }

Since data-mode is on <html> (outside the component), <html> doesn't have the component's scope attribute, so the selector never matches.

Solution: Wrap mode selectors with :global():

/* Before - doesn't work */
[data-mode="vibrant"] .card-inner { border: 1px solid purple; }

/* After - works! */
:global([data-mode="vibrant"]) .card-inner { border: 1px solid purple; }

Applied bulk replacement across v5, v6, v7:

  • [data-mode="light"]:global([data-mode="light"])
  • [data-mode="dark"]:global([data-mode="dark"])
  • [data-mode="vibrant"]:global([data-mode="vibrant"])

3. Mode-Aware Orb Component

Updated ImageAbstract--Orb--Half.astro to respond to mode changes:

  • Added --fx-orb-color CSS token to matter-theme.css for each mode:

    • Light: var(--color-marjorelle-purple) (dark purple visible on light bg)
    • Dark: var(--color-alabaster) (white visible on dark void)
    • Vibrant: #ffffff (bright white for maximum contrast)
  • Changed default color prop from '#ffffff' to 'auto'

  • Added getOrbColor() function that reads --fx-orb-color CSS variable

  • Added MutationObserver watching data-mode attribute changes on <html>

  • Three.js color uniform updates in real-time when mode switches

4. Effect Tokens in Theme

Added --fx-* effect tokens to src/styles/themes/matter-theme.css:

/* Per-mode effect tokens */
--fx-glow-opacity: 0.08 | 0.25 | 0.5;
--fx-glow-spread: 10px | 25px | 50px;
--fx-glow-color: rgba(102, 67, 226, X);
--fx-text-shadow: none | 0 0 40px... | 0 0 80px...;
--fx-text-glow: ...;
--fx-hero-gradient: ...;
--fx-orbit-color: ...;
--fx-orbit-glow: ...;
--fx-node-glow: ...;
--fx-card-bg: ...;
--fx-card-border: ...;
--fx-card-border-hover: ...;
--fx-card-shadow: ...;
--fx-card-shadow-hover: ...;
--fx-headline-gradient: ...;
--fx-orb-color: ...;

5. Design System Pages

Created two design system pages for comparison:

  • /design-system/vibrant — Shows original vibrant-only designs
  • /design-system/needs-modes — Shows mode-aware versions with mode switcher buttons

6. Documentation

Created issue resolution document at: context-v/Resolving-Mode-Switching-Across-Multiple-Components.md

Documents the problem, failed attempts, root cause discovery, and solution for future reference.


Files Changed Summary

New Files

  • src/layouts/sections/narratives/vibrant/WhyNowWhyUs-v5.astro — Original v5 preserved
  • src/layouts/sections/narratives/vibrant/WhyNowWhyUs-v6.astro — Original v6 preserved
  • src/layouts/sections/narratives/vibrant/WhyNowWhyUs-v7.astro — Original v7 preserved
  • src/layouts/sections/narratives/needs-modes/WhyNowWhyUs-v5.astro — Mode-aware v5
  • src/layouts/sections/narratives/needs-modes/WhyNowWhyUs-v6.astro — Mode-aware v6
  • src/pages/design-system/vibrant.astro — Vibrant showcase page
  • src/pages/design-system/needs-modes.astro — Mode switcher test page
  • context-v/Resolving-Mode-Switching-Across-Multiple-Components.md — Documentation

Modified Files

  • src/components/flare/ImageAbstract--Orb--Half.astro — Mode-aware color handling
  • src/styles/themes/matter-theme.css — Added --fx-* effect tokens and --fx-orb-color

Key Lessons Learned

  1. Astro CSS Scoping: When targeting attributes on <html> or <body> from within a component, use :global() to prevent Astro from scoping that part of the selector.

  2. Start From Working Code: When adding modes to a component, start from the working vibrant version and add light/dark support, rather than abstracting everything and rebuilding.

  3. Three.js + CSS Variables: WebGL components can't use CSS directly but can read computed CSS variable values via JavaScript and update uniforms. Use MutationObserver to watch for attribute changes.


Follow-Ups

  • Apply same :global() pattern to any future mode-aware components
  • Consider extracting the mode-switching pattern into a reusable mixin or utility
  • Test mode transitions on mobile devices
  • Finalize which hero variant to use for production