A functional programming case for Tailwind
Cascading Style Sheets (CSS) separated content from styling, but came with inherent challenges.
Global Scope
CSS styles are globally scoped across the HTML document, meaning any style rule can affect any element on the page, even unintentionally.
-
Naming Collisions: When multiple rules use the same selectors or class names, they can override each other, resulting in unexpected visual outcomes. This issue escalates with the size of the codebase and is particularly problematic when integrating third-party libraries.
-
Specificity Wars: CSS resolves conflicts using specificity rules. As the codebase grows, developers often add unnecessary specificity, including the use of
!important
, to force styles to render correctly. This practice increases complexity and complicates maintenance.
Lack of Encapsulation
The absence of native encapsulation in CSS leads to several issues:
-
Style Leakage: Styles intended for a specific component can “leak” and affect other parts of the page, leading to inconsistent UIs and hard-to-track bugs.
-
Difficulty Reusing Components: Developers aim to create components whose styles are independent of the environment they are used in. Without encapsulation, styles from one component can clash with or be overridden by others, hampering reusability.
Solutions to CSS Naming Collisions
BEM (Block Element Modifier) (Yandex)
BEM uses strict naming conventions that divide CSS into blocks
, elements
, and modifiers
to minimize naming conflicts.
-
Example:
<div class="button button--primary"> <span class="button__text">Button</span> </div>
Scalable and Modular Architecture for CSS (Jonathan Snook)
Similar to BEM, SMACSS provides a structured approach to organizing CSS, categorizing rules into base
, layout
, module
, state
, and theme
to mitigate naming collisions.
-
Example:
.btn { /* module */ } .btn-primary { /* state */ }
Atomic/Utility-First CSS (Tailwind)
Predefined utility classes apply styles directly, inherently avoiding naming collisions.
-
Example:
<div class="bg-blue-500 text-white font-bold py-2 px-4 rounded"> Button </div>
Component-Based CSS (Styled Components)
This method scopes CSS to specific components, preventing global styles and reducing style conflicts and leaks.
-
Example:
import styled from 'styled-components'; const Button = styled.button` background: #3498db; color: white; padding: 8px 16px; border-radius: 4px; border: none; &:hover { background: #2980b9; } `;
Shadow DOM
Part of the Web Components standard, Shadow DOM encapsulates HTML and CSS within a component’s shadow tree, eliminating naming collisions.
-
Example:
<custom-button></custom-button>
Build Tools for CSS Encapsulation
CSS Modules
CSS Modules automatically scope CSS at the component level, ensuring unique class names per module and effectively preventing naming collisions.
-
Example:
// Button.module.css .button { background: #3498db; color: white; padding: 8px 16px; border-radius: 4px; border: none; } .button:hover { background: #2980b9; }
// Button.js import styles from './Button.module.css'; const Button = () => <button className={styles.button}>Button</button>;
Angular
Angular uses View Encapsulation to prevent styles from affecting other components unintentionally.
-
Example:
// app.component.ts import { Component } from '@angular/core'; @Component({ selector: 'app-root', template: `<button class="button">Button</button>`, styles: [` .button { background: #3498db; color: white; padding: 8px 16px; border-radius: 4px; border: none; &:hover { background: #2980b9; } } `], encapsulation: ViewEncapsulation.Emulated // Default behavior, emulating Shadow DOM }) export class AppComponent {}
Vue
With Vue, styles are scoped to the component by adding the scoped
attribute to a <style>
tag.
-
Example:
<template> <button class="button">Button</button> </template> <style scoped> .button { background: #3498db; color: white; padding: 8px 16px; border-radius: 4px; border: none; &:hover { background: #2980b9; } } </style>
Svelte
Svelte automatically scopes styles written inside a component file by default, ensuring styles do not leak to other components.
-
Example:
<script> // Svelte component logic </script> <style> button { background: #3498db; color: white; padding: 8px 16px; border-radius: 4px; border: none; } button:hover { background: #2980b9; } </style> <button>Button</button>
CSS Variables
CSS variables enable defining reusable custom properties. Although they don’t directly solve naming collisions, they centralize commonly used values, improving maintainability.
-
Example:
:root { --primary-color: #3498db; } .button { background-color: var(--primary-color); }
My Takeaway
BEM and SMACSS rely on developers to maintain consistent naming conventions, which introduces mutable state issues where class names might be reused across different contexts, leading to unpredictable behavior. This reliance on careful naming creates an overhead that contradicts the simplicity and predictability valued in functional programming.
Component-based CSS solutions, such as Styled Components and Shadow DOM, provide strong encapsulation, but Styled Components depend on component state or props, leading to side effects and imperative coding patterns. While Shadow DOM ensures encapsulation, it restricts reusability and introduces integration complexity.
Build tools improve CSS management, but CSS Modules and Angular’s View Encapsulation introduce context dependency and performance overhead. Vue’s Scoped Styles and Svelte’s Scoped CSS suffer from a static nature and implicit dependencies, adding side effects and complexity.
For me, Tailwind’s utility-first approach treats styles as a set of predictable, reusable utility functions that produce the same output given the same input. This, and having no mutable state, eliminates the need for naming conventions and the risk of unintended style overrides. Additionally, Tailwind is framework-agnostic, ensuring that it integrates seamlessly into any tech stack. The way Tailwind allows classes to be composed sequentially in HTML, much like a functional pipeline, enables a clear, declarative approach to styling.