Build Pure CSS Tabs Using the :target Pseudo-Class
The CSS :target pseudo-class allows developers to create
functional, interactive tabbed interfaces entirely without JavaScript by
utilizing URL fragment identifiers. When a user clicks an anchor link
containing a specific hash, the browser matches that fragment to an
element with an identical id attribute, triggering the
:target state. This article explains the technical
mechanics of the :target pseudo-class, how to structure and
style a CSS-only tab system, and the advantages and trade-offs of this
approach.
The Core Mechanism of the :target Pseudo-Class
Every URL can include a fragment identifier—a hash symbol
(#) followed by an anchor name. When the browser navigates
to a URL containing a fragment, such as example.com/#tab2,
the document identifies any element with id="tab2" as the
target element.
CSS provides the :target pseudo-class to match and apply
styles exclusively to this targeted element. In a tabbed interface, this
dynamic state replaces the need for JavaScript event listeners and class
toggling (element.classList.add('active')).
Structuring the HTML
To build a pure CSS tabbed interface, you need navigation links that
reference specific identifiers, along with content containers marked
with matching id attributes:
<div class="tabs-container">
<!-- Tab Navigation -->
<nav class="tab-nav">
<a href="#tab1">Tab 1</a>
<a href="#tab2">Tab 2</a>
<a href="#tab3">Tab 3</a>
</nav>
<!-- Tab Panels -->
<div class="tab-panels">
<article id="tab1" class="tab-content">
<h2>Content Panel 1</h2>
<p>This is the first panel.</p>
</article>
<article id="tab2" class="tab-content">
<h2>Content Panel 2</h2>
<p>This is the second panel.</p>
</article>
<article id="tab3" class="tab-content">
<h2>Content Panel 3</h2>
<p>This is the third panel.</p>
</article>
</div>
</div>Implementing the CSS Logic
The visibility of each tab panel is managed by switching display
states depending on whether the element matches the :target
selector:
/* Hide all panels by default */
.tab-content {
display: none;
}
/* Display only the panel whose ID matches the current URL hash */
.tab-content:target {
display: block;
}When a user clicks the anchor link
<a href="#tab2">Tab 2</a>, the URL hash updates
to #tab2. The element
<article id="tab2"> instantly matches
:target, switching its property from
display: none; to display: block;.
Simultaneously, any previously targeted element loses its
:target state and is hidden again.
Handling the Default Active Tab
A primary limitation of the :target pseudo-class is that
no element is targeted when the page initially loads without a URL
fragment. Developers handle this in one of two ways:
- URL Redirection or Default Hash: Linking users
directly to the default state (e.g., loading
page.html#tab1directly). - Sibling Combinator Overrides: Setting the first
panel to
display: blockby default, then hiding it when any subsequent tab becomes targeted:
/* Show the first tab by default */
.tab-content:first-child {
display: block;
}
/* If any tab is targeted, hide the default first tab */
.tab-panels:has(:target) .tab-content:first-child:not(:target) {
display: none;
}
/* Show the targeted tab */
.tab-content:target {
display: block;
}Advantages and Considerations
Using :target for tabbed interfaces offers significant
performance and functional benefits:
- Zero JavaScript: The interface works even if JavaScript fails to load, is disabled, or is blocked by client-side settings.
- Deep Linking: Users can share links directly to a specific tab, and bookmarking preserves the active tab state.
- Native History Support: Each tab click generates a browser history entry, allowing users to navigate between tabs using the browser's Back and Forward buttons.
However, consider usability trade-offs before deploying this pattern:
- Page Scrolling: Browsers automatically scroll the targeted element into view. If panels are positioned poorly, clicking a tab can cause an unwanted vertical jump.
- History Pollution: Because every tab switch creates a new history entry, users must press the browser's back button multiple times to return to the previous website.
- Accessibility: Native ARIA roles
(
role="tablist",role="tab",aria-selected="true") cannot be dynamically updated without JavaScript, requiring careful implementation to ensure assistive technologies can parse the interface properly.