Component
Tabs
Line tabs for sections, segmented tabs for switching a view in place.
stablev0.4.0 Chromium 103 ui-scale safe
Install
Copies the file into
components/ds/, with its registry dependencies and npm packages. See Installation for the one-time setup.bunx shadcn@3 add @fastrp/tabsExamples
Default
Los Santos, FAST’in ana şehri.
QE
Bu haftanın sıralaması.
API
<Tabs />Takes the props of what it wrapsRadix's tabs, sounding `tab` on a change — by click or by the arrow keys, which move the selection without a click.
<TabsList />| Prop | Type | Default | Notes |
|---|---|---|---|
| hints | { prev: string; next: string; } | — | Keys shown at either end, as a game's bumpers: `{ prev: 'Q', next: 'E' }`. Labels only. |
| variant | "menu" | "line" | "game" | game | `game`: a dark well with the active tab lit white — the weapon wheel's WEAPONS | ITEMS. `menu`: separate italic blocks over the world, the active one white — an in-game menu's sections, as `MenuScreen` places them. `line`: labels on a hairline, the active one underlined in the accent — sections of a page. |
Dependencies
npm
- @radix-ui/react-tabs@^1.1.2
Registry
- @fastrp/key-hint
- @fastrp/ui-sounds
- @fastrp/utils
Source
components/ds/tabs.tsxShowHide
'use client';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import type * as React from 'react';
import { Kbd } from '@/components/ds/key-hint';
import { useUiSound } from '@/components/ds/ui-sounds';
import { cn } from '@/lib/utils';
// @fast-ds [email protected]
/**
* Radix's tabs, sounding `tab` on a change — by click or by the arrow keys, which move the
* selection without a click.
*/
function Tabs({ onValueChange, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {
const sound = useUiSound();
return (
<TabsPrimitive.Root
data-slot="tabs"
onValueChange={(value) => {
sound('tab');
onValueChange?.(value);
}}
{...props}
/>
);
}
export type TabsListProps = React.ComponentProps<typeof TabsPrimitive.List> & {
/**
* `game`: a dark well with the active tab lit white — the weapon wheel's WEAPONS | ITEMS.
* `menu`: separate italic blocks over the world, the active one white — an in-game menu's
* sections, as `MenuScreen` places them.
* `line`: labels on a hairline, the active one underlined in the accent — sections of a page.
*/
variant?: 'game' | 'menu' | 'line';
/** Keys shown at either end, as a game's bumpers: `{ prev: 'Q', next: 'E' }`. Labels only. */
hints?: { prev: string; next: string };
};
function TabsList({ className, variant = 'game', hints, children, ...props }: TabsListProps) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
data-variant={variant}
className={cn(
'group/tabs inline-flex w-fit items-center text-muted-foreground',
variant === 'game' &&
'gap-1 rounded-lg border border-foreground/10 bg-ink/60 p-1 shadow-[inset_0_1px_2px_rgb(0_0_0/0.35)]',
variant === 'menu' && 'gap-menu-gap',
variant === 'line' && 'gap-6 border-b border-foreground/10',
className,
)}
{...props}
>
{hints && <Bumper variant={variant} label={hints.prev} />}
{children}
{hints && <Bumper variant={variant} label={hints.next} />}
</TabsPrimitive.List>
);
}
/** The key at either end of a tab row. In a menu it stands in its own block, as a bumper does. */
function Bumper({ variant, label }: { variant: TabsListProps['variant']; label: string }) {
if (variant === 'menu') {
return (
<span aria-hidden className="menu-block flex h-11 w-11 items-center justify-center">
<Kbd variant="solid">{label}</Kbd>
</span>
);
}
return (
<Kbd size="sm" className="mx-1" aria-hidden>
{label}
</Kbd>
);
}
function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
'relative inline-flex select-none items-center justify-center gap-2 whitespace-nowrap font-display text-base tracking-[0.04em] transition-[color,background-color,box-shadow,transform] duration-fast active:scale-[0.97] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-selection disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-foreground [&_svg]:size-4 [&_svg]:shrink-0',
// game: the active tab lights up white, dark type — the menu's own selection
'group-data-[variant=game]/tabs:h-8 group-data-[variant=game]/tabs:rounded-md group-data-[variant=game]/tabs:px-4 group-data-[variant=game]/tabs:data-[state=active]:bg-foreground group-data-[variant=game]/tabs:data-[state=active]:text-background group-data-[variant=game]/tabs:data-[state=active]:shadow-[0_0.25rem_1rem_-0.25rem_hsl(var(--foreground)/0.4)]',
// menu: an italic block of its own; the active one white
'group-data-[variant=menu]/tabs:menu-block group-data-[variant=menu]/tabs:h-11 group-data-[variant=menu]/tabs:px-5 group-data-[variant=menu]/tabs:font-menu group-data-[variant=menu]/tabs:text-menu-tab group-data-[variant=menu]/tabs:text-foreground group-data-[variant=menu]/tabs:text-hud group-data-[variant=menu]/tabs:hover:bg-[hsl(var(--menu-block)/0.92)] group-data-[variant=menu]/tabs:data-[state=active]:bg-foreground group-data-[variant=menu]/tabs:data-[state=active]:text-background group-data-[variant=menu]/tabs:data-[state=active]:[text-shadow:none]',
// line: an accent bar over the list's hairline
'group-data-[variant=line]/tabs:-mb-px group-data-[variant=line]/tabs:border-b-2 group-data-[variant=line]/tabs:border-transparent group-data-[variant=line]/tabs:pb-2.5 group-data-[variant=line]/tabs:pt-1 group-data-[variant=line]/tabs:data-[state=active]:border-primary',
className,
)}
{...props}
/>
);
}
function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn('mt-5 focus-visible:outline-none data-[state=active]:animate-rise', className)}
{...props}
/>
);
}
export { Tabs, TabsContent, TabsList, TabsTrigger };