jQuery Tabs: 17 Examples + the jQuery UI Options

Tutorials written for jQuery UI 1.8 call add, remove, select and rotate, methods no current release still has. The widget remains the easiest route for sites that already load jQuery UI: a list of anchors, matching panels and **one .tabs() call**.

Below are 17 jQuery tabs examples you can try and copy, followed by the options, events and fixes that keep them working.

By Bogdan Sandu · Updated October 28, 2026

jQuery UI Tabs options in action

Six demos built on the jQuery UI Tabs widget that ships with WordPress, each one showing a single option or method. No ThemeRoller theme is loaded: every rule targets the ui-tabs classes the widget adds, scoped to the demo wrapper. Without JavaScript the markup reads as a list of jump links followed by plain sections with headings.

Default tabs with a custom theme

  • .tabs()
  • .ui-tabs-active
  • no ThemeRoller

Overview

The Halden desk lamp folds flat, dims in ten steps and runs for 40 hours on one charge.

  • Warm to daylight, 2700 K to 5000 K
  • USB-C charging, 3 hours to full
  • Weighted oak base that never tips

Specs

Height
42 cm
Weight
1.1 kg
Light output
480 lm
Battery
4000 mAh

Reviews

4.7 out of 5, from 128 reviews

Bright enough for sketching, and the dimmer goes low enough for reading in bed.

Mara, verified buyer

Click a tab, then try the arrow keys.

One .tabs() call with no options, styled from scratch. The widget hides inactive panels with inline display none, so the only CSS it really needs is the layout of the tab strip. The panel headings are hidden once the ui-tabs class appears, which keeps them for the no-JavaScript version only.

View code
.jq-prod {
  width: min(100%, 520px);
  border-radius: 16px;
  background: #fff;
  color: #1d2433;
  box-shadow: 0 1px 2px rgba(16, 24, 40, .06), 0 12px 32px rgba(16, 24, 40, .1);
  font: inherit;
  overflow: hidden;
}
.jq-prod__nav {
  display: flex;
  flex-wrap: wrap;
  gap: 0 2px;
  margin: 0;
  padding: 0 8px;
  border-bottom: 1px solid #e3e6ec;
  list-style: none;
}
.jq-prod__nav > li {
  margin: 0;
  padding: 0;
  border-radius: 6px 6px 0 0;
  outline: none;
}
.jq-prod__nav a {
  display: block;
  padding: 15px 10px 12px;
  border-bottom: 3px solid transparent;
  color: #4a5468;
  font-weight: 600;
  font-size: 15px;
  text-decoration: none;
  cursor: pointer;
}
.jq-prod__nav a:hover {
  color: #1d2433;
}
.jq-prod .ui-tabs-active a {
  border-bottom-color: #1847f0;
  color: #1847f0;
}
.jq-prod__nav a:focus-visible,
.jq-prod .ui-tabs-tab:focus-visible {
  outline: 3px solid #1847f0;
  outline-offset: -3px;
}
.jq-prod__count {
  display: inline-block;
  margin-left: 4px;
  padding: 1px 7px;
  border-radius: 999px;
  background: #eef1f6;
  color: #4a5468;
  font-size: 12px;
}
.jq-prod__panel {
  padding: 20px 22px 24px;
}
.jq-prod__h {
  margin: 0 0 8px;
  color: #1d2433;
  font-family: inherit;
  font-size: 17px;
  letter-spacing: 0;
}
.jq-prod.ui-tabs .jq-prod__h {
  display: none;
}
.jq-prod__lead {
  margin: 0 0 14px;
  font-size: 16px;
  line-height: 1.55;
}
.jq-prod__ticks {
  display: grid;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.jq-prod__ticks li {
  position: relative;
  margin: 0;
  padding-left: 26px;
  color: #3a4356;
  font-size: 15px;
}
.jq-prod__ticks li::before {
  content: "";
  position: absolute;
  left: 2px;
  top: 5px;
  width: 12px;
  height: 7px;
  border-left: 2px solid #1847f0;
  border-bottom: 2px solid #1847f0;
  transform: rotate(-45deg);
}
.jq-prod__specs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: 10px;
  margin: 0;
}
.jq-prod__specs div {
  padding: 12px 14px;
  border-radius: 10px;
  background: #f4f6fa;
}
.jq-prod__specs dt {
  color: #5a6478;
  font-size: 13px;
}
.jq-prod__specs dd {
  margin: 2px 0 0;
  color: #1d2433;
  font-weight: 700;
  font-size: 18px;
}
.jq-prod__score {
  margin: 0 0 12px;
  color: #3a4356;
  font-size: 15px;
}
.jq-prod__score strong {
  color: #1d2433;
  font-size: 28px;
  margin-right: 4px;
}
.jq-prod__quote {
  margin: 0;
  padding: 14px 16px;
  border: 0;
  border-left: 3px solid #1847f0;
  border-radius: 0 10px 10px 0;
  background: #f4f6fa;
  color: #1d2433;
  font: inherit;
  font-size: 15px;
  font-style: normal;
}
.jq-prod__quote p {
  margin: 0 0 6px;
  line-height: 1.5;
}
.jq-prod__quote footer {
  color: #5a6478;
  font-size: 13px;
}

Collapsible tabs that start closed

  • collapsible: true
  • active: false
  • :has()

Pick a topic to see the details.

Delivery

Orders placed before 2 pm ship the same day. Standard delivery takes 2 to 4 working days and is free over 60 EUR.

Returns

Send anything back within 30 days, unused and in its box. The return label is in the parcel.

Warranty

Two years on every product, five on frames and hinges. Register the serial number to speed up claims.

Click a tab, then try the arrow keys.

With collapsible set to true, clicking the open tab closes it, and active: false starts the widget with every panel closed. A :has() rule shows a short prompt while nothing is open. Without collapsible, active: false is ignored and the first tab opens.

View code
.jq-coll {
  width: min(100%, 500px);
  color: #17332a;
  font: inherit;
}
.jq-coll__nav {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.jq-coll__nav > li {
  margin: 0;
  padding: 0;
  border-radius: 12px;
  outline: none;
}
.jq-coll__nav a {
  display: block;
  padding: 14px 8px;
  border: 1.5px solid #c9dcd3;
  border-radius: 12px;
  background: #fff;
  color: #17332a;
  font-weight: 600;
  font-size: 15px;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: background-color .2s, border-color .2s, color .2s;
}
.jq-coll__nav a:hover {
  border-color: #0f6e4f;
}
.jq-coll .ui-tabs-active a {
  border-color: #0f6e4f;
  background: #0f6e4f;
  color: #fff;
}
.jq-coll__nav a:focus-visible,
.jq-coll .ui-tabs-tab:focus-visible {
  outline: 3px solid #0f6e4f;
  outline-offset: 3px;
}
.jq-coll__empty {
  display: none;
  margin: 10px 0 0;
  padding: 18px;
  border: 1.5px dashed #b7cec4;
  border-radius: 12px;
  color: #4b6359;
  font-size: 15px;
  text-align: center;
}
.jq-coll.ui-tabs:not(:has(.ui-tabs-active)) .jq-coll__empty {
  display: block;
}
.jq-coll__panel {
  margin-top: 10px;
  padding: 18px 20px;
  border-radius: 12px;
  background: #fff;
  box-shadow: 0 8px 24px rgba(15, 60, 45, .1);
}
.jq-coll__panel p {
  margin: 0;
  font-size: 15px;
  line-height: 1.55;
}
.jq-coll__h {
  margin: 0 0 6px;
  color: #17332a;
  font-family: inherit;
  font-size: 17px;
  letter-spacing: 0;
}
.jq-coll.ui-tabs .jq-coll__h {
  display: none;
}

Hover activation with the event option

  • event: "mouseover"
  • option setter
  • radio group

Hover the room names, then switch to click.

The event option takes any event name, and the radios switch it at runtime with .tabs("option", "event", value). Hover activation is risky: moving the pointer diagonally to a link in the panel crosses other tabs and swaps the panel away, and touch screens have no hover to preview with. Keyboard handling does not change either way.

View code
.jq-hov-wrap {
  display: grid;
  gap: 12px;
  width: min(100%, 540px);
  color: #111827;
  font: inherit;
}
.jq-hov-mode {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 14px;
  margin: 0;
  padding: 0;
  border: 0;
  font-size: 14px;
}
.jq-hov-mode legend {
  float: left;
  margin-right: 4px;
  padding: 0;
  color: #4b5563;
  font-weight: 600;
}
.jq-hov-mode label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 28px;
  cursor: pointer;
}
.jq-hov-mode input {
  width: 18px;
  height: 18px;
  margin: 0;
  accent-color: #111827;
}
.jq-hov {
  border-radius: 14px;
  background: #fff;
  box-shadow: 0 10px 30px rgba(17, 24, 39, .1);
  overflow: hidden;
}
.jq-hov__nav {
  display: flex;
  flex-wrap: wrap;
  margin: 0;
  padding: 0 8px;
  background: #111827;
  list-style: none;
}
.jq-hov__nav > li {
  margin: 0;
  padding: 0;
  outline: none;
}
.jq-hov__nav a {
  display: block;
  padding: 14px 14px 11px;
  border-bottom: 3px solid transparent;
  color: #d1d5db;
  font-weight: 600;
  font-size: 15px;
  text-decoration: none;
  cursor: pointer;
}
.jq-hov .ui-tabs-active a {
  border-bottom-color: #f6c445;
  color: #fff;
}
.jq-hov__nav a:focus-visible,
.jq-hov .ui-tabs-tab:focus-visible {
  outline: 3px solid #f6c445;
  outline-offset: -3px;
}
.jq-hov__panel {
  padding: 18px 20px 20px;
}
.jq-hov__h {
  margin: 0 0 8px;
  color: #111827;
  font-family: inherit;
  font-size: 17px;
  letter-spacing: 0;
}
.jq-hov.ui-tabs .jq-hov__h {
  display: none;
}
.jq-hov__links {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.jq-hov__links li {
  margin: 0;
}
.jq-hov__links a {
  display: block;
  padding: 10px 12px;
  border-radius: 8px;
  background: #f3f4f6;
  color: #111827;
  font-size: 15px;
  text-decoration: none;
}
.jq-hov__links a:hover {
  background: #fde9a8;
}
.jq-hov__links a:focus-visible {
  outline: 3px solid #111827;
  outline-offset: 2px;
}

heightStyle: content, auto and fill

  • heightStyle
  • refresh
  • output
heightStyle

Short

One line of text.

Medium

Two sentences of copy. Enough to wrap onto a second line on most screens.

Long

The tallest panel. With auto, every other panel grows to this height, so the content below the widget stops jumping when visitors switch tabs.

With fill, it scrolls inside the frame instead.

Panel heights: measuring

Click a tab, then try the arrow keys.

content lets each panel take its own height, auto sets every panel to the tallest one, and fill stretches the panels to the height of the parent, here the dashed frame. Setting the option back to content does not clear the pixel heights the other modes wrote, so the script clears them first. The readout lists the three panel heights.

View code
.jq-hs-wrap {
  display: grid;
  gap: 12px;
  width: min(100%, 480px);
  color: #241a3d;
  font: inherit;
}
.jq-hs-mode {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0;
  padding: 0;
  border: 0;
}
.jq-hs-mode legend {
  margin-bottom: 6px;
  padding: 0;
  color: #4d4366;
  font-family: ui-monospace, Consolas, monospace;
  font-size: 13px;
}
.jq-hs-mode label {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  padding: 0 14px;
  border: 1.5px solid #cfc6e6;
  border-radius: 999px;
  background: #fff;
  font-family: ui-monospace, Consolas, monospace;
  font-size: 14px;
  cursor: pointer;
}
.jq-hs-mode input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.jq-hs-mode label:has(:checked) {
  border-color: #6b3fd4;
  background: #6b3fd4;
  color: #fff;
}
.jq-hs-mode label:has(:focus-visible) {
  outline: 3px solid #6b3fd4;
  outline-offset: 2px;
}
.jq-hs-frame {
  height: 250px;
  padding: 8px;
  border: 2px dashed #b9acdc;
  border-radius: 14px;
  box-sizing: border-box;
}
.jq-hs {
  border-radius: 10px;
  background: #fff;
  box-shadow: 0 6px 18px rgba(36, 26, 61, .1);
}
.jq-hs__nav {
  display: flex;
  margin: 0;
  padding: 0 8px;
  border-bottom: 1px solid #e6e1f2;
  list-style: none;
}
.jq-hs__nav > li {
  margin: 0;
  padding: 0;
  outline: none;
}
.jq-hs__nav a {
  display: block;
  padding: 12px 12px 9px;
  border-bottom: 3px solid transparent;
  color: #4d4366;
  font-weight: 600;
  font-size: 15px;
  text-decoration: none;
  cursor: pointer;
}
.jq-hs .ui-tabs-active a {
  border-bottom-color: #6b3fd4;
  color: #6b3fd4;
}
.jq-hs__nav a:focus-visible,
.jq-hs .ui-tabs-tab:focus-visible {
  outline: 3px solid #6b3fd4;
  outline-offset: -3px;
}
.jq-hs__panel {
  padding: 12px 16px;
  box-sizing: border-box;
}
.jq-hs__panel p {
  margin: 0 0 8px;
  font-size: 15px;
  line-height: 1.5;
}
.jq-hs__panel p:last-child {
  margin-bottom: 0;
}
.jq-hs__h {
  margin: 0 0 6px;
  color: #241a3d;
  font-family: inherit;
  font-size: 16px;
  letter-spacing: 0;
}
.jq-hs.ui-tabs .jq-hs__h {
  display: none;
}
.jq-hs-out {
  display: block;
  padding: 10px 12px;
  border-radius: 8px;
  background: #ece8f6;
  color: #241a3d;
  font-family: ui-monospace, Consolas, monospace;
  font-size: 13px;
}

Start on the last tab, step with buttons

  • active: -1
  • .tabs("option", "active")
  • activate event

Version 2.4, March

  • New CSV export for reports
  • Fix Timezone offset in weekly digests

Version 2.5, June

  • New Shared dashboards with view-only links
  • Fix Chart labels overlapping on narrow screens

Version 2.6, September

  • New Dark theme for the whole app
  • New Keyboard shortcuts panel
  • Fix Slow search on large workspaces

Click a tab, then try the arrow keys.

A negative active value counts back from the last tab, so active: -1 opens the newest release. The Older and Newer buttons set the active option from code, and the activate event keeps their disabled state and the position label in sync.

View code
.jq-rel {
  width: min(100%, 480px);
  border: 1px solid #cfe2e4;
  border-radius: 14px;
  background: #fff;
  color: #10343a;
  font: inherit;
}
.jq-rel__nav {
  display: flex;
  gap: 6px;
  margin: 0;
  padding: 12px 12px 0;
  list-style: none;
}
.jq-rel__nav > li {
  margin: 0;
  padding: 0;
  border-radius: 8px;
  outline: none;
}
.jq-rel__nav a {
  display: block;
  padding: 8px 14px;
  border-radius: 8px;
  color: #33585e;
  font-family: ui-monospace, Consolas, monospace;
  font-weight: 600;
  font-size: 14px;
  text-decoration: none;
  cursor: pointer;
}
.jq-rel__nav a:hover {
  background: #eef6f7;
}
.jq-rel .ui-tabs-active a {
  background: #0d6e78;
  color: #fff;
}
.jq-rel__nav a:focus-visible,
.jq-rel .ui-tabs-tab:focus-visible {
  outline: 3px solid #0d6e78;
  outline-offset: 2px;
}
.jq-rel__panel {
  padding: 16px 18px 6px;
}
.jq-rel__h {
  margin: 0 0 10px;
  color: #10343a;
  font-family: inherit;
  font-size: 17px;
  letter-spacing: 0;
}
.jq-rel__list {
  display: grid;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.jq-rel__list li {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin: 0;
  font-size: 15px;
  line-height: 1.45;
}
.jq-rel__tag {
  flex: none;
  padding: 2px 8px;
  border-radius: 6px;
  background: #d8efe9;
  color: #0b4f45;
  font-weight: 700;
  font-size: 12px;
}
.jq-rel__tag--fix {
  background: #fbe7d3;
  color: #7a3a07;
}
.jq-rel__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-top: 10px;
  padding: 12px;
  border-top: 1px solid #e3eff0;
}
.jq-rel__btn {
  min-width: 76px;
  min-height: 40px;
  padding: 0 14px;
  border: 1.5px solid #0d6e78;
  border-radius: 8px;
  background: #fff;
  color: #0d6e78;
  font: inherit;
  font-weight: 600;
  font-size: 14px;
  cursor: pointer;
}
.jq-rel__btn:hover:not(:disabled) {
  background: #0d6e78;
  color: #fff;
}
.jq-rel__btn:disabled {
  border-color: #c6d6d8;
  color: #6c8589;
  cursor: not-allowed;
}
.jq-rel__btn:focus-visible {
  outline: 3px solid #0d6e78;
  outline-offset: 2px;
}
.jq-rel__pos {
  color: #33585e;
  white-space: nowrap;
  font-size: 14px;
}

Disabled tab unlocked by a checkbox

  • disabled: [2]
  • enable / disable
  • aria-disabled

Cart

Linen throw, sand64.00 EUR
Ceramic mug, set of 228.00 EUR

Open Shipping to unlock Payment.

Shipping

Ilse Varga
Kastanienweg 12
10115 Berlin

Payment

Payment opens once the address is confirmed. This demo stops here.

Click a tab, then try the arrow keys.

The disabled option takes an array of indexes, and the widget adds aria-disabled and ui-state-disabled to those tabs. Arrow keys skip them. Ticking the checkbox calls .tabs("enable", 2), and unticking calls disable again. The API docs say the open tab cannot be disabled, but the source does not enforce it, so never disable the tab a visitor is on.

View code
.jq-chk {
  width: min(100%, 460px);
  border-radius: 16px;
  background: #fff;
  color: #2a1a10;
  box-shadow: 0 12px 30px rgba(60, 30, 10, .12);
  font: inherit;
  overflow: hidden;
}
.jq-chk__nav {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  margin: 0;
  padding: 0;
  background: #fbf3ec;
  list-style: none;
}
.jq-chk__nav > li {
  margin: 0;
  padding: 0;
  outline: none;
}
.jq-chk__nav a {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 14px 6px 11px;
  border-bottom: 3px solid #f0dccb;
  color: #5f4433;
  font-weight: 600;
  font-size: 15px;
  text-decoration: none;
  cursor: pointer;
}
.jq-chk .ui-tabs-active a {
  border-bottom-color: #c2410c;
  background: #fff;
  color: #9a330a;
}
.jq-chk .ui-state-disabled a {
  color: #8d7a6e;
  cursor: not-allowed;
}
.jq-chk .ui-state-disabled a::before {
  content: "";
  width: 10px;
  height: 8px;
  margin-top: 4px;
  border: 2px solid currentColor;
  border-radius: 2px;
  box-shadow: 0 -5px 0 -2px #fbf3ec, 0 -6px 0 0 currentColor;
}
.jq-chk__nav a:focus-visible,
.jq-chk .ui-tabs-tab:focus-visible {
  outline: 3px solid #c2410c;
  outline-offset: -3px;
}
.jq-chk__panel {
  padding: 18px 20px 20px;
}
.jq-chk__h {
  margin: 0 0 10px;
  color: #2a1a10;
  font-family: inherit;
  font-size: 17px;
  letter-spacing: 0;
}
.jq-chk.ui-tabs .jq-chk__h {
  display: none;
}
.jq-chk__row {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px solid #f1e6dd;
  font-size: 15px;
}
.jq-chk__hint {
  margin: 12px 0 0;
  color: #6b5242;
  font-size: 14px;
}
.jq-chk__addr {
  margin: 0 0 14px;
  padding: 12px 14px;
  border-radius: 10px;
  background: #fbf3ec;
  font-size: 15px;
  line-height: 1.5;
}
.jq-chk__confirm {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 32px;
  font-size: 15px;
  cursor: pointer;
}
.jq-chk__confirm input {
  width: 20px;
  height: 20px;
  margin: 0;
  accent-color: #c2410c;
}
.jq-chk__confirm input:focus-visible {
  outline: 3px solid #c2410c;
  outline-offset: 2px;
}

Styling jQuery UI Tabs

The same widget in five looks: vertical, inside a card, dark, pills, and a version that turns into a jQuery UI accordion when its container gets narrow. Only CSS and the classes option change between them.

Vertical settings tabs

  • classes option
  • .ui-tabs-vertical
  • aria-orientation

Profile

How other people in the workspace see you.

Notifications

Pick what reaches your inbox.

Security

Two-step sign-in is on. Last sign-in: today, 09:12, Lisbon.

Billing

Team plan, 8 seats, renews on 1 December.

The classes option adds ui-tabs-vertical to the container, and a grid puts the tab list beside the panel instead of the float layout from the jQuery UI demo. The widget never sets aria-orientation, so the script adds it. Up and Down already move between tabs, so the keyboard needs no change. Below 460px of container width the list stacks above the panel.

View code
.jq-vert-wrap {
  width: min(100%, 600px);
  container-type: inline-size;
}
.jq-vert {
  border-radius: 16px;
  background: #fff;
  color: #1f2430;
  box-shadow: 0 10px 30px rgba(31, 36, 48, .1);
  font: inherit;
  overflow: hidden;
}
.jq-vert.ui-tabs-vertical {
  display: grid;
  grid-template-columns: 170px 1fr;
  min-height: 260px;
}
.jq-vert__nav {
  margin: 0;
  padding: 12px 0;
  background: #f5f2ee;
  list-style: none;
}
.jq-vert__nav > li {
  margin: 0;
  padding: 0;
  outline: none;
}
.jq-vert__nav a {
  display: block;
  padding: 11px 18px;
  border-left: 3px solid transparent;
  color: #4b5160;
  font-weight: 600;
  font-size: 15px;
  text-decoration: none;
  cursor: pointer;
}
.jq-vert__nav a:hover {
  color: #1f2430;
}
.jq-vert .ui-tabs-active a {
  border-left-color: #b02a5b;
  background: #fff;
  color: #b02a5b;
}
.jq-vert__nav a:focus-visible,
.jq-vert .ui-tabs-tab:focus-visible {
  outline: 3px solid #b02a5b;
  outline-offset: -3px;
}
.jq-vert__panel {
  padding: 20px 22px;
}
.jq-vert__h {
  margin: 0 0 4px;
  color: #1f2430;
  font-family: inherit;
  font-size: 18px;
  letter-spacing: 0;
}
.jq-vert__sub {
  margin: 0 0 16px;
  color: #5b6170;
  font-size: 14px;
  line-height: 1.5;
}
.jq-vert__field {
  display: grid;
  gap: 4px;
  margin-bottom: 12px;
  color: #4b5160;
  font-size: 13px;
  font-weight: 600;
}
.jq-vert__field input {
  width: 100%;
  min-height: 40px;
  padding: 0 12px;
  border: 1.5px solid #d9d4cd;
  border-radius: 8px;
  background: #fff;
  color: #1f2430;
  font: inherit;
  font-size: 15px;
  font-weight: 400;
  box-sizing: border-box;
}
.jq-vert__field input:focus-visible {
  border-color: #b02a5b;
  outline: 2px solid #b02a5b;
  outline-offset: 1px;
}
.jq-vert__check {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 34px;
  font-size: 15px;
  cursor: pointer;
}
.jq-vert__check input {
  width: 18px;
  height: 18px;
  margin: 0;
  accent-color: #b02a5b;
}
.jq-vert__check input:focus-visible {
  outline: 3px solid #b02a5b;
  outline-offset: 2px;
}
.jq-vert__btn {
  min-height: 42px;
  padding: 0 16px;
  border: 0;
  border-radius: 8px;
  background: #1f2430;
  color: #fff;
  font: inherit;
  font-weight: 600;
  font-size: 14px;
  cursor: pointer;
}
.jq-vert__btn:hover {
  background: #b02a5b;
}
.jq-vert__btn:focus-visible {
  outline: 3px solid #b02a5b;
  outline-offset: 2px;
}
@container (max-width: 460px) {
  .jq-vert.ui-tabs-vertical {
    grid-template-columns: 1fr;
    min-height: 0;
  }
  .jq-vert__nav {
    padding: 6px 0;
  }
  .jq-vert__nav a {
    padding: 9px 16px;
  }
}

Tabs inside a trip card

  • show: fadeIn
  • matchMedia
  • card layout
A deep blue fjord between grey granite cliffs, seen from a rocky ledge

Fjord ridge loop

14 km, 5 h, 860 m of climbing

Overview

A steep start through birch forest, then an open granite ridge with the fjord below you for most of the day.

Route

  • Trailhead car park0 km
  • Ridge viewpoint6 km
  • Lake hut10 km

Packing

Boots with grip on wet rock, a wind layer, 2 litres of water and a paper map. Mobile signal drops on the ridge.

A compact tab strip under a photo, with a short fadeIn through the show option. The script checks prefers-reduced-motion first and passes show: false when the visitor asked for less motion, because jQuery animations ignore the CSS media query.

View code
.jq-card {
  width: min(100%, 360px);
  border-radius: 18px;
  background: #fff;
  color: #1c2b24;
  box-shadow: 0 14px 34px rgba(20, 40, 30, .16);
  font: inherit;
  overflow: hidden;
}
.jq-card__img {
  display: block;
  width: 100%;
  height: 170px;
  object-fit: cover;
}
.jq-card__body {
  padding: 16px 18px 18px;
}
.jq-card__title {
  margin: 0;
  color: #1c2b24;
  font-family: "Fraunces", Georgia, serif;
  font-size: 22px;
  font-weight: 600;
  letter-spacing: 0;
}
.jq-card__meta {
  margin: 2px 0 12px;
  color: #50615a;
  font-size: 14px;
}
.jq-card__nav {
  display: flex;
  gap: 4px;
  margin: 0;
  padding: 4px;
  border-radius: 10px;
  background: #eef3f0;
  list-style: none;
}
.jq-card__nav > li {
  flex: 1;
  margin: 0;
  padding: 0;
  border-radius: 7px;
  outline: none;
}
.jq-card__nav a {
  display: block;
  padding: 8px 4px;
  border-radius: 7px;
  color: #3d5047;
  font-weight: 600;
  font-size: 14px;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}
.jq-card .ui-tabs-active a {
  background: #1f5f45;
  color: #fff;
}
.jq-card__nav a:focus-visible,
.jq-card .ui-tabs-tab:focus-visible {
  outline: 3px solid #1f5f45;
  outline-offset: 1px;
}
.jq-card__panel {
  min-height: 92px;
  padding: 14px 2px 0;
}
.jq-card__panel p {
  margin: 0;
  font-size: 15px;
  line-height: 1.5;
}
.jq-card__h {
  margin: 0 0 6px;
  color: #1c2b24;
  font-family: inherit;
  font-size: 15px;
  letter-spacing: 0;
}
.jq-card__tabs.ui-tabs .jq-card__h {
  display: none;
}
.jq-card__steps {
  margin: 0;
  padding: 0;
  list-style: none;
}
.jq-card__steps li {
  display: flex;
  justify-content: space-between;
  margin: 0;
  padding: 6px 0;
  border-bottom: 1px dashed #cbd8d1;
  font-size: 14px;
}
.jq-card__steps li span:last-child {
  color: #50615a;
  font-family: ui-monospace, Consolas, monospace;
}

Dark install tabs for docs

  • dark theme
  • segmented strip
  • pre / code
Install

npm

npm install @fieldkit/charts

pnpm

pnpm add @fieldkit/charts

yarn

yarn add @fieldkit/charts

The package-manager switcher from documentation sites, in a dark theme. Text sits at 7:1 contrast or better on the panel, and the active tab is marked by fill and weight rather than color alone. The panels hold real pre and code blocks, so they still read as three commands without JavaScript.

View code
.jq-dark {
  width: min(100%, 480px);
  border: 1px solid #2a3140;
  border-radius: 12px;
  background: #11161f;
  color: #e6e9ef;
  font: inherit;
  overflow: hidden;
}
.jq-dark__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 10px 12px;
  border-bottom: 1px solid #2a3140;
  background: #171d28;
}
.jq-dark__label {
  color: #aab3c2;
  font-size: 13px;
  font-weight: 600;
}
.jq-dark__nav {
  display: flex;
  gap: 2px;
  margin: 0;
  padding: 3px;
  border-radius: 8px;
  background: #0b0f15;
  list-style: none;
}
.jq-dark__nav > li {
  margin: 0;
  padding: 0;
  border-radius: 6px;
  outline: none;
}
.jq-dark__nav a {
  display: block;
  min-width: 44px;
  padding: 6px 12px;
  border-radius: 6px;
  color: #aab3c2;
  font-family: ui-monospace, Consolas, monospace;
  font-size: 13px;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}
.jq-dark__nav a:hover {
  color: #fff;
}
.jq-dark .ui-tabs-active a {
  background: #2b3445;
  color: #7ee0b5;
  font-weight: 700;
}
.jq-dark__nav a:focus-visible,
.jq-dark .ui-tabs-tab:focus-visible {
  outline: 2px solid #7ee0b5;
  outline-offset: 1px;
}
.jq-dark__panel {
  padding: 16px;
}
.jq-dark__h {
  margin: 0 0 8px;
  color: #e6e9ef;
  font-family: ui-monospace, Consolas, monospace;
  font-size: 14px;
  letter-spacing: 0;
}
.jq-dark.ui-tabs .jq-dark__h {
  display: none;
}
.jq-dark__code {
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: #e6e9ef;
  font-family: ui-monospace, Consolas, monospace;
  font-size: 15px;
  white-space: pre-wrap;
  word-break: break-word;
}
.jq-dark__code code {
  padding: 0;
  background: none;
  color: inherit;
  font: inherit;
}
.jq-dark__code code::before {
  content: "$ ";
  color: #7ee0b5;
}

Pill tabs for a festival schedule

  • pill buttons
  • border-radius
  • flex-wrap

Friday

  1. Gates open, food market
  2. The Low Tides, main stage
  3. Marlo Quinn, tent stage

Saturday

  1. Kids workshop, print your own poster
  2. Copper Hollow, main stage
  3. Night set, Fennel and the Bees

Sunday

  1. Slow breakfast by the lake
  2. Acoustic hour, garden stage
  3. Closing set, Sola Brass

Rounded pills on a warm palette, with the tab list wrapping when the labels outgrow the width. The pill shape lives on the anchor, while the widget keeps focus on the list item, so both get the same radius for the focus ring to follow.

View code
.jq-pill {
  width: min(100%, 460px);
  padding: 18px;
  border-radius: 20px;
  background: #fbf4ea;
  color: #3b2116;
  font: inherit;
  box-sizing: border-box;
}
.jq-pill__nav {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 14px;
  padding: 0;
  list-style: none;
}
.jq-pill__nav > li {
  margin: 0;
  padding: 0;
  border-radius: 999px;
  outline: none;
}
.jq-pill__nav a {
  display: block;
  padding: 10px 18px;
  border: 1.5px solid #e2c9b5;
  border-radius: 999px;
  background: #fff;
  color: #6a3b26;
  font-weight: 600;
  font-size: 15px;
  text-decoration: none;
  cursor: pointer;
  transition: background-color .2s, color .2s, border-color .2s;
}
.jq-pill__nav a:hover {
  border-color: #b4462b;
}
.jq-pill .ui-tabs-active a {
  border-color: #b4462b;
  background: #b4462b;
  color: #fff;
}
.jq-pill__nav a:focus-visible,
.jq-pill .ui-tabs-tab:focus-visible {
  outline: 3px solid #b4462b;
  outline-offset: 3px;
}
.jq-pill__h {
  margin: 0 0 8px;
  color: #3b2116;
  font-family: inherit;
  font-size: 17px;
  letter-spacing: 0;
}
.jq-pill.ui-tabs .jq-pill__h {
  display: none;
}
.jq-pill__list {
  display: grid;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.jq-pill__list li {
  display: flex;
  align-items: baseline;
  gap: 14px;
  margin: 0;
  padding: 12px 14px;
  border-radius: 12px;
  background: #fff;
  font-size: 15px;
}
.jq-pill__list time {
  flex: none;
  color: #9a3a22;
  font-family: ui-monospace, Consolas, monospace;
  font-weight: 700;
  font-size: 14px;
}

Tabs that become an accordion when narrow

  • ResizeObserver
  • .accordion()
  • destroy

Ingredients

  • 400 g ripe tomatoes
  • 1 red onion, thinly sliced
  • 2 tbsp olive oil and a pinch of salt

Method

Cut the tomatoes into wedges, salt them and leave them for ten minutes. Add the onion and oil, then toss just before serving.

Tips

Never refrigerate the tomatoes first. Cold dulls the flavour and turns the flesh mealy.

One block of markup feeds both widgets: the list drives the tabs, and the h3 before each panel becomes an accordion header. A ResizeObserver watches the width, destroys one widget and creates the other below 400px, carrying the open index across. The button squeezes the demo to phone width so you can see the switch on a desktop.

View code
.jq-acc-wrap {
  display: grid;
  justify-items: center;
  gap: 12px;
  width: min(100%, 540px);
  color: #263016;
  font: inherit;
}
.jq-acc-toggle {
  min-height: 36px;
  padding: 0 14px;
  border: 1.5px solid #4d6b1f;
  border-radius: 999px;
  background: #fff;
  color: #3c5418;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
.jq-acc-toggle[aria-pressed="true"] {
  background: #4d6b1f;
  color: #fff;
}
.jq-acc-toggle:focus-visible {
  outline: 3px solid #4d6b1f;
  outline-offset: 2px;
}
.jq-acc {
  width: 100%;
  border-radius: 14px;
  background: #fff;
  box-shadow: 0 10px 28px rgba(38, 48, 22, .12);
  overflow: hidden;
}
.jq-acc-wrap.is-narrow .jq-acc {
  max-width: 340px;
}
.jq-acc__nav {
  display: flex;
  margin: 0;
  padding: 0 10px;
  border-bottom: 1px solid #e4e9da;
  list-style: none;
}
.jq-acc__nav > li {
  margin: 0;
  padding: 0;
  outline: none;
}
.jq-acc__nav a {
  display: block;
  padding: 14px 14px 11px;
  border-bottom: 3px solid transparent;
  color: #4f5a3c;
  font-weight: 600;
  font-size: 15px;
  text-decoration: none;
  cursor: pointer;
}
.jq-acc .ui-tabs-active a {
  border-bottom-color: #4d6b1f;
  color: #3c5418;
}
.jq-acc__nav a:focus-visible,
.jq-acc .ui-tabs-tab:focus-visible {
  outline: 3px solid #4d6b1f;
  outline-offset: -3px;
}
.jq-acc__h {
  margin: 0;
  padding: 14px 18px 0;
  color: #263016;
  font-family: inherit;
  font-size: 16px;
  letter-spacing: 0;
}
.jq-acc.ui-tabs > .jq-acc__h,
.jq-acc.ui-accordion > .jq-acc__nav {
  display: none;
}
.jq-acc.ui-accordion > .jq-acc__h {
  position: relative;
  padding: 15px 44px 15px 18px;
  border-top: 1px solid #e4e9da;
  cursor: pointer;
  outline: none;
}
.jq-acc.ui-accordion > .jq-acc__h:first-of-type {
  border-top: 0;
}
.jq-acc.ui-accordion > .jq-acc__h::after {
  content: "";
  position: absolute;
  right: 20px;
  top: 50%;
  width: 8px;
  height: 8px;
  margin-top: -6px;
  border-right: 2px solid #4d6b1f;
  border-bottom: 2px solid #4d6b1f;
  transform: rotate(45deg);
}
.jq-acc.ui-accordion > .ui-accordion-header-active::after {
  margin-top: -2px;
  transform: rotate(-135deg);
}
.jq-acc.ui-accordion > .jq-acc__h:focus-visible {
  outline: 3px solid #4d6b1f;
  outline-offset: -3px;
}
.jq-acc__panel {
  padding: 12px 18px 18px;
}
.jq-acc__panel p {
  margin: 0;
  font-size: 15px;
  line-height: 1.55;
}
.jq-acc__list {
  margin: 0;
  padding-left: 18px;
  font-size: 15px;
  line-height: 1.7;
}
.jq-acc__list li {
  margin: 0;
}

Sorting, loading, deep links and ARIA

The jobs people usually reach for a plugin for: reorderable tabs with jQuery UI Sortable, a loading state for remote panels, a URL hash that follows the open tab, and a live readout of the ARIA attributes the widget writes.

Sortable editor tabs

  • jquery-ui-sortable
  • refresh
  • move buttons

index.html

<main class="app">
  <h1>Field notes</h1>
</main>

styles.css

.app {
  max-width: 60ch;
  margin-inline: auto;
}

app.js

const notes = [];
notes.push("first entry");

README.md

# Field notes
A tiny notebook app.

Drag a file tab, or use the arrow buttons.

jQuery UI Sortable, also bundled with WordPress, makes the tab list draggable along the x axis, and a refresh call after each drop re-reads the order. Sortable is built on mouse events (WordPress also bundles the jquery-touch-punch handle for touch drags), so the two buttons move the open tab for keyboard and touch users, and a status line announces the new position.

View code
.jq-sort {
  width: min(100%, 540px);
  border: 1px solid #d5d9e0;
  border-radius: 12px;
  background: #fff;
  color: #1e2633;
  font: inherit;
  overflow: hidden;
}
.jq-sort__bar {
  display: flex;
  align-items: stretch;
  background: #eef0f3;
  border-bottom: 1px solid #d5d9e0;
}
.jq-sort__nav {
  display: flex;
  flex: 1;
  flex-wrap: wrap;
  min-width: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}
.jq-sort__nav > li {
  margin: 0;
  padding: 0;
  border-right: 1px solid #d5d9e0;
  background: #eef0f3;
  outline: none;
}
.jq-sort__nav > li.ui-sortable-helper {
  box-shadow: 0 6px 16px rgba(30, 38, 51, .2);
}
.jq-sort__nav a {
  display: block;
  padding: 10px 14px 9px;
  border-top: 2px solid transparent;
  color: #4b5567;
  font-family: ui-monospace, Consolas, monospace;
  font-size: 13px;
  text-decoration: none;
  cursor: grab;
}
.jq-sort .ui-tabs-active {
  background: #fff;
}
.jq-sort .ui-tabs-active a {
  border-top-color: #d97706;
  color: #1e2633;
}
.jq-sort__nav a:focus-visible,
.jq-sort .ui-tabs-tab:focus-visible {
  outline: 2px solid #b45309;
  outline-offset: -2px;
}
.jq-sort__move {
  display: flex;
  flex: none;
  align-items: center;
  gap: 2px;
  padding: 0 6px;
}
.jq-sort__btn {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: #4b5567;
  cursor: pointer;
}
.jq-sort__btn:hover {
  background: #dfe3e9;
  color: #1e2633;
}
.jq-sort__btn:focus-visible {
  outline: 2px solid #b45309;
  outline-offset: 1px;
}
.jq-sort__btn span {
  width: 8px;
  height: 8px;
  margin-left: 3px;
  border-left: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
}
.jq-sort__btn--next span {
  margin: 0 3px 0 0;
  transform: rotate(-135deg);
}
.jq-sort__panel {
  padding: 16px 18px;
}
.jq-sort__h {
  margin: 0 0 8px;
  color: #1e2633;
  font-family: ui-monospace, Consolas, monospace;
  font-size: 14px;
  letter-spacing: 0;
}
.jq-sort.ui-tabs .jq-sort__h {
  display: none;
}
.jq-sort__code {
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: #1e2633;
  font-family: ui-monospace, Consolas, monospace;
  font-size: 14px;
  line-height: 1.6;
  white-space: pre-wrap;
}
.jq-sort__code code {
  padding: 0;
  background: none;
  color: inherit;
  font: inherit;
}
.jq-sort__status {
  min-height: 1.4em;
  margin: 0;
  padding: 8px 18px 12px;
  color: #5a6375;
  font-size: 13px;
}

Loading state for remote panels

  • beforeActivate
  • ui-tabs-loading
  • aria-busy

Open orders

Order 40912Packing
Order 40917Awaiting stock

Shipped orders

Returned orders

A real remote tab has a URL as its href, and the widget fetches it with jQuery.ajax, firing beforeLoad with ui.jqXHR and ui.ajaxSettings first. This demo makes no request: beforeActivate adds the same ui-tabs-loading class and aria-busy the widget would set, waits 700 ms, then fills the panel from a local template. The skeleton shimmer only runs when motion is allowed.

View code
.jq-rem {
  width: min(100%, 480px);
  border-radius: 14px;
  background: #fff;
  color: #2b1420;
  box-shadow: 0 10px 28px rgba(60, 20, 40, .12);
  font: inherit;
  overflow: hidden;
}
.jq-rem__nav {
  display: flex;
  margin: 0;
  padding: 0 10px;
  border-bottom: 1px solid #f0dfe6;
  list-style: none;
}
.jq-rem__nav > li {
  margin: 0;
  padding: 0;
  outline: none;
}
.jq-rem__nav a {
  position: relative;
  display: block;
  padding: 14px 14px 11px;
  border-bottom: 3px solid transparent;
  color: #5e4350;
  font-weight: 600;
  font-size: 15px;
  text-decoration: none;
  cursor: pointer;
}
.jq-rem .ui-tabs-active a {
  border-bottom-color: #9f1239;
  color: #9f1239;
}
.jq-rem .ui-tabs-loading a::after {
  content: "";
  display: inline-block;
  width: 10px;
  height: 10px;
  margin-left: 8px;
  border: 2px solid #f0c6d4;
  border-top-color: #9f1239;
  border-radius: 50%;
  vertical-align: -1px;
}
.jq-rem__nav a:focus-visible,
.jq-rem .ui-tabs-tab:focus-visible {
  outline: 3px solid #9f1239;
  outline-offset: -3px;
}
.jq-rem__panel {
  min-height: 150px;
  padding: 10px 18px 16px;
}
.jq-rem__h {
  margin: 6px 0 8px;
  color: #2b1420;
  font-family: inherit;
  font-size: 16px;
  letter-spacing: 0;
}
.jq-rem.ui-tabs .jq-rem__h {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.jq-rem__order {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 11px 0;
  border-bottom: 1px solid #f5e9ee;
  font-size: 15px;
}
.jq-rem__order span:last-child {
  color: #6b4b5a;
  font-size: 14px;
}
.jq-rem__skeleton {
  display: grid;
  gap: 10px;
  padding-top: 10px;
}
.jq-rem__skeleton::before,
.jq-rem__skeleton::after {
  content: "";
  height: 18px;
  border-radius: 6px;
  background: #f3e6ec;
}
.jq-rem__skeleton::after {
  width: 70%;
}
@media (prefers-reduced-motion: no-preference) {
  .jq-rem .ui-tabs-loading a::after {
    animation: jq-rem-spin .8s linear infinite;
  }
  .jq-rem__skeleton::before,
  .jq-rem__skeleton::after {
    animation: jq-rem-pulse 1s ease-in-out infinite alternate;
  }
}
@keyframes jq-rem-spin {
  to {
    transform: rotate(360deg);
  }
}
@keyframes jq-rem-pulse {
  to {
    opacity: .45;
  }
}

URL hash that follows the open tab

  • history.replaceState
  • activate event
  • location.hash

Install

Upload the plugin folder, activate it, and the settings screen appears under Tools.

Configure

Paste your site key, choose which post types to include, then save.

Deploy

Clear the page cache once, then check the front end in a private window.

Address ends in #

When active is not passed, the widget opens the tab whose panel id matches location.hash at creation, so deep links work with no extra code. The activate callback writes the new hash with history.replaceState, which updates the address bar without scrolling or adding a history entry.

View code
.jq-hash {
  width: min(100%, 500px);
  border-radius: 14px;
  background: #fff;
  color: #14261f;
  box-shadow: 0 10px 28px rgba(20, 38, 31, .1);
  font: inherit;
  overflow: hidden;
}
.jq-hash__nav {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0;
  padding: 12px;
  background: #e9f5ef;
  list-style: none;
}
.jq-hash__nav > li {
  margin: 0;
  padding: 0;
  border-radius: 8px;
  outline: none;
}
.jq-hash__nav a {
  display: block;
  padding: 8px 14px;
  border-radius: 8px;
  color: #22513f;
  font-weight: 600;
  font-size: 15px;
  text-decoration: none;
  cursor: pointer;
}
.jq-hash__nav a:hover {
  background: #d3ece0;
}
.jq-hash .ui-tabs-active a {
  background: #047857;
  color: #fff;
}
.jq-hash__nav a:focus-visible,
.jq-hash .ui-tabs-tab:focus-visible {
  outline: 3px solid #047857;
  outline-offset: 2px;
}
.jq-hash__panel {
  padding: 16px 18px 4px;
}
.jq-hash__panel p {
  margin: 0;
  font-size: 15px;
  line-height: 1.55;
}
.jq-hash__h {
  margin: 0 0 6px;
  color: #14261f;
  font-family: inherit;
  font-size: 17px;
  letter-spacing: 0;
}
.jq-hash.ui-tabs .jq-hash__h {
  display: none;
}
.jq-hash__url {
  margin: 14px 18px 16px;
  padding: 9px 12px;
  border-radius: 8px;
  background: #f2f4f3;
  color: #44554e;
  font-size: 13px;
  overflow-wrap: anywhere;
}
.jq-hash__frag {
  padding: 0;
  background: none;
  color: #14261f;
  font-family: ui-monospace, Consolas, monospace;
  font-size: 13px;
  font-weight: 700;
}

Live readout of the ARIA the widget writes

  • role="tab"
  • aria-selected
  • roving tabindex

North

Light snow above 900 m, minus 2 at dawn.

Coast

Strong westerly wind, showers clearing by noon.

South

Dry and bright, 16 degrees in the afternoon.

    Focus a tab, then press the arrow keys, Home and End.

    jQuery UI puts role="tablist" on the list, role="tab" on each li and role="tabpanel" on each panel, linked by aria-controls and aria-labelledby. Arrow keys move focus and activate after a 300 ms delay, Home and End jump to the ends, Space activates at once, and Ctrl plus an arrow moves focus only. The readout below the tabs updates on every focus and activation.

    View code
    .jq-aria {
      display: grid;
      gap: 12px;
      width: min(100%, 520px);
      color: #1d1b16;
      font: inherit;
    }
    .jq-aria__tabs {
      border: 1.5px solid #1d1b16;
      border-radius: 12px;
      background: #fff;
      overflow: hidden;
    }
    .jq-aria__nav {
      display: flex;
      margin: 0;
      padding: 0;
      border-bottom: 1.5px solid #1d1b16;
      list-style: none;
    }
    .jq-aria__nav > li {
      flex: 1;
      margin: 0;
      padding: 0;
      border-right: 1.5px solid #1d1b16;
      outline: none;
    }
    .jq-aria__nav > li:last-child {
      border-right: 0;
    }
    .jq-aria__nav a {
      display: block;
      padding: 12px 8px;
      color: #1d1b16;
      font-weight: 600;
      font-size: 15px;
      text-align: center;
      text-decoration: none;
      cursor: pointer;
    }
    .jq-aria .ui-tabs-active {
      background: #1d1b16;
    }
    .jq-aria .ui-tabs-active a {
      color: #fff;
    }
    .jq-aria__nav a:focus-visible,
    .jq-aria .ui-tabs-tab:focus-visible {
      outline: 3px solid #d14b16;
      outline-offset: -5px;
    }
    .jq-aria__panel {
      padding: 14px 16px;
    }
    .jq-aria__panel p {
      margin: 0;
      font-size: 15px;
    }
    .jq-aria__h {
      margin: 0 0 4px;
      color: #1d1b16;
      font-family: inherit;
      font-size: 16px;
      letter-spacing: 0;
    }
    .jq-aria__tabs.ui-tabs .jq-aria__h {
      display: none;
    }
    .jq-aria__log {
      display: grid;
      gap: 6px;
      min-height: 40px;
      margin: 0;
      padding: 12px;
      border-radius: 10px;
      background: #f4f1ea;
      list-style: none;
      font-family: ui-monospace, Consolas, monospace;
      font-size: 12px;
      line-height: 1.5;
    }
    .jq-aria__log li {
      margin: 0;
      padding: 6px 8px;
      border-left: 3px solid transparent;
      border-radius: 4px;
      overflow-wrap: anywhere;
    }
    .jq-aria__log li.is-focused {
      border-left-color: #d14b16;
      background: #fff;
    }
    .jq-aria__log strong {
      display: block;
      color: #1d1b16;
    }
    .jq-aria__log span {
      color: #4a463c;
    }

    Tabs without jQuery UI

    Two smaller builds for comparison: tabs written in a few lines of plain jQuery, and the same component in vanilla JavaScript with no library at all.

    Tabs in plain jQuery, no jQuery UI

    • jQuery only
    • aria-selected
    • .toggle()

    This week

    1,284 visits

    Up 8% on last week

    This month

    5,903 visits

    Up 3% on last month

    This year

    61,450 visits

    Down 2% on last year

    Click a tab, or focus one and use the arrow keys, Home and End.

    About twenty lines of jQuery: the script adds the tab roles, flips aria-selected and tabindex, and shows one panel with .toggle(). Arrow keys, Home and End are included because role="tab" promises them. What it leaves out compared with jQuery UI: Ajax panels, disabled tabs, the activation delay and the options API.

    View code
    .jq-plain {
      width: min(100%, 440px);
      padding: 8px;
      border-radius: 18px;
      background: #0c4a75;
      color: #fff;
      font: inherit;
      box-sizing: border-box;
    }
    .jq-plain__nav {
      display: flex;
      gap: 4px;
      margin: 0;
      padding: 0;
      list-style: none;
    }
    .jq-plain__nav > li {
      flex: 1;
      margin: 0;
      padding: 0;
    }
    .jq-plain__nav a {
      display: block;
      padding: 10px 6px;
      border-radius: 12px;
      color: #d6e8f5;
      font-weight: 600;
      font-size: 14px;
      text-align: center;
      text-decoration: none;
      cursor: pointer;
    }
    .jq-plain__nav a:hover {
      color: #fff;
    }
    .jq-plain__nav a[aria-selected="true"] {
      background: #fff;
      color: #0c4a75;
    }
    .jq-plain__nav a:focus-visible {
      outline: 3px solid #ffd166;
      outline-offset: 1px;
    }
    .jq-plain__panel {
      padding: 22px 16px 16px;
      border-radius: 12px;
    }
    .jq-plain__panel:focus-visible {
      outline: 3px solid #ffd166;
      outline-offset: -3px;
    }
    .jq-plain__h {
      margin: 0 0 4px;
      color: #fff;
      font-family: inherit;
      font-size: 15px;
      letter-spacing: 0;
    }
    .jq-plain.is-ready .jq-plain__h {
      display: none;
    }
    .jq-plain__big {
      margin: 0;
      color: #fff;
      font-family: "Fraunces", Georgia, serif;
      font-size: 44px;
      font-weight: 600;
      line-height: 1.1;
    }
    .jq-plain__big span {
      color: #b9d6ea;
      font-family: inherit;
      font-size: 18px;
      font-weight: 400;
    }
    .jq-plain__delta {
      margin: 6px 0 0;
      color: #d6e8f5;
      font-size: 14px;
    }

    The same tabs in vanilla JavaScript

    • no library
    • hidden attribute
    • roving tabindex

    Espresso

    Dose
    18 g
    Yield
    36 g
    Time
    28 s

    Filter

    Dose
    15 g
    Water
    250 g
    Time
    3 min

    Cold brew

    Dose
    100 g
    Water
    1 l
    Time
    16 h

    Click a tab, or focus one and use the arrow keys, Home and End.

    The plain jQuery demo rewritten without any library, at roughly the same length. The hidden attribute replaces .toggle(), dataset and classList replace .attr() and .addClass(), and nothing waits for a script to load, so it works on pages that never enqueue jQuery.

    View code
    .jq-van {
      width: min(100%, 440px);
      border-top: 4px solid #1a1a1a;
      background: #fff;
      color: #1a1a1a;
      box-shadow: 0 10px 28px rgba(0, 0, 0, .08);
      font: inherit;
    }
    .jq-van__nav {
      display: flex;
      flex-wrap: wrap;
      margin: 0;
      padding: 0 14px;
      border-bottom: 1px solid #e6e6e6;
      list-style: none;
    }
    .jq-van__nav > li {
      margin: 0;
      padding: 0;
    }
    .jq-van__nav a {
      display: block;
      padding: 14px 12px 11px;
      border-bottom: 3px solid transparent;
      color: #555;
      font-weight: 600;
      font-size: 15px;
      text-decoration: none;
      cursor: pointer;
    }
    .jq-van__nav a:hover {
      color: #1a1a1a;
    }
    .jq-van__nav a[aria-selected="true"] {
      border-bottom-color: #c81e4b;
      color: #1a1a1a;
    }
    .jq-van__nav a:focus-visible {
      outline: 3px solid #c81e4b;
      outline-offset: -3px;
    }
    .jq-van__panel {
      padding: 18px;
    }
    .jq-van__panel:focus-visible {
      outline: 3px solid #c81e4b;
      outline-offset: -3px;
    }
    .jq-van__h {
      margin: 0 0 10px;
      color: #1a1a1a;
      font-family: inherit;
      font-size: 16px;
      letter-spacing: 0;
    }
    .jq-van.is-ready .jq-van__h {
      display: none;
    }
    .jq-van__recipe {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 8px;
      margin: 0;
    }
    .jq-van__recipe div {
      padding: 12px 10px;
      border: 1px solid #e6e6e6;
      text-align: center;
    }
    .jq-van__recipe dt {
      color: #666;
      font-size: 12px;
      font-weight: 600;
      letter-spacing: .04em;
      text-transform: uppercase;
    }
    .jq-van__recipe dd {
      margin: 4px 0 0;
      color: #1a1a1a;
      font-family: "Fraunces", Georgia, serif;
      font-size: 24px;
      font-weight: 600;
    }

    What Are jQuery Tabs?

    Tabs hide every content panel except one and put a row of clickable headers above them. Click a header and its panel takes over. In jQuery projects the word gets stretched across several different things, and tutorials mix them freely, so it pays to know which one a snippet targets before you start debugging.

    The official version is the jQuery UI Tabs widget, added in jQuery UI 1.0 and started with a single .tabs() call. Plenty of sites skip it and write a few click handlers that hide every panel and show the chosen one. Then there are small plugins such as jQuery-Tabs by jralph and jQuery EasyTabs, which avoid jQuery UI altogether.

    All of them rest on the same idea. Some plain JavaScript event handling toggles which panel is visible while the tab list stays in the DOM.

    Where they split is markup and styling. jQuery UI generates its own classes and ARIA attributes. EasyTabs describes itself as tabs without hidden styling, so the CSS is entirely yours.

    I use the jQuery UI widget as the reference for the rest of this article, since it ships with documented options, events and keyboard behavior. Products such as jQWidgets jqxTabs and W2UI tabs are separate widget sets with their own APIs, so their settings do not carry over.

    jQuery still runs on 64.4% of all websites (W3Techs, October 2026), so tab code written for it stays in circulation.

    jQuery UI is another story. It has been in maintenance mode since 2021, according to the jQuery UI blog, which means security fixes and compatibility updates. New features are not coming.

    How to Create jQuery Tabs with jQuery UI

    Setup from an empty page goes like this.

    1. Load jQuery, jQuery UI and a theme stylesheet. The official API example pulls jQuery 3.7.1 and jQuery UI 1.13.3 from code.jquery.com, and npm install jquery-ui@1.14.2 installs the 1.14.2 release.
    2. Write the tab list as a ul or ol, with each li holding an anchor that has a hash href.
    3. Add one panel element per tab, with an id equal to that anchor’s hash.
    4. Call .tabs() on the outer container once the DOM is ready.
    5. Reload and confirm the first panel is open, since active defaults to 0.

    The markup itself is plain HTML markup, nothing exotic. Tab titles go in list items as anchors, and a panel can be any valid element.

    <div id="tabs">
    <ul>
    <li><a href="#fragment-1">One</a></li>
    <li><a href="#fragment-2">Two</a></li>
    </ul>
    <div id="fragment-1"><p>First panel.</p></div>
    <div id="fragment-2"><p>Second panel.</p></div>
    </div>
    
    <script>
    $(function () {
      $("#tabs").tabs();
    });
    </script>

    The official docs say outright that the widget needs its functional CSS or it will not work. So the theme stylesheet isn’t optional decoration.

    By default the widget converts the first descendant list inside the container. Put the tab list before any other list, or override the _getList extension point.

    jQuery UI 1.14.2 was tested against jQuery 1.12.4, 2.2.4, 3.6.4, 3.7.1 and 4.0.0 (jQuery UI 1.14.2 release post, 2026). jQuery 4.0.0 shipped on 17 January 2026 and dropped support for IE 10 and older (jQuery team).

    Options That Control How jQuery Tabs Behave

    Every option can be set at initialization or changed later with .tabs("option", name, value). Defaults and accepted values are in the table.

    OptionDefaultAccepted valuesEffect
    active0integer, falseWhich panel is open
    classesui-corner classesobjectExtra classes per widget element
    collapsiblefalsebooleanLets the open panel close
    disabledfalseboolean, arrayDisables all tabs or listed indexes
    event“click”stringEvent that activates a tab
    heightStyle“content”auto, fill, contentHow panel height is set
    hidenullboolean, number, string, objectAnimation when a panel closes
    shownullboolean, number, string, objectAnimation when a panel opens

    Choosing the starting tab

    active takes a zero-based index, so active: 1 opens the second tab.

    A positive integer counts from the front, a negative one counts back from the last panel, and false collapses everything. That last value only works when collapsible is true. Put the two together and you get a widget that starts closed.

    Opening a tab from the URL hash

    The widget takes an index, not a hash, so a deep link needs a few lines of your own code.

    var hash = window.location.hash;
    var index = $("#tabs ul a").index($('#tabs ul a[href="' + hash + '"]'));
    
    $("#tabs").tabs({ active: index > -1 ? index : 0 });

    The fallback to 0 matters. A value of -1 would select the last panel, because negative numbers count backward.

    To keep the address bar in sync, call history.replaceState inside the activate callback with the new tab’s anchor href. That uses the History API and leaves the back button alone.

    Remembering the last active tab

    Write the new index to Web Storage inside the activate callback, then read it back and pass it to active at initialization.

    var saved = parseInt(sessionStorage.getItem("activeTab"), 10);
    
    $("#tabs").tabs({
      active: isNaN(saved) ? 0 : saved,
      activate: function (event, ui) {
        if (ui.newTab.length) {
          sessionStorage.setItem("activeTab", ui.newTab.index());
        }
      }
    });

    The length check covers collapsible tabs. When a panel collapses, ui.newTab is an empty jQuery object, and its index would come back as -1.

    Older tutorials use the cookie option for this. It was removed in jQuery UI 1.10 (jQuery UI 1.10 upgrade guide), so the storage code has to be yours.

    Switching on hover and animating the change

    Set event: "mouseover" and tabs activate on hover. Touch screens have no hover, so click is the safer default for public pages, and I’d stick with it.

    Animating the change goes through show and hide.

    • Both take a duration in milliseconds
    • A jQuery method name such as "slideDown" also works
    • A jQuery UI effect name such as "fold" needs the optional Effects Core
    • An object like { effect: "explode", duration: 1000 } sets effect, delay, duration and easing

    Leave out the effect inside an object and the widget falls back to fadeOut for hide and fadeIn for show.

    How to Load jQuery Tabs Content with Ajax

    Set a tab anchor’s href to a URL instead of a hash, and the widget loads that URL into the panel when the tab is activated.

    <li><a href="/panels/pricing.html">Pricing</a></li>

    The widget sends the request through jQuery.ajax, so it behaves like any standard Ajax request. A URL on another origin needs CORS headers from the server.

    beforeActivate fires first and can cancel the switch. beforeLoad comes next, just before the request, and exposes ui.tab, ui.panel, ui.jqXHR and ui.ajaxSettings. It can cancel the load too, though the panel still activates. Once the response fills the panel, load fires.

    The ajaxSettings object is editable in beforeLoad, but jQuery has already applied prefilters, processed data and chosen the request type. The official API documentation puts beforeLoad under the same limits as the beforeSend callback.

    Error handling is yours to write. The jqXHR object is a standard jQuery object, so .fail() catches HTTP errors.

    $("#tabs").tabs({
      beforeLoad: function (event, ui) {
        ui.jqXHR.fail(function () {
          ui.panel.html("This tab could not be loaded.");
        });
      }
    
    });

    Call .tabs("load", 1) to fetch a panel by index, or pass the href instead.

    While a request runs, the tab’s list item carries the ui-tabs-loading class. That class is the hook for a CSS spinner, because the spinner option from the 1.8 era was removed in jQuery UI 1.10.

    Events and Methods for Controlling jQuery Tabs from Code

    The Tabs widget fires 5 events and exposes 8 methods (official API documentation, jQuery UI 1.14). That is enough to switch tabs from code and react whenever one changes.

    create runs once when the widget is built, and ui.tab and ui.panel hold the active pair, or empty jQuery objects if the widget starts collapsed. beforeActivate runs before a switch and can cancel it with event.preventDefault(). Then there is activate, which runs after the switch and its animation finish. It does not fire for the initial tab, so use create for that.

    Each event works as an option callback or as a bound event, for example $(".selector").on("tabsactivate", fn). The ui object carries newTab, oldTab, newPanel and oldPanel.

    On the method side, start with option, which reads or sets any option, so .tabs("option", "active", 2) switches tabs from code. The rest are smaller.

    • disable and enable take an index or an href. The docs say the selected tab cannot be disabled, but the widget source does not block it, so check active first
    • load fetches a remote panel
    • refresh processes tabs added or removed in the DOM and recalculates panel height
    • instance returns the widget instance, or undefined if the element was never initialized
    • destroy restores the pre-init state, and widget returns the container

    To disable several tabs at once, set the disabled option to an array such as [1, 2, 3].

    Adding and removing tabs after jQuery UI 1.9

    The 1.8 API had add and remove methods. The 1.9 upgrade guide deprecated both and pointed to refresh, and the 1.10 guide lists them as removed.

    1.8 featureStatusUse instead
    add and remove methodsRemoved in 1.10Edit the DOM, then call refresh
    select methodRemoved in 1.10Set the active option
    select eventRemoved in 1.10beforeActivate event
    rotate methodRemoved in 1.9No built-in replacement

    A March 2011 post on the jQuery UI blog laid out a plan to support the old APIs in 1.9 and drop them in 2.0. The 1.9 upgrade guide moved the removal up to 1.10, so tutorials written for 1.8 break on any current version.

    Adding a tab now means inserting the list item and the panel yourself, then telling the widget.

    var id = "fragment-4";
    
      $("#tabs ul").append('<li><a href="#' + id + '">Four</a></li>');
      $("#tabs").append('<div id="' + id + '"><p>Fourth panel.</p></div>');
      $("#tabs").tabs("refresh");

    Removal is the same in reverse. Delete the li and its panel, then call refresh.

    Are jQuery Tabs Accessible?

    The widget handles the basics. It sets the tab roles, tracks selection state and moves focus with the arrow keys, but panel focus and vertical orientation are left to you, and the arrow keys also block page scrolling.

    According to the widget source (jquery-ui repository, main branch, October 2026), the list gets role="tablist", each li gets role="tab" and each panel gets role="tabpanel". Those ARIA attributes are linked through aria-controls and aria-labelledby.

    On its own the widget maintains aria-selected and aria-expanded on tabs and aria-hidden on panels. Disabled tabs get aria-disabled. Remote panels get aria-live="polite", plus aria-busy="true" while a request runs, and a roving tabindex keeps only the active tab in the Tab order.

    Against the W3C WAI-ARIA Authoring Practices Guide tabs pattern (2026), the keyboard model differs in a few spots.

    BehaviorjQuery UI TabsAPG tabs pattern
    Arrows, Home, EndMove focus, wrap, activate after 300 msMove focus, wrap, activation optional
    Up and DownAlso move focus and block page scrollingUnused in horizontal lists
    Space and EnterSpace activates, Enter activates or togglesEither key activates
    Vertical listsNo aria-orientation setSet aria-orientation="vertical"
    Panel in Tab orderNo tabindex on panelsAdd tabindex="0" if no focusable content

    The 300 ms delay restarts on every arrow press, so a tab that focus only passes through never activates. Hold Ctrl or Cmd with an arrow key to move focus without activating at all.

    The APG recommends automatic activation only when panels appear without noticeable latency, which usually means preloaded content. Ajax tabs break that condition, so the delay and the Ctrl route matter more there.

    Add tabindex="0" to any panel without focusable content. The widget never sets it, and the Bootstrap 5.3 documentation gives the same advice for its own tabs.

    How to Style jQuery Tabs

    The widget’s structural CSS and a theme stylesheet do most of the visual work, and your own overrides, keyed to the ui-tabs classes, handle the rest.

    The official API example loads the Smoothness theme, themes/smoothness/jquery-ui.css, from code.jquery.com. A custom build needs core.css, tabs.css and theme.css, the 3 files the widget source declares as its CSS dependencies.

    • ui-tabs marks the outer container, and ui-tabs-collapsible joins it when collapsible is on
    • The tab list carries ui-tabs-nav
    • Each list item gets ui-tabs-tab, with ui-tabs-active added to the open one
    • ui-tabs-anchor sits on the link inside each tab
    • Panels use ui-tabs-panel, and only the active one is visible

    The theme’s selectors are specific, so load your stylesheet after it and anchor overrides to an id.

    #tabs .ui-tabs-nav .ui-tabs-tab.ui-tabs-active {
      background: #0b5fff;
      border-color: #0b5fff;
    }
    
    #tabs .ui-tabs-nav .ui-tabs-tab.ui-tabs-active a {
      color: #fff;
    }

    ThemeRoller designs and downloads custom themes, and its gallery holds 25 ready-made ones, from Base and Smoothness to Vader (jQuery UI ThemeRoller, 2026). To hang your own class on the container without touching markup, pass classes: { "ui-tabs": "my-tabs" }.

    Vertical tabs

    Add ui-tabs-vertical and ui-helper-clearfix through the classes option. The CSS then floats the list left, clears each tab, and lets the panel fill the space beside it.

    .ui-tabs-vertical .ui-tabs-nav { float: left; width: 12em; }
    .ui-tabs-vertical .ui-tabs-nav .ui-tabs-tab { clear: left; width: 100%; margin: 0 0 .2em; }
    .ui-tabs-vertical .ui-tabs-panel { overflow: hidden; }

    The widget does not set the orientation for ARIA, so you do. Up and Down already move focus, which means the key handling needs no change.

    $("#tabs").tabs({
      classes: { "ui-tabs": "ui-tabs-vertical ui-helper-clearfix" }
    });
    
    $("#tabs > ul").attr("aria-orientation", "vertical");

    Responsive layout

    The widget has no responsive mode. None of its 8 options changes the layout by viewport.

    Panel height is the one value it computes. With heightStyle set to “auto” or “fill”, pixel heights are fixed at creation, so call .tabs("refresh") after a resize. Switching the option back to “content” later does not clear those heights, so reset the panels’ inline height first.

    Below a breakpoint, a media query can stack the tab list into a column.

    @media (max-width: 600px) {
      #tabs .ui-tabs-nav .ui-tabs-tab { float: none; margin: 0; }
      #tabs .ui-tabs-nav .ui-tabs-anchor { float: none; display: block; }
    }

    A tab row that fits at 1200 px rarely fits at 360 px, which is a responsive design problem the widget leaves to you. Plugins such as Responsive Tabs and Easy responsive tabs to accordion switch to an accordion at a CSS breakpoint instead. Both are listed on plugins.jquery.com with releases from 2013 and 2014, and the site now points visitors to npm, where Responsive Tabs has newer releases.

    Why jQuery Tabs Stop Working

    Most of what breaks here is documented somewhere, either in the widget source or in the upgrade guides and jQuery 4 release notes. The table maps each symptom to its fix.

    CauseSymptomFix
    Stylesheet missingWidget does not behave as tabsLoad the theme, or core.css and tabs.css
    Anchor matches no panelMismatching fragment identifier errorMatch the hash to a panel id inside the container
    Base element on the pageIn-page tab fires an Ajax requestRemove it, or write the full page URL plus hash
    Code from the 1.8 eraRemoved methods error, removed options are ignoredSwitch to refresh and active
    jQuery 4 upgradeTypeError from a removed jQuery functionReplace calls with native equivalents

    The mismatching fragment error, jQuery UI Tabs: Mismatching fragment identifier., is thrown when neither the clicked tab’s panel nor the currently open panel can be found (widget source, October 2026). The widget looks for panels inside the container only, so a panel placed outside it counts as missing.

    A base element on the page changes how #tab-1 resolves. The widget treats an anchor as an in-page tab only when its origin, pathname and query string match the current page, so the tab ends up handled as remote.

    The sources also disagree about the active option. The API documentation lists a default of 0 and an index value, while the widget source defaults it to null and, at creation, opens the tab whose panel id matches location.hash.

    Passing active yourself switches that detection off, and the source has no hashchange handler, so the check runs once. Set active only when there is no hash to honor.

    var saved = parseInt(sessionStorage.getItem("activeTab"), 10);
    var options = {};
    
    if (!window.location.hash && !isNaN(saved)) {
      options.active = saved;
    }
    
    $("#tabs").tabs(options);

    An out-of-range active value, such as a stored index for a tab that no longer exists, falls back to the first tab at creation. With collapsible on, it falls back to a collapsed widget instead.

    If the console reports that jQuery is not defined, the init script ran before jQuery loaded or jQuery never loaded. Load order is jQuery, then jQuery UI, then your code.

    A wrong script or stylesheet path appears in Chrome DevTools as a failed to load resource message in the Console and a red row in the Network tab.

    jQuery 4.0.0 removed jQuery.isArray, jQuery.parseJSON, jQuery.trim, jQuery.type, jQuery.now, jQuery.isNumeric, jQuery.isFunction and jQuery.isWindow, among others (jQuery team, 2026). Any plugin or init code that still calls them throws, so update jQuery UI together with jQuery.

    jQuery Migrate restores removed core APIs and, in its development build, logs console warnings that name the calls. Load it after jQuery and pair Migrate 4.0.2 with jQuery 4.0.0, and a site running anything older should move to jQuery 3.x first (jQuery Migrate README, 2026).

    Migrate patches jQuery core only, so removed Tabs methods stay removed.

    Should You Use jQuery UI Tabs or Another Approach?

    Use the jQuery UI Tabs widget when jQuery UI is already on the page. Loading jQuery, jQuery UI and a theme stylesheet for a single tab strip is hard to justify.

    ApproachDependenciesStylingBest fit
    jQuery UI Tabs widgetjQuery, jQuery UI, theme CSSTheme plus overridesAjax tabs, built-in ARIA and keyboard
    jQuery-Tabs pluginjQuery onlyYour own CSSPlain show-and-hide tabs
    jQuery EasyTabsjQuery, plus a hashchange or Address plugin for back-button supportYour own CSSBookmarkable, auto-cycling tabs
    Bootstrap 5 tabsBootstrap JS, no jQueryBootstrap CSSSites already on Bootstrap
    Vanilla JavaScript or CSS-onlyNoneYour own CSSPages without jQuery

    The jQuery UI widget brings Ajax, ARIA and keyboard behavior with it, at the price of 3 assets to load. jQuery-Tabs is the thinnest jQuery route, though its README leaves Ajax loading and sorting to your own jQuery. EasyTabs adds bookmarkable tabs and interval cycling, and no styling at all, so every rule is yours.

    Bootstrap 5 tabs need no jQuery. The roles, ARIA attributes and panel tabindex are yours to write, though, and dropdowns inside tabs are unsupported (Bootstrap 5.3 docs). Going vanilla JavaScript or CSS-only drops dependencies entirely and hands you every keyboard and ARIA behavior to build.

    On a Bootstrap 5 site, the framework’s own tab component is the natural pick. Its documented API is plain JavaScript, for example new bootstrap.Tab(el).

    For a static page with 3 panels, CSS-only tabs need no script. They give up the widget’s keyboard handling.

    GitHub Engineering’s 2018 write-up on removing jQuery from github.com listed a 30 kB dependency among its reasons. The team also built features as plain HTML first, with JavaScript added as progressive enhancement.

    jQuery Tabs FAQ

    Should I use tabs or an accordion?

    Tabs work for 3 to 6 parallel sections that visitors compare on a wide screen. Longer lists and narrow screens are accordion territory.

    Both jQuery UI widgets keep one open panel at a time, through a single active value in their API documentation (2026).

    Can jQuery tabs rotate automatically?

    Not out of the box, but a timer does the job. Run a setInterval timer that raises the active option by one and wraps to 0 after the last tab, and pause it while a tab has focus or hover.

    jQuery EasyTabs includes interval cycling if you want it built in.

    Is content inside jQuery tabs indexed by Google?

    Yes for in-page panels, because they sit in the HTML at load and Google indexes the rendered HTML (Google Search Central, 2026).

    Ajax panels differ, since the widget fetches them when their tab activates. Keep anything you need indexed in in-page tabs.

    Can you nest jQuery tabs or place them inside an accordion?

    Yes. Call the widget on each container separately and keep every panel id unique, because the widget finds its panels by id inside its own container.

    Accordion panels accept arbitrary markup per the jQuery UI API documentation, so a tab set fits inside one.

    How do you use jQuery UI tabs in WordPress?

    Enqueue the jquery-ui-tabs handle, which core registers with jquery-ui-core as its dependency, and that in turn depends on jQuery. WordPress 6.8 and 6.9 bundle jQuery UI 1.13.3 on jQuery 3.7.1 and load the UI files in the footer. WordPress loads jQuery in noConflict mode, so write jQuery(function ($) { ... }) instead of calling $ directly.

    Core ships no stylesheet for the tabs widget, so the widget still needs a theme stylesheet or your own CSS, like the demos above. To skip code entirely, adding tabs in WordPress with a block or plugin works too.

    A Safe Upgrade Order for Legacy jQuery Tabs

    Repair the jQuery UI Tabs calls first. Move jQuery to 3.x after that, and leave jQuery 4 for last. Removed Tabs methods throw errors that stop a script before jQuery Migrate can log anything useful, which is why the order matters.

    • Replace removed Tabs calls with refresh and the active option
    • Move jQuery to 3.x and clear every Migrate 3.x warning
    • Test jQuery 4.0.0 with Migrate 4.0.2, fixing each logged call

    The price is 3 releases instead of 1, which buys a rollback point after each stage.

    As of October 2026 and the jQuery 4.0.0 notes, jQuery 5.0 is the announced next stage of IE support removal (jQuery team, 2026), and it would add a fourth round. Run each stage through cross-browser compatibility checks before it ships.