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/tabs

Examples

Default

Open alone
Los Santos, FAST’in ana şehri.
Bu haftanın sıralaması.

API

<Tabs />Takes the props of what it wraps

Radix's tabs, sounding `tab` on a change — by click or by the arrow keys, which move the selection without a click.

<TabsList />
PropTypeDefaultNotes
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

Source

components/ds/tabs.tsxShow
'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 };