32 CSS Tab Designs21 / 32
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
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><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;
}.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);
})();/* 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);
})();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);
})();
```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
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 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.