Bridging JavaScript and CSS Architectures
Welcome, JavaScript developers! As you transition into full-stack Python development, understanding CSS architecture is crucial for building maintainable web applications. Your background in JavaScript has already equipped you with many important concepts that translate well to CSS organization—modular thinking, separation of concerns, and state management.
In this session, we'll explore CSS architecture with a focus on approaches that will feel familiar to JavaScript developers. We'll draw parallels between JavaScript patterns and their CSS counterparts, examine component-based CSS strategies that align with modern JavaScript frameworks, and look at tools that bring programming-like features to your stylesheets.
By the end of this session, you'll understand how to structure CSS in ways that complement your JavaScript knowledge and create cohesive, maintainable codebases that bridge both languages effectively.
File Organization
For today's session, we'll use the following files:
- CSS Folder:
styles/in your project root - Example Files:
styles/css_architecture_examples.css - HTML File:
css_architecture_for_js_devs.htmlin your project root
Make sure to create these files and link them properly before we begin the exercises.
Mental Models: From JavaScript to CSS
JavaScript developers often face challenges with CSS because the mental models differ. Let's bridge that gap by comparing familiar JavaScript concepts with their CSS counterparts:
Modularity and Encapsulation
| JavaScript Concept | CSS Counterpart |
|---|---|
| Modules (ES modules, CommonJS) | CSS Modules, Scoped CSS, BEM namespacing |
| Private variables/methods | Encapsulated styles with Shadow DOM or CSS Modules |
| Export/import system | Sass @import, CSS @import, or bundler imports |
/* JavaScript module */
// math.js
export function add(a, b) {
return a + b;
}
// app.js
import { add } from './math.js';
console.log(add(2, 3)); // 5
/* CSS "module" with BEM */
/* button.css */
.button {
padding: 10px 15px;
border-radius: 4px;
}
.button--primary {
background-color: #0066cc;
color: white;
}
/* Using CSS Modules in React */
import styles from './Button.module.css';
function Button() {
return (
<button className={styles.button}>Click me</button>
);
}
Key mindset shift: Just as you wouldn't pollute the global JavaScript scope with variables, avoid global CSS classes. Namespace your styles to create "modules" that don't interfere with each other.
State Management
| JavaScript Concept | CSS Counterpart |
|---|---|
| State (React state, Redux) | CSS classes for state (.is-active, .is-disabled) |
| Conditional rendering | State-based classes and CSS selectors |
| Reactive updates | CSS transitions and animations triggered by class changes |
/* JavaScript state */
// React component with state
const [isActive, setIsActive] = useState(false);
return (
<button
className={isActive ? 'button active' : 'button'}
onClick={() => setIsActive(!isActive)}
>
Toggle
</button>
);
/* CSS state representation */
.button {
background-color: #f8f9fa;
transition: background-color 0.3s;
}
.button.active {
background-color: #0066cc;
color: white;
}
Key mindset shift: Think of CSS classes as state indicators. Just as you manage state in JavaScript, use classes to reflect UI state in your styles.
Composition vs Inheritance
| JavaScript Concept | CSS Counterpart |
|---|---|
| Composition over inheritance | Utility classes, CSS composition, multiple classes |
| Higher-order functions | Mixins in Sass, custom properties as parameters |
| Functional composition | Combining utility classes or CSS functions |
/* JavaScript composition */
// Composition of functions
const double = x => x * 2;
const increment = x => x + 1;
const transform = compose(double, increment);
transform(5); // double(increment(5)) = double(6) = 12
/* CSS composition with utility classes */
<button class="btn rounded shadow-sm text-primary">Click Me</button>
/* CSS composition with Sass mixins */
@mixin rounded {
border-radius: 4px;
}
@mixin shadowed {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.button {
@include rounded;
@include shadowed;
background-color: white;
}
Key mindset shift: Instead of creating deeply nested hierarchies of styles (inheritance), compose functionality by combining smaller, focused style modules (composition).
Component-Based CSS Architectures
Modern JavaScript development is heavily component-oriented. Let's explore CSS approaches that align well with component-based thinking:
BEM for Component-Based Structure
BEM (Block, Element, Modifier) provides a naming convention that maps perfectly to component thinking:
- Block: The component itself (e.g.,
.card) - Element: A part of the component (e.g.,
.card__title) - Modifier: A variant or state (e.g.,
.card--featured)
/* BEM with a React-like component structure */
/* Card.css */
.card {
border: 1px solid #ddd;
border-radius: 4px;
padding: 20px;
}
.card__title {
font-size: 1.2rem;
margin-top: 0;
}
.card__content {
color: #666;
}
.card__footer {
margin-top: 20px;
padding-top: 10px;
border-top: 1px solid #eee;
}
.card--featured {
border-color: #0066cc;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* Equivalent React component structure */
function Card({ title, children, featured }) {
return (
<div className={`card ${featured ? 'card--featured' : ''}`}>
<h2 className="card__title">{title}</h2>
<div className="card__content">{children}</div>
<div className="card__footer">
<button className="card__button">Read more</button>
</div>
</div>
);
}
Why it works for JS devs: BEM creates a visual mapping between your CSS classes and your component structure, making the relationship between markup, styles, and components explicit.
CSS Modules for Component Isolation
CSS Modules automatically scope CSS to components, similar to JavaScript modules:
/* CSS Module example */
/* Button.module.css */
.button {
padding: 10px 15px;
border-radius: 4px;
font-weight: bold;
}
.primary {
background-color: #0066cc;
color: white;
}
/* React component using CSS Module */
import styles from './Button.module.css';
function Button({ primary, children }) {
return (
<button
className={`${styles.button} ${primary ? styles.primary : ''}`}
>
{children}
</button>
);
}
/* Compiled HTML might look like */
<button class="Button_button_ax7yz Button_primary_bc8u3">
Click me
</button>
Why it works for JS devs: CSS Modules bring true encapsulation to CSS, eliminating global scope issues just like ES modules do for JavaScript. The tooling handles unique class names automatically.
Styled Components and CSS-in-JS
CSS-in-JS approaches let you write styles directly in your JavaScript components:
/* Styled-components example */
import styled from 'styled-components';
const Button = styled.button`
padding: 10px 15px;
border-radius: 4px;
font-weight: bold;
background-color: ${props => props.primary ? '#0066cc' : 'transparent'};
color: ${props => props.primary ? 'white' : '#0066cc'};
border: 1px solid #0066cc;
&:hover {
background-color: ${props => props.primary ? '#0055aa' : '#e6f7ff'};
}
`;
function App() {
return (
<div>
<Button primary>Primary Button</Button>
<Button>Secondary Button</Button>
</div>
);
}
Why it works for JS devs: CSS-in-JS brings styles directly into your JavaScript code, eliminating context switching and allowing you to use JavaScript features (variables, functions, conditions) to generate styles.
Utility-First CSS (Tailwind CSS)
Utility-first approaches use small, single-purpose classes composed directly in HTML, similar to functional composition in JavaScript:
/* Utility-first approach (Tailwind CSS) */
<button class="px-4 py-2 rounded bg-blue-600 text-white hover:bg-blue-700 transition-colors">
Click me
</button>
/* Equivalent in traditional CSS */
.button {
padding: 0.5rem 1rem;
border-radius: 0.25rem;
background-color: #2563eb;
color: white;
transition: background-color 0.3s;
}
.button:hover {
background-color: #1d4ed8;
}
Why it works for JS devs: Utility-first CSS focuses on composition over inheritance and moves styling decisions into the markup, similar to how props control component appearance in React. This approach creates a tight feedback loop between markup and styling.
CSS Architecture in JavaScript Framework Contexts
Let's examine CSS architecture approaches optimized for specific JavaScript frameworks:
React and CSS
React's component model works well with several CSS approaches:
- CSS Modules: Scoped CSS files imported directly into components
- Styled-components/Emotion: CSS-in-JS libraries that create styled React components
- Tailwind CSS: Utility classes composed in JSX
- CSS-in-JS with hooks: Libraries like useStyling or styled-hook
/* React with multiple styling approaches */
// 1. CSS Modules
import styles from './Button.module.css';
function CSSModuleButton({ primary, children }) {
return (
<button className={`${styles.button} ${primary ? styles.primary : ''}`}>
{children}
</button>
);
}
// 2. Styled-components
import styled from 'styled-components';
const StyledButton = styled.button`
/* styles here */
`;
function StyledComponentButton({ primary, children }) {
return (
<StyledButton primary={primary}>{children}</StyledButton>
);
}
// 3. Tailwind CSS
function TailwindButton({ primary, children }) {
return (
<button
className={`px-4 py-2 rounded ${
primary ? 'bg-blue-600 text-white' : 'bg-white text-blue-600 border border-blue-600'
}`}
>
{children}
</button>
);
}
Key consideration: With React, choose an approach that keeps styles close to your components. React's virtual DOM works best when style changes are localized to the components being updated.
Vue and CSS
Vue offers built-in component-scoped CSS:
- Scoped CSS: Vue's
<style scoped>feature automatically namespaces CSS - CSS Modules: Vue supports CSS Modules with
<style module> - Single-File Components: Keep HTML, CSS, and JS in one .vue file
/* Vue Single-File Component with scoped CSS */
<template>
<button :class="['button', { 'button--primary': primary }]">
<slot></slot>
</button>
</template>
<script>
export default {
props: {
primary: Boolean
}
}
</script>
<style scoped>
.button {
padding: 10px 15px;
border-radius: 4px;
font-weight: bold;
}
.button--primary {
background-color: #0066cc;
color: white;
}
</style>
Key consideration: Vue's built-in scoping makes CSS organization more straightforward. Take advantage of Single-File Components to keep component code and styles together.
Angular and CSS
Angular provides component-level style encapsulation:
- Component Styles: Styles defined in the @Component decorator
- View Encapsulation: Angular's emulated encapsulation adds unique attributes
- NgClass and NgStyle: Directives for dynamic styling
/* Angular component with encapsulated styles */
import { Component } from '@angular/core';
@Component({
selector: 'app-button',
template: `
<button [class.primary]="isPrimary" (click)="togglePrimary()">
Click me
</button>
`,
styles: [`
button {
padding: 10px 15px;
border-radius: 4px;
font-weight: bold;
background-color: transparent;
border: 1px solid #0066cc;
color: #0066cc;
}
button.primary {
background-color: #0066cc;
color: white;
}
`]
})
export class ButtonComponent {
isPrimary = false;
togglePrimary() {
this.isPrimary = !this.isPrimary;
}
}
Key consideration: Angular's ViewEncapsulation ensures styles don't leak between components. Structure your CSS according to Angular's component architecture for the best results.
Managing State with CSS
JavaScript developers are accustomed to state management. Here's how to apply similar concepts in CSS:
CSS Classes as State Indicators
Use consistent class naming for states, following patterns from JavaScript state management:
/* State classes for UI components */
.dropdown {
/* Default/closed state */
}
.dropdown.is-open {
/* Open state */
}
.button {
/* Default state */
}
.button.is-loading {
/* Loading state */
}
.button.is-disabled {
/* Disabled state */
}
.form-field {
/* Default state */
}
.form-field.has-error {
/* Error state */
}
JavaScript integration:
// Toggle state with JavaScript
function toggleDropdown(dropdownId) {
const dropdown = document.getElementById(dropdownId);
dropdown.classList.toggle('is-open');
}
// React state to CSS class mapping
function Button({ isLoading, isDisabled, children }) {
const buttonClasses = [
'button',
isLoading ? 'is-loading' : '',
isDisabled ? 'is-disabled' : ''
].filter(Boolean).join(' ');
return (
<button className={buttonClasses} disabled={isDisabled}>
{isLoading ? 'Loading...' : children}
</button>
);
}
Parallel to JavaScript: Just as you might have an isLoading state in JavaScript, use an .is-loading class in CSS. This creates a clear relationship between your JavaScript state and visual representation.
CSS Custom Properties for Dynamic State
CSS Custom Properties (variables) can act like reactive state in your styles:
/* CSS Custom Properties for theme state */
:root {
/* Default theme */
--primary-color: #0066cc;
--text-color: #333;
--background-color: #fff;
}
/* Theme state changes */
.theme-dark {
--primary-color: #5cbbff;
--text-color: #f8f9fa;
--background-color: #222;
}
/* Using the dynamic state */
.button {
background-color: var(--primary-color);
color: white;
}
body {
color: var(--text-color);
background-color: var(--background-color);
}
JavaScript integration:
// Toggle theme with JavaScript
function toggleDarkMode() {
document.documentElement.classList.toggle('theme-dark');
}
// Setting custom property values directly with JS
function setCustomAccentColor(color) {
document.documentElement.style.setProperty('--accent-color', color);
}
Parallel to JavaScript: CSS Custom Properties function like reactive variables in JavaScript frameworks. Changing the value updates all instances where the variable is used, similar to reactive state.
State Machines in CSS
For complex UI components, you can implement state machine patterns with CSS:
/* CSS state machine for a multi-step form */
.form-container {
/* Common styles */
}
/* State: Step 1 active */
.form-container[data-step="1"] .step-1 {
display: block;
}
.form-container[data-step="1"] .step-2,
.form-container[data-step="1"] .step-3 {
display: none;
}
.form-container[data-step="1"] .progress-indicator {
width: 33.33%;
}
/* State: Step 2 active */
.form-container[data-step="2"] .step-2 {
display: block;
}
.form-container[data-step="2"] .step-1,
.form-container[data-step="2"] .step-3 {
display: none;
}
.form-container[data-step="2"] .progress-indicator {
width: 66.66%;
}
/* State: Step 3 active */
.form-container[data-step="3"] .step-3 {
display: block;
}
.form-container[data-step="3"] .step-1,
.form-container[data-step="3"] .step-2 {
display: none;
}
.form-container[data-step="3"] .progress-indicator {
width: 100%;
}
JavaScript integration:
// State machine in JavaScript
const formStateMachine = {
currentState: 1,
nextStep() {
if (this.currentState < 3) {
this.currentState++;
this.updateDOM();
}
},
prevStep() {
if (this.currentState > 1) {
this.currentState--;
this.updateDOM();
}
},
updateDOM() {
document.querySelector('.form-container')
.setAttribute('data-step', this.currentState);
}
};
Parallel to JavaScript: This approach mirrors state machine concepts in JavaScript, where distinct states trigger specific UI representations. The data-* attributes act as the interface between JavaScript state and CSS styling.
Scaling CSS Architecture for Large Applications
JavaScript developers working on large applications need CSS architecture patterns that scale:
ITCSS (Inverted Triangle CSS)
ITCSS organizes CSS by specificity and reach, creating layers from generic to explicit:
/* ITCSS Layer Structure */
/* 1. Settings - Variables, config */
:root {
--primary-color: #0066cc;
--spacing-unit: 8px;
}
/* 2. Tools - Mixins, functions */
@mixin center-content {
display: flex;
align-items: center;
justify-content: center;
}
/* 3. Generic - Resets, normalize */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* 4. Elements - Bare HTML elements */
body {
font-family: Arial, sans-serif;
line-height: 1.6;
}
h1, h2, h3 {
margin-bottom: calc(var(--spacing-unit) * 2);
}
/* 5. Objects - Structural patterns, no cosmetics */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 calc(var(--spacing-unit) * 2);
}
.grid {
display: grid;
gap: calc(var(--spacing-unit) * 2);
}
/* 6. Components - Specific UI components */
.card {
border: 1px solid #ddd;
border-radius: 4px;
padding: calc(var(--spacing-unit) * 2);
}
.button {
display: inline-block;
padding: var(--spacing-unit) calc(var(--spacing-unit) * 2);
background-color: var(--primary-color);
color: white;
border-radius: 4px;
}
/* 7. Utilities - Helpers, overrides */
.text-center { text-align: center; }
.mt-1 { margin-top: var(--spacing-unit); }
.mt-2 { margin-top: calc(var(--spacing-unit) * 2); }
Why it works for JS devs: ITCSS creates a clear mental model for where different types of styles belong, similar to the separation of concerns in JavaScript architecture.
Atomic Design
Atomic Design structures components from simple to complex, similar to component composition in JS frameworks:
- Atoms: Basic building blocks (buttons, inputs, labels)
- Molecules: Simple combinations of atoms (search form, menu item)
- Organisms: More complex components (header, product grid)
- Templates: Page-level component arrangements
- Pages: Specific instances of templates with real content
/* Atomic Design structure in CSS organization */
/* atoms/_buttons.css */
.button {
padding: 10px 15px;
border-radius: 4px;
font-weight: bold;
}
.button--primary {
background-color: var(--color-primary);
color: white;
}
/* atoms/_inputs.css */
.input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
/* molecules/_search-form.css */
.search-form {
display: flex;
}
.search-form__input {
flex: 1;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.search-form__button {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
/* organisms/_header.css */
.header {
display: flex;
justify-content: space-between;
padding: 20px;
background-color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.header__logo {
font-weight: bold;
}
.header__search {
width: 300px;
}
Why it works for JS devs: Atomic Design's component hierarchy mirrors how you'd structure components in React or Vue, providing a familiar mental model for building complex interfaces from smaller parts.
Feature-Based Organization
Organize CSS by feature or domain rather than by technical type, similar to feature folders in JavaScript applications:
/* Feature-based CSS organization */
/* features/authentication/_login-form.css */
.login-form { /* styles */ }
.login-form__input { /* styles */ }
.login-form__button { /* styles */ }
/* features/products/_product-card.css */
.product-card { /* styles */ }
.product-card__image { /* styles */ }
.product-card__title { /* styles */ }
.product-card__price { /* styles */ }
/* features/checkout/_payment-form.css */
.payment-form { /* styles */ }
.payment-form__section { /* styles */ }
.payment-form__submit { /* styles */ }
Why it works for JS devs: This organization mirrors modern JavaScript feature-based architecture, aligning your CSS structure with your application's domain concepts rather than technical concerns.
CSS-in-JS Approaches for JavaScript Developers
CSS-in-JS solutions bring styling directly into your JavaScript workflow, eliminating the context switch between languages:
Styled-components
Create React components with attached styles using template literals:
/* Styled-components example */
import styled from 'styled-components';
// Creating a styled component
const Button = styled.button`
padding: 10px 15px;
border-radius: 4px;
background-color: ${props => props.primary ? '#0066cc' : 'transparent'};
color: ${props => props.primary ? 'white' : '#0066cc'};
border: 1px solid #0066cc;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: ${props => props.primary ? '#0055aa' : '#e6f7ff'};
}
${props => props.large && `
font-size: 18px;
padding: 12px 20px;
`}
`;
// Using the styled component
function App() {
return (
<div>
<Button primary>Primary Button</Button>
<Button>Secondary Button</Button>
<Button primary large>Large Primary Button</Button>
</div>
);
}
Benefits for JS devs:
- Write styles in JavaScript directly
- Component props can control styling
- Styles are scoped to components automatically
- Use JavaScript variables, functions, and logic in your styles
- Only the styles needed are included in the bundle
Emotion
Similar to styled-components but with more flexibility:
/* Emotion example */
/** @jsx jsx */
import { css, jsx } from '@emotion/react';
const buttonBase = css`
padding: 10px 15px;
border-radius: 4px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
`;
const primaryStyle = css`
background-color: #0066cc;
color: white;
border: 1px solid #0066cc;
&:hover {
background-color: #0055aa;
}
`;
const secondaryStyle = css`
background-color: transparent;
color: #0066cc;
border: 1px solid #0066cc;
&:hover {
background-color: #e6f7ff;
}
`;
function Button({ primary, children }) {
return (
<button
css={[
buttonBase,
primary ? primaryStyle : secondaryStyle
]}
>
{children}
</button>
);
}
Benefits for JS devs:
- Composition-based approach using arrays of styles
- Works with and without React
- Can be used with the css prop, styled API, or as plain objects
- Good integration with TypeScript
JSS (JavaScript Style Sheets)
Write styles as JavaScript objects:
/* JSS example */
import { createUseStyles } from 'react-jss';
// Define styles as JavaScript objects
const useStyles = createUseStyles({
button: {
padding: '10px 15px',
borderRadius: 4,
fontWeight: 'bold',
cursor: 'pointer',
transition: 'all 0.3s',
},
primaryButton: {
backgroundColor: '#0066cc',
color: 'white',
border: '1px solid #0066cc',
'&:hover': {
backgroundColor: '#0055aa',
},
},
secondaryButton: {
backgroundColor: 'transparent',
color: '#0066cc',
border: '1px solid #0066cc',
'&:hover': {
backgroundColor: '#e6f7ff',
},
},
});
function Button({ primary, children }) {
const classes = useStyles();
return (
<button className={`${classes.button} ${primary ? classes.primaryButton : classes.secondaryButton}`}>
{children}
</button>
);
}
Benefits for JS devs:
- Pure JavaScript object syntax for styles
- Works well with TypeScript for type-checking styles
- Framework-agnostic with adapters for various libraries
- Good performance with built-in caching
Choosing a CSS-in-JS Approach
Consider these factors when selecting a CSS-in-JS library:
- Runtime vs. build time: Some solutions inject styles at runtime, others extract CSS at build time
- Bundle size: Runtime libraries add to your JavaScript bundle size
- Framework integration: Some solutions are tightly coupled to specific frameworks
- Developer experience: Syntax and workflow preferences matter
- Type safety: TypeScript integration varies between libraries
CSS Preprocessors for JavaScript Developers
CSS preprocessors add programming features to CSS, making it more familiar to JavaScript developers:
Sass/SCSS
Sass adds variables, nesting, mixins, and more to CSS:
/* Sass example */
// Variables
$primary-color: #0066cc;
$border-radius: 4px;
$spacing: 8px;
// Mixins (like JS functions)
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
@mixin button-variant($bg-color, $text-color) {
background-color: $bg-color;
color: $text-color;
&:hover {
background-color: darken($bg-color, 10%);
}
}
// Nesting (like component hierarchy)
.card {
border: 1px solid #ddd;
border-radius: $border-radius;
padding: $spacing * 2;
&__header {
margin-bottom: $spacing;
h2 {
margin: 0;
}
}
&__content {
color: #666;
}
&__footer {
margin-top: $spacing * 2;
@include flex-center;
}
// Modifiers (like component props)
&--featured {
border-color: $primary-color;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
// Button component with variants
.button {
padding: $spacing ($spacing * 2);
border-radius: $border-radius;
border: none;
font-weight: bold;
cursor: pointer;
&--primary {
@include button-variant($primary-color, white);
}
&--secondary {
@include button-variant(transparent, $primary-color);
border: 1px solid $primary-color;
}
}
JavaScript parallels:
- Variables are like JavaScript constants
- Mixins are like JavaScript functions
- Nesting is like scoping in JavaScript
- @import is like JavaScript imports
- Mathematical operations similar to JavaScript expressions
PostCSS
PostCSS is a tool for transforming CSS with JavaScript plugins, making it highly configurable:
/* PostCSS example with plugins */
/* Using cssNext features */
:root {
--primary-color: #0066cc;
--spacing: 8px;
}
/* Nesting plugin */
.card {
border-radius: 4px;
padding: calc(var(--spacing) * 2);
& .card__title {
color: var(--primary-color);
}
}
/* Autoprefixer plugin automatically adds vendor prefixes */
.container {
display: flex;
user-select: none;
}
/* postcss.config.js */
module.exports = {
plugins: [
require('postcss-preset-env')({
features: {
'nesting-rules': true
}
}),
require('autoprefixer'),
require('cssnano')
]
}
Why it works for JS devs: PostCSS uses JavaScript for processing CSS, allowing you to create custom transformations using familiar JavaScript syntax. You can add only the features you need through plugins.
Testing CSS Architecture
JavaScript developers are familiar with testing concepts that can be applied to CSS:
Visual Regression Testing
Capture screenshots and compare them to detect unwanted visual changes:
/* Using Jest and Puppeteer for visual regression testing */
// visual.test.js
const puppeteer = require('puppeteer');
const { toMatchImageSnapshot } = require('jest-image-snapshot');
expect.extend({ toMatchImageSnapshot });
describe('Button Component', () => {
let browser;
let page;
beforeAll(async () => {
browser = await puppeteer.launch();
page = await browser.newPage();
await page.goto('http://localhost:3000/components/button');
});
afterAll(async () => {
await browser.close();
});
it('renders primary button correctly', async () => {
const button = await page.$('.button--primary');
const image = await button.screenshot();
expect(image).toMatchImageSnapshot();
});
it('renders secondary button correctly', async () => {
const button = await page.$('.button--secondary');
const image = await button.screenshot();
expect(image).toMatchImageSnapshot();
});
});
Why it works for JS devs: This approach integrates with JavaScript testing frameworks like Jest, providing a familiar testing workflow for component styling.
CSS Unit Testing
Test specific CSS properties with tools like Jest and JSDOM:
/* CSS unit testing with Jest */
// button.test.js
import { render } from '@testing-library/react';
import Button from './Button';
describe('Button', () => {
it('has correct base styles', () => {
const { getByRole } = render(<Button>Click me</Button>);
const button = getByRole('button');
const styles = window.getComputedStyle(button);
expect(styles.padding).toBe('10px 15px');
expect(styles.borderRadius).toBe('4px');
expect(styles.fontWeight).toBe('bold');
});
it('has primary styles when primary prop is true', () => {
const { getByRole } = render(<Button primary>Click me</Button>);
const button = getByRole('button');
const styles = window.getComputedStyle(button);
expect(styles.backgroundColor).toBe('#0066cc');
expect(styles.color).toBe('white');
});
});
Why it works for JS devs: This approach lets you test CSS as part of your JavaScript component tests, ensuring that style changes don't break component appearance.
Style Linting
Enforce CSS best practices with linting tools:
/* Stylelint configuration */
// .stylelintrc.js
module.exports = {
extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
plugins: ['stylelint-order'],
rules: {
'color-hex-case': 'lower',
'color-hex-length': 'short',
'selector-class-pattern': '^[a-z][a-zA-Z0-9_-]+$',
'selector-max-id': 0,
'order/properties-alphabetical-order': true
}
};
// Command line usage
// npx stylelint "src/**/*.css" --fix
Why it works for JS devs: Stylelint works similar to ESLint for JavaScript, providing automated checks and fixes for CSS code quality issues.
CSS Performance Optimization for JavaScript Developers
Apply JavaScript performance optimization thinking to CSS:
Code Splitting for CSS
Load CSS only when needed, similar to JavaScript code splitting:
/* CSS code splitting with Webpack */
// JavaScript entry points
// home.js
import './styles/common.css';
import './styles/home.css';
// product.js
import './styles/common.css';
import './styles/product.css';
// Webpack configuration
// webpack.config.js
module.exports = {
entry: {
home: './src/home.js',
product: './src/product.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css'
})
]
};
Why it works for JS devs: This approach lets you bundle CSS with your JavaScript modules, loading styles only when they're needed just like lazy-loaded JavaScript.
Critical CSS
Inline critical styles and defer non-critical styles for faster initial render:
/* Critical CSS implementation */
// Using the critical package with Gulp
const gulp = require('gulp');
const critical = require('critical');
gulp.task('critical', () => {
return critical.generate({
inline: true,
base: 'dist/',
src: 'index.html',
target: {
html: 'index-critical.html',
css: 'critical.css'
},
width: 1300,
height: 900,
minify: true
});
});
/* Result in HTML */
<head>
<style>
/* Critical CSS inlined here */
body{font-family:sans-serif}
.header{height:60px;background:#fff}
.hero{height:500px}
</style>
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>
</head>
Why it works for JS devs: This approach is similar to JavaScript performance optimization techniques like code splitting and prefetching, focusing on delivering essential code first and deferring the rest.
Reducing Specificity and Selector Performance
Optimize CSS selectors for better rendering performance:
/* Inefficient selectors */
.header ul li a.nav-link {
color: blue;
}
/* More efficient selectors */
.nav-link {
color: blue;
}
Why it matters: While modern browsers have optimized CSS performance, keeping selectors simple still provides benefits for maintainability and reduces style conflicts.
Hands-On Exercise: Component-Based CSS Architecture
Let's apply what we've learned by building a small component library with a thoughtful CSS architecture.
Component Structure
We'll build the following components:
- Button (primary, secondary, large variants)
- Card (standard, featured variants)
- Form input (standard, error states)
- Navigation bar
CSS Architecture Approach
We'll use a BEM-based component architecture with a few key principles:
- Each component has its own CSS file
- Design tokens stored as CSS custom properties
- State classes to manage component states
- Consistent naming conventions
Design Tokens (variables.css)
/* variables.css */
:root {
/* Colors */
--color-primary: #0066cc;
--color-primary-dark: #0055aa;
--color-secondary: #6c757d;
--color-secondary-dark: #5a6268;
--color-success: #28a745;
--color-danger: #dc3545;
--color-warning: #ffc107;
--color-info: #17a2b8;
--color-light: #f8f9fa;
--color-dark: #343a40;
--color-white: #ffffff;
/* Text colors */
--text-primary: #212529;
--text-secondary: #6c757d;
--text-light: #f8f9fa;
/* Border colors */
--border-color: #dee2e6;
--border-color-focus: #80bdff;
/* Spacing */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
/* Typography */
--font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-size-base: 16px;
--font-size-sm: 14px;
--font-size-lg: 18px;
--font-weight-normal: 400;
--font-weight-bold: 700;
--line-height-base: 1.5;
/* Borders */
--border-radius: 4px;
--border-radius-lg: 8px;
--border-width: 1px;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
/* Animation */
--transition-base: all 0.2s ease-in-out;
}
Component CSS Files
/* components/button.css */
.button {
display: inline-block;
font-family: var(--font-family-base);
font-size: var(--font-size-base);
font-weight: var(--font-weight-bold);
line-height: var(--line-height-base);
text-align: center;
white-space: nowrap;
vertical-align: middle;
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--border-radius);
border: var(--border-width) solid transparent;
cursor: pointer;
transition: var(--transition-base);
}
/* Variants */
.button--primary {
background-color: var(--color-primary);
border-color: var(--color-primary);
color: var(--color-white);
}
.button--primary:hover {
background-color: var(--color-primary-dark);
border-color: var(--color-primary-dark);
}
.button--secondary {
background-color: var(--color-secondary);
border-color: var(--color-secondary);
color: var(--color-white);
}
.button--secondary:hover {
background-color: var(--color-secondary-dark);
border-color: var(--color-secondary-dark);
}
.button--outline {
background-color: transparent;
border-color: var(--color-primary);
color: var(--color-primary);
}
.button--outline:hover {
background-color: var(--color-primary);
color: var(--color-white);
}
/* Sizes */
.button--large {
font-size: var(--font-size-lg);
padding: var(--spacing-md) var(--spacing-lg);
}
.button--small {
font-size: var(--font-size-sm);
padding: var(--spacing-xs) var(--spacing-sm);
}
/* States */
.button.is-disabled,
.button:disabled {
opacity: 0.65;
pointer-events: none;
}
.button.is-loading {
position: relative;
color: transparent;
}
.button.is-loading::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 1em;
height: 1em;
margin-top: -0.5em;
margin-left: -0.5em;
border-radius: 50%;
border: 2px solid currentColor;
border-right-color: transparent;
animation: button-loading-spinner 0.75s linear infinite;
}
@keyframes button-loading-spinner {
to {
transform: rotate(360deg);
}
}
/* components/card.css */
.card {
background-color: var(--color-white);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
box-shadow: var(--shadow-sm);
overflow: hidden;
}
.card__header {
padding: var(--spacing-md);
border-bottom: var(--border-width) solid var(--border-color);
background-color: var(--color-light);
}
.card__title {
margin: 0;
font-size: var(--font-size-lg);
font-weight: var(--font-weight-bold);
}
.card__content {
padding: var(--spacing-md);
}
.card__footer {
padding: var(--spacing-md);
border-top: var(--border-width) solid var(--border-color);
background-color: var(--color-light);
}
/* Variants */
.card--featured {
border-color: var(--color-primary);
box-shadow: var(--shadow-md);
}
.card--featured .card__header {
background-color: var(--color-primary);
color: var(--color-white);
}
/* components/form.css */
.form-group {
margin-bottom: var(--spacing-md);
}
.form-label {
display: block;
margin-bottom: var(--spacing-xs);
font-weight: var(--font-weight-bold);
}
.form-input {
display: block;
width: 100%;
padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--font-size-base);
line-height: var(--line-height-base);
color: var(--text-primary);
background-color: var(--color-white);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
transition: var(--transition-base);
}
.form-input:focus {
border-color: var(--border-color-focus);
outline: 0;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
/* States */
.form-input.has-error {
border-color: var(--color-danger);
}
.form-input.has-error:focus {
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}
.form-error-message {
margin-top: var(--spacing-xs);
color: var(--color-danger);
font-size: var(--font-size-sm);
}
/* components/navbar.css */
.navbar {
display: flex;
align-items: center;
padding: var(--spacing-md) var(--spacing-lg);
background-color: var(--color-dark);
color: var(--color-white);
}
.navbar__brand {
margin-right: var(--spacing-xl);
font-size: var(--font-size-lg);
font-weight: var(--font-weight-bold);
color: var(--color-white);
text-decoration: none;
}
.navbar__nav {
display: flex;
list-style: none;
margin: 0;
padding: 0;
}
.navbar__item {
margin-right: var(--spacing-md);
}
.navbar__link {
color: rgba(255, 255, 255, 0.75);
text-decoration: none;
transition: var(--transition-base);
}
.navbar__link:hover,
.navbar__link.is-active {
color: var(--color-white);
}
Main CSS File
/* main.css */
/* Import variables first */
@import 'variables.css';
/* Base styles */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-family-base);
font-size: var(--font-size-base);
line-height: var(--line-height-base);
color: var(--text-primary);
background-color: var(--color-light);
}
/* Container utility */
.container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 var(--spacing-md);
}
/* Import components */
@import 'components/button.css';
@import 'components/card.css';
@import 'components/form.css';
@import 'components/navbar.css';
HTML Implementation
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Component Library</title>
<link rel="stylesheet" href="main.css">
</head>
<body>
<nav class="navbar">
<a href="#" class="navbar__brand">Component Library</a>
<ul class="navbar__nav">
<li class="navbar__item">
<a href="#buttons" class="navbar__link">Buttons</a>
</li>
<li class="navbar__item">
<a href="#cards" class="navbar__link">Cards</a>
</li>
<li class="navbar__item">
<a href="#forms" class="navbar__link">Forms</a>
</li>
</ul>
</nav>
<div class="container">
<section id="buttons" style="margin: 2rem 0;">
<h2>Buttons</h2>
<div style="margin: 1rem 0;">
<button class="button button--primary">Primary Button</button>
<button class="button button--secondary">Secondary Button</button>
<button class="button button--outline">Outline Button</button>
</div>
<div style="margin: 1rem 0;">
<button class="button button--primary button--large">Large Button</button>
<button class="button button--primary button--small">Small Button</button>
</div>
<div style="margin: 1rem 0;">
<button class="button button--primary is-disabled">Disabled Button</button>
<button class="button button--primary is-loading">Loading Button</button>
</div>
</section>
<section id="cards" style="margin: 2rem 0;">
<h2>Cards</h2>
<div style="display: flex; gap: 1rem; margin: 1rem 0;">
<div class="card" style="width: 300px;">
<div class="card__header">
<h3 class="card__title">Standard Card</h3>
</div>
<div class="card__content">
<p>This is a standard card component with header, content, and footer sections.</p>
</div>
<div class="card__footer">
<button class="button button--primary">Action</button>
</div>
</div>
Featured Card
This is a featured card with a highlighted appearance to draw attention.