Skip to content

Latest commit

 

History

History
448 lines (352 loc) · 18.4 KB

File metadata and controls

448 lines (352 loc) · 18.4 KB

Modular CSS: CSS-in-JS, BEM, and SCSS Modules

Modular CSS refers to the practice of writing CSS in a way that scopes styles locally to components or modules, preventing global scope conflicts and improving maintainability and reusability. This approach is crucial in large-scale applications where managing a global CSS namespace can become challenging.

This document explores three popular methodologies for achieving Modular CSS:

  1. CSS-in-JS
  2. BEM (Block, Element, Modifier)
  3. SCSS Modules (or CSS Modules with Sass)

Think of it like a wardrobe organizer. Without modular CSS, your styles are like throwing all your clothes into one big pile — finding the right shirt means digging through everything, and pulling one thing out often drags others with it. Modular CSS is like having labeled drawers and compartments: each component gets its own drawer, styles do not leak between drawers, and you can rearrange or remove one drawer without messing up the rest.


1. CSS-in-JS

CSS-in-JS is a technique where CSS is written directly within JavaScript files, often alongside the components they style. This allows for dynamic styling, better encapsulation, and easier management of styles on a per-component basis.

Core Concepts:

  • Scoped Styles: Styles are typically scoped to the component, meaning they won't unintentionally affect other parts of the application. This is often achieved by generating unique class names.
  • Dynamic Styling: JavaScript variables and logic can be used to dynamically change styles based on props, state, or themes.
  • Colocation: Styles are defined in the same file as the component, making it easier to understand and maintain the relationship between markup and styling.
  • Critical CSS Extraction: Many CSS-in-JS libraries can extract only the necessary CSS for the initial page load, improving performance.

Popular Libraries:

  • Styled Components: Allows you to write actual CSS code to style your components using tagged template literals.
  • Emotion: Similar to Styled Components, offering powerful and flexible styling capabilities. It also supports a css prop for inline-like styling with more power.
  • JSS (JavaScript Style Sheets): A more low-level library that provides a framework for writing CSS in JavaScript.

Benefits:

  • True Encapsulation: Styles are truly scoped, eliminating global namespace collisions.
  • Dynamic Styling: Easy to create styles that respond to application state or props.
  • Dead Code Elimination: Unused styles can be more easily identified and removed by bundlers.
  • Improved Developer Experience: Keeping styles close to components can streamline development.
  • Theming: Simplifies the implementation of theming capabilities.

Drawbacks:

  • Learning Curve: Requires understanding new libraries and concepts.
  • Performance Overhead: Can introduce a runtime overhead due to style computation and injection, though many libraries are highly optimized.
  • Bundle Size: Can increase JavaScript bundle size if not managed carefully.
  • Tooling: Might require specific tooling or Babel plugins.
  • CSS Extraction Complexity: Server-side rendering (SSR) and critical CSS extraction can sometimes be complex to set up.

Use Cases:

  • Component-based architectures (e.g., React, Vue, Angular).
  • Applications requiring dynamic theming.
  • Large-scale applications where CSS maintainability is a major concern.
  • Design systems where components need to be highly configurable.

Best Practices:

  • Choose a library that fits your needs: Evaluate features, performance, and community support.
  • Leverage theming: Use theme providers for consistent styling across the application.
  • Optimize for performance: Be mindful of runtime style computations and consider SSR or static extraction.
  • Keep components small and focused: This makes styling easier to manage.
  • Write readable styles: Even though it's JavaScript, aim for CSS-like clarity.

Code Example (Styled Components with React):

// MyButton.js
import React from 'react';
import styled, { ThemeProvider } from 'styled-components';

// Define a theme
const theme = {
  primaryColor: 'dodgerblue',
  secondaryColor: 'lightgray',
  borderRadius: '4px',
  padding: '0.5em 1em',
};

// Create a styled button component
const StyledButton = styled.button`
  background-color: ${props => props.primary ? props.theme.primaryColor : props.theme.secondaryColor};
  color: white;
  border: none;
  border-radius: ${props => props.theme.borderRadius};
  padding: ${props => props.theme.padding};
  font-size: 1em;
  cursor: pointer;
  transition: background-color 0.3s ease;

  &:hover {
    background-color: ${props => props.primary ? 'royalblue' : 'darkgray'};
  }

  // Example of adapting based on props
  ${props => props.large && `
    font-size: 1.2em;
    padding: 0.75em 1.5em;
  `}
`;

const MyButton = ({ children, primary, large, ...props }) => {
  return (
    <ThemeProvider theme={theme}>
      <StyledButton primary={primary} large={large} {...props}>
        {children}
      </StyledButton>
    </ThemeProvider>
  );
};

export default MyButton;

// App.js (Usage)
// import MyButton from './MyButton';
// <MyButton primary>Primary Button</MyButton>
// <MyButton large>Large Secondary Button</MyButton>

2. BEM (Block, Element, Modifier)

BEM is a naming methodology for CSS classes that aims to create a clear, transparent, and maintainable structure for your stylesheets. It helps in understanding the relationship between HTML and CSS, and makes CSS more reusable and less prone to conflicts.

Core Concepts:

  • Block: A standalone entity that is meaningful on its own. Represents a high-level component.
    • Examples: header, menu, button, search-form
    • Class name: .block
  • Element: A part of a block that has no standalone meaning and is semantically tied to its block.
    • Examples: menu__item, search-form__input, header__logo
    • Class name: .block__element (double underscore separator)
  • Modifier: A flag on a block or element used to change appearance, behavior, or state.
    • Examples: button--disabled, menu__item--active, search-form--compact
    • Class name: .block--modifier or .block__element--modifier (double hyphen separator)

Benefits:

  • Modularity: Styles are scoped by convention, reducing the risk of conflicts.
  • Reusability: Blocks and elements can be reused across the project.
  • Maintainability: The naming convention makes CSS easier to read, understand, and maintain.
  • Scalability: Well-suited for large projects with many developers.
  • Clear Structure: Provides a clear relationship between HTML structure and CSS rules.
  • Specificity Management: Helps keep CSS specificity low and manageable.

Drawbacks:

  • Verbose Class Names: Class names can become long and sometimes feel unwieldy.
  • Strict Naming Convention: Requires discipline from the team to adhere to the convention.
  • HTML Bloat: Can lead to more classes in the HTML markup.
  • Not Truly Scoped: Relies on convention rather than a technical scoping mechanism, so conflicts are still possible if the convention is not followed.

Use Cases:

  • Large websites and web applications.
  • Projects with multiple developers.
  • When a clear and consistent CSS architecture is needed without relying on JavaScript for styling.
  • Often used with CSS preprocessors like Sass or Less to manage verbosity.

Best Practices:

  • Be Consistent: Strictly follow the BEM naming convention.
  • Blocks for Reusability: Design blocks to be independent and reusable.
  • Elements Belong to Blocks: Elements should not be used outside their parent block.
  • Modifiers for Variations: Use modifiers to represent variations in state or appearance.
  • Avoid Deep Nesting of Elements: Try to keep element structures relatively flat (e.g., .block__element__sub-element is generally discouraged).
  • Combine with Preprocessors: Use Sass/Less to make BEM more manageable (e.g., using nesting and parent selectors).

Code Example (HTML and CSS):

HTML:

<form class="search-form search-form--inline">
  <input class="search-form__input" type="text" placeholder="Search...">
  <button class="search-form__button search-form__button--primary" type="submit">
    Search
  </button>
</form>

<nav class="menu">
  <ul class="menu__list">
    <li class="menu__item menu__item--active">
      <a href="#" class="menu__link">Home</a>
    </li>
    <li class="menu__item">
      <a href="#" class="menu__link">About</a>
    </li>
    <li class="menu__item menu__item--disabled">
      <a href="#" class="menu__link">Services (soon)</a>
    </li>
  </ul>
</nav>

CSS (or SCSS for better organization):

/* search-form Block */
.search-form {
  display: flex;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 10px;
}

.search-form--inline {
  padding: 5px;
}

/* Elements of search-form */
.search-form__input {
  flex-grow: 1;
  border: 1px solid #ddd;
  padding: 8px;
  border-radius: 3px;
  margin-right: 5px;
}

.search-form__button {
  padding: 8px 15px;
  border: none;
  background-color: #eee;
  color: #333;
  cursor: pointer;
  border-radius: 3px;
}

/* Modifier for search-form__button */
.search-form__button--primary {
  background-color: dodgerblue;
  color: white;
}

.search-form__button--primary:hover {
  background-color: royalblue;
}


/* menu Block */
.menu {
  font-family: Arial, sans-serif;
}

.menu__list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
}

/* Elements of menu */
.menu__item {
  margin-right: 15px;
}

/* Modifier for menu__item */
.menu__item--active .menu__link {
  font-weight: bold;
  color: dodgerblue;
}

.menu__item--disabled .menu__link {
  color: #aaa;
  pointer-events: none; /* Not clickable */
}

.menu__link {
  text-decoration: none;
  color: #333;
}

.menu__link:hover {
  text-decoration: underline;
}

3. SCSS Modules (or CSS Modules with Sass)

CSS Modules allow you to write CSS where all class names and animation names are scoped locally by default. When used with a preprocessor like SCSS (Sass), you get the benefits of both local scope and the powerful features of Sass (variables, mixins, nesting, etc.).

Core Concepts:

  • Local Scope by Default: Every class name written in a CSS Module file is automatically made unique by appending a hash (e.g., .button might become .MyComponent_button__a1b2c).
  • Explicit Globals: If you need a global class, you must explicitly define it (e.g., :global(.my-global-class)).
  • Composition: Allows you to compose styles from other classes.
  • Integration with Build Tools: CSS Modules are typically processed by build tools like Webpack or Parcel, which handle the class name transformations.
  • JavaScript Integration: The generated unique class names are imported into JavaScript files as an object, allowing you to apply them to your components.

Benefits:

  • True Local Scope: Eliminates global CSS conflicts by default.
  • Reusability: Components with their styles are truly self-contained and reusable.
  • Maintainability: Easier to manage styles as the project grows, as changes to one module won't affect others.
  • Sass Features: When combined with SCSS, you can use variables, mixins, functions, and nesting for more powerful and organized stylesheets.
  • Clear Dependencies: The import styles from './styles.module.scss'; syntax makes style dependencies explicit.

Drawbacks:

  • Build Tool Dependency: Requires a build setup (e.g., Webpack with css-loader).
  • Generated Class Names: Debugging can sometimes be trickier due to hashed class names, though source maps help.
  • Dynamic Styling: Less straightforward for dynamic styling based on props compared to CSS-in-JS, though possible by conditionally applying classes.
  • Learning Curve: Understanding how CSS Modules work and integrate with JavaScript.

Use Cases:

  • Component-based JavaScript frameworks (React, Vue, Angular).
  • Projects where strong CSS encapsulation is desired without writing CSS in JavaScript.
  • When leveraging the power of SCSS/Sass alongside modularity.

Best Practices:

  • Consistent File Naming: Use a consistent naming convention for module files (e.g., [ComponentName].module.scss or styles.module.scss).
  • Use composes for Style Sharing: Prefer composes from other local class names or from other CSS Modules for sharing styles over mixins if the goal is purely composition of existing rules.
  • Minimize Global Styles: Use :global sparingly.
  • Leverage Sass Features: Use variables for theming, mixins for reusable patterns, and nesting for readability.
  • Organize Styles Logically: Even within a module, keep your SCSS organized.

Code Example (React with SCSS Modules):

Button.module.scss:

// Define some SCSS variables
$primary-color: dodgerblue;
$secondary-color: lightgray;
$text-color: white;
$border-radius: 4px;
$padding: 0.5em 1em;

// Base button style
.button {
  color: $text-color;
  border: none;
  border-radius: $border-radius;
  padding: $padding;
  font-size: 1em;
  cursor: pointer;
  transition: background-color 0.3s ease;

  // Nesting for hover state
  &:hover {
    opacity: 0.9;
  }
}

// Modifier for primary button
.primary {
  background-color: $primary-color;
  &:hover {
    background-color: darken($primary-color, 10%);
  }
}

// Modifier for secondary button
.secondary {
  background-color: $secondary-color;
  color: #333; // Different text color for light background
  &:hover {
    background-color: darken($secondary-color, 10%);
  }
}

// Modifier for large button
.large {
  font-size: 1.2em;
  padding: 0.75em 1.5em;
}

// Example of a shared utility class within the module
.clickable {
  cursor: pointer;
}

// Example of composing styles
.fancyButton {
  composes: button; // Inherits all styles from .button
  border: 2px solid $primary-color;
  font-weight: bold;
}

Button.js (React Component):

import React from 'react';
import styles from './Button.module.scss'; // Import the SCSS module

// Helper to combine class names
const cx = (...classNames) => classNames.filter(Boolean).join(' ');

const Button = ({ children, type = 'secondary', size, onClick, isFancy }) => {
  const buttonClass = cx(
    styles.button, // Base class
    type === 'primary' && styles.primary,
    type === 'secondary' && styles.secondary,
    size === 'large' && styles.large,
    isFancy && styles.fancyButton, // Apply fancyButton styles
    styles.clickable // Apply clickable utility
  );

  return (
    <button className={buttonClass} onClick={onClick}>
      {children}
    </button>
  );
};

export default Button;

// App.js (Usage)
// import Button from './Button';
// <Button type="primary">Primary Button</Button>
// <Button type="secondary" size="large">Large Secondary</Button>
// <Button isFancy>Fancy Button</Button>

Common Beginner Doubts

  1. Which modular CSS approach is the "best"?

    • There's no single "best" approach; it depends on the project's needs, team preferences, and existing stack.
    • CSS-in-JS is great for dynamic styling and tight component coupling in JS-heavy apps.
    • BEM is excellent for a convention-based approach in projects where CSS/HTML separation is preferred, or when not using a JS framework heavily.
    • CSS/SCSS Modules offer a good balance, providing true scope with the familiarity of CSS/SCSS and good integration with JS frameworks.
  2. Can I use BEM with CSS Modules or CSS-in-JS?

    • Yes, you can. With CSS Modules, BEM can still provide a semantic structure to your class names before they are hashed (e.g., styles['button--primary']).
    • With CSS-in-JS, the need for BEM's naming convention is reduced because styles are already scoped, but some teams might still use BEM-like naming for component variations within the styled component definition for clarity.
  3. How do CSS Modules prevent global scope issues if I still write plain CSS?

    • CSS Modules work by transforming your class names during the build process. A class like .title in MyComponent.module.css might become MyComponent_title__xYz123 in the actual browser output. This unique name prevents it from clashing with another .title class defined elsewhere.
  4. Is CSS-in-JS bad for performance?

    • It can be if not used carefully. Some libraries have a runtime cost for injecting styles. However, many modern CSS-in-JS libraries are highly optimized, support server-side rendering (SSR), and can extract static CSS at build time, mitigating performance concerns. Always benchmark and profile if performance is critical.
  5. Do I need a JavaScript framework to use these?

    • CSS-in-JS: Almost always tied to JavaScript components/frameworks.
    • BEM: Purely a CSS naming convention; can be used with any HTML/CSS project, with or without JS frameworks.
    • CSS/SCSS Modules: Typically require a build step (like Webpack, Parcel) which are common in projects using JS frameworks, but can be set up for non-framework projects too if a build process is acceptable.
  6. What about utility-first CSS frameworks like Tailwind CSS? How do they fit in?

    • Utility-first CSS (e.g., Tailwind CSS) is another approach to styling that focuses on composing interfaces by applying small, single-purpose utility classes directly in the HTML. It can be seen as a form of modularity at a very granular level. It can be used alongside or as an alternative to the methods described here. For instance, you might use CSS Modules for component-level structure and Tailwind for fine-grained styling within those components.
  7. How do I handle global styles (like resets or base typography) with CSS Modules or CSS-in-JS?

    • CSS Modules: Use the :global syntax (e.g., :global(body) { margin: 0; }) in a dedicated global CSS Module file, or import a regular CSS file for global styles.
    • CSS-in-JS: Most libraries provide a way to inject global styles (e.g., createGlobalStyle in Styled Components).

This comprehensive overview should provide a solid understanding of Modular CSS approaches. Choosing the right one depends on your project requirements, team familiarity, and desired trade-offs between developer experience, performance, and scalability.