Bruno DelorenceBruno
Appearance
Appearance

Choose your reading theme.

Contact

Item card

New

A game inventory’s detail card: name, rarity stars, stat and effect chips and an italic description, re-entering with a stagger as the cursor moves.

item-card/item-card.tsx
Live · interactive

Tempered Longsword

  • Attack 38, equipped 22
  • Critical Hit

Forged twice over in a mountain kiln, its edge holds through a long campaign. Strikes that land after a perfect dodge cut far deeper.

Inventory

The live component, numbered part by part. Every measurement is read from it as it is drawn. Point at a row to outline its part, or at a part to find its row.

When to use

  • The detail pane of a game inventory: the card for whatever the cursor rests on, swapped as fast as the cursor moves.
  • Answering “is this worth carrying?” at a glance: the one number that matters for its kind, against what is equipped.
  • Items whose effects need a word or a timer: a trait such as Durability Up, or a meal’s boost with its level and mm:ss.
  • A shop, chest or loot screen that shows one item at a time beside a grid or list of slots.
  • Flavour text the player reads only when they linger, under the numbers they read first.

When not to use

  • Browsing many items at once to compare. Use an item grid, with this card beside it for the one in focus.
  • A short note on something pointed at in the world or a HUD. Use a tooltip or an action prompt.
  • Buying and selling, where the price and the player’s money lead. Use a shop list.
  • A full character sheet with every stat. Use a stats panel or a menu tab.
  • A product card outside a game. Use a plain card; this plate, the stars and the chips speak the language of a game menu.

You can add this component with the shadcn CLI or by hand:

Into components/item-card of any React project, with no setup:

Terminal
npx shadcn@latest add https://brunodelorence.com/r/item-card.json

Either way it needs React 19 and a bundler that supports CSS Modules, such as Next.js or Vite. It has a dark colour scheme: it follows your page’s color-scheme, light unless the page sets one, or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.

Usage

item-detail.tsx
import { ItemCard, type Item } from './item-card';const ITEMS: Item[] = [  {    name: 'Tempered Longsword',    kind: 'weapon',    stars: 3,    value: 38,    compare: 22,    effects: [{ icon: 'critical', label: 'Critical Hit' }],    description: 'Forged twice over in a mountain kiln, its edge holds through a long campaign.',  },  {    name: 'Spiced Mountain Skewer',    kind: 'food',    hearts: 6,    effects: [{ icon: 'attack', level: 2, duration: 1800 }],    description: 'Grilled meat and fiery peppers on a stick.',  },];export function ItemDetail({ hovered }: { hovered: string }) {  return <ItemCard bare items={ITEMS} selected={hovered} />;}

Each state below is a live instance, locked so you can study it.

The pieces it is built from, each with its measurements read live.

The card a game’s inventory shows for whatever the cursor rests on: the name in bold italic, one to three stars on its top edge, the number that matters for its kind (attack, defence, or hearts for food), the equipped value it would replace, effect chips with coloured icons (Durability Up, Climb Speed Up, a meal’s timed boost with its hourglass) and a short italic description. Each new item re-enters row by row: the name slides in, the rule draws, the stars pop, the chips rise, an upgrade flashes. Pick an item from the slots below, or use the arrow keys.

  • An item is data: a name, a kind (weapon, shield, armour, food, material), 0–3 stars, a value and the equipped value to compare, hearts for food, a list of effects and a description. The kind picks the white stat icon beside the value: a sword for attack, a shield or a tunic for defence.
  • Every size is in em of a 16 px card, the kit’s own px divided by 16: a 498 × 132 plate, a 23 px bold italic name, a 1 px rule with a 2 px dark line beneath it, 20 px chips, 13 px italic text on a 14.7 px pitch in a 308 px column. A narrow stage draws it at 14 px through a container query, and --item-card-size sets it by hand.
  • The plate is opaque: a thin dark outline, a light hairline inside it, and a fill that stays dark under the text and lightens towards the right edge. The chips are darker still, lit from below by a lighter rim on their sides and foot. So the card reads the same on a light page or a dark one; only its shadow, its outer edge, the stars’ outline and the slots’ focus ring follow the colour scheme through light-dark() tokens.
  • An effect with a label is a coloured icon and words, green for a boost, white for a trait. A timed effect (a meal’s) repeats its icon once per level (one to three), then a green hourglass and mm:ss in tabular figures. Hearts draw up to five; past five, the first heart carries a small count. All icons are small flat SVGs in a 24-unit box, filled with currentColor; a thin dark line separates overlapping parts.
  • The content is keyed by the item, so a new item remounts it and replays its entry: the name slides in 0.35 em from the left (240 ms), the rule draws from the left (320 ms), the stars pop from 0.4 with an overshoot 55 ms apart, the chips rise 45 ms apart with their icons popping, and the description fades in last. Three stars get one glint across them. A value higher than the compared one nudges the arrow and flashes its box green. There is no exit: a new item replaces the old at once, as fast cursor moves demand.
  • On the stage, a row of inventory slots picks the item, the cursor a cream frame that snaps on with a small pop. While the card is on screen and untouched, the cursor walks the slots every 2.6 s; any click, tap or key hands it to the reader. bare draws the card alone, for the selected item.
PropTypeDefaultDescription
itemsItem[]a sword, a shield, a tunic, two meals and a flowerThe items, each { name, kind, description, stars?, value?, compare?, hearts?, effects?, id? }. kind is weapon, shield, armor, food or material. An effect is { icon, label?, level?, duration?, tone? }: icon is critical, durability, durability-plus, climb, attack, defense, stealth, speed, shock, heat, cold, flame, throw, heart, extra-heart or stamina; duration in seconds makes it a timer chip.
selectedstring—The id (or name) of the item shown, controlled: changing it replays the card’s entry. The demo is off.
defaultSelectedstringthe first itemUncontrolled, the item it starts on.
onSelectedChange(id: string, item: Item) => void—Called when a slot is picked by click, tap or the arrow keys, or by the demo.
barebooleanfalseThe card alone, without the slots and the stage’s room, to place in a menu of your own. Its size is 16 px, or --item-card-size.
autoplaybooleantrueWhile on screen and untouched, the cursor walks the slots every 2.6 s. Uncontrolled only.
colorScheme'light' | 'dark' | 'system'—Forces light or dark for the card’s shadow and edge, the stars’ outline and the slots’ ring; by default it follows the page. The plate, text and chips keep their colours.
classNamestring—Added to the root.

The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.

item-card.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import {  useCallback,  useEffect,  useId,  useRef,  useState,  type CSSProperties,  type KeyboardEvent,} from 'react';import {  CompareArrow,  EFFECT_TINT,  Hourglass,  Icon,  Star,  type EffectIconName,  type IconName,  type StatIconName,} from './icons';import { useAutoplay } from './use-autoplay';
icons.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.import type { ReactNode } from 'react';/* * The card's icons: flat, bold shapes in a 24-unit box. The stat icons (sword, shield, tunic) are * white; each effect has its own colour so it reads at a glance (blue gem for durability, green * slope for climbing, yellow bolt for shock...). Shapes are filled with currentColor, so a chip * tints its icon; the small up/down arrows that say "boost" or "resist" stay white. A thin dark * line (the card's own ink) separates overlapping parts, as on the slots' art. All decorative: * the words beside them say what they are. */const INK = '#2b2a27';/** A straight sword, point up, along the vertical axis of a 24 box (rotated by its caller). */const blade = (  <>    <path d="M12 1 14.6 4v10.6H9.4V4L12 1Z" />    <rect x="6.2" y="14.6" width="11.6" height="2.4" rx="0.6" />    <rect x="10.8" y="17" width="2.4" height="3.6" />    <circle cx="12" cy="21.6" r="1.7" />    <path d="M12 3.6v10" stroke={INK} strokeWidth="1" />  </>
use-autoplay.ts
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useCallback, useEffect, useRef, useState } from 'react';/* * When a demo may play on its own: while part of it is on screen and the tab is visible, never * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it * takes back over `idle` ms after their last input. */export function useAutoplay<T extends HTMLElement>(idle = 6000) {  const ref = useRef<T>(null);  const [visible, setVisible] = useState(false);  const [reduced, setReduced] = useState(true);  const [taken, setTaken] = useState(false);  const timer = useRef(0);  useEffect(() => {    const element = ref.current;    if (!element) return;    let onScreen = false;    const update = () => setVisible(onScreen && !document.hidden);    const observer = new IntersectionObserver(
item-card.module.css
/* Item card (item-card.tsx): an inventory's detail card, on its own dark plate, centred on a plain   stage with a row of inventory slots under it. Every size of the card is in em of a 16 px card   (the kit's sizes, px / 16), so a narrow stage draws it a little smaller (a container query) and   --item-card-size sets it by hand.   The plate, the text and the chips keep their colours on any page: a game draws this card over   its own dark screen, and the kit's plate is already dark. Only the plate's shadow, its outer   edge, the stars' outline (they sit on the plate's top edge, half outside) and the slots' focus   ring follow the colour scheme, through light-dark() tokens. No filter touches a shape.   Motion: the plate rises once; each item's content is keyed, so a new item replays its entry   keyframes, staggered by row (--i on the stars and chips). */.root {  /* The plate: fill (darker where the text is), outer edge, inner hairline, shadow. */  --plate: #33322f;  --plate-far: #57544f;  --edge: #43423e;  --hairline: #71706d;  --shade: rgb(30 28 22 / 0.22);  /* The text, the rule and its shadow. */  --text: #e9e1d1;  --rule: #7f7f7c;  --rule-shade: #1d1c1a;
  • The card is real text: the name is a heading, the chips a list, the description a paragraph. Each chip carries hidden words for its icons and boxes (“Attack 38, equipped 22”, “Restores 9 hearts”, “Attack up, for 30:00”); the stars are an image named “3 stars”; all art is aria-hidden.
  • The slots are a radio group named Inventory, each radio named by its item, with one tab stop: the arrow keys move and pick, Home and End jump to the ends, and the focus follows.
  • A polite live region reads the whole item (name, stars, stats, effects, description) when the reader picks one; the demo’s own moves are not announced.
  • Every control shows a :focus-visible ring.
  • Reduced motion: no slides, draws, pops, glints or flashes; the content crossfades in 120 ms. The demo never plays.