Navigation
Tabs.
Mono uppercase tabs with an orange underline on the active tab, plus a boxed variant for switching between panels.
Turtle 07 is running quarry.lua on a 16 by 16 area, 32 blocks deep.
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
export default function TabsDemo() {
return (
<Tabs defaultValue="program" className="w-full max-w-md">
<TabsList>
<TabsTrigger value="program">Program</TabsTrigger>
<TabsTrigger value="fuel">Fuel</TabsTrigger>
<TabsTrigger value="log">Log</TabsTrigger>
</TabsList>
<TabsContent value="program" className="text-sm leading-relaxed text-muted-foreground">
Turtle 07 is running <span className="font-mono text-foreground">quarry.lua</span> on a 16 by 16 area, 32
blocks deep.
</TabsContent>
<TabsContent value="fuel" className="text-sm leading-relaxed text-muted-foreground">
4,120 fuel left. The turtle returns to the chest when it drops below 500.
</TabsContent>
<TabsContent value="log" className="font-mono text-xs leading-relaxed text-[#d9b774]">
[12:04] layer 9 done
<br />
[12:06] inventory full, unloading
<br />
[12:07] back to work
</TabsContent>
</Tabs>
);
}Installation#
With the CLI
$npx lanterncn add tabsThis also installs the Lantern theme and any Lantern UI components it depends on. The shadcn CLI works directly too: npx shadcn@latest add httptim/lantern-ui/tabs. See Installation if your project is not set up yet.
Manually
Install the dependencies
npm install radix-ui class-variance-authorityCopy the source into your project
components/ui/tabs.tsx"use client"; import * as React from "react"; import { Tabs as TabsPrimitive } from "radix-ui"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; type TabsVariant = "line" | "boxed"; const TabsVariantContext = React.createContext<TabsVariant>("line"); function Tabs({ className, variant = "line", ...props }: React.ComponentProps<typeof TabsPrimitive.Root> & { variant?: TabsVariant }) { return ( <TabsVariantContext.Provider value={variant}> <TabsPrimitive.Root data-slot="tabs" data-variant={variant} className={cn("flex flex-col gap-4 data-[orientation=vertical]:flex-row", className)} {...props} /> </TabsVariantContext.Provider> ); } const tabsListVariants = cva("inline-flex max-w-full items-center overflow-x-auto [scrollbar-width:none]", { variants: { variant: { line: "w-full gap-6 border-b", boxed: "w-fit gap-1 rounded-md border bg-card p-1", }, }, defaultVariants: { variant: "line" }, }); function TabsList({ className, variant, ...props }: React.ComponentProps<typeof TabsPrimitive.List> & VariantProps<typeof tabsListVariants>) { const contextVariant = React.useContext(TabsVariantContext); return ( <TabsPrimitive.List data-slot="tabs-list" className={cn(tabsListVariants({ variant: variant ?? contextVariant }), className)} {...props} /> ); } const tabsTriggerVariants = cva( "inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 font-mono text-[11px] font-semibold tracking-[0.16em] whitespace-nowrap uppercase transition-colors outline-none disabled:pointer-events-none disabled:opacity-40 focus-visible:ring-2 focus-visible:ring-ring/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", { variants: { variant: { line: "relative -mb-px h-10 border-b-2 border-transparent px-0.5 text-muted-foreground hover:text-foreground data-[state=active]:border-primary data-[state=active]:text-foreground", boxed: "h-8 rounded-sm px-3 text-muted-foreground hover:text-foreground data-[state=active]:bg-secondary data-[state=active]:text-primary data-[state=active]:shadow-[inset_0_0_0_1px_var(--color-input)]", }, }, defaultVariants: { variant: "line" }, }, ); function TabsTrigger({ className, variant, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger> & VariantProps<typeof tabsTriggerVariants>) { const contextVariant = React.useContext(TabsVariantContext); return ( <TabsPrimitive.Trigger data-slot="tabs-trigger" className={cn(tabsTriggerVariants({ variant: variant ?? contextVariant }), className)} {...props} /> ); } function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) { return ( <TabsPrimitive.Content data-slot="tabs-content" className={cn("flex-1 rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/25", className)} {...props} /> ); } export { Tabs, TabsContent, TabsList, TabsTrigger, tabsListVariants, tabsTriggerVariants };Update the import paths to match your project
The source imports
cnfrom@/lib/utils.
Usage#
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";<Tabs defaultValue="program">
<TabsList>
<TabsTrigger value="program">Program</TabsTrigger>
<TabsTrigger value="log">Log</TabsTrigger>
</TabsList>
<TabsContent value="program">...</TabsContent>
<TabsContent value="log">...</TabsContent>
</Tabs>Set variant="boxed" on Tabs to get the segmented style. The list and triggers pick it up automatically.
The tab list scrolls sideways when there are more tabs than fit on a phone.
Examples#
Boxed
Hub details
The name and address shown in the directory.
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
export default function TabsBoxed() {
return (
<Tabs variant="boxed" defaultValue="hub" className="w-full max-w-sm">
<TabsList>
<TabsTrigger value="hub">Hub</TabsTrigger>
<TabsTrigger value="guestbook">Guestbook</TabsTrigger>
</TabsList>
<TabsContent value="hub">
<Card>
<CardHeader>
<CardTitle>Hub details</CardTitle>
<CardDescription>The name and address shown in the directory.</CardDescription>
</CardHeader>
<CardContent className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="tabs-hub-name">Name</Label>
<Input id="tabs-hub-name" defaultValue="Quarry hub" />
</div>
<div className="grid gap-2">
<Label htmlFor="tabs-hub-address">Address</Label>
<Input id="tabs-hub-address" defaultValue="quarry.hub" />
</div>
</CardContent>
<CardFooter>
<Button>Save hub</Button>
</CardFooter>
</Card>
</TabsContent>
<TabsContent value="guestbook">
<Card>
<CardHeader>
<CardTitle>Guestbook</CardTitle>
<CardDescription>Pin a welcome message above the signatures.</CardDescription>
</CardHeader>
<CardContent className="grid gap-2">
<Label htmlFor="tabs-guestbook">Welcome message</Label>
<Input id="tabs-guestbook" defaultValue="Sign in and say where you are from." />
</CardContent>
<CardFooter>
<Button>Save guestbook</Button>
</CardFooter>
</Card>
</TabsContent>
</Tabs>
);
}