CSS-Only Inverted Corner Tabs With Anchor Positioning

CSS-Only Inverted Corner Tabs With Anchor Positioning
CSS-Only Inverted Corner Tabs With Anchor Positioning
This is a CSS-only tabs component that creates animated hash-link navigation for small content panels with anchor positioning, 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 panel id.
  • Keep one link marked with .active for 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-active to set the main accent color.
  • Change --nav-link-padding to make tabs larger or tighter.
  • Change --nav-indicator-radius to 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.

Leave a Reply

Your email address will not be published. Required fields are marked *

Discover more from RSS Feeds Cloud

Subscribe now to keep reading and get access to the full archive.

Continue reading