32 CSS Tab Designs21 / 32

CSS + JSMIT licensed

Bracket Marks

The active tab is framed by typographic brackets — `[` on the left, `]` on the right — that physically slide between tabs. A single shared pair of pseudo-elements does the work.

Published Updated

Live Demo
Try it

The code

<div class="tt09">
  <nav class="tt09n" data-tt09>
    <button class="tt09b active" data-t>One</button>
    <button class="tt09b" data-t>Two</button>
    <button class="tt09b" data-t>Three</button>
    <span class="tt09bL">[</span>
    <span class="tt09bR">]</span>
  </nav>
  <div class="tt09p active" data-p>The first marker.</div>
  <div class="tt09p" data-p>The second marker.</div>
  <div class="tt09p" data-p>The third marker.</div>
</div>
.tt09 {
  min-height: 100vh;
  background: #25222e;
  padding: 24px 22px 18px;
  font-family: ui-serif, Georgia, serif;
  width: 100%;
}

.tt09n {
  position: relative;
  display: flex;
  gap: 18px;
  justify-content: center;
}

.tt09b {
  border: 0;
  background: transparent;
  padding: 6px 8px;
  font: 600 16px/1 ui-serif, Georgia, serif;
  color: rgba(232, 226, 208, 0.5);
  cursor: pointer;
  transition: color 0.3s;
}

.tt09b:hover {
  color: rgba(232, 226, 208, 0.85);
}

.tt09b.active {
  color: #e8e2d0;
}

.tt09bL,
.tt09bR {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  font: 200 36px/1 ui-serif, Georgia, serif;
  color: #dc1e3a;
  pointer-events: none;
  transition: left 0.5s cubic-bezier(0.65, 0, 0.35, 1);
}

.tt09bL {
  left: 0;
}

.tt09bR {
  left: 0;
}

.tt09p {
  display: none;
  padding-top: 16px;
  font: italic 12px/1.6 ui-serif, Georgia, serif;
  color: rgba(232, 226, 208, 0.7);
  text-align: center;
}

.tt09p.active {
  display: block;
}
/* Bracket Marks — toggle .active and slide [ ] frame around active button.
   Re-positions the brackets on viewport resize. */
(function () {
  var nav = document.querySelector(".tt09n");
  if (!nav) return;
  var btns = nav.querySelectorAll("[data-t]");
  var bL = nav.querySelector(".tt09bL");
  var bR = nav.querySelector(".tt09bR");
  var pnls = document.querySelectorAll(".tt09p");
  var current = null;

  function reposition() {
    if (!current || !bL || !bR) return;
    bL.style.left = current.offsetLeft - 20 + "px";
    bR.style.left = current.offsetLeft + current.offsetWidth + 4 + "px";
  }
  function activate(btn) {
    current = btn;
    btns.forEach(function (b) {
      b.classList.toggle("active", b === btn);
    });
    var i = Array.prototype.indexOf.call(btns, btn);
    pnls.forEach(function (p, j) {
      p.classList.toggle("active", j === i);
    });
    reposition();
  }
  btns.forEach(function (b) {
    b.addEventListener("click", function () {
      activate(b);
    });
  });
  window.addEventListener("resize", reposition);
  var initial = nav.querySelector("[data-t].active") || btns[0];
  if (initial) activate(initial);
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Tab Design from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Bracket Marks
Source: https://codefronts.com/navigation/css-tabs/bracket-marks/

The active tab is framed by typographic brackets — `[` on the left, `]` on the right — that physically slide between tabs. A single shared pair of pseudo-elements does the work.
## HTML
```html
<div class="tt09">
  <nav class="tt09n" data-tt09>
    <button class="tt09b active" data-t>One</button>
    <button class="tt09b" data-t>Two</button>
    <button class="tt09b" data-t>Three</button>
    <span class="tt09bL">[</span>
    <span class="tt09bR">]</span>
  </nav>
  <div class="tt09p active" data-p>The first marker.</div>
  <div class="tt09p" data-p>The second marker.</div>
  <div class="tt09p" data-p>The third marker.</div>
</div>
```
## CSS
```css
.tt09 {
  min-height: 100vh;
  background: #25222e;
  padding: 24px 22px 18px;
  font-family: ui-serif, Georgia, serif;
  width: 100%;
}

.tt09n {
  position: relative;
  display: flex;
  gap: 18px;
  justify-content: center;
}

.tt09b {
  border: 0;
  background: transparent;
  padding: 6px 8px;
  font: 600 16px/1 ui-serif, Georgia, serif;
  color: rgba(232, 226, 208, 0.5);
  cursor: pointer;
  transition: color 0.3s;
}

.tt09b:hover {
  color: rgba(232, 226, 208, 0.85);
}

.tt09b.active {
  color: #e8e2d0;
}

.tt09bL,
.tt09bR {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  font: 200 36px/1 ui-serif, Georgia, serif;
  color: #dc1e3a;
  pointer-events: none;
  transition: left 0.5s cubic-bezier(0.65, 0, 0.35, 1);
}

.tt09bL {
  left: 0;
}

.tt09bR {
  left: 0;
}

.tt09p {
  display: none;
  padding-top: 16px;
  font: italic 12px/1.6 ui-serif, Georgia, serif;
  color: rgba(232, 226, 208, 0.7);
  text-align: center;
}

.tt09p.active {
  display: block;
}
```

## JavaScript
```js
/* Bracket Marks — toggle .active and slide [ ] frame around active button.
   Re-positions the brackets on viewport resize. */
(function () {
  var nav = document.querySelector(".tt09n");
  if (!nav) return;
  var btns = nav.querySelectorAll("[data-t]");
  var bL = nav.querySelector(".tt09bL");
  var bR = nav.querySelector(".tt09bR");
  var pnls = document.querySelectorAll(".tt09p");
  var current = null;

  function reposition() {
    if (!current || !bL || !bR) return;
    bL.style.left = current.offsetLeft - 20 + "px";
    bR.style.left = current.offsetLeft + current.offsetWidth + 4 + "px";
  }
  function activate(btn) {
    current = btn;
    btns.forEach(function (b) {
      b.classList.toggle("active", b === btn);
    });
    var i = Array.prototype.indexOf.call(btns, btn);
    pnls.forEach(function (p, j) {
      p.classList.toggle("active", j === i);
    });
    reposition();
  }
  btns.forEach(function (b) {
    b.addEventListener("click", function () {
      activate(b);
    });
  });
  window.addEventListener("resize", reposition);
  var initial = nav.querySelector("[data-t].active") || btns[0];
  if (initial) activate(initial);
})();
```

How this works

Bracket Marks is a JavaScript-assisted tab pattern that mimics an IDE selection marker. Three .tt09b buttons sit in a centered flex row on a bruised-purple #25222e stage, set in ui-serif at 600 16px/1. Two absolutely-positioned spans (.tt09bL holding the open bracket and .tt09bR holding the close bracket) live at the end of the nav — at 200 36px/1 ui-serif in crimson #dc1e3a. On load they both sit at left: 0, then JavaScript re-parks them around whichever button carries .active.

The bracket geometry is computed in a single reposition() function: bL.style.left = current.offsetLeft - 20 + "px" parks the opening bracket 20px to the left of the active button, and bR.style.left = current.offsetLeft + current.offsetWidth + 4 + "px" parks the closing bracket 4px to the right of the button's trailing edge. Both spans carry a transition: left 0.5s cubic-bezier(0.65, 0, 0.35, 1), so when a new button is clicked, the two brackets slide in tandem across the nav — the same shared pair frames each active tab in turn, rather than duplicating brackets per label.

The IIFE is roughly forty lines, dependency-free, and reads once at parse time. It wires click handlers on [data-t] buttons that toggle .active on the button and on the matching [data-p] panel. A window resize handler re-calls reposition() so the brackets stay locked as the viewport changes. Buttons are semantic button elements, keyboard-native without role="tab", and the panels are simple divs shown via .active class rather than CSS :has(), keeping browser support wide. Copy-paste, scoped under .tt09, framework-agnostic, and SSR-safe — the JS only runs after .tt09n exists.

Make it yours

  • Swap crimson #dc1e3a for an editor accent — VS Code blue #569cd6 reads code-editor; a mint #7fdbca reads Nord. Only the .tt09bL, .tt09bR color declaration needs the change.
  • Swap open and close brackets for other typographic marks. Curly braces read as JSON; angle brackets read as JSX; guillemets read as French editorial. All work at the same 36px thin weight without JS changes.
  • Retune the offsets. The current -20 and +4 pixels are tuned for 36px brackets at 600-weight labels. Larger brackets (48px) want -28/+6; smaller (24px) want -14/+2. Re-derive both if you change bracket font-size.
  • Change the bracket weight from 200 (hairline) to 800 (bold) for a heavier IDE-selection feel. Hairline reads as literary marginalia; bold reads as functional editor UI. The color stays crimson either way.
  • Slow the slide from 0.5s to 0.8s for a more deliberate typewriter shift, or speed to 0.28s for snappier editor register. Keep the cubic-bezier(0.65, 0, 0.35, 1) ease — it lands the brackets with a firm resolve that fits the bracketing metaphor.

Gotchas — read before shipping

  • The brackets are positioned via offsetLeft/offsetWidth. Both read 0 while the parent is display: none. If .tt09 mounts inside a hidden tab or accordion, call reposition() after the container becomes visible or the brackets will collapse to the left edge on first paint.
  • The IIFE grabs the first .tt09n only. Rendering two Bracket Marks demos on one page will leave the second one inert. Refactor the IIFE into a per-nav loop keyed on a data attribute before shipping duplicates or shared component libraries.
  • The initial paint has both brackets at left: 0. Users who see the page before DOMContentLoaded may catch a frame with both brackets stacked. Either inline the JS above the nav or set initial left values in CSS matching the first button.
  • The bracket color #dc1e3a against the #25222e ground meets 4.5:1 for the brackets themselves, but the inactive label color rgba(232, 226, 208, 0.5) falls short of WCAG 2.1 4.5:1 body-text contrast. Push inactive to 0.7 alpha for tier-1 accessibility.
  • The resize listener has no debounce. On repeat resize events it will thrash offsetLeft reads. For dashboards or long-lived apps, wrap reposition in a requestAnimationFrame guard; INP stays safe on demo pages but degrades under sustained resize.
  • There is no prefers-reduced-motion guard on the bracket left transition. The slide is a signature part of the pattern, but users with reduced motion still see it — add a media block that zeros the transition and lets the brackets snap instantly.

Browser support

ChromeSafariFirefoxEdge
88+14+78+88+

The JS uses only querySelectorAll, offsetLeft, and classList. No :has(), no variable fonts, no modern CSS. Panels swap via the .active class rather than :has(), so this ships with the widest support of the cluster.

Techniques used in this demo

Search CodeFronts

Loading…