Tabs
A set of layered content sections where users can switch between them, displaying one at a time.
Tab 1
Tab 2
Tab 3
---import Card from '@bejamas/ui/components/Card.astro';import Tabs from '@bejamas/ui/components/Tabs.astro';---
<Tabs> <Tabs part="list" class="mb-4"> <Tabs part="trigger" aria-selected="true">All Posts</Tabs> <Tabs part="trigger">Company News</Tabs> <Tabs part="trigger">Changelog</Tabs> </Tabs> <Tabs part="content"> <Card><Card part="content">Tab 1</Card></Card> </Tabs> <Tabs part="content" hidden> <Card><Card part="content">Tab 2</Card></Card> </Tabs> <Tabs part="content" hidden> <Card><Card part="content">Tab 3</Card></Card> </Tabs></Tabs>Installation
Section titled “Installation” bunx bejamas add tabs npx bejamas add tabs pnpm dlx bejamas add tabs yarn dlx bejamas add tabs---/** * @component Tabs * @title Tabs * @description A set of layered content sections where users can switch between them, displaying one at a time. * @status stable * * @preview * <Tabs> * <Tabs part="list" class="mb-4"> * <Tabs part="trigger" aria-selected="true">All Posts</Tabs> * <Tabs part="trigger">Company News</Tabs> * <Tabs part="trigger">Changelog</Tabs> * </Tabs> * <Tabs part="content"> * <Card><Card part="content">Tab 1</Card></Card> * </Tabs> * <Tabs part="content" hidden> * <Card><Card part="content">Tab 2</Card></Card> * </Tabs> * <Tabs part="content" hidden> * <Card><Card part="content">Tab 3</Card></Card> * </Tabs> * </Tabs> */
import { cn } from "@bejamas/ui/lib/utils";import type { HTMLAttributes, HTMLTag } from "astro/types";import Button from "@bejamas/ui/components/Button.astro";
type TabsPart = "root" | "list" | "trigger" | "content";
type TabsRootProps = { part?: "root"; as?: HTMLTag; class?: string;} & HTMLAttributes<"div">;
type TabsListProps = { part: "list"; as?: HTMLTag; class?: string;} & HTMLAttributes<"div">;
type TabsTriggerProps = { part: "trigger"; as?: HTMLTag; class?: string;} & HTMLAttributes<"button">;
type TabsContentProps = { part: "content"; as?: HTMLTag; class?: string;} & HTMLAttributes<"div">;
type Props = | TabsRootProps | TabsListProps | TabsTriggerProps | TabsContentProps;
const { part: rawPart, as: rawTag, class: className = "", ...rest} = Astro.props as Props;
const part: TabsPart = (rawPart ?? "root") as TabsPart;---
{ part === "root" && (() => { const Tag = (rawTag as HTMLTag) ?? "div"; return ( <Tag {...rest} data-slot="tabs" data-tabs class={cn(className)}> <slot name="list" /> <slot /> </Tag> ); })()}
{ part === "list" && (() => { const Tag = (rawTag as HTMLTag) ?? "div"; return ( <Tag {...rest} slot="list" role="tablist" data-slot="tabs-list" class={cn("flex gap-2", className)} > <slot /> </Tag> ); })()}
{ part === "trigger" && (() => { const TriggerTag = (rawTag as HTMLTag) ?? undefined; if (TriggerTag) { return ( <TriggerTag {...rest} role="tab" data-slot="tabs-trigger" class={cn( "inline-flex items-center justify-center rounded-md px-3 py-1.5 text-sm transition-colors aria-selected:bg-primary aria-selected:text-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50", className, )} > <slot /> </TriggerTag> ); } return ( <Button role="tab" variant="ghost" size="sm" data-slot="tabs-trigger" class={cn( "aria-selected:bg-primary aria-selected:text-primary-foreground", className, )} {...(rest as any)} > <slot /> </Button> ); })()}
{ part === "content" && (() => { const Tag = (rawTag as HTMLTag) ?? "div"; return ( <Tag {...rest} role="tabpanel" data-slot="tabs-content" class={cn("", className)} > <slot /> </Tag> ); })()}
<script type="module"> (() => { const init = (container) => { const tabs = Array.from(container.querySelectorAll('[role="tab"]')); const panels = Array.from( container.querySelectorAll('[role="tabpanel"]'), ); if (!tabs.length || !panels.length) return;
const count = Math.min(tabs.length, panels.length);
// give stable IDs and link them const uid = container.id || "tabs-" + Math.random().toString(36).slice(2, 8); tabs.slice(0, count).forEach((tab, i) => { if (!tab.id) tab.id = `${uid}-tab-${i}`; if (!panels[i].id) panels[i].id = `${uid}-panel-${i}`; tab.setAttribute("aria-controls", panels[i].id); panels[i].setAttribute("aria-labelledby", tab.id); if (tab.tagName === "BUTTON" && !tab.hasAttribute("type")) tab.type = "button"; });
let active = tabs.findIndex( (t) => t.getAttribute("aria-selected") === "true", ); if (active < 0) active = 0;
const setActive = (i, focus = false) => { for (let j = 0; j < count; j++) { const on = j === i; tabs[j].setAttribute("aria-selected", on ? "true" : "false"); tabs[j].tabIndex = on ? 0 : -1; panels[j].hidden = !on; } if (focus) tabs[i].focus(); active = i; };
tabs.slice(0, count).forEach((tab, i) => { tab.addEventListener("click", (e) => { if (tab.tagName === "A" && tab.hash) e.preventDefault(); // avoid jumping setActive(i, false); }); tab.addEventListener("keydown", (e) => { if (e.key === "ArrowRight") { e.preventDefault(); setActive((active + 1) % count, true); } if (e.key === "ArrowLeft") { e.preventDefault(); setActive((active - 1 + count) % count, true); } }); });
setActive(active, false); };
document.querySelectorAll("[data-tabs]").forEach(init); })();</script>