Modern CSS Techniques for Today's Web Developer
Modern CSS Techniques for Today's Web Developer
CSS has evolved dramatically in recent years, transforming from a simple styling language into a powerful system for creating complex layouts and dynamic user interfaces. This guide explores modern CSS techniques that every web developer should know in 2023 and beyond.
The Evolution of CSS
CSS has come a long way since its introduction in 1996. What began as a simple way to separate content from presentation has evolved into a sophisticated language with features that rival JavaScript for certain visual tasks. Modern CSS now includes:
- Powerful layout systems (Grid and Flexbox)
- Custom properties (variables)
- Logical properties
- Container and media queries
- Animation capabilities
- Color functions and gradients
- Advanced selectors and combinators
Let's explore these features and how they can improve your development workflow.
Layout Systems
CSS Grid
CSS Grid is a two-dimensional layout system that has revolutionized web layout design. It allows for precise control over rows and columns, making complex layouts more intuitive to create.
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-gap: 1rem;
}
This creates a responsive grid where items automatically fill the available space, with each item taking at least 250px of width.
Advanced Grid Techniques
Grid Areas
Grid areas allow you to create named template areas for more intuitive layout design:
.dashboard {
display: grid;
grid-template-areas:
"header header header"
"sidebar main main"
"sidebar footer footer";
grid-template-columns: 1fr 3fr 1fr;
grid-template-rows: auto 1fr auto;
min-height: 100vh;
}
.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main { grid-area: main; }
.footer { grid-area: footer; }
Subgrid
Subgrid (now supported in most modern browsers) allows grid items to inherit the grid definition of their parent:
.parent-grid {
display: grid;
grid-template-columns: repeat(9, 1fr);
grid-template-rows: auto auto;
}
.child-grid {
grid-column: 2 / 7;
display: grid;
grid-template-columns: subgrid;
}
Flexbox
Flexbox is a one-dimensional layout system perfect for distributing space along a single axis. It's ideal for navigation bars, card layouts, and centering elements.
.flex-container {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
}
Practical Flexbox Patterns
Holy Grail Layout
.holy-grail {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.holy-grail__header,
.holy-grail__footer {
flex: 0 0 auto;
}
.holy-grail__main {
display: flex;
flex: 1 0 auto;
}
.holy-grail__content {
flex: 1 0 auto;
}
.holy-grail__nav,
.holy-grail__aside {
flex: 0 0 12rem;
}
.holy-grail__nav {
order: -1;
}
@media (max-width: 768px) {
.holy-grail__main {
flex-direction: column;
}
.holy-grail__nav,
.holy-grail__aside {
flex: 0 0 auto;
}
}
Card Grid with Equal Height
.card-grid {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.card {
flex: 1 0 300px;
display: flex;
flex-direction: column;
}
.card__body {
flex: 1 0 auto;
}
.card__footer {
margin-top: auto;
}
When to Use Grid vs. Flexbox
-
Use Grid when:
- You need to control both rows and columns
- You're creating a complex overall page layout
- You need to overlap elements
- You want to align elements in two dimensions
-
Use Flexbox when:
- You need a one-dimensional layout (row OR column)
- You want to distribute space among items
- You need to align items within a container
- You're working with dynamic or unknown sizes
Often, the best approach is to use Grid for the overall page layout and Flexbox for the components within that layout.
Custom Properties (CSS Variables)
Custom properties have transformed how we write and maintain CSS by allowing us to define reusable values.
:root {
--primary-color: #3490dc;
--secondary-color: #ffed4a;
--danger-color: #e3342f;
--spacing-unit: 0.5rem;
--border-radius: 4px;
--transition-speed: 0.3s;
}
.button {
background-color: var(--primary-color);
padding: calc(var(--spacing-unit) * 2) calc(var(--spacing-unit) * 4);
border-radius: var(--border-radius);
transition: all var(--transition-speed) ease-in-out;
}
.button:hover {
background-color: color-mix(in srgb, var(--primary-color) 80%, black);
}
.button--danger {
background-color: var(--danger-color);
}
Dynamic Custom Properties
Custom properties can be updated with JavaScript, enabling dynamic styling without inline styles:
// Change theme based on user preference
function setTheme(theme) {
if (theme === 'dark') {
document.documentElement.style.setProperty('--background-color', '#121212');
document.documentElement.style.setProperty('--text-color', '#ffffff');
} else {
document.documentElement.style.setProperty('--background-color', '#ffffff');
document.documentElement.style.setProperty('--text-color', '#121212');
}
}
// Update custom property based on scroll position
window.addEventListener('scroll', () => {
const scrollPercentage = window.scrollY / (document.body.scrollHeight - window.innerHeight);
document.documentElement.style.setProperty('--scroll', scrollPercentage.toString());
});
Scoped Custom Properties
Custom properties can be scoped to specific components, making them more maintainable:
.card {
--card-padding: 1.5rem;
--card-border-radius: 8px;
padding: var(--card-padding);
border-radius: var(--card-border-radius);
}
.card__header {
margin-bottom: var(--card-padding);
}
Logical Properties
Logical properties and values allow you to write CSS that adapts to different writing modes and text directions, making internationalization easier.
.container {
/* Instead of this */
margin-left: 1rem;
margin-right: 1rem;
padding-top: 2rem;
/* Use this */
margin-inline: 1rem;
padding-block-start: 2rem;
}
.sidebar {
/* Instead of this */
float: left;
/* Use this */
float: inline-start;
}
Logical properties map to physical properties based on the document's writing mode:
Logical Property | LTR Writing Mode | RTL Writing Mode |
---|---|---|
margin-inline-start | margin-left | margin-right |
margin-inline-end | margin-right | margin-left |
padding-block-start | padding-top | padding-top |
border-inline-start | border-left | border-right |
Container Queries
Container queries allow you to style elements based on their parent container's size rather than just the viewport size, enabling truly responsive components.
/* Define a container */
.card-container {
container-type: inline-size;
container-name: card;
}
/* Style based on container width */
@container card (min-width: 400px) {
.card {
display: flex;
}
.card__image {
flex: 0 0 40%;
}
.card__content {
flex: 1;
}
}
@container card (max-width: 399px) {
.card__image {
margin-bottom: 1rem;
}
}
This allows the card component to adapt based on its container's width, not just the viewport width.
Modern Selectors and Combinators
CSS now offers powerful selectors that reduce the need for additional markup or JavaScript.
:is() and :where() Pseudo-Classes
These pseudo-classes simplify complex selector lists:
/* Instead of this */
header a:hover,
footer a:hover,
main a:hover {
color: purple;
}
/* Use this */
:is(header, footer, main) a:hover {
color: purple;
}
The key difference between :is()
and :where()
is specificity: :is()
takes the highest specificity of its arguments, while :where()
has zero specificity.
:has() Relational Pseudo-Class
The :has()
selector (now widely supported) allows you to select elements based on their children or adjacent elements:
/* Style a section that contains an image */
section:has(img) {
padding: 1rem;
}
/* Style a form field that's required and has a value */
input:is([required]):has(+ span.error) {
border-color: red;
}
/* Style a card differently if it has a footer */
.card:has(.card__footer) {
padding-bottom: 0.5rem;
}
/* Style a paragraph that contains a link */
p:has(a) {
padding-right: 1rem;
background: url('external-link.svg') no-repeat right center;
}
:focus-visible Pseudo-Class
The :focus-visible
pseudo-class applies styles only when the focus should be visible to the user (typically keyboard navigation):
.button:focus {
/* Basic focus styles for all browsers */
outline: 2px solid transparent;
}
.button:focus-visible {
/* Enhanced focus styles for keyboard navigation */
outline: 2px solid currentColor;
outline-offset: 2px;
}
Modern Color Features
CSS now offers advanced color manipulation features that were previously only possible with preprocessors.
Color Functions
:root {
--brand-color: #3490dc;
}
.button {
/* Lighten a color */
background-color: color-mix(in srgb, var(--brand-color) 70%, white);
/* Darken a color on hover */
&:hover {
background-color: color-mix(in srgb, var(--brand-color) 70%, black);
}
}
/* Using relative color syntax */
.alert {
--alert-bg: lch(from var(--brand-color) l c h);
--alert-text: lch(from var(--alert-bg) calc(l - 60%) c h);
background-color: var(--alert-bg);
color: var(--alert-text);
}
Color Spaces
Modern CSS supports advanced color spaces beyond the traditional RGB:
.gradient {
/* LCH color space for smoother gradients */
background: linear-gradient(
to right,
lch(50% 100 0), /* Vibrant red */
lch(50% 100 180) /* Vibrant teal */
);
}
.button {
/* Using oklch for perceptually uniform colors */
background-color: oklch(65% 0.3 30);
color: oklch(98% 0.01 30);
}
Responsive Typography with Fluid Sizing
Fluid typography allows text to scale smoothly between viewport sizes without breakpoints.
Using clamp()
:root {
--fluid-min-width: 320;
--fluid-max-width: 1200;
--fluid-min-size: 16;
--fluid-max-size: 24;
--fluid-min: calc(var(--fluid-min-size) / 16 * 1rem);
--fluid-max: calc(var(--fluid-max-size) / 16 * 1rem);
--fluid-viewport-min: calc(var(--fluid-min-width) / 16 * 1rem);
--fluid-viewport-max: calc(var(--fluid-max-width) / 16 * 1rem);
}
.heading {
/* Fluid typography formula using clamp */
font-size: clamp(
var(--fluid-min),
calc(1rem + (var(--fluid-max-size) - var(--fluid-min-size)) *
(100vw - var(--fluid-viewport-min)) /
(var(--fluid-max-width) - var(--fluid-min-width))
),
var(--fluid-max)
);
}
A simpler approach using CSS custom properties:
/* Simplified fluid typography */
:root {
--step-0: clamp(1rem, 0.92rem + 0.39vw, 1.25rem);
--step-1: clamp(1.25rem, 1.08rem + 0.87vw, 1.8rem);
--step-2: clamp(1.56rem, 1.25rem + 1.58vw, 2.5rem);
--step-3: clamp(1.95rem, 1.4rem + 2.75vw, 3.5rem);
--step-4: clamp(2.44rem, 1.52rem + 4.62vw, 5rem);
--step-5: clamp(3.05rem, 1.54rem + 7.54vw, 7rem);
}
h1 { font-size: var(--step-4); }
h2 { font-size: var(--step-3); }
h3 { font-size: var(--step-2); }
h4 { font-size: var(--step-1); }
p { font-size: var(--step-0); }
Modern CSS Animation
CSS now offers powerful animation capabilities that can replace JavaScript for many use cases.
Keyframe Animations
@keyframes fade-in {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.card {
animation: fade-in 0.5s ease-out forwards;
}
/* Staggered animations for lists */
.list-item {
opacity: 0;
animation: fade-in 0.5s ease-out forwards;
}
.list-item:nth-child(1) { animation-delay: 0.1s; }
.list-item:nth-child(2) { animation-delay: 0.2s; }
.list-item:nth-child(3) { animation-delay: 0.3s; }
.list-item:nth-child(4) { animation-delay: 0.4s; }
.list-item:nth-child(5) { animation-delay: 0.5s; }
Animation Best Practices
.animation {
/* Use GPU-accelerated properties for smooth animations */
transform: translateZ(0);
will-change: transform, opacity;
/* Respect user preferences */
@media (prefers-reduced-motion: reduce) {
animation: none;
transition: none;
}
}
Scroll-Driven Animations
The new scroll-driven animations API allows for animations tied to scroll position:
@keyframes parallax {
from { transform: translateY(0); }
to { transform: translateY(-50%); }
}
.parallax-element {
animation: parallax linear;
animation-timeline: scroll();
animation-range: 0 100vh;
}
.reveal-on-scroll {
opacity: 0;
transform: translateY(20px);
animation: fade-in 1s ease-out forwards;
animation-timeline: view();
animation-range: entry 10% cover 30%;
}
Aspect Ratio
The aspect-ratio
property simplifies maintaining proportional dimensions:
.video-container {
aspect-ratio: 16 / 9;
width: 100%;
background: #000;
}
.profile-image {
aspect-ratio: 1 / 1;
object-fit: cover;
width: 100%;
}
.card {
aspect-ratio: 3 / 4;
/* Card content */
}
Modern CSS Resets and Defaults
Modern CSS resets focus on sensible defaults rather than stripping all styles:
/* Modern CSS Reset */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 100%;
scroll-behavior: smooth;
}
body {
min-height: 100vh;
text-rendering: optimizeSpeed;
line-height: 1.5;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
/* Remove animations for people who've turned them off */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
CSS Custom Utilities
Create your own utility classes for common patterns:
/* Visually hidden but accessible to screen readers */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
/* Container with responsive padding */
.container {
width: 100%;
max-width: 1200px;
margin-inline: auto;
padding-inline: clamp(1rem, 5vw, 3rem);
}
/* Responsive spacing utilities */
.mt-auto { margin-top: auto; }
.mb-auto { margin-bottom: auto; }
.space-y > * + * { margin-top: var(--space, 1rem); }
.space-x > * + * { margin-left: var(--space, 1rem); }
CSS and JavaScript Integration
Modern CSS works seamlessly with JavaScript for enhanced functionality.
CSS Custom Properties with JavaScript
// Get computed CSS values
const styles = getComputedStyle(document.documentElement);
const primaryColor = styles.getPropertyValue('--primary-color').trim();
// Set CSS custom properties
document.documentElement.style.setProperty('--primary-color', '#ff0000');
// Create theme toggle
const themeToggle = document.getElementById('theme-toggle');
themeToggle.addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
});
CSS Transitions with JavaScript
const menu = document.querySelector('.menu');
const menuButton = document.querySelector('.menu-button');
menuButton.addEventListener('click', () => {
// Get current state to force reflow before adding class
const isHidden = menu.classList.contains('is-hidden');
if (isHidden) {
menu.classList.remove('is-hidden');
menu.classList.add('is-visible');
} else {
menu.classList.remove('is-visible');
menu.classList.add('is-hidden');
}
});
CSS Architecture for Modern Applications
Component-Based CSS
Modern CSS architecture often follows component-based principles:
/* Component: Card */
.card {
--card-padding: 1.5rem;
--card-radius: 8px;
display: flex;
flex-direction: column;
padding: var(--card-padding);
border-radius: var(--card-radius);
background-color: var(--surface-color, white);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.card__header {
margin-bottom: 1rem;
}
.card__title {
font-size: var(--step-1);
font-weight: 600;
}
.card__body {
flex: 1;
}
.card__footer {
margin-top: auto;
padding-top: 1rem;
border-top: 1px solid var(--border-color, #eee);
}
/* Variants */
.card--featured {
border-left: 4px solid var(--primary-color);
}
.card--compact {
--card-padding: 1rem;
}
CSS Custom Properties for Theming
:root {
/* Light theme (default) */
--color-text: #333;
--color-background: #fff;
--color-primary: #3490dc;
--color-secondary: #38a169;
--color-accent: #805ad5;
--color-muted: #718096;
--color-border: #e2e8f0;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
[data-theme="dark"] {
--color-text: #f7fafc;
--color-background: #1a202c;
--color-primary: #63b3ed;
--color-secondary: #68d391;
--color-accent: #b794f4;
--color-muted: #a0aec0;
--color-border: #2d3748;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.4), 0 1px 2px 0 rgba(0, 0, 0, 0.2);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
}
/* Apply theme variables */
body {
color: var(--color-text);
background-color: var(--color-background);
}
.button-primary {
background-color: var(--color-primary);
color: white;
}
.card {
background-color: var(--color-background);
border: 1px solid var(--color-border);
box-shadow: var(--shadow);
}
Implementing Modern CSS in Next.js
Next.js offers several approaches to implementing modern CSS.
Global Styles
// pages/_app.js
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default MyApp;
CSS Modules
// components/Button.js
import styles from './Button.module.css';
export default function Button({ children, variant = 'primary' }) {
return (
<button className={`${styles.button} ${styles[`button--${variant}`]}`}>
{children}
</button>
);
}
/* Button.module.css */
.button {
padding: 0.5rem 1rem;
border-radius: 4px;
font-weight: 600;
transition: background-color 0.3s ease;
}
.button--primary {
background-color: var(--color-primary);
color: white;
}
.button--secondary {
background-color: var(--color-secondary);
color: white;
}
.button--outline {
background-color: transparent;
border: 2px solid var(--color-primary);
color: var(--color-primary);
}
CSS-in-JS with styled-components or Emotion
// components/Button.js with styled-components
import styled from 'styled-components';
const StyledButton = styled.button`
padding: 0.5rem 1rem;
border-radius: 4px;
font-weight: 600;
transition: background-color 0.3s ease;
background-color: ${props =>
props.variant === 'primary' ? 'var(--color-primary)' :
props.variant === 'secondary' ? 'var(--color-secondary)' : 'transparent'
};
color: ${props =>
props.variant === 'outline' ? 'var(--color-primary)' : 'white'
};
border: ${props =>
props.variant === 'outline' ? '2px solid var(--color-primary)' : 'none'
};
`;
export default function Button({ children, variant = 'primary', ...props }) {
return (
<StyledButton variant={variant} {...props}>
{children}
</StyledButton>
);
}
Tailwind CSS Integration
// components/Button.js with Tailwind CSS
export default function Button({ children, variant = 'primary', ...props }) {
const baseClasses = 'px-4 py-2 rounded font-semibold transition-colors';
const variantClasses = {
primary: 'bg-blue-500 hover:bg-blue-600 text-white',
secondary: 'bg-green-500 hover:bg-green-600 text-white',
outline: 'bg-transparent border-2 border-blue-500 text-blue-500 hover:bg-blue-50'
};
return (
<button
className={`${baseClasses} ${variantClasses[variant]}`}
{...props}
>
{children}
</button>
);
}
Performance Optimization
Critical CSS
Inline critical CSS to improve First Contentful Paint:
// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { extractCritical } from '@emotion/server';
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx);
const page = await ctx.renderPage();
const styles = extractCritical(page.html);
return { ...initialProps, ...page, styles };
}
render() {
return (
<Html lang="en">
<Head>
<style
data-emotion-css={this.props.ids?.join(' ')}
dangerouslySetInnerHTML={{ __html: this.props.css }}
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
Reducing Unused CSS
Use PurgeCSS to remove unused CSS:
// next.config.js with Tailwind CSS and PurgeCSS
module.exports = {
webpack: (config, { dev, isServer }) => {
// Only run in production builds
if (!dev && !isServer) {
const { PurgeCSSPlugin } = require('purgecss-webpack-plugin');
const glob = require('glob');
const path = require('path');
config.plugins.push(
new PurgeCSSPlugin({
paths: glob.sync(`${path.join(process.cwd(), 'pages')}/**/*`, { nodir: true }),
safelist: ['html', 'body'],
}),
);
}
return config;
},
};
Accessibility Considerations
Modern CSS should prioritize accessibility:
/* Ensure sufficient color contrast */
:root {
--color-text: #333;
--color-background: #fff;
}
/* Support reduced motion preferences */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* Focus styles */
:focus {
outline: 2px solid transparent;
}
:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* Skip to content link */
.skip-link {
position: absolute;
top: -40px;
left: 0;
padding: 8px;
background-color: var(--color-primary);
color: white;
z-index: 100;
transition: top 0.2s;
}
.skip-link:focus {
top: 0;
}
Conclusion
Modern CSS has evolved into a powerful, expressive language that can handle complex layouts, animations, and styling needs with fewer hacks and workarounds than ever before. By embracing these modern techniques, you can write more maintainable, performant, and accessible CSS that scales with your application.
The key takeaways:
- Use Grid and Flexbox for layout, choosing the right tool for each job
- Leverage custom properties for maintainable, themeable CSS
- Embrace logical properties for better internationalization
- Use container queries for truly responsive components
- Take advantage of modern selectors to write more efficient CSS
- Implement fluid typography for better responsive text
- Optimize performance with critical CSS and code splitting
- Prioritize accessibility in all your CSS decisions
By combining these techniques with a thoughtful architecture, you can create web experiences that are both visually impressive and technically sound.