Components
Tabs
A tab panel with keyboard navigation, wrapping Morphos's Tabs primitive.
Installation
npx kosmesis add tabspnpm dlx kosmesis add tabsyarn dlx kosmesis add tabsbunx kosmesis add tabsInstall the following dependencies:
npm install @morphos/layoutpnpm add @morphos/layoutyarn add @morphos/layoutbun add @morphos/layoutCopy and paste the following code into your project.
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/csspnpm add @morphos/layout @praxisjs/cssyarn add @morphos/layout @praxisjs/cssbun add @morphos/layout @praxisjs/cssCopy and paste the following code into your project.
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
| Prop | Type | Default |
|---|---|---|
value | string | — |
defaultValue | string | — |
orientation | "horizontal" | "vertical" | "horizontal" |
onValueChange | (value: string) => void | — |
TabsList
| Prop | Type | Default |
|---|---|---|
tabs | Tabs | — required |
aria-label | string | — |
aria-labelledby | string | — |
TabsTrigger
| Prop | Type | Default |
|---|---|---|
tabs | Tabs | — required |
value | string | — required |
disabled | boolean | — |
TabsContent
| Prop | Type | Default |
|---|---|---|
tabs | Tabs | — required |
value | string | — required |