Info List
alphav0.2.0Card-framed list of icon-prefixed rows — primary and secondary text, optional actions, per-row links, comfortable and compact variants.
Context
Use for sidebar info cards on detail pages — Event Details, Contact, Address, Account Settings, Product Specs, Restaurant Info, Listing Attributes. Comfortable variant: stacked primary/secondary/action rows with separators (kasder Etkinlik Bilgileri pattern). Compact variant: inline single-line rows, no separators (kasder İletişim pattern). Migration origin: kasder kas-social-front-v0 events/[id]/page.tsx Etkinlik Bilgileri + İletişim sidebar cards.
Installation
pnpm dlx shadcn@latest initpnpm dlx shadcn@latest add @ilinxa/info-listAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/info-list-fixturesCLI can't resolve @ilinxa? The namespace is listed in the official shadcn registry directory, so current CLIs need no configuration. If yours can't resolve it (older or pinned versions, self-hosted mirrors), register it manually in components.json:
"registries": {
"@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}Preview
Etkinlik Bilgileri
31 Mayıs 2026 Pazar
09:00 - 18:00
İstanbul Kongre Merkezi
Darülbedai Caddesi No:3, 34367 Harbiye/Şişli/İstanbul
Haritada Gör500 Kişilik Kapasite
423 kişi kayıtlı
Demo source
"use client"; import { CircleUser } from "lucide-react";import { Tabs, TabsContent, TabsTrigger } from "@/components/ui/tabs";import { SwipeTabsList } from "@/components/site/swipe-tabs-list";import { InfoList } from "./info-list";import { dummyContactItems, dummyContactItemsTr, dummyEventDetails, dummyEventDetailsTr,} from "./dummy-data";import type { InfoListItem } from "./types"; const peopleItems = [ { id: "p1", name: "Prof. Dr. Ahmet Yılmaz", role: "Şehir Plancısı", image: "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=80&h=80&fit=crop&crop=faces", }, { id: "p2", name: "Dr. Elif Kaya", role: "Çevre Mühendisi", image: "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=80&h=80&fit=crop&crop=faces", }, { id: "p3", name: "Mehmet Demir", role: "Akıllı Şehir Uzmanı", image: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=80&h=80&fit=crop&crop=faces", },]; const peopleItemsAsInfoListItems: InfoListItem[] = peopleItems.map((p) => ({ id: p.id, icon: CircleUser, primary: p.name, secondary: p.role,})); export default function InfoListDemo() { return ( <Tabs defaultValue="default" className="w-full"> <SwipeTabsList> <TabsTrigger value="default">Default (TR)</TabsTrigger> <TabsTrigger value="compact">Compact (TR)</TabsTrigger> <TabsTrigger value="stacked">Both stacked</TabsTrigger> <TabsTrigger value="bare">Bare</TabsTrigger> <TabsTrigger value="custom">Custom renderItem</TabsTrigger> </SwipeTabsList> {/* 1. Default — kasder Etkinlik Bilgileri verbatim */} <TabsContent value="default" className="mt-6"> <div className="max-w-md mx-auto"> <InfoList heading="Etkinlik Bilgileri" variant="comfortable" items={dummyEventDetailsTr} /> </div> </TabsContent> {/* 2. Compact — kasder İletişim verbatim */} <TabsContent value="compact" className="mt-6"> <div className="max-w-md mx-auto"> <InfoList heading="İletişim" variant="compact" items={dummyContactItemsTr} /> </div> </TabsContent> {/* 3. Both stacked — full sidebar shape */} <TabsContent value="stacked" className="mt-6"> <div className="max-w-md mx-auto space-y-6"> <InfoList heading="Etkinlik Bilgileri" variant="comfortable" items={dummyEventDetailsTr} /> <InfoList heading="İletişim" variant="compact" items={dummyContactItemsTr} /> </div> </TabsContent> {/* 4. Bare — framed=false for embedded use */} <TabsContent value="bare" className="mt-6"> <div className="max-w-md mx-auto rounded-2xl border border-border/50 bg-card p-6 space-y-6"> <div> <h2 className="text-xl font-bold mb-4">Listing Details</h2> <InfoList framed={false} variant="comfortable" items={dummyEventDetails} /> </div> <div> <h2 className="text-xl font-bold mb-4">Contact</h2> <InfoList framed={false} variant="compact" items={dummyContactItems} /> </div> </div> </TabsContent> {/* 5. Custom renderItem — speakers with avatars */} <TabsContent value="custom" className="mt-6"> <div className="max-w-md mx-auto space-y-2"> <p className="text-sm text-muted-foreground"> <code>renderItem(item)</code> — full row takeover. Here: avatar replaces the icon column for a speaker list. </p> <InfoList heading="Konuşmacılar" variant="comfortable" items={peopleItemsAsInfoListItems} renderItem={(item) => { const person = peopleItems.find((p) => p.id === item.id); return ( <div className="flex items-center gap-3"> <img src={person?.image} alt={person?.name ?? ""} loading="lazy" className="w-12 h-12 rounded-full object-cover shrink-0 border-2 border-primary/20" /> <div className="min-w-0 flex-1"> <p className="font-medium text-foreground"> {item.primary} </p> <p className="text-sm text-muted-foreground"> {item.secondary} </p> </div> </div> ); }} /> </div> </TabsContent> </Tabs> );} Usage
When to use
Reach for InfoList for sidebar info cards on detail pages — Event Details, Contact Info, Address, Account Settings, Product Specs, Restaurant Info, Listing Attributes. Two visual flavors: comfortable (stacked primary / secondary / action with separators between rows) and compact(inline single-line rows, no separators).
Minimal example
import { InfoList } from "@/components/info-list";
import { Calendar, MapPin, Users } from "lucide-react";
<InfoList
heading="Event Details"
variant="comfortable"
items={[
{ id: "date", icon: Calendar, primary: "May 31, 2026", secondary: "09:00 - 18:00" },
{ id: "loc", icon: MapPin, primary: "Istanbul Conference Center", secondary: "Şişli/Istanbul" },
{ id: "cap", icon: Users, primary: "500-person capacity", secondary: "423 registered" },
]}
/>;Two variants
comfortable— stacked content (primary bold + secondary muted + optional action). Iconw-5 h-5 text-primary. Separators between rows by default. Use for sidebar info cards with rich per-row content.compact— inline single-line rows. Iconw-4 h-4 text-muted-foreground. No separators by default. Use for contact lists, footer columns, dense info summaries. Compact ignoressecondaryandaction— they imply a richer row than the variant supports.
Linked rows
Pass href per item to wrap primary in the polymorphic linkComponent. Consumer passes full URLs — no auto-prefixing for tel: / mailto:.
<InfoList
variant="compact"
items={[
{ id: "phone", icon: Phone, primary: "+90 (212) 555 0123", href: "tel:+902125550123" },
{ id: "email", icon: Mail, primary: "info@example.com", href: "mailto:info@example.com" },
]}
/>Action slot
Comfortable rows can render an actionReactNode below the secondary line — typically a link button (e.g. "View on map"):
{
id: "location",
icon: MapPin,
primary: event.location,
secondary: event.address,
action: (
<Button variant="link" className="px-0 h-auto">
View on map <ExternalLink className="w-3 h-3" />
</Button>
),
}Polymorphic linkComponent
import NextLink from "next/link";
<InfoList
variant="compact"
linkComponent={NextLink}
items={[
{ id: "profile", icon: User, primary: "View profile", href: "/profile" },
]}
/>Custom renderItem
Bypass the default row layout entirely with renderItem(item) — useful for swapping icon for an avatar, embedding a status badge, or any non-icon row anatomy:
<InfoList
items={items}
renderItem={(item) => (
<div className="flex items-center gap-3">
<Avatar src={people[item.id].image} />
<div>
<p className="font-medium">{item.primary}</p>
<p className="text-sm text-muted-foreground">{item.secondary}</p>
</div>
</div>
)}
/>Frame + separators
framed: true(default) — card chrome (bg-card rounded-2xl p-6 border). Passframed={false}for embedded use inside an existing card.separated— auto-defaults per variant: comfortable=true, compact=false. CSS top-border on each row except the first; override by passing the prop.
Empty state
Empty items array → renders emptyState if provided, else <p role="status"> with labels.emptyText(default "No information.").
Notes
- Renders
<ul role="list">; sectionaria-labelledbyresolves to the heading id (viauseId) when heading is supplied. - Decorative icons are
aria-hidden="true"; link's accessible name is the visibleprimarytext. - Heading defaults to
h3(sidebar info cards typically nest under pageh2). Bump viaheadingAs="h2"when standalone.
Features
- Two variants — comfortable (stacked, separators, w-5 primary icon) and compact (inline, no separators, w-4 muted icon)
- Required icon + primary; optional secondary + action + href per item
- Polymorphic per-row link via linkComponent (default 'a') — works with tel: / mailto: / etc.
- Frame toggle (framed=true card chrome / framed=false bare)
- Separator toggle (auto-defaults per variant; overridable)
- Custom renderItem slot for full row takeover
- Optional section heading with configurable level (h2/h3/h4)
- Soft-failure on optional fields
- Empty state slot + labels.emptyText fallback
- i18n via labels object
- <ul role='list'> semantics + section aria-labelledby
- Compact variant intentionally ignores secondary + action (documented limitation)