Mastering the CSS content Property: A Complete Guide

Learn how to use generated content with ::before and ::after pseudo-elements to create stunning visual effects without cluttering your HTML markup.

What is the CSS content Property?

The CSS content property is one of the most powerful yet underutilized features in modern web development. It enables developers to insert generated content, style elements dynamically, and create visual effects without adding extra HTML markup. This capability is particularly valuable when building professional websites with Next.js where clean markup and maintainable code are essential priorities.

The CSS content property is used in conjunction with pseudo-elements (::before and ::after) to insert generated content into the document. While this content appears visually on the page, it is not part of the actual DOM, making it an excellent tool for decorative elements and visual enhancements without cluttering HTML structure. According to the MDN Web Docs on pseudo-elements, pseudo-elements are powerful tools that allow you to style specific parts of an element or generate content around existing elements.

When you use ::before or ::after pseudo-elements, they exist in the document but remain invisible until the content property is specified. The content property is mandatory for these pseudo-elements to render anything visible. Even an empty string (content: "";) creates a visible pseudo-element that can be styled with dimensions, background colors, and positioning.

Key capabilities include:

  • Adding decorative icons and visual elements to buttons, links, and cards
  • Creating custom list bullets and markers that match your brand aesthetic
  • Displaying dynamic attribute values using the attr() function
  • Building automatic numbering systems with counters for documentation
  • Styling form elements without requiring JavaScript

The content property exemplifies how modern CSS enables sophisticated designs with clean, maintainable code. When used appropriately, it reduces the need for wrapper elements and keeps your HTML semantic and accessible.

Syntax and Values

The content property accepts several types of values that unlock different capabilities for your designs. Understanding these value types is essential for choosing the right approach for each use case.

String Values

Plain text content appears directly in the rendered output. String values are perfect for adding symbols, labels, or decorative text alongside elements:

.arrow-link::after {
 content: " →";
 transition: transform 0.2s ease;
}

.badge::before {
 content: "New";
 font-size: 0.75em;
 padding: 2px 6px;
 background: #0066cc;
 color: white;
 border-radius: 3px;
 margin-right: 8px;
}

URL Values

External resources like images or SVG icons can be embedded directly. This technique is commonly used for icon systems where you want to avoid additional HTTP requests or keep icons CSS-based:

.external-link::before {
 content: url("/images/external-icon.svg");
 display: inline-block;
 width: 14px;
 height: 14px;
 margin-right: 6px;
 vertical-align: middle;
}

Modern approaches often prefer SVG data URIs or CSS masks over inline images for better performance and control.

Attribute Values

The attr() function reads HTML attribute values dynamically and displays them as content. This is particularly powerful for data-driven interfaces and accessible form designs:

[data-required]::after {
 content: "*";
 color: #dc3545;
 margin-left: 4px;
}

.price-tag::after {
 content: attr(data-currency);
 font-size: 0.8em;
 vertical-align: super;
}

Counter Values

CSS counters provide automatic numbering without JavaScript. The MDN Web Docs specifications detail how counter-reset, counter-increment, and counter() work together:

/* Reset counter at document start */
body {
 counter-reset: chapter;
}

/* Increment and display */
h2::before {
 counter-increment: chapter;
 content: "Chapter " counter(chapter) ": ";
}

/* Nested counters for subsections */
h3::before {
 content: counter(chapter) "." counter(section) " ";
}

Quote Values

Special quote-related values work with the quotes property for typographic blockquote styling:

blockquote {
 quotes: "❝" "❞" "'" "'";
}

blockquote::before {
 content: open-quote;
 font-size: 3em;
 line-height: 0;
 color: #ccc;
}

blockquote::after {
 content: close-quote;
 font-size: 3em;
 line-height: 0;
 color: #ccc;
}

Empty String

The empty string is crucial for creating purely decorative pseudo-elements that need dimensions but no visible text:

.decorative-border::before {
 content: "";
 position: absolute;
 top: 0;
 left: 0;
 width: 100%;
 height: 4px;
 background: linear-gradient(90deg, #0066cc, #00cc99);
}
Core Use Cases for the content Property

Practical applications that every web developer should know

Decorative Icons

Add arrows, icons, and visual indicators to links, buttons, and components without extra HTML elements. Perfect for maintaining clean markup.

Custom List Bullets

Replace default list markers with custom icons, emojis, or styled text using ::before with content. Works alongside ::marker for advanced control.

Dynamic Attribute Display

Use attr() to display HTML attribute values as content, perfect for tooltips, data labels, and dynamic form indicators.

Automatic Numbering

Create section numbering, outlines, and hierarchical counters without JavaScript. Ideal for documentation and structured content.

Form Enhancements

Custom checkboxes, toggle switches, and required field indicators with pure CSS. No JavaScript required for interactive form elements.

Visual Effects

Gradient overlays, decorative borders, and hover effects without markup changes. Keeps components self-contained and maintainable.

Practical Applications

Decorative Icons and Visual Elements

One of the most common applications of the content property is adding decorative elements to links, buttons, and other interactive components. Instead of wrapping icons in additional HTML elements, pseudo-elements can generate them directly through CSS. This approach keeps your markup clean and makes it easier to maintain consistent styling across your Next.js application.

As demonstrated in CSS-Tricks' practical guide, pseudo-elements are excellent for creating visual effects that would otherwise require additional DOM nodes:

.button::after {
 content: " →";
 transition: transform 0.2s ease;
}

.button:hover::after {
 transform: translateX(4px);
}

.external-link::after {
 content: " " url("/images/external-icon.svg");
 display: inline-block;
 width: 12px;
 height: 12px;
 margin-left: 6px;
 vertical-align: middle;
}

.card::before {
 content: "";
 position: absolute;
 top: 0;
 left: 0;
 width: 100%;
 height: 4px;
 background: linear-gradient(90deg, #0066cc, #00cc99);
}

Custom List Bullets

Creating custom list bullets is straightforward with the ::before pseudo-element. This technique allows for emoji bullets, custom icons, or styled text markers that match your design system:

.checklist li::before {
 content: "✓";
 margin-right: 0.75em;
 color: #28a745;
 font-weight: bold;
}

.feature-list li::before {
 content: "★";
 margin-right: 0.5em;
 color: #ffc107;
}

For native list marker styling, the ::marker pseudo-element provides dedicated control over list bullets and numbers:

.styled-list li::marker {
 color: #0066cc;
 font-weight: bold;
}

Dynamic Attribute Display

The attr() function allows you to read the value of any HTML attribute and display it as content. This is particularly useful for tooltips, data visualization, and dynamic labeling in modern web applications. When building performant web applications, this technique helps reduce JavaScript dependencies:

[data-label]::after {
 content: attr(data-label);
 font-size: 0.85em;
 color: #666;
 margin-left: 8px;
 font-weight: 500;
}

.image-container {
 position: relative;
}

.image-container::after {
 content: attr(alt);
 position: absolute;
 bottom: 12px;
 left: 12px;
 background: rgba(0, 0, 0, 0.75);
 color: white;
 padding: 6px 12px;
 border-radius: 4px;
 font-size: 0.9em;
}

Automatic Numbering with Counters

CSS counters provide a powerful way to create automatic numbering systems without JavaScript. They are ideal for documentation, nested lists, and hierarchical content structures. The MDN Web Docs specifications explain how counters maintain state across elements:

/* Define and reset counters */
documentation {
 counter-reset: chapter section figure;
}

chapter {
 counter-reset: section;
}

chapter::before {
 counter-increment: chapter;
 content: counter(chapter) ". ";
 font-weight: bold;
 color: #0066cc;
}

section::before {
 counter-increment: section;
 content: counter(chapter) "." counter(section) " ";
 color: #666;
}

Broken Image Styling

When images fail to load, browsers display a default broken image icon. Using pseudo-elements, you can create custom placeholders that maintain visual consistency with your design. This is particularly valuable for SEO-optimized websites where user experience matters:

img {
 position: relative;
 display: block;
 min-height: 150px;
}

img::before {
 content: "";
 display: block;
 background-color: #f5f5f5;
 border: 2px dashed #ddd;
 border-radius: 8px;
 position: absolute;
 inset: 0;
}

img::after {
 content: "Image: " attr(alt);
 display: flex;
 align-items: center;
 justify-content: center;
 font-weight: 500;
 color: #999;
 position: absolute;
 inset: 0;
 text-align: center;
 padding: 1rem;
}

Advanced Applications

Custom Blockquote Styling

Blockquotes benefit greatly from pseudo-element styling. Using open-quote and close-quote values along with custom styling creates visually appealing quote designs that enhance your content's visual hierarchy:

blockquote {
 position: relative;
 padding: 1.5em 2.5em;
 font-style: italic;
 font-size: 1.1em;
 line-height: 1.6;
 border-left: 4px solid #0066cc;
 background: #f8f9fa;
 border-radius: 0 8px 8px 0;
}

blockquote::before {
 content: open-quote;
 font-size: 4em;
 color: #0066cc;
 line-height: 0;
 vertical-align: -0.4em;
 position: absolute;
 top: 0.2em;
 left: 0.2em;
 opacity: 0.3;
}

blockquote::after {
 content: close-quote;
 font-size: 4em;
 color: #0066cc;
 line-height: 0;
 vertical-align: -0.8em;
 position: absolute;
 bottom: 0;
 right: 0.2em;
 opacity: 0.3;
}

Form Enhancements

The content property enables extensive customization of form elements without JavaScript. Custom checkboxes, toggle switches, and required field indicators can all be created using pseudo-elements, creating a consistent design system. This approach reduces dependencies and improves performance for AI-powered web applications:

/* Required field indicator */
.required-field label::after {
 content: "*";
 color: #dc3545;
 margin-left: 4px;
 font-weight: bold;
}

/* Custom checkbox */
input[type="checkbox"] {
 appearance: none;
 width: 1.5em;
 height: 1.5em;
 border: 2px solid #333;
 border-radius: 4px;
 position: relative;
 cursor: pointer;
 transition: all 0.2s ease;
}

input[type="checkbox"]:checked::before {
 content: "✓";
 position: absolute;
 top: 50%;
 left: 50%;
 transform: translate(-50%, -50%);
 color: #28a745;
 font-weight: bold;
 font-size: 1.2em;
}

/* Toggle switch */
.toggle-switch {
 position: relative;
 width: 3em;
 height: 1.5em;
}

.toggle-switch::before {
 content: "";
 position: absolute;
 inset: 0;
 background: #ccc;
 border-radius: 1em;
 transition: background 0.3s ease;
}

.toggle-switch::after {
 content: "";
 position: absolute;
 top: 2px;
 left: 2px;
 width: 1.3em;
 height: 1.3em;
 background: white;
 border-radius: 50%;
 transition: transform 0.3s ease;
}

.toggle-switch input:checked::before {
 background: #28a745;
}

.toggle-switch input:checked::after {
 transform: translateX(1.5em);
}

Gradient Overlays and Background Effects

Pseudo-elements excel at creating visual effects that layer on top of existing content without affecting the content flow:

.gradient-overlay::before {
 content: "";
 position: absolute;
 inset: 0;
 background: linear-gradient(135deg, rgba(0, 102, 204, 0.1), rgba(0, 204, 153, 0.1));
 pointer-events: none;
 z-index: 1;
}

.card-hover::before {
 content: "";
 position: absolute;
 inset: 0;
 border-radius: inherit;
 padding: 2px;
 background: linear-gradient(45deg, #0066cc, #00cc99);
 -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
 mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
 -webkit-mask-composite: xor;
 mask-composite: exclude;
 opacity: 0;
 transition: opacity 0.3s ease;
}

.card-hover:hover::before {
 opacity: 1;
}

Best Practices for Performance

When to Use Generated Content

While the content property is powerful, it should be used judiciously to maintain both performance and accessibility. As noted in the DEV Community's comprehensive guide, understanding when to use generated content versus actual HTML elements is crucial.

Use content for:

  • Decorative visual elements that don't convey essential meaning
  • Icons that are purely aesthetic and don't carry semantic information
  • Dynamic indicators based on attributes (like required fields)
  • Visual enhancements that don't affect the document's semantic structure
  • Animations and transitions on purely decorative elements

Use actual HTML elements for:

  • Essential content that screen readers must access
  • Interactive elements that need focus management
  • Content that users might need to copy, select, or search for
  • Information that should be available when CSS is disabled

Performance Considerations

Pseudo-elements add to the rendering pipeline and can impact paint performance when overused, especially with animations. Following CSS best practices for animation performance:

  • Limit the number of animated pseudo-elements on frequently updated components
  • Use will-change sparingly for complex animations to hint browsers
  • Test on lower-powered devices to ensure smooth performance
  • Avoid excessive use of pseudo-elements in frequently re-rendered components
  • Prefer transform and opacity animations that don't trigger repaints
.animated-element::before {
 will-change: transform;
 transition: transform 0.3s ease, opacity 0.3s ease;
}

Accessibility Considerations

Content generated via the content property is generally not accessible to screen readers, which is why it should be reserved for decorative purposes. To ensure your website meets accessibility standards:

  • Use content only for decorative elements that don't convey essential information
  • Ensure essential information is present in the actual HTML markup
  • Use aria-label or aria-describedby attributes when content pseudo-elements need associated text
  • Test with actual screen readers like NVDA, JAWS, or VoiceOver
/* Decorative only - screen readers ignore this */
.decoration::before {
 content: "★";
}

/* Screen reader accessible alternative */
.accessible::after {
 content: "featured";
 position: absolute;
 width: 1px;
 height: 1px;
 padding: 0;
 margin: -1px;
 overflow: hidden;
 clip: rect(0, 0, 0, 0);
 white-space: nowrap;
 border: 0;
}

WCAG Compliance Notes:

  • WCAG 2.1 Success Criterion 1.3.1 (Info and Relationships) requires that information conveyed through pseudo-elements must also be available in the DOM
  • Use visually hidden text for essential information that pseudo-elements display
  • Test keyboard navigation to ensure pseudo-element visual states don't mislead users about interactivity

Common Pitfalls and Solutions

Content Property Requirements

The most common mistake developers make is forgetting that the content property is mandatory for pseudo-elements to render. Even when you want a purely decorative element with dimensions and background, you must specify the content property:

Common mistake:

/* This pseudo-element will NOT appear */
.element::before {
 width: 20px;
 height: 20px;
 background: blue;
}

Correct approach:

/* Now the pseudo-element appears */
.element::before {
 content: "";
 width: 20px;
 height: 20px;
 background: blue;
}

Browser Compatibility

While basic content property support is excellent across all modern browsers, some values have varying levels of support:

FeatureSupportNotes
String valuesExcellentUniversal support
URL valuesExcellentUniversal support
Empty stringsExcellentUniversal support
attr() functionGoodFull support for string attributes
Counter functionsGoodNested counters widely supported
Quote valuesGoodRequires quotes property setting
::markerModernGood support in recent browsers

For projects supporting older browsers, test attr() with non-string attributes and advanced counter features.

Specificity and Inheritance

Pseudo-elements have the same specificity as regular elements in their selector. When chained with pseudo-classes, specificity follows standard CSS cascade rules:

/* Specificity: 0,0,1,1 */
div::before { }

/* Specificity: 0,0,2,1 */
div.container::before { }

/* Specificity: 0,1,2,1 */
div.container:hover::before { }

/* Specificity: 0,2,2,1 */
div.container:hover::before::marker { }

Debugging Strategies

When pseudo-elements don't appear as expected:

  1. Verify content is set: Even an empty string is required
  2. Check positioning: Pseudo-elements are inline by default; use display: block or position: absolute
  3. Inspect element size: Ensure the pseudo-element has dimensions
  4. Test z-index: Pseudo-elements may be hidden behind other content
  5. Check for content overrides: Later CSS rules may override your styles

Common Mistakes to Avoid

  • Forgetting content: ""; when creating purely decorative elements
  • Using generated content for essential information
  • Overusing pseudo-elements leading to performance issues
  • Neglecting accessibility testing
  • Assuming pseudo-elements are accessible to screen readers

Integration with Modern Frameworks

Using content in Next.js Applications

The content property works seamlessly with CSS-in-JS solutions, CSS modules, and Tailwind CSS in Next.js applications. For Tailwind projects, use arbitrary values or custom utilities for pseudo-element content:

/* Custom utility for before pseudo-element */
@layer utilities {
 .before-content::before {
 content: var(--before-content);
 }
}

Tailwind arbitrary value approach:

<div class="before:content-['→'] hover:before:translate-x-1">
 Hover me
</div>

<a class="before:content-[url('/images/icon.svg')] before:mr-2 ...">
 Link with icon
</a>

CSS Module approach:

/* Button.module.css */
.button::after {
 content: " →";
 transition: transform 0.2s ease;
}

.button:hover::after {
 transform: translateX(4px);
}

Responsive Design with Generated Content

Generated content can be made responsive using standard CSS techniques including media queries and container queries. This ensures your responsive web designs adapt smoothly across devices:

.element::before {
 content: "Mobile";
 font-size: 12px;
 padding: 8px 16px;
}

@media (min-width: 768px) {
 .element::before {
 content: "Tablet or Desktop";
 font-size: 16px;
 padding: 12px 24px;
 }
}

Container queries for component-based responsiveness:

.card::before {
 content: "Standard";
 font-size: 0.8em;
 color: #666;
}

@container (min-width: 400px) {
 .card::before {
 content: "Wide card";
 font-size: 1em;
 }
}

Animation and Transitions

Pseudo-elements support full animation capabilities for dynamic effects. The key is to animate only properties that don't trigger expensive repaints:

.hover-button {
 position: relative;
 overflow: hidden;
}

.hover-button::before {
 content: "";
 position: absolute;
 inset: 0;
 background: linear-gradient(45deg, #0066cc, #00cc99);
 z-index: -1;
 transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
 transform: scaleX(0);
 transform-origin: left;
}

.hover-button:hover::before {
 transform: scaleX(1);
}

/* Skeleton loading animation */
.skeleton::after {
 content: "";
 position: absolute;
 inset: 0;
 background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
 animation: shimmer 1.5s infinite;
}

@keyframes shimmer {
 0% { transform: translateX(-100%); }
 100% { transform: translateX(100%); }
}

Performance Optimization in Component Libraries

When using pseudo-elements in reusable components for enterprise web applications:

  • Define animations on pseudo-elements with will-change only when necessary
  • Use CSS custom properties for dynamic content values
  • Test with Chrome DevTools Performance tab to measure paint impact
  • Consider using CSS containment for isolated component rendering
.component {
 contain: layout paint;
}

.component::before {
 content: var(--icon, "");
 will-change: transform;
 transition: transform 0.2s ease;
}

Frequently Asked Questions

Summary and Key Takeaways

The CSS content property, combined with pseudo-elements, is an essential tool for modern web development. It enables sophisticated visual effects while keeping your markup clean and maintainable.

What You Learned

  1. Syntax mastery: The content property requires a value to render pseudo-elements--even an empty string creates a visible element that can be styled
  2. Value variety: From plain strings to counters, each content type serves specific purposes across your web development projects
  3. Practical applications: Decorative icons, dynamic attributes using attr(), automatic numbering with counters, and form element enhancements
  4. Performance awareness: Generated content impacts rendering--use judiciously and prefer transform and opacity for animations
  5. Accessibility first: Generated content isn't accessible to screen readers--reserve it for decorative purposes and ensure essential information is in the HTML
  6. Framework integration: Works seamlessly with Next.js, Tailwind CSS, and modern CSS-in-JS solutions

Best Practices Recap

  • Use content for visual enhancement, not essential information
  • Always include content: ""; when styling pseudo-elements without visible text
  • Test accessibility with real screen readers like NVDA, JAWS, or VoiceOver
  • Monitor performance when animating pseudo-elements on lower-powered devices
  • Leverage counters for documentation sections and hierarchical numbering
  • Choose ::marker for list styling and ::before/::after for decorative content

Next Steps

Apply these techniques to your projects:

  1. Audit your UI for decorative elements that could use pseudo-elements instead of additional HTML
  2. Implement CSS counters for documentation sections and hierarchical content structures
  3. Create custom form elements with pure CSS for consistent, maintainable designs
  4. Build accessible, performant interfaces with generated content that respects screen readers

The content property exemplifies how modern CSS enables sophisticated designs with clean, maintainable code. By understanding when and how to use generated content effectively, you can build professional websites that are both beautiful and accessible.


Related Resources:

Ready to Level Up Your Web Development Skills?

Our team of expert developers specializes in building modern, performant websites using the latest CSS techniques and best practices. From Next.js applications to responsive designs, we create websites that stand out.