Game UI

Tile

An inventory or weapon-wheel slot, with the selection frame.

stablev0.3.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/tile

Examples

Default

Open alone

API

<Tile />

One slot of an inventory, a weapon wheel or a shop: a translucent square, its item centred, a colour rising from its foot, and a dark band with the label and count. The selected one takes the white frame and pops — the way GTA VI marks the slot under the stick.

PropTypeDefaultNotes
childrenReactNode—The item's icon or image.
countReactNode—A count or an ammo figure, right-aligned in the foot.
emptybooleanfalseAn empty slot: a faint plus where the item would be.
labelReactNode—A name or a slot label under the icon.
selectedbooleanfalseThe focused slot: the white frame and the glow at full strength.
tone"primary" | "none" | "pink" | "teal" | "purple" | "cyan"primary

Dependencies

npm

  • lucide-react@^0.469.0

Registry

  • @fastrp/utils

Source

components/ds/tile.tsxShow
import { Plus } from 'lucide-react';
import type * as React from 'react';
import { cn } from '@/lib/utils';

// @fast-ds [email protected]

/** The colour that rises from a tile's foot: a category, a rarity. */
const GLOW = {
  primary: 'from-primary/70',
  pink: 'from-pink/70',
  teal: 'from-teal/70',
  purple: 'from-purple/70',
  cyan: 'from-cyan/70',
  none: 'from-transparent',
} as const;

export type TileProps = Omit<React.ComponentProps<'button'>, 'children'> & {
  /** The item's icon or image. */
  children?: React.ReactNode;
  /** A name or a slot label under the icon. */
  label?: React.ReactNode;
  /** A count or an ammo figure, right-aligned in the foot. */
  count?: React.ReactNode;
  /** The focused slot: the white frame and the glow at full strength. */
  selected?: boolean;
  /** An empty slot: a faint plus where the item would be. */
  empty?: boolean;
  tone?: keyof typeof GLOW;
};

/**
 * One slot of an inventory, a weapon wheel or a shop: a translucent square, its item centred,
 * a colour rising from its foot, and a dark band with the label and count. The selected one
 * takes the white frame and pops — the way GTA VI marks the slot under the stick.
 */
function Tile({
  children,
  label,
  count,
  selected = false,
  empty = false,
  tone = 'primary',
  className,
  type = 'button',
  ...props
}: TileProps) {
  return (
    <button
      data-slot="tile"
      data-selected={selected}
      aria-pressed={selected}
      data-sfx="select"
      data-sfx-hover="navigate"
      type={type}
      className={cn(
        'group relative flex aspect-square w-full select-none flex-col overflow-hidden rounded-md border border-foreground/10 bg-foreground/[0.06] text-foreground transition-[background-color,box-shadow,transform] duration-fast hover:bg-foreground/10 active:scale-[0.97] focus-visible:outline-none focus-visible:select-frame data-[selected=true]:animate-pop data-[selected=true]:bg-foreground/[0.12] data-[selected=true]:select-frame',
        className,
      )}
      {...props}
    >
      <span
        aria-hidden
        className={cn(
          'pointer-events-none absolute inset-x-0 bottom-0 h-1/2 bg-gradient-to-t to-transparent opacity-0 transition-opacity duration-base group-hover:opacity-50 group-data-[selected=true]:opacity-100',
          GLOW[tone],
        )}
      />
      <span className="relative flex min-h-0 flex-1 items-center justify-center p-3 [&_img]:max-h-full [&_img]:object-contain [&_img]:[-webkit-user-drag:none] [&_svg]:size-9 [&_svg]:stroke-[1.5]">
        {empty ? <Plus className="text-foreground/30" /> : children}
      </span>
      {(label || count !== undefined) && (
        <span className="relative flex items-center justify-between gap-2 bg-ink/70 px-2.5 py-1.5 font-display text-label uppercase">
          <span className="truncate text-foreground/85">{label}</span>
          {count !== undefined && <span className="tabular-nums text-foreground">{count}</span>}
        </span>
      )}
    </button>
  );
}

export { Tile };