Skip to content

ListGroup

Data Display

Semantic lists for resource links, single-choice actions, and static information.

Single-choice settingsPlain resource linksDisabled itemEmpty list
Open preview

Responsive viewport — resize the window to explore. Direction and density apply only to this preview. Theme applies to the docs too.

Usage

Provide unique ids and descriptive labels. Items with href render links; items without href render action buttons when onSelect is supplied, or static content otherwise. The component does not mix link navigation with selection callbacks. Own selectedId when using choices, and render only noninteractive content in leading/trailing slots to avoid nested controls.

Import
import { ListGroup } from "@/components/ui/list-group";

Shipped as editable source in src/components/ui/list-group.tsx in both apps. Copy the implementation below when moving it to another project. Copy the complete example from Source code for all imports and state. Add "use client" when using hooks or event handlers. See installation and theming for project setup.

Component source

The complete implementation is included in both apps. It uses the React, Radix, Lucide, and shared-core dependencies already installed in Apex. Keep your project’s semantic theme tokens when reusing it.

src/components/ui/list-group.tsx
import type { ReactNode } from "react";
import { ChevronRight } from "lucide-react";
import { cn } from "@dashboardpack/core/lib/utils";
export interface ListGroupItem {
  id: string;
  label: string;
  description?: string;
  leading?: ReactNode;
  trailing?: ReactNode;
  href?: string;
  disabled?: boolean;
}
export interface ListGroupProps {
  label: string;
  items: ListGroupItem[];
  selectedId?: string;
  onSelect?: (id: string) => void;
  variant?: "bordered" | "plain";
  className?: string;
}
export function ListGroup({
  label,
  items,
  selectedId,
  onSelect,
  variant = "bordered",
  className,
}: ListGroupProps) {
  return (
    <ul
      aria-label={label}
      className={cn(
        "min-w-0 divide-y overflow-hidden",
        variant === "bordered" && "rounded-xl border",
        className,
      )}
    >
      {items.length === 0 && (
        <li className="p-4 text-sm text-muted-foreground">
          No items available.
        </li>
      )}
      {items.map((item) => {
        const content = (
          <>
            {item.leading && (
              <span aria-hidden className="shrink-0">
                {item.leading}
              </span>
            )}
            <span className="min-w-0 flex-1">
              <span className="block text-sm font-medium">{item.label}</span>
              {item.description && (
                <span className="mt-1 block text-xs text-muted-foreground">
                  {item.description}
                </span>
              )}
            </span>
            {item.trailing}
            {item.href && (
              <ChevronRight
                aria-hidden
                className="size-4 shrink-0 rtl:rotate-180"
              />
            )}
          </>
        );
        const style = cn(
          "flex w-full items-center gap-3 p-4 text-start outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
          item.disabled
            ? "opacity-50"
            : (item.href || onSelect) && "hover:bg-muted/50",
          selectedId === item.id && "bg-primary/5",
        );
        return (
          <li key={item.id}>
            {item.href && !item.disabled ? (
              <a
                className={style}
                href={item.href}
                aria-current={selectedId === item.id ? "page" : undefined}
              >
                {content}
              </a>
            ) : onSelect && !item.href ? (
              <button
                type="button"
                className={style}
                disabled={item.disabled}
                aria-pressed={selectedId === item.id}
                onClick={() => onSelect(item.id)}
              >
                {content}
              </button>
            ) : (
              <div className={style} aria-disabled={item.disabled || undefined}>
                {content}
              </div>
            )}
          </li>
        );
      })}
    </ul>
  );
}

API reference

Common props and defaults. Slot-specific props are named below; native elements also accept their standard HTML attributes. The component TypeScript definitions are the complete type reference.

ListGroup props
PropType / DefaultDescription
labelstring

Default: required

Accessible name for the list.
itemsListGroupItem[]

Default: required

Unique id, label, optional description, leading/trailing content, href, and disabled.
selectedIdstring

Default: —

Controlled selected action or current link id.
onSelect(id: string) => void

Default: —

Enable action buttons for items without href.
variantbordered | plain

Default: bordered

Outer border treatment.
classNamestring

Default: —

Extend the component container with theme-aware utility classes.

Build with these too