CSS has a global scope problem. A style defined in one file affects every element in the entire page. If Component A defines .button { color: red } and Component B defines .button { color: blue }, whichever loads last wins. Every other component with a .button class inherits the conflict.
The web development industry has invented dozens of solutions: CSS Modules, Styled Components, CSS-in-JS, BEM naming conventions, Tailwind utility classes, Shadow DOM. Each solves the scoping problem but introduces its own complexity. CSS Modules require a build step. Styled Components couple styles to JavaScript runtime. BEM requires disciplined naming that humans inevitably violate. Tailwind replaces CSS knowledge with class name knowledge.
Session 031 built FLIN's approach: scoped CSS that is automatically isolated to the component that defines it, with computed styles that react to state changes. No build step. No naming conventions. No runtime overhead. The component's styles apply only to the component's elements, and they update when the component's state changes.
The Problem: Global CSS in Components
Without scoping, component styles leak. Consider two components:
flin// Header.flin
<style>
h1 { color: blue; font-size: 2rem; }
.link { text-decoration: none; }
</style>
<header>
<h1>{props.title}</h1>
<a class="link" href="/">Home</a>
</header>
// Article.flin
<style>
h1 { color: black; font-size: 1.5rem; }
.link { color: green; }
</style>
<article>
<h1>{props.title}</h1>
<a class="link" href={props.url}>Read more</a>
</article>Without scoping, both h1 rules apply to all h1 elements on the page. The Header's blue h1 and the Article's black h1 conflict. The last one loaded wins, creating unpredictable behavior.
The Solution: Automatic Scoping
FLIN's component compiler automatically scopes CSS rules to the component that defines them. Each component receives a unique identifier, and every CSS selector within the component is augmented with that identifier:
flin// What the developer writes in Button.flin:
<style>
.btn { padding: 8px 16px; border: none; cursor: pointer; }
.btn:hover { opacity: 0.8; }
.btn-primary { background: #007bff; color: white; }
</style>
// What the compiler produces:
<style>
.btn[data-flin-c42] { padding: 8px 16px; border: none; cursor: pointer; }
.btn[data-flin-c42]:hover { opacity: 0.8; }
.btn-primary[data-flin-c42] { background: #007bff; color: white; }
</style>The data-flin-c42 attribute is added to every element in the component's template. The CSS selector is augmented with the same attribute. The result: styles defined in Button.flin can only affect elements in Button.flin. They cannot leak to Header, Article, or any other component.
The scoping identifier (c42) is derived from a hash of the component's file path. It is stable across renders (the same component always gets the same identifier) and unique across components (no two components share an identifier).
How Scoping Works: The Compilation Pipeline
The scoping process happens during component compilation, in three steps:
Step 1: Extract <style> blocks. The parser identifies style blocks in the component source and separates them from the template.
Step 2: Generate scope identifier. A deterministic hash of the component's file path produces a short identifier like c42 or c7f.
Step 3: Transform selectors and elements. Every CSS selector is augmented with [data-flin-{id}]. Every template element receives the data-flin-{id} attribute.
rustfn scope_css(css: &str, scope_id: &str) -> String {
let attr = format!("[data-flin-{}]", scope_id);
css.lines().map(|line| {
if line.contains('{') {
// This is a selector line -- add scope attribute
let selector_end = line.find('{').unwrap();
let selector = &line[..selector_end].trim();
let rest = &line[selector_end..];
// Handle pseudo-classes and pseudo-elements
if let Some(pseudo_pos) = selector.find(':') {
let base = &selector[..pseudo_pos];
let pseudo = &selector[pseudo_pos..];
format!("{}{}{} {}", base, attr, pseudo, rest)
} else {
format!("{}{} {}", selector, attr, rest)
}
} else {
line.to_string()
}
}).collect::<Vec<_>>().join("\n")
}The transformation is straightforward: insert the scope attribute selector before any pseudo-class or pseudo-element, and after the base selector. .btn:hover becomes .btn[data-flin-c42]:hover. .btn::after becomes .btn[data-flin-c42]::after.
Computed Styles: Dynamic Values
Static CSS handles most styling needs, but components often need styles that depend on runtime values. A progress bar whose width reflects a percentage. A card whose border color depends on its status. A text element whose size is controlled by a prop.
FLIN handles this through dynamic style attributes:
flin// Progress.flin
value = props.value || 0
max = props.max || 100
percentage = round(value / max * 100)
color = {
if percentage < 25 then "danger"
else if percentage < 50 then "warning"
else if percentage < 75 then "info"
else "success"
}
<style>
.progress-track { background: #e2e8f0; border-radius: 9999px; height: 8px; }
.progress-bar { border-radius: 9999px; height: 100%; transition: width 300ms; }
</style>
<div class="progress-track">
<div class="progress-bar"
style="width: {percentage}%; background: var(--{color})">
</div>
</div>The style attribute uses FLIN's string interpolation ({percentage}%) to inject computed values. When value changes (because the underlying data changes), the percentage is recomputed, and the style attribute is updated. The CSS transition makes the width change animate smoothly.
This is different from CSS-in-JS (where styles are JavaScript objects) and different from Tailwind (where styles are class names). FLIN uses standard CSS in <style> blocks for static styles and inline style attributes for dynamic values. Static where possible, dynamic where necessary.
Conditional Classes
Components often need to apply different CSS classes based on state:
flin// Card.flin
active = props.active || false
variant = props.variant || "default"
<style>
.card { border: 1px solid #e2e8f0; border-radius: 0.5rem; padding: 1rem; }
.card-active { border-color: #007bff; box-shadow: 0 0 0 3px rgba(0,123,255,0.25); }
.card-elevated { box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
</style>
<div class="card {if active then 'card-active' else ''} {if variant == 'elevated' then 'card-elevated' else ''}">
{props.children}
</div>The conditional expression inside the class attribute is evaluated at render time. If active is true, the element gets the card-active class (in addition to card). If variant is "elevated", it gets card-elevated.
All three classes are scoped. card-active in this component does not conflict with card-active in any other component.
CSS Variables for Theme Integration
Components use CSS custom properties (variables) to integrate with the design token system:
flin<style>
.btn {
background: var(--flin-primary);
color: var(--flin-white);
padding: var(--flin-space-2) var(--flin-space-4);
border-radius: var(--flin-radius-md);
font-size: var(--flin-text-base);
transition: var(--flin-transition-fast);
}
.btn:hover {
background: var(--flin-primary-hover);
}
</style>The --flin-* CSS variables are set by the ThemeProvider component based on the active design tokens. When the theme switches from light to dark, the CSS variables change, and every component that references them updates automatically -- without re-rendering the component tree. The browser's CSS engine handles the visual update, which is faster than re-rendering through FLIN's component system.
Style Isolation Guarantees
FLIN's scoped CSS provides three guarantees:
1. Styles do not leak out. A component's styles cannot accidentally affect other components. The scope attribute ensures that selectors match only elements within the same component.
2. Styles do not leak in. Global styles do not override component styles (unless they use !important, which is a code smell regardless of the framework). The scope attribute adds specificity that prevents unintended overrides.
3. Identical class names do not conflict. Two components can both define .container with different styles. Each .container is scoped to its own component. No naming conventions required.
These guarantees hold without any developer effort. The developer writes standard CSS inside <style> blocks. The compiler handles the scoping. There is no BEM to learn, no CSS Modules to configure, no styled-components to import.
When Scoping Is Not Enough
There are legitimate cases where a component needs to style elements outside its own scope. A Modal needs to add overflow: hidden to the <body> element. A ThemeProvider needs to set CSS variables on the :root element.
For these cases, FLIN provides the :global() escape hatch:
flin<style>
/* Scoped (default) */
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); }
/* Global (escapes scoping) */
:global(body.modal-open) { overflow: hidden; }
</style>:global() wraps a selector that should not be scoped. The compiler emits it without the scope attribute. This escape hatch exists for the rare cases where component isolation needs to be broken deliberately. It is visually distinct in the code (the :global() wrapper) so reviewers can identify intentional scope breaks.
Performance: Scoping Overhead
The scoping mechanism adds one HTML attribute per element (data-flin-c42) and one attribute selector per CSS rule ([data-flin-c42]). The performance impact:
- HTML size increase: ~15 bytes per element. For a page with 500 elements, that is 7.5KB. Negligible.
- CSS selector performance: Attribute selectors are slower than class selectors in theory, but the difference is measured in nanoseconds per selector match. For a stylesheet with 200 rules, the total overhead is microseconds. Imperceptible.
- Compilation overhead: The scope transformation adds approximately 1ms per component during compilation. For 365 components, that is 365ms at startup -- absorbed into the initial compilation that already takes seconds.
The real performance benefit of scoped CSS is in developer productivity. No time spent debugging "where is this style coming from?" No time spent inventing unique class names. No time spent resolving style conflicts between components.
Scoped CSS vs. The Alternatives
| Approach | Build Step | Runtime Cost | Learning Curve | Isolation |
|---|---|---|---|---|
| Global CSS | No | None | Low | None |
| BEM | No | None | Medium | Convention-based |
| CSS Modules | Yes | None | Low | File-based |
| Styled Components | No | High | Medium | Component-based |
| Tailwind | Yes | None | High | Utility-based |
| Shadow DOM | No | Medium | High | Browser-native |
| FLIN Scoped | Compile-time | Negligible | None | Automatic |
FLIN's approach combines the zero-learning-curve of global CSS (write standard selectors) with the isolation of CSS Modules (scoped to the component) and the dynamism of Styled Components (computed styles via interpolation). The scoping happens at compile time, so there is no runtime library and no client-side performance cost.
Write standard CSS. Get automatic scoping. Use interpolation for dynamic values. That is the entire mental model.
This is Part 89 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO built scoped CSS and computed styles into a component system.
Series Navigation: - [88] FlinUI Enterprise Components - [89] Scoped CSS and Computed Styles (you are here) - [90] The Component Lifecycle - [91] Slots and Content Projection