Pseudo-Elements and State-Based Styling: Critical Approaches to Dynamic CSS
CompleteURL: https://ruvebal.github.io/web-atelier-udit/lessons/en/pseudo-elements-and-state-styling/
Pseudo-Elements and State-Based Styling: Critical Approaches to Dynamic CSS
🎯 Learning Objectives
By the end of this lesson, you will:
- Quickly distinguish between pseudo-classes and pseudo-elements
- Improve interaction states without sacrificing accessibility
- Use pseudo-elements for decoration without hiding meaningful content
- Judge when a selector can impact runtime performance
- Document styling decisions to sustain progressive enhancement
Five Key Takeaways
- Pseudo-classes react to state (
:hover,:focus-visible,:nth-child()) - Pseudo-elements provide visual scaffolding (
::before,::after,::selection) - Accessibility demands visible focus and synchronized ARIA attributes
- Performance benefits from short selector chains and shallow nesting
- Progressive enhancement leans on
@supportsand resilient:focusfallbacks for legacy browsers
Pseudo-Selector Map
Pseudo-classes by intent
| Intent | Key selectors | Practical use |
|---|---|---|
| Interaction | :hover, :focus-visible, :active, :focus-within |
Immediate, accessible feedback |
| Structure | :first-child, :nth-of-type(odd), :last-child |
Visual rhythm without extra classes |
| Forms | :required, :valid, :placeholder-shown, :has() |
Inline validation and stateful layouts |
| JavaScript-free state | :target, :checked, :focus-within |
Accordions, tabs, popovers controlled with CSS |
/* Quick references while auditing interaction states */
/*
Ensure visible focus indication for accessible navigation.
The :focus-visible pseudo-class matches when the element
should indicate focus (e.g. via keyboard, not mouse).
This highlights links in navigation with a 2px outline
using the --focus-ring CSS variable for color, and
separates the outline from the element for clarity.
*/
.nav a:focus-visible {
outline: 2px solid var(--focus-ring); /* Prominent focus for keyboard users */
outline-offset: 2px; /* Space between link and outline */
}
input:required:invalid {
border-color: #f87171;
box-shadow: 0 0 0 1px rgba(248, 113, 113, 0.35);
}
Pseudo-elements by purpose
| Purpose | Selectors | Rapid use case |
|---|---|---|
| Visual decoration | ::before, ::after, ::marker |
Inline icons, badges, separators |
| Typography | ::first-letter, ::first-line, ::selection |
Drop caps, highlighted leads, custom selection |
| Experience | ::placeholder, ::backdrop, ::cue |
Helper text, overlays, media cues |
/* Lightweight decoration without touching HTML */
.badge::before {
content: '●';
color: currentColor;
margin-inline-end: 0.25rem;
}
Demo: Typography with pseudo-elements (CSS only)
JavaScript-Free Patterns
Pseudo-classes can cover many interactions when you combine semantic HTML with native controls:
:focus-withinkeeps menus, accordions, or tooltips open for keyboard and touch users.:checkedanddetails[open]toggle panels or disclosure widgets without scripting.:targetpowers deep-linked modals or tabs via the URL hash.
<input type="checkbox" id="info-toggle" class="accordion__toggle" />
<label for="info-toggle" class="accordion__label">Reveal content</label>
<div class="accordion__panel">Accessible accordion content without JavaScript.</div>
.accordion__toggle {
position: absolute;
inline-size: 1px;
block-size: 1px;
opacity: 0;
}
.accordion__panel {
max-block-size: 0;
overflow: hidden;
transition: max-block-size 0.3s ease;
}
.accordion__toggle:checked + .accordion__label + .accordion__panel {
max-block-size: 20rem;
}
Demo: CSS-only accessible tooltip
Express Workshop: Accessible Dropdown
- Lean HTML structure
<nav class="dropdown">
<button class="dropdown__trigger" aria-haspopup="true" aria-expanded="false">Menu</button>
<ul class="dropdown__content" hidden>
<li><a href="#home">Home</a></li>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
</nav>
- Key interaction states
.dropdown {
position: relative;
--focus-ring: 2px solid #2563eb;
}
.dropdown__trigger {
padding: 0.75rem 1rem;
min-height: 44px;
}
.dropdown__trigger:focus {
outline: var(--focus-ring);
outline-offset: 2px;
}
.dropdown__trigger:focus:not(:focus-visible) {
outline: none;
}
- Pseudo-elements + controlled reveal
CSS
.dropdown__trigger::before {
content: '▾';
margin-right: 0.5rem;
transition: transform 0.2s ease;
}
.dropdown[data-expanded='true'] .dropdown__trigger::before {
transform: rotate(180deg);
}
.dropdown__content {
position: absolute;
inset-inline-start: 0;
right: 0;
margin-top: 0.5rem;
background: white;
border: 1px solid #e2e8f0;
box-shadow: 0 8px 16px rgba(15, 23, 42, 0.1);
list-style: none;
padding: 0;
}
.dropdown__content[hidden] {
display: none;
}
.dropdown__content li:nth-child(odd) {
background: #f7fafc;
}
.dropdown__content li:not(:last-child) {
border-bottom: 1px solid #e2e8f0;
}
- Dropdown behavioral script
The following JavaScript keeps aria-expanded, the hidden attribute, and the data-expanded state synchronized—so styles and pseudo-elements stay accurate. Adapt this logic to the framework or UI library you prefer.
// Accessible dropdown pattern: syncs ARIA, hidden, and data attributes for style and state.
const dropdown = document.querySelector('.dropdown');
const trigger = dropdown.querySelector('.dropdown__trigger');
const content = dropdown.querySelector('.dropdown__content');
// Initialize visual state
dropdown.dataset.expanded = trigger.getAttribute('aria-expanded');
trigger.addEventListener('click', () => {
const expanded = trigger.getAttribute('aria-expanded') === 'true';
const newState = String(!expanded);
trigger.setAttribute('aria-expanded', newState);
content.hidden = expanded;
dropdown.dataset.expanded = newState;
});
The script toggles
hidden, keepsaria-expandedaccurate, and updatesdata-expandedfor pseudo-element styling. Feel free to adapt it to your framework or Web Components setup.
Live demo: Accessible dropdown
Critical Checklist
- Visible focus ensured with
:focus-visibleand a sensible:focusfallback Tab,Enter, andEscenable complete keyboard navigationhiddenreflects the same state asaria-expanded- Prefer straightforward selectors (
.dropdown__item) before reaching for:nth-child()
Guided Exercises
Exercise 1 · Rapid Audit
- Audit an existing dropdown with the checklist
- Capture two accessibility improvements and log them as an issue or PR
- Prioritize fixes that immediately help keyboard-only users
Exercise 2 · CSS-Only Dropdown
- Use
:focus-withinas your primary trigger - Cap transitions at 300 ms and honor
prefers-reduced-motion - Test on mobile, dark mode, and high-contrast themes before shipping
Demo: Combined patterns (CSS + pseudo-elements)
Best Practices
- Keep critical content in HTML; reserve pseudo-elements for supporting visuals
- Pair
:hoverwith:focusto cover pointer and keyboard input - Gate progressive enhancements behind
@supports selector(:focus-visible) - Measure performance in DevTools and run axe DevTools for accessibility regressions
Essential Resources
Conclusion
Pseudo-classes and pseudo-elements extend CSS so interfaces can react without abandoning semantic HTML. Combine them with accessible patterns and performance measurements to ship components that scale gracefully.
Now take these techniques to your GitHub repositories, experiment with styling variants, and share your results by shipping an accessible dropdown, capturing two design decisions in the README, and posting a screenshot or GIF of the final behavior.