
corner-shape, custom properties, and :target state.The active tab merges into the panel below it, and the side corners cut inward to create a shaped tab edge.
Features:
- CSS-only tab switching through fragment links.
- Animated active indicator that follows the current tab.
- Inverted side corners on the active tab.
- Theme control through descriptive CSS variables.
- Hover and focus states with fallback rules for legacy browsers.
How to use it:
1. Create the HTML structure. Add a wrapper, a nav row with anchor links, and one content panel for each tab. Each link must point to the id of its matching panel.
- Every
href="#..."value must match one panelid. - Keep one link marked with
.activefor the default state. - Do not reuse the same IDs elsewhere on the page.
<div class="tabbed-content">
<nav class="tabs-nav">
<a href="#overview" class="active">Overview</a>
<a href="#specs">Specs</a>
<a href="#pricing">Pricing</a>
<a href="#notes">Notes</a>
<div class="indicator" aria-hidden="true"></div>
</nav>
<section class="tabs-panels">
<div id="overview">
Product overview text goes here.
</div>
<div id="specs">
Technical specs go here.
</div>
<div id="pricing">
Pricing details go here.
</div>
<div id="notes">
Extra notes and support details go here.
</div>
</section>
</div>2. Define the CSS custom properties. These control the indicator color, radius, spacing, text color, and panel transitions.
:root {
--nav-indicator-hover: light-dark(rgb(202 213 226 / 0.75), rgb(49 65 88 / 0.75));
--nav-indicator-active: dodgerblue;
--nav-indicator-radius: 10px;
--nav-indicator-inverted-radius: calc(var(--nav-indicator-radius) / 1.5);
--nav-indicator-inverted-width-before: var(--nav-indicator-inverted-radius);
--nav-indicator-inverted-width-after: var(--nav-indicator-inverted-radius);
--nav-trans-duration: 350ms;
--nav-trans-easing: ease-in-out;
--nav-link-color: light-dark(black, white);
--nav-link-color-hover: light-dark(black, white);
--nav-link-color-active: white;
--nav-link-padding: 0.5em 1.5em;
--content-bg-color: var(--nav-indicator-active);
--content-text-color: white;
--content-radius-tl: 10px;
--content-radius-tr: 10px;
--content-radius-bl: 10px;
--content-radius-br: 10px;
--content-trans-duration: 350ms;
--content-trans-easing: ease-in-out;
}
.tabbed-content {
width: min(100%, 720px);
}
.tabbed-content .tabs-nav {
position: relative;
display: flex;
align-items: center;
scroll-target-group: auto;
anchor-name: --hovered-option;
}
.tabbed-content .tabs-nav > a {
display: block;
padding: var(--nav-link-padding);
color: var(--nav-link-color);
text-decoration: none;
transition: color 200ms ease-in-out;
}
.tabbed-content .tabs-nav > a.active {
anchor-name: --active-option;
color: var(--nav-link-color-active);
}3. The .indicator element sits behind the active tab. It reads the current anchor position from --active-option, then moves with CSS anchor positioning.
.tabbed-content .tabs-nav .indicator {
position: absolute;
z-index: -1;
pointer-events: none;
background: var(--nav-indicator-active);
border-radius: var(--nav-indicator-radius) var(--nav-indicator-radius) 0 0;
transition-property: top, right, bottom, left;
transition-duration: var(--nav-trans-duration);
transition-timing-function: var(--nav-trans-easing);
position-anchor: --active-option;
top: anchor(top);
left: anchor(left);
right: anchor(right);
bottom: anchor(bottom);
}4. Add the hover indicator next. This creates the preview state when the pointer moves across links.
.tabbed-content .tabs-nav::before {
content: "";
position: absolute;
z-index: -1;
pointer-events: none;
background: var(--nav-indicator-hover);
border-radius: var(--nav-indicator-inverted-radius);
transition: var(--nav-trans-duration) var(--nav-trans-easing);
position-anchor: --hovered-option;
top: calc(anchor(bottom) + 5px);
left: calc(anchor(left) + 10px);
right: calc(anchor(right) + 10px);
bottom: calc(anchor(bottom) + 5px);
}
.tabbed-content .tabs-nav:has(a:hover)::before,
.tabbed-content .tabs-nav:has(a:focus-visible)::before {
top: calc(anchor(top) + 5px);
}
.tabbed-content .tabs-nav > a:is(:hover, :focus-visible) {
color: var(--nav-link-color-hover);
anchor-name: --hovered-option;
}5. Add the inverted side corners for the active tab.
@supports (position-anchor: --test) and (corner-shape: scoop) {
.tabbed-content .tabs-nav .indicator::before,
.tabbed-content .tabs-nav .indicator::after {
content: "";
position: absolute;
bottom: 0;
aspect-ratio: 1;
background-color: inherit;
corner-shape: scoop;
transition: width var(--nav-trans-duration) linear;
}
.tabbed-content .tabs-nav .indicator::before {
right: 100%;
width: var(--nav-indicator-inverted-width-before);
border-radius: var(--nav-indicator-inverted-radius) 0 0 0;
}
.tabbed-content .tabs-nav .indicator::after {
left: 100%;
width: var(--nav-indicator-inverted-width-after);
border-radius: 0 var(--nav-indicator-inverted-radius) 0 0;
}
}6. Stack all panels in one grid cell, hide them by default, and reveal the current target panel with :target.
.tabbed-content .tabs-panels {
display: grid;
min-height: 220px;
padding: 1rem;
background: var(--content-bg-color);
border-radius: var(--content-radius-tl) var(--content-radius-tr)
var(--content-radius-bl) var(--content-radius-br);
transition: border-radius var(--nav-trans-duration) var(--nav-trans-easing);
}
.tabbed-content .tabs-panels > div {
grid-area: 1 / 1;
color: var(--content-text-color);
filter: blur(10px) opacity(0);
pointer-events: none;
transition: filter var(--content-trans-duration) var(--content-trans-easing);
}
.tabbed-content .tabs-panels > div:target {
filter: blur(0) opacity(1);
pointer-events: auto;
transition-delay: var(--content-trans-duration);
}7. Connect each fragment target back to its matching nav link so the indicator moves to the correct tab.
body:has(:target#overview) .tabs-nav > a[href="#overview"],
body:has(:target#specs) .tabs-nav > a[href="#specs"],
body:has(:target#pricing) .tabs-nav > a[href="#pricing"],
body:has(:target#notes) .tabs-nav > a[href="#notes"] {
anchor-name: --active-option;
color: var(--nav-link-color-active);
}8. The first and last tab need a small shape adjustment. Remove one side cut when the active tab touches an outer edge.
body:not(:has(:target)):has(.active),
body:has(:target#overview) {
--nav-indicator-inverted-width-before: 0;
--content-radius-tl: 0;
}
body:has(:target#notes) {
--nav-indicator-inverted-width-after: 0;
--content-radius-tr: 0;
}9. The advanced effect depends on modern CSS. Add a simpler fallback so the tabs still work when position-anchor is not available.
@supports not (position-anchor: --test) {
.tabbed-content .tabs-nav > a {
border-radius: var(--nav-indicator-radius) var(--nav-indicator-radius) 0 0;
}
.tabbed-content .tabs-nav > a:is(:hover, :focus-visible) {
color: hotpink;
outline: 1px dashed hotpink;
}
.tabbed-content .tabs-nav > a:target-current {
background: var(--nav-indicator-active);
}
}10. Once the core logic works, adjust these values first:
- Change
--nav-indicator-activeto set the main accent color. - Change
--nav-link-paddingto make tabs larger or tighter. - Change
--nav-indicator-radiusto make the tab edge softer or sharper. - Change
--content-radius-*values to reshape the panel corners. - Change the transition variables to speed up or slow down movement.
11. For a dark theme, start with these two variables:
:root {
--nav-indicator-active: #2563eb;
--content-bg-color: #2563eb;
}The post CSS-Only Inverted Corner Tabs With Anchor Positioning appeared first on CSS Script.
Discover more from RSS Feeds Cloud
Subscribe to get the latest posts sent to your email.
