Kosmesis
Components

Tabs

A tab panel with keyboard navigation, wrapping Morphos's Tabs primitive.

Installation

npx kosmesis add tabs
pnpm dlx kosmesis add tabs
yarn dlx kosmesis add tabs
bunx kosmesis add tabs

Install the following dependencies:

npm install @morphos/layout
pnpm add @morphos/layout
yarn add @morphos/layout
bun add @morphos/layout

Copy and paste the following code into your project.

tabs.tsx
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import { Tab as MorphosTab, TabList as MorphosTabList, TabPanel as MorphosTabPanel, Tabs as MorphosTabs,  type TabListProps as MorphosTabListProps,  type TabPanelProps as MorphosTabPanelProps,  type TabProps as MorphosTabProps,  type TabsProps as MorphosTabsProps } from "@morphos/layout";import { cn } from "@/lib/utils";export type TabsProps = MorphosTabsProps;/** * Extends (not wraps) Morphos's `Tabs` so `new Tabs({ defaultValue: "a" })` still yields a real * instance with `.selectedValue`/`.select()`/`.navigate()` — what `TabsList`/`Trigger`/`Content` * need via their `tabs` prop. */@Component()export class Tabs extends MorphosTabs {  render() {    return (      <div id={this.id} class={cn("flex flex-col gap-2", this.class)} data-orientation={this.orientation}>        {this.children}      </div>    );  }}export type TabsListProps = MorphosTabListProps;@Component()export class TabsList extends StatelessComponent<TabsListProps> {  render() {    const { class: cls, ...rest } = this.props;    return (      <MorphosTabList        class={cn(          "inline-flex h-9 w-fit items-center justify-center rounded-lg bg-muted p-[3px] text-muted-foreground",          cls,        )}        {...rest}      />    );  }}export type TabsTriggerProps = MorphosTabProps;@Component()export class TabsTrigger extends StatelessComponent<TabsTriggerProps> {  render() {    const { class: cls, ...rest } = this.props;    return (      <MorphosTab        class={cn(          "inline-flex flex-1 items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-2 py-1 text-sm font-medium text-foreground transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 data-selected:bg-background data-selected:shadow-sm dark:text-muted-foreground dark:data-selected:text-foreground dark:data-selected:border-input dark:data-selected:bg-input/30",          cls,        )}        {...rest}      />    );  }}export type TabsContentProps = MorphosTabPanelProps;@Component()export class TabsContent extends StatelessComponent<TabsContentProps> {  render() {    const { class: cls, ...rest } = this.props;    return <MorphosTabPanel class={cn("flex-1 outline-none", cls)} {...rest} />;  }}

Install the following dependencies:

npm install @morphos/layout @praxisjs/css
pnpm add @morphos/layout @praxisjs/css
yarn add @morphos/layout @praxisjs/css
bun add @morphos/layout @praxisjs/css

Copy and paste the following code into your project.

tabs.tsx
import { StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import { Tab as MorphosTab, TabList as MorphosTabList, TabPanel as MorphosTabPanel, Tabs as MorphosTabs,  type TabListProps as MorphosTabListProps,  type TabPanelProps as MorphosTabPanelProps,  type TabProps as MorphosTabProps } from "@morphos/layout";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class TabsStyles extends Stylesheet {  $root = this.css({ display: "flex", flexDirection: "column", gap: "0.5rem" });  $list = this.css({    display: "inline-flex",    height: "2.25rem",    width: "fit-content",    alignItems: "center",    justifyContent: "center",    borderRadius: "0.5rem",    backgroundColor: t.muted,    padding: "3px",    color: t.mutedForeground,  });  $trigger = this.css({    display: "inline-flex",    flex: "1 1 0%",    alignItems: "center",    justifyContent: "center",    gap: "0.375rem",    whiteSpace: "nowrap",    borderRadius: "0.375rem",    border: "1px solid transparent",    padding: "0.25rem 0.5rem",    fontSize: "0.875rem",    fontWeight: 500,    color: t.foreground,    transition: "color 120ms ease, box-shadow 120ms ease",  })    .focusVisible({ boxShadow: `0 0 0 3px color-mix(in oklab, ${t.ring} 50%, transparent)`, outline: "1px solid" })    .disabled({ pointerEvents: "none", opacity: 0.5 })    .on("&[data-disabled]", { pointerEvents: "none", opacity: 0.5 })    .on("&[data-selected]", { backgroundColor: t.background, boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)" });  $content = this.css({ flex: "1 1 0%", outline: "none" });}/** * Extends (not wraps) Morphos's `Tabs` so `new Tabs({ defaultValue: "a" })` still yields a real * instance with `.selectedValue`/`.select()`/`.navigate()` — what `TabsList`/`Trigger`/`Content` * need via their `tabs` prop. */@Component()export class Tabs extends MorphosTabs {  @Styled(TabsStyles) $s!: TabsStyles;  render() {    return (      <div id={this.id} class={cx(this.$s.$root, this.class)} data-orientation={this.orientation}>        {this.children}      </div>    );  }}export type TabsListProps = MorphosTabListProps;@Component()export class TabsList extends StatelessComponent<TabsListProps> {  @Styled(TabsStyles) $s!: TabsStyles;  render() {    const { class: cls, ...rest } = this.props;    return <MorphosTabList class={cx(this.$s.$list, cls)} {...rest} />;  }}export type TabsTriggerProps = MorphosTabProps;@Component()export class TabsTrigger extends StatelessComponent<TabsTriggerProps> {  @Styled(TabsStyles) $s!: TabsStyles;  render() {    const { class: cls, ...rest } = this.props;    return <MorphosTab class={cx(this.$s.$trigger, cls)} {...rest} />;  }}export type TabsContentProps = MorphosTabPanelProps;@Component()export class TabsContent extends StatelessComponent<TabsContentProps> {  @Styled(TabsStyles) $s!: TabsStyles;  render() {    const { class: cls, ...rest } = this.props;    return <MorphosTabPanel class={cx(this.$s.$content, cls)} {...rest} />;  }}

Examples

Usage

import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";

@Component()
class AccountSettings extends StatefulComponent {
  @State() tabs = new Tabs({ defaultValue: "account" });

  render() {
    return (
      <Tabs defaultValue="account" class="w-96">
        <TabsList tabs={this.tabs} aria-label="Settings">
          <TabsTrigger tabs={this.tabs} value="account">Account</TabsTrigger>
          <TabsTrigger tabs={this.tabs} value="password">Password</TabsTrigger>
        </TabsList>
        <TabsContent tabs={this.tabs} value="account">Account settings.</TabsContent>
        <TabsContent tabs={this.tabs} value="password">Password settings.</TabsContent>
      </Tabs>
    );
  }
}

<Tabs> mounted in the tree and this.tabs are two separate objects, kept in sync via matching props (defaultValue) — see the same note on Radio Group for why. TabsList, TabsTrigger, and TabsContent all read/drive selection through this.tabs.

Props

Tabs

PropTypeDefault
valuestring
defaultValuestring
orientation"horizontal" | "vertical""horizontal"
onValueChange(value: string) => void

TabsList

PropTypeDefault
tabsTabs— required
aria-labelstring
aria-labelledbystring

TabsTrigger

PropTypeDefault
tabsTabs— required
valuestring— required
disabledboolean

TabsContent

PropTypeDefault
tabsTabs— required
valuestring— required

On this page