CSS Integration with JavaScript and Frameworks
1. CSS Object Model (CSSOM) and DOM Manipulation
| API | Method/Property | Usage | Example |
|---|---|---|---|
| element.style | Inline style manipulation | Set/get inline styles directly | el.style.color = 'red'; |
| getComputedStyle() | Get computed values | Read final CSS values after cascade | getComputedStyle(el).fontSize |
| classList | add, remove, toggle, contains | Manage CSS classes programmatically | el.classList.add('active') |
| CSSStyleSheet | insertRule, deleteRule | Dynamically modify stylesheets | sheet.insertRule('.new { }') |
| document.styleSheets | Access all stylesheets | Iterate through all CSS files | document.styleSheets[0] |
| CSS.supports() | Feature detection | Check if browser supports CSS property | CSS.supports('display', 'grid') |
| matchMedia() | Media query matching | Listen to media query changes in JS | matchMedia('(min-width: 768px)') |
| CSS Typed OM | CSSUnitValue, CSSStyleValue | Typed CSS values in JS (Houdini) | el.attributeStyleMap.set('width', CSS.px(100)) |
Example: Manipulation Examples
<!-- 1. Direct style manipulation -->
const box = document.querySelector('.box');
// Set individual properties
box.style.backgroundColor = '#3498db';
box.style.width = '200px';
box.style.transform = 'translateX(50px)';
// Set multiple properties with cssText
box.style.cssText = 'background: red; width: 200px; height: 200px;';
// Read computed styles
const computedStyle = getComputedStyle(box);
console.log(computedStyle.width); // "200px" (computed, not always same as set)
console.log(computedStyle.backgroundColor); // "rgb(52, 152, 219)"
<!-- 2. Class manipulation (preferred over direct style) -->
box.classList.add('active', 'highlighted');
box.classList.remove('inactive');
box.classList.toggle('visible');
if (box.classList.contains('active')) { /* ... */ }
<!-- 3. Dynamic stylesheet creation -->
const sheet = new CSSStyleSheet();
sheet.insertRule(`
.dynamic-class {
color: white;
background: linear-gradient(45deg, #667eea, #764ba2);
padding: 1rem;
}
`);
document.adoptedStyleSheets = [sheet];
<!-- 4. CSS feature detection -->
if (CSS.supports('display', 'grid')) {
document.body.classList.add('grid-supported');
} else {
document.body.classList.add('flexbox-fallback');
}
// Check property-value pair
if (CSS.supports('backdrop-filter', 'blur(10px)')) {
// Use backdrop-filter
}
<!-- 5. Media query listeners -->
const mediaQuery = window.matchMedia('(min-width: 768px)');
function handleMediaChange(e) {
if (e.matches) {
console.log('Tablet/Desktop view');
document.body.classList.add('large-screen');
} else {
console.log('Mobile view');
document.body.classList.remove('large-screen');
}
}
mediaQuery.addEventListener('change', handleMediaChange);
handleMediaChange(mediaQuery); // Initial check
Example: CSS Typed OM (Houdini) API
<!-- Modern typed CSS values API -->
const element = document.querySelector('.box');
// Old way: String manipulation
element.style.width = (parseInt(element.style.width) + 10) + 'px';
// New way: Typed values
const currentWidth = element.attributeStyleMap.get('width');
element.attributeStyleMap.set('width', CSS.px(currentWidth.value + 10));
// Math operations
element.attributeStyleMap.set('width', CSS.calc(CSS.percent(50), CSS.px(20)));
// Transform manipulation
element.attributeStyleMap.set('transform', new CSSTransformValue([
new CSSTranslate(CSS.px(100), CSS.px(50)),
new CSSRotate(CSS.deg(45))
]));
// Read computed values
const computedStyleMap = element.computedStyleMap();
const fontSize = computedStyleMap.get('font-size'); // CSSUnitValue {value: 16, unit: "px"}
console.log(fontSize.value); // 16
console.log(fontSize.unit); // "px"
Performance Considerations:
- Avoid style thrashing: Batch reads (getComputedStyle) before writes (style.property)
- Use classes over inline styles: classList changes trigger single style recalc vs multiple for inline
- Debounce/throttle: Limit style updates in scroll/resize handlers
- RequestAnimationFrame: Schedule style changes before next paint for smooth animations
- CSS containment: Use
containproperty to isolate style recalculation scope
2. CSS-in-JS Implementation Patterns
| Library | Approach | Pros | Cons |
|---|---|---|---|
| styled-components | Tagged templates, runtime | Dynamic styles, theming, SSR, TypeScript | Runtime overhead, bundle size, learning curve |
| Emotion | CSS prop, runtime/compile | Flexible API, composition, SSR, fast | Runtime cost, requires babel plugin |
| Vanilla Extract | Zero-runtime, build-time | TypeScript-first, no runtime, type-safe | Less flexible, static styles only |
| Linaria | Zero-runtime, build-time | No runtime cost, standard CSS | Limited dynamic styles, build step required |
| Stitches | Near-zero runtime | Variants, theming, tiny runtime, TypeScript | New library, smaller ecosystem |
| Panda CSS | Build-time, atomic CSS | Zero runtime, type-safe, utility-first | Requires build step, new ecosystem |
Example: styled-components Pattern
// Installation: npm install styled-components
import styled from 'styled-components';
// Basic styled component
const Button = styled.button`
background: ${props => props.primary ? '#3498db' : '#ecf0f1'};
color: ${props => props.primary ? 'white' : '#2c3e50'};
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
`;
// Usage
<Button primary>Primary Button</Button>
<Button>Secondary Button</Button>
// Extending styles
const IconButton = styled(Button)`
padding: 0.5rem;
border-radius: 50%;
`;
// Theming
import { ThemeProvider } from 'styled-components';
const theme = {
colors: {
primary: '#3498db',
secondary: '#2ecc71',
text: '#2c3e50'
},
spacing: {
sm: '0.5rem',
md: '1rem',
lg: '2rem'
}
};
const ThemedButton = styled.button`
background: ${props => props.theme.colors.primary};
padding: ${props => props.theme.spacing.md};
color: white;
`;
<ThemeProvider theme={theme}>
<ThemedButton>Themed Button</ThemedButton>
</ThemeProvider>
Example: Emotion CSS Prop Pattern
// Installation: npm install @emotion/react
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
// Object styles
const buttonStyle = {
backgroundColor: '#3498db',
color: 'white',
padding: '0.75rem 1.5rem',
borderRadius: '4px',
border: 'none',
cursor: 'pointer',
'&:hover': {
backgroundColor: '#2980b9'
}
};
// Template literal styles
const containerStyle = css`
display: flex;
gap: 1rem;
padding: 2rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
`;
// Usage with css prop
function MyComponent() {
return (
<div css={containerStyle}>
<button css={buttonStyle}>Click me</button>
{/* Inline styles */}
<span css={{
fontSize: '1.2rem',
fontWeight: 'bold',
color: 'white'
}}>
Text
</span>
</div>
);
}
// Composition
const baseButton = css`
padding: 0.75rem 1.5rem;
border: none;
cursor: pointer;
`;
const primaryButton = css`
${baseButton}
background: #3498db;
color: white;
`;
Example: Vanilla Extract (Zero-runtime)
// styles.css.ts (build-time processed)
import { style, createTheme } from '@vanilla-extract/css';
// Type-safe theme
export const [themeClass, vars] = createTheme({
color: {
primary: '#3498db',
secondary: '#2ecc71',
text: '#2c3e50'
},
spacing: {
small: '0.5rem',
medium: '1rem',
large: '2rem'
}
});
// Type-safe styles
export const button = style({
backgroundColor: vars.color.primary,
color: 'white',
padding: vars.spacing.medium,
borderRadius: '4px',
border: 'none',
cursor: 'pointer',
':hover': {
backgroundColor: vars.color.secondary
}
});
// Component.tsx
import * as styles from './styles.css';
function Button() {
return (
<div className={styles.themeClass}>
<button className={styles.button}>
Click me
</button>
</div>
);
}
// Result: Static CSS file generated at build time, zero runtime
CSS-in-JS Trade-offs:
- ⚠️ Runtime cost: styled-components/Emotion add 10-20KB + runtime style injection
- ⚠️ Server-side rendering: Requires additional setup for SSR to avoid FOUC
- ⚠️ DevTools: Harder to debug with generated class names (use displayName)
- ⚠️ Build complexity: Zero-runtime solutions need build configuration
- ✅ When to use: Complex dynamic theming, component libraries, type-safe styles
- ✅ When to avoid: Simple static sites, performance-critical apps, marketing pages
3. CSS Modules and PostCSS Integration
| Tool | Purpose | Key Features |
|---|---|---|
| CSS Modules | Local scoping | Automatic class name hashing, composition, explicit dependencies |
| PostCSS | CSS transformation | Plugin ecosystem, autoprefixer, future CSS syntax |
| postcss-preset-env | Future CSS today | Nesting, custom properties, custom media queries |
| Autoprefixer | Vendor prefixes | Automatic -webkit/-moz/-ms prefixes based on browserslist |
| cssnano | CSS minification | Compression, optimization, duplicate removal |
Example: CSS Modules Implementation
<!-- Button.module.css -->
.button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.primary {
composes: button;
background: #3498db;
color: white;
}
.secondary {
composes: button;
background: #ecf0f1;
color: #2c3e50;
}
.large {
padding: 1rem 2rem;
font-size: 1.2rem;
}
<!-- Button.jsx -->
import styles from './Button.module.css';
function Button({ variant = 'primary', size, children }) {
const classNames = [
styles[variant],
size === 'large' && styles.large
].filter(Boolean).join(' ');
return <button className={classNames}>{children}</button>;
}
<Button variant="primary" size="large">Click me</Button>
<!-- Generated HTML -->
<button class="Button_primary__a1b2c Button_large__d3e4f">Click me</button>
<!-- Benefits -->
✅ No naming conflicts (hashed class names)
✅ Explicit dependencies (import/export)
✅ Dead code elimination (unused styles removed)
✅ Composition with 'composes' keyword
Example: PostCSS Configuration
<!-- postcss.config.js -->
module.exports = {
plugins: [
// 1. Enable future CSS syntax
require('postcss-preset-env')({
stage: 3,
features: {
'nesting-rules': true,
'custom-properties': true,
'custom-media-queries': true,
'custom-selectors': true
}
}),
// 2. Add vendor prefixes
require('autoprefixer')({
overrideBrowserslist: [
'last 2 versions',
'> 1%',
'not dead',
'not ie 11'
]
}),
// 3. Optimize for production
process.env.NODE_ENV === 'production' && require('cssnano')({
preset: ['default', {
discardComments: { removeAll: true },
normalizeWhitespace: true,
minifyFontValues: true,
convertValues: true
}]
})
].filter(Boolean)
};
<!-- Input CSS (future syntax) -->
:root {
--color-primary: #3498db;
}
@custom-media --tablet (min-width: 768px);
.container {
background: var(--color-primary);
& .title {
font-size: 2rem;
&:hover {
color: red;
}
}
@media (--tablet) {
padding: 2rem;
}
}
<!-- Output CSS (browser-compatible) -->
:root {
--color-primary: #3498db;
}
.container {
background: var(--color-primary);
}
.container .title {
font-size: 2rem;
}
.container .title:hover {
color: red;
}
@media (min-width: 768px) {
.container {
padding: 2rem;
}
}
PostCSS Plugin Recommendations:
- postcss-import: Inline @import rules (like Sass imports)
- postcss-nested: Sass-like nesting syntax
- postcss-custom-properties: Compile CSS variables for IE11
- postcss-flexbugs-fixes: Fix flexbox browser bugs automatically
- postcss-normalize: Use parts of normalize.css based on browserslist
- stylelint: Lint CSS during PostCSS build process
4. Web Components and Shadow DOM Styling
| Styling Method | Scope | Use Case |
|---|---|---|
| <style> in Shadow DOM | Component-scoped | Encapsulated styles, no global leakage |
| :host selector | Style component itself | Outer container styling of custom element |
| :host-context() | Conditional styling | Style based on ancestor elements |
| ::slotted() | Projected content | Style content passed via <slot> |
| ::part() | External styling | Expose internal elements for styling (CSS Shadow Parts) |
| Constructable Stylesheets | Shared styles | Reuse stylesheets across multiple shadow roots |
| CSS Custom Properties | Inherit through shadow | Theming, customizable component properties |
Example: Web Component with Shadow DOM Styling
<!-- Define custom element -->
class FancyButton extends HTMLElement {
constructor() {
super();
// Create shadow root
this.attachShadow({ mode: 'open' });
// Add styles
this.shadowRoot.innerHTML = `
<style>
/* :host targets the component itself (<fancy-button>) */
:host {
display: inline-block;
--button-bg: #3498db;
--button-color: white;
}
/* :host with state */
:host([disabled]) {
opacity: 0.6;
pointer-events: none;
}
/* :host-context styles based on ancestors */
:host-context(.dark-theme) {
--button-bg: #2c3e50;
}
/* Internal styles (encapsulated) */
button {
background: var(--button-bg);
color: var(--button-color);
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
font-size: 1rem;
transition: transform 0.2s;
}
button:hover {
transform: translateY(-2px);
}
/* Style slotted content */
::slotted(svg) {
width: 1rem;
height: 1rem;
margin-right: 0.5rem;
}
</style>
<button part="button">
<slot name="icon"></slot>
<slot>Click me</slot>
</button>
`;
}
}
customElements.define('fancy-button', FancyButton);
<!-- Usage -->
<fancy-button>Regular Button</fancy-button>
<fancy-button disabled>Disabled Button</fancy-button>
<fancy-button>
<svg slot="icon">...</svg>
With Icon
</fancy-button>
<!-- Style from outside using CSS Shadow Parts -->
<style>
fancy-button::part(button) {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
/* Customize with CSS variables */
fancy-button {
--button-bg: #2ecc71;
--button-color: white;
}
</style>
Example: Constructable Stylesheets (Reusable Styles)
<!-- Create shared stylesheet -->
const sharedStyles = new CSSStyleSheet();
sharedStyles.replaceSync(`
* {
box-sizing: border-box;
}
:host {
display: block;
font-family: system-ui, -apple-system, sans-serif;
}
button {
cursor: pointer;
transition: all 0.2s;
}
`);
// Apply to multiple shadow roots
class ComponentA extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.adoptedStyleSheets = [sharedStyles];
shadow.innerHTML = `<button>Component A</button>`;
}
}
class ComponentB extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.adoptedStyleSheets = [sharedStyles];
shadow.innerHTML = `<button>Component B</button>`;
}
}
// Benefits: Styles are shared in memory, no duplication
Shadow DOM Styling Best Practices:
- Use CSS Custom Properties for theming (they inherit through shadow boundaries)
- Expose ::part() pseudo-elements for external customization of internal elements
- Use Constructable Stylesheets to share common styles across components
- Keep styles minimal - shadow DOM already provides encapsulation
- Use :host-context() sparingly - prefer CSS variables for theming
- Test in browsers that don't support Shadow DOM (polyfills may be needed)
5. Framework-specific CSS Solutions
| Framework | CSS Solution | Approach | Example |
|---|---|---|---|
| React | CSS Modules, styled-components, Tailwind | className prop, css prop, utility classes | <div className={styles.container}> |
| Vue | Scoped styles, CSS Modules | <style scoped> in SFC | <style scoped>.container { }</style> |
| Angular | ViewEncapsulation, global styles | Emulated, ShadowDOM, None | styleUrls: ['./component.css'] |
| Svelte | Scoped by default | <style> in component (auto-scoped) | <style>.button { }</style> |
| Next.js | CSS Modules, Tailwind, styled-jsx | Built-in CSS Modules, global CSS | import styles from './page.module.css' |
| Astro | Scoped styles, global styles | <style> scoped by default | <style>.title { }</style> |
Example: React CSS Patterns
<!-- 1. CSS Modules -->
// Button.module.css
.button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
}
.primary { background: #3498db; color: white; }
.secondary { background: #ecf0f1; color: #2c3e50; }
// Button.jsx
import styles from './Button.module.css';
export default function Button({ variant = 'primary', children }) {
return (
<button className={`${styles.button} ${styles[variant]}`}>
{children}
</button>
);
}
<!-- 2. Tailwind CSS -->
export default function Button({ variant = 'primary', children }) {
const baseClasses = 'px-6 py-3 rounded border-none cursor-pointer';
const variantClasses = {
primary: 'bg-blue-500 text-white hover:bg-blue-600',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300'
};
return (
<button className={`${baseClasses} ${variantClasses[variant]}`}>
{children}
</button>
);
}
<!-- 3. Styled-components -->
import styled from 'styled-components';
const StyledButton = styled.button`
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
background: ${props => props.variant === 'primary' ? '#3498db' : '#ecf0f1'};
color: ${props => props.variant === 'primary' ? 'white' : '#2c3e50'};
`;
export default function Button({ variant = 'primary', children }) {
return <StyledButton variant={variant}>{children}</StyledButton>;
}
Example: Vue Scoped Styles
<!-- Button.vue -->
<template>
<button :class="['button', variant]">
<slot></slot>
</button>
</template>
<script setup>
defineProps({
variant: {
type: String,
default: 'primary'
}
});
</script>
<style scoped>
.button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.primary {
background: #3498db;
color: white;
}
.primary:hover {
background: #2980b9;
}
.secondary {
background: #ecf0f1;
color: #2c3e50;
}
/* Deep selector for child components */
:deep(.icon) {
margin-right: 0.5rem;
}
/* Global selector */
:global(.dark-mode) .button {
border: 1px solid rgba(255,255,255,0.2);
}
</style>
<!-- CSS Modules in Vue -->
<style module>
.button {
padding: 0.75rem 1.5rem;
}
</style>
<template>
<button :class="$style.button">Click</button>
</template>
Example: Angular ViewEncapsulation
// button.component.ts
import { Component, Input, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-button',
template: `
<button [class]="'button ' + variant">
<ng-content></ng-content>
</button>
`,
styleUrls: ['./button.component.css'],
encapsulation: ViewEncapsulation.Emulated // Default
// Options:
// - Emulated: Scoped styles with attribute selectors (default)
// - ShadowDom: Native Shadow DOM encapsulation
// - None: Global styles (no encapsulation)
})
export class ButtonComponent {
@Input() variant: string = 'primary';
}
// button.component.css
.button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
}
.primary {
background: #3498db;
color: white;
}
/* :host selector for component itself */
:host {
display: inline-block;
}
:host(.large) .button {
padding: 1rem 2rem;
font-size: 1.2rem;
}
/* ::ng-deep for piercing encapsulation (deprecated) */
::ng-deep .icon {
margin-right: 0.5rem;
}
Example: Svelte Scoped Styles
<!-- Button.svelte -->
<script>
export let variant = 'primary';
</script>
<button class="button {variant}">
<slot></slot>
</button>
<style>
/* Automatically scoped - no manual work needed */
.button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
}
.primary {
background: #3498db;
color: white;
}
.secondary {
background: #ecf0f1;
color: #2c3e50;
}
/* :global() for global styles */
:global(body.dark-mode) .button {
border: 1px solid rgba(255,255,255,0.2);
}
/* Dynamic styles with variables */
.button {
--button-bg: #3498db;
background: var(--button-bg);
}
</style>
<!-- Usage -->
<Button variant="primary">Click me</Button>
Framework CSS Solution Comparison:
| Consideration | Best Choice | Reason |
|---|---|---|
| Zero config scoping | Vue, Svelte, Angular | Built-in scoped styles, no extra setup |
| Dynamic theming | styled-components, Emotion | Full JavaScript power for theme logic |
| Performance critical | Tailwind, CSS Modules, Vanilla Extract | Zero/minimal runtime, static extraction |
| Type safety | Vanilla Extract, Panda CSS, Stitches | TypeScript-first with autocomplete |
| Server-side rendering | CSS Modules, Tailwind, styled-components | Mature SSR support, no hydration issues |
CSS Integration with JavaScript Best Practices
- Prefer classList over inline styles - Better performance, reusability, maintainability
- Use getComputedStyle() for reading values, avoid layout thrashing with batch reads/writes
- Choose CSS-in-JS for dynamic theming/component libraries, avoid for static sites
- Use CSS Modules for scoped styles with zero runtime cost and explicit dependencies
- Configure PostCSS with preset-env, autoprefixer, cssnano for modern CSS features
- Leverage Shadow DOM for true encapsulation in Web Components
- Expose customization with ::part() and CSS Custom Properties in Web Components
- Follow framework conventions: Vue/Svelte scoped styles, React CSS Modules/Tailwind, Angular ViewEncapsulation
- Consider zero-runtime solutions (Vanilla Extract, Panda CSS) for performance-critical apps
- Use Constructable Stylesheets to share styles across multiple Shadow DOM instances