Skip to main content

SVG Design System Integration Requires Raw HSL Extraction, Not CSS Classes

The Assumption That Breaks

When you build a data visualization in React with d3 or Canvas, you expect to use the same design tokens as the rest of your app. The oompa-landing component graph uses Tailwind utilities everywhere else — bg-factory-gold-500, text-factory-chocolate-700 — so the natural instinct is to apply the same pattern to SVG node colors:
// Feels right, but doesn't work:
<circle
  className="fill-factory-gold-500"
  r={radius}
/>
This fails silently. The circle renders but doesn’t apply the color from the Tailwind utility.

Why It Fails

1. SVG Scope Isolation

SVG elements exist in their own paint server context. They don’t inherit CSS styles the way HTML elements do. Tailwind’s utility classes generate CSS rules, but SVG presentation attributes (fill, stroke, opacity) are not CSS properties — they’re XML attributes with their own resolution rules.

2. Presentation Attributes vs CSS Properties

AttributeTypeStyling Method
fill="red"Presentation attributeInline value only
style="color: red"Inline styleCSS property
class="text-red-500"Tailwind utilityCSS rule in stylesheet
Tailwind’s fill-factory-gold-500 class generates:
.fill-factory-gold-500 {
  fill: hsl(var(--factory-gold-500));
}
But SVG renderers don’t reliably apply CSS rules to presentation attributes.

3. CSS Custom Properties in SVG Context

Even if you try to use CSS custom properties directly:
<circle fill="var(--factory-gold-500)" />
This often fails because:
  • The SVG namespace has stricter rules about variable resolution
  • Some browsers resolve var(--factory-gold-500) as a string literal, not a computed value
  • The computed value is hsl(38 92% 50%), but the attribute receives the literal string "var(--factory-gold-500)"

The Solution: Extract Raw HSL Strings

Instead of relying on Tailwind utilities, extract the underlying HSL values and use them as inline attributes:
// From globals.css: --factory-gold-500: 38 92% 50%;
const getCategoryColor = (category: CategoryKey): string => {
  const colors: Record<CategoryKey, string> = {
    flows: 'hsl(38 92% 50%)',        // factory-gold-500
    containers: 'hsl(32 95% 44%)',   // factory-gold-600
  };
  return colors[category] || 'hsl(22 20% 40%)';
};
Then apply directly to SVG:
<circle
  fill={getCategoryColor(node.category)}
  r={radius}
/>
Why this works: HSL strings are valid SVG paint values. The browser’s SVG engine parses them directly, no CSS resolution needed.

Maintenance Cost

This approach requires an extra maintenance step:
  1. Design system owner updates globals.css: --factory-gold-500: 38 92% 50%38 92% 48%
  2. Frontend engineer must manually update the getCategoryColor map
  3. No automation to sync the values
Mitigation: Add a comment showing which token each HSL maps to:
const getCategoryColor = (category: CategoryKey): string => {
  return {
    flows: 'hsl(38 92% 50%)',  // ← factory-gold-500 from globals.css line 42
  };
};
This makes the dependency visible and prevents accidental drift.

Lessons for Future Data Visualization Work

  1. Don’t assume CSS utilities work in SVG. Test early with a prototype.
  2. Extract tokens at the component level. Create a getToken() function that returns raw values.
  3. Document the mapping. Include source line numbers or token names in comments.
  4. Plan for dark mode. If your design system has light + dark variants, extract both at module load time.
  5. Use Tailwind for non-SVG markup only. Reserve CSS utilities for HTML elements where they reliably work.

Platform Impact

This pattern should be documented as a required doctrine for any future data visualization components (dashboards, analytics, network graphs, timelines). Including it in the codebase review checklist will prevent repeated rediscovery of this CSS-in-SVG footgun.
Related session: Command graph redesign (oompa.tools/commands) — 31 slash commands visualized as chocolate/gold pill-shaped nodes with full-name labels. The design system integration challenge revealed this pattern.