The Vertical Spacing Challenge
Managing vertical spacing between typographic elements in long-form content has traditionally been one of the more frustrating aspects of CSS. Developers have relied on wrapper classes, complex margin overrides, and careful structural planning to achieve consistent, professional typography.
The challenges include:
- First and last elements need special handling at container boundaries
- Sections defined by headings require larger spacing above them
- Headings immediately following other headings need reduced spacing
- CMS-driven content has unpredictable element ordering
The CSS :has() pseudo-class fundamentally changes this equation by allowing us to style elements based on what follows them--opening entirely new possibilities for elegant, maintainable typography systems.
As part of our web development services, we leverage modern CSS techniques to build maintainable, scalable frontend systems that perform exceptionally well across all devices.
Traditional Approaches and Their Limitations
Historically, developers have used several techniques to manage vertical spacing, each with significant drawbacks:
Wrapper Class Method
.prose > * {
margin-bottom: 1.5rem;
}
.prose > *:first-child {
margin-top: 0;
}
.prose > *:last-child {
margin-bottom: 0;
}
Problems: Requires specific HTML structure, content must be direct children of wrapper, doesn't work well with mixed CMS and hardcoded content.
Mixed Margin Direction
Using both margin-top and margin-bottom with adjacent sibling selectors:
p {
margin-bottom: 1.5rem;
}
h2, h3 {
margin-top: 3rem;
margin-bottom: 1rem;
}
h2 + h2,
h2 + h3,
h3 + h3,
h3 + h2 {
margin-top: 1rem;
}
Problems: Complex to maintain, mixes two margin directions, relies on margin collapsing behavior which can be unpredictable.
These approaches reflect older patterns before modern CSS selectors were available. Our team specializes in modern CSS architecture that embraces current standards for better maintainability.
For an alternative perspective on modern CSS layouts, see our guide on preventing grid blowouts which demonstrates how CSS Grid simplifies complex layout challenges.
Understanding the CSS :has() Pseudo-Class
The :has() pseudo-class is a relational selector that checks if an element has certain descendants or siblings. It represents an element if any of the relative selectors passed as arguments match at least one element.
Syntax
/* Selects a section that contains a featured article */
section:has(.featured) {
border: 2px solid blue;
}
/* Selects a heading followed immediately by another heading */
h1:has(+ h2) {
margin-bottom: 0.25rem;
}
/* Selects a paragraph that follows a heading */
p:has(+ h2) {
margin-bottom: 1.5rem;
}
Browser Support
:has() is now Baseline 2023 with full support across Chrome, Edge, Safari, and Firefox. You can confidently use it in production websites without significant compatibility concerns.
Specificity
The :has() pseudo-class takes on the specificity of the most specific selector in its arguments, similar to :is() and :not(). Using :where() within :has() keeps specificity at zero for easier overrides.
For more on modern CSS selectors, see our guide on CSS database queries which explores advanced selector techniques for dynamic content.
The :has() Solution for Vertical Spacing
Here's a complete implementation using :has() for context-aware vertical spacing:
/* Base typographic spacing - all elements get bottom margin */
:where(p, h2, h3, h4, ul, ol, blockquote) {
margin-bottom: 1.5rem;
}
/* Reduce spacing for stacked headings using :has() */
:where(h2):has(+ :where(h2, h3)) {
margin-bottom: 0.5rem;
}
:where(h3):has(+ :where(h2, h3)) {
margin-bottom: 0.5rem;
}
/* List items get tighter spacing */
:where(li):has(+ li) {
margin-bottom: 0.5rem;
}
/* Remove bottom margin from last element */
:where(*):has(+ :last-child) {
margin-bottom: 0;
}
/* Or using :last-child directly on content container children */
.content > :where(:last-child) {
margin-bottom: 0;
}
How This Works
- Base spacing: All typographic elements get consistent
margin-bottom - Stacked heading detection:
:has(+ h2, h3)detects when a heading is followed by another heading - Reduced spacing: Stacked headings get smaller margin-bottom instead of the full spacing
- Boundary handling: First and last elements can be targeted for special treatment
No wrapper class required--this CSS works on any HTML structure.
Compare this to our guide on CSS Grid layouts for another modern CSS technique that simplifies layout management. For understanding when to use different CSS approaches, see our comparison of HTML preprocessor features.
No Wrapper Required
Works on any HTML structure without requiring specific container classes or nesting.
Consistent Margin Direction
Using only margin-bottom simplifies the mental model and reduces debugging time.
No Margin Collapsing
Avoids the unpredictable behavior of CSS margin collapsing entirely.
Clean Override Patterns
No need to set values only to immediately override them with more specific selectors.
Content-Aware Styling
Spacing adapts automatically based on what elements actually follow each other.
Easy Maintenance
Selectors clearly express intent and adapt to any content order automatically.
Alternative Approaches
While :has() is powerful, other solutions exist for different scenarios:
Lobotomized Owl Selector
.flow > * + * {
margin-top: 1.5rem;
}
Simple but less precise. Works well for consistent spacing but doesn't handle heading stacks differently.
Margin-Top Only Approach
.flow > * {
margin-top: 1.5rem;
}
.flow > h2 + *,
.flow > h3 + * {
margin-top: 0.5rem;
}
Avoids margin collapsing but requires careful ordering of rules.
Tailwind Typography Plugin
For projects using Tailwind CSS, the Typography plugin provides comprehensive long-form text styling:
<article class="prose prose-lg">
{{ content }}
</article>
Includes spacing, font sizing, link colors, and more--excellent for rapid development.
Each approach has its place depending on your project requirements. Our frontend development team can help you choose the right approach for your specific needs.
For teams considering whether to move away from Sass, see our analysis on whether it's time to ung Sass and how modern CSS reduces the need for preprocessors.
Best Practices for Implementation
Use CSS Custom Properties
:root {
--spacing-base: 1rem;
--spacing-heading: 1.5rem;
--spacing-tight: 0.5rem;
}
:where(p) {
margin-bottom: var(--spacing-base);
}
:where(h2):has(+ :where(h2, h3)) {
margin-bottom: var(--spacing-tight);
}
Test with Real Content
- Test with actual CMS-generated content
- Try unexpected element orderings
- Verify spacing at different viewport sizes
Keep Specificity Low
Using :where() keeps specificity at zero, making it easy for components to override base styles when needed.
Document Your System
Create documentation explaining your spacing system so team members can maintain it consistently.
For more on CSS properties and best practices, see our comprehensive guide on how many CSS properties exist and strategies for maintaining large, organized stylesheets.