32 CSS Tab Designs

Vertical Dots

Sidebar nav with a connecting timeline rule. The active item's dot fills in solid teal with a soft outer glow; inactive dots stay hollow. Vertical layout. Pure CSS.

Pure CSS MIT licensed

Vertical Dots the 8th of 32 designs in the 32 CSS Tab Designs collection. The design is implemented in pure CSS — no JavaScript required. Copy the HTML and CSS panels below into your project. Because the demo is pure CSS, it works in any framework or templating engine you happen to use. The design honours prefers-reduced-motion and uses real semantic markup, so it ships accessibility-ready out of the box.

Live preview

Open in playground

The code

<div class="tt28">
  <nav class="tt28n">
    <input type="radio" name="tt28" id="tt28-r1" checked />
    <label class="tt28b" for="tt28-r1"><span class="tt28dot"></span>Profile</label>
    <input type="radio" name="tt28" id="tt28-r2" />
    <label class="tt28b" for="tt28-r2"><span class="tt28dot"></span>Account</label>
    <input type="radio" name="tt28" id="tt28-r3" />
    <label class="tt28b" for="tt28-r3"><span class="tt28dot"></span>Billing</label>
    <input type="radio" name="tt28" id="tt28-r4" />
    <label class="tt28b" for="tt28-r4"><span class="tt28dot"></span>Logs</label>
  </nav>
</div>
.tt28 {
  background: #0d1117;
  padding: 18px 22px;
  font-family: ui-sans-serif, system-ui, sans-serif;
  min-height: 220px;
  box-sizing: border-box;
  width: 100%;
}
/* nav padding-left = 30px → that x defines a "rail column" centered at x=15.
   Both the timeline rule and the active dot center on x=15 so they sit on
   the same vertical axis regardless of dot size or label padding. */
.tt28n {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-left: 30px;
  min-width: 0;
}
.tt28n::before {
  content: "";
  position: absolute;
  left: 14px;
  top: 14px;
  bottom: 14px;
  width: 2px;
  background: rgba(56, 189, 248, 0.18);
  border-radius: 1px;
}
.tt28n input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.tt28b {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 6px 8px;
  font:
    600 12px/1 ui-sans-serif,
    system-ui;
  color: rgba(255, 255, 255, 0.55);
  cursor: pointer;
  border-radius: 4px;
  transition:
    color 0.22s,
    background 0.22s;
  white-space: nowrap;
}
.tt28dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 1.5px solid rgba(56, 189, 248, 0.45);
  background: #0d1117;
  flex-shrink: 0;
  /* margin-left = -(button padding-left + dot half-width - line center)
     = -(8 + 6 - 15) = -(-1) ... actually: button starts at nav-padding-left
     (30px). Dot needs left edge at x=9 so center sits at x=15 (line center).
     Button content starts at x=38 (30 + 8 padding). To put dot's left edge
     at x=9, margin-left = 9 - 38 = -29px. */
  margin-left: -29px;
  transition:
    background 0.25s,
    border-color 0.25s,
    box-shadow 0.3s;
  position: relative;
  z-index: 1;
}
.tt28b:hover {
  color: rgba(255, 255, 255, 0.85);
  background: rgba(56, 189, 248, 0.06);
}
.tt28n input:checked + .tt28b {
  color: #7dd3fc;
}
.tt28n input:checked + .tt28b .tt28dot {
  background: #38bdf8;
  border-color: #38bdf8;
  box-shadow: 0 0 10px rgba(56, 189, 248, 0.55);
}
.tt28n input:focus-visible + .tt28b {
  outline: 1px dashed #38bdf8;
  outline-offset: 3px;
}

Search CodeFronts

Loading…