Skip to content
v1.0.3

Tabs

A horizontal section switcher — underline (line) or pill style, with optional icons and count badges. The React/Vue component is the tab strip only (controlled); you render the active panel yourself. In plain HTML, the behavior layer also toggles panels via aria-controls.

bash
jlds add tabs

Usage

html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/jarooda/jlds@main/registry/css/index.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/jarooda/jlds@main/registry/css/tabs.css">
<!-- behavior layer: tab selection + panel toggling + arrow keys -->
<script src="https://cdn.jsdelivr.net/gh/jarooda/jlds@main/registry/js/core.js" defer></script>
<script src="https://cdn.jsdelivr.net/gh/jarooda/jlds@main/registry/js/tabs.js" defer></script>

<div class="jl-tabs jl-tabs--line" role="tablist">
  <button type="button" role="tab" class="jl-tab" aria-selected="true" aria-controls="p-overview">Overview</button>
  <button type="button" role="tab" class="jl-tab" aria-selected="false" aria-controls="p-activity">Activity</button>
  <button type="button" role="tab" class="jl-tab" aria-selected="false" aria-controls="p-settings">Settings</button>
</div>
<div id="p-overview" role="tabpanel">Overview panel</div>
<div id="p-activity" role="tabpanel" hidden>Activity panel</div>
<div id="p-settings" role="tabpanel" hidden>Settings panel</div>
vue
<script setup lang="ts">
import { ref } from "vue"
import { Tabs } from "@/components/ui/tabs"

const tab = ref("overview")
</script>

<template>
  <Tabs
    v-model="tab"
    :items="[
      { value: 'overview', label: 'Overview' },
      { value: 'activity', label: 'Activity' },
      { value: 'settings', label: 'Settings' },
    ]"
  />
  <div v-if="tab === 'overview'">Overview panel</div>
  <div v-else-if="tab === 'activity'">Activity panel</div>
  <div v-else>Settings panel</div>
</template>
tsx
import { useState } from "react"
import { Tabs } from "@/components/ui/tabs"

const [tab, setTab] = useState("overview")

<Tabs
  value={tab}
  onChange={setTab}
  items={[
    { value: "overview", label: "Overview" },
    { value: "activity", label: "Activity" },
    { value: "settings", label: "Settings" },
  ]}
/>
{tab === "overview" && <div>Overview panel</div>}

Pill variant

variant="pill" for a segmented, background-filled style.

html
<div class="jl-tabs jl-tabs--pill" role="tablist">
  <button type="button" role="tab" class="jl-tab" aria-selected="true">All</button>
  <button type="button" role="tab" class="jl-tab" aria-selected="false">Active</button>
  <button type="button" role="tab" class="jl-tab" aria-selected="false">Archived</button>
</div>
vue
<template>
  <Tabs v-model="tab" variant="pill" :items="['All', 'Active', 'Archived']" />
</template>
tsx
<Tabs value={tab} onChange={setTab} variant="pill" items={["All", "Active", "Archived"]} />

Count badges

Pass count on an item for a trailing badge.

html
<button type="button" role="tab" class="jl-tab" aria-selected="true">
  Inbox <span class="jl-tab__count">12</span>
</button>
vue
<template>
  <Tabs
    v-model="tab"
    :items="[
      { value: 'inbox', label: 'Inbox', count: 12 },
      { value: 'sent', label: 'Sent' },
      { value: 'spam', label: 'Spam', count: 3 },
    ]"
  />
</template>
tsx
<Tabs
  value={tab}
  onChange={setTab}
  items={[
    { value: "inbox", label: "Inbox", count: 12 },
    { value: "sent", label: "Sent" },
    { value: "spam", label: "Spam", count: 3 },
  ]}
/>

Props

React

Tabs extends React.HTMLAttributes<HTMLDivElement> (minus onChange).

PropTypeDefaultDescription
items(string | { value, label, icon?, count? })[]The tabs
valuestringActive tab value (controlled)
onChange(value: string) => voidFires with the selected value
variant"line" | "pill""line"Underline or pill style

Vue

Same options. Use v-model for the value; also emits change.

CSS classes (HTML)

ClassPurpose
.jl-tabsThe strip (role="tablist")
.jl-tabs--line / --pillVariant
.jl-tabA tab (role="tab" + aria-selected; aria-controls pairs it with a panel)
.jl-tab__countTrailing count badge

In HTML, give each tab aria-controls="panelId" and each panel a matching id (+ role="tabpanel", hidden on inactive ones) — the behavior layer shows/hides them and handles arrow-key navigation.