List With Actions
Overview
The List With Actions component extends the basic list functionality by adding contextual action buttons (like Edit, Delete, View) to each list item. Actions are intelligently revealed on hover or focus on desktop devices, while remaining always visible on mobile for better touch interaction.
Preview
- Preview
- Code
- Design landing page
- Implement authentication
- Review pull requests
import { ListWithActions } from './components/ui/list-with-actions';
function MyComponent() {
const actions = [
{ id: 'view', label: 'View', onClick: (index) => console.log('View', index) },
{ id: 'edit', label: 'Edit', onClick: (index) => console.log('Edit', index) },
{ id: 'delete', label: 'Delete', onClick: (index) => console.log('Delete', index) },
];
return (
<ListWithActions
items={['Item 1', 'Item 2', 'Item 3']}
actions={actions}
spacing="md"
/>
);
}
Installation
- CLI
- Manual
ignix add component list-with-actions
import React, { useCallback, useMemo } from "react";
import { cn } from "../../../utils/cn";
import { ListBasic, type ListBasicProps } from "../list-basic";
export interface ListAction {
id: string;
label: React.ReactNode;
ariaLabel?: string;
onClick: (index: number) => void;
}
export interface ListWithActionsProps {
items: React.ReactNode[];
actions: ListAction[];
type?: ListBasicProps["type"];
spacing?: "sm" | "md" | "lg";
className?: string;
itemClassName?: string;
}
const ListWithActionsComponent: React.FC<ListWithActionsProps> = ({
items,
actions,
type,
spacing = "md",
className,
itemClassName,
}) => {
const listClassName = useMemo(
() => cn("w-full", "text-foreground", "list-none", className),
[className]
);
const rowClasses = useMemo(
() =>
cn(
"flex items-center justify-between gap-3 rounded-md border border-border/60 bg-background px-3 py-2",
"transition-colors",
"hover:bg-muted/60 focus-within:bg-muted/60",
"group",
itemClassName
),
[itemClassName]
);
const createActionClickHandler = useCallback(
(action: ListAction, index: number) => () => {
action.onClick(index);
},
[]
);
return (
<ListBasic type={type} spacing={spacing} className={listClassName}>
{items.map((item, index) => (
<li key={index} className={rowClasses} tabIndex={0}>
<div className={cn("flex-1 min-w-0", type && "flex items-center gap-2")}>
{type && (
<span className="w-6 shrink-0 text-xs font-medium text-muted-foreground text-right">
{type === "ordered" ? `${index + 1}.` : "•"}
</span>
)}
<div className="flex-1 min-w-0">{item}</div>
</div>
<div
className={cn(
"flex items-center gap-1",
"hidden group-hover:flex group-focus-within:flex",
"sm:flex"
)}
>
{actions.map((action) => (
<button
key={action.id}
type="button"
className="inline-flex items-center justify-center rounded-md px-2 py-1 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
aria-label={action.ariaLabel}
onClick={createActionClickHandler(action, index)}
>
{action.label}
</button>
))}
</div>
</li>
))}
</ListBasic>
);
};
export const ListWithActions = React.memo(ListWithActionsComponent);
Usage
Import the component:
import { ListWithActions } from './components/ui/list-with-actions';
Basic Usage
The simplest way to use ListWithActions is with the items and actions props:
function BasicList() {
const actions = [
{ id: 'view', label: 'View', onClick: (index) => console.log('View', index) },
{ id: 'edit', label: 'Edit', onClick: (index) => console.log('Edit', index) },
{ id: 'delete', label: 'Delete', onClick: (index) => console.log('Delete', index) },
];
return (
<ListWithActions
items={['Item 1', 'Item 2', 'Item 3']}
actions={actions}
/>
);
}
With Ordered List
To show numbered markers, pass the type prop:
function OrderedList() {
const actions = [
{ id: 'edit', label: 'Edit', onClick: (index) => {} },
{ id: 'delete', label: 'Delete', onClick: (index) => {} },
];
return (
<ListWithActions
type="ordered"
items={['Step 1', 'Step 2', 'Step 3']}
actions={actions}
/>
);
}
Without Markers
If you don't want bullets or numbers, simply omit the type prop:
function NoMarkers() {
return (
<ListWithActions
items={['Task 1', 'Task 2', 'Task 3']}
actions={actions}
/>
);
}
Custom Spacing
Control the spacing between items:
function CustomSpacing() {
return (
<ListWithActions
items={['Item 1', 'Item 2', 'Item 3']}
actions={actions}
spacing="lg" // Options: 'sm', 'md', 'lg'
/>
);
}
Rich Content Items
You can pass complex JSX as items:
function RichContent() {
const items = [
(
<div className="flex flex-col">
<span className="font-medium">Analytics Dashboard</span>
<span className="text-xs text-muted-foreground">Last updated 2 hours ago</span>
</div>
),
(
<div className="flex flex-col">
<span className="font-medium">Marketing Campaign</span>
<span className="text-xs text-muted-foreground">Running · Ends in 3 days</span>
</div>
),
];
return (
<ListWithActions
items={items}
actions={actions}
spacing="lg"
/>
);
}
Interactive Demo
- Preview
- Code
💡 Hover over items or use Tab to focus them to see the action buttons. On mobile, actions are always visible.
const actions = [
{ id: 'view', label: 'View', onClick: (index) => console.log('View', index) },
{ id: 'edit', label: 'Edit', onClick: (index) => console.log('Edit', index) },
{ id: 'delete', label: 'Delete', onClick: (index) => console.log('Delete', index) },
];
<ListWithActions
items={undefined}
actions={actions}
type="unordered"
spacing="md"
/>
Variants
Actions Behavior
Actions appear on hover/focus on desktop and are always visible on mobile:
Actions Behavior
- Preview
- Code
- Task 1
- Task 2
- Task 3
Hover over items or press Tab to focus them. Actions appear on hover/focus on desktop and are always visible on mobile.
<ListWithActions
items={['Item 1', 'Item 2', 'Item 3']}
actions={actions}
/>
Ordered List
Use numbered markers for sequential items:
Ordered List
- Preview
- Code
- 1.Step 1: Gather requirements
- 2.Step 2: Design the solution
- 3.Step 3: Implement features
<ListWithActions
type="ordered"
items={[
'Step 1: Gather requirements',
'Step 2: Design the solution',
'Step 3: Implement features',
]}
actions={actions}
/>
Rich Content
Display complex content in list items:
Rich Content
- Preview
- Code
- Analytics DashboardLast updated 2 hours ago
- Marketing CampaignRunning · Ends in 3 days
- Billing IntegrationIn review · 4 comments
const items = [
<div className="flex flex-col">
<span className="font-medium">Analytics Dashboard</span>
<span className="text-xs text-muted-foreground">Last updated 2 hours ago</span>
</div>,
// ... more items
];
<ListWithActions
items={items}
actions={actions}
spacing="lg"
/>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | React.ReactNode[] | required | Array of items to display in the list. Can be strings or React nodes. |
actions | ListAction[] | required | Array of action definitions. Each action must have id, label, and onClick. |
type | 'unordered' | 'ordered' | undefined | Type of list marker. When provided, shows bullets (unordered) or numbers (ordered). When omitted, no markers are shown. |
spacing | 'sm' | 'md' | 'lg' | 'md' | Spacing size between list items. 'sm' (8px), 'md' (12px), 'lg' (16px). |
className | string | undefined | Additional CSS classes to apply to the list container. |
itemClassName | string | undefined | Additional CSS classes to apply to each list row. |
ListAction Interface
| Property | Type | Required | Description |
|---|---|---|---|
id | string | required | Unique identifier for the action (used as React key). |
label | React.ReactNode | required | Visible label or icon for the action button. |
ariaLabel | string | optional | Accessible label announced to screen readers. |
onClick | (index: number) => void | required | Callback fired when the action is clicked. Receives the item's index. |
Examples
Task Management
A common use case for managing tasks with actions:
const tasks = ['Design landing page', 'Implement auth', 'Write tests'];
const taskActions = [
{ id: 'complete', label: 'Complete', onClick: (i) => markComplete(i) },
{ id: 'edit', label: 'Edit', onClick: (i) => editTask(i) },
{ id: 'delete', label: 'Delete', onClick: (i) => deleteTask(i) },
];
<ListWithActions items={tasks} actions={taskActions} />
User List
Displaying users with management actions:
const users = ['John Doe', 'Jane Smith', 'Bob Johnson'];
const userActions = [
{ id: 'view', label: 'View Profile', onClick: (i) => viewUser(i) },
{ id: 'edit', label: 'Edit', onClick: (i) => editUser(i) },
{ id: 'delete', label: 'Remove', onClick: (i) => removeUser(i) },
];
<ListWithActions items={users} actions={userActions} spacing="md" />
Step-by-Step Guide
Using ordered list for instructions:
const steps = [
'Open the application',
'Navigate to Settings',
'Select your preferences',
'Click Save to apply changes',
];
const stepActions = [
{ id: 'view', label: 'View Details', onClick: (i) => showDetails(i) },
];
<ListWithActions
type="ordered"
items={steps}
actions={stepActions}
spacing="md"
/>
Accessibility
The ListWithActions component is designed with accessibility in mind:
- Keyboard navigation: Each row is focusable (
tabIndex={0}), allowing keyboard users to reveal actions by focusing on items - Screen reader support: Actions include
ariaLabelprops for better screen reader announcements - Focus management: Actions become visible when a row receives focus, ensuring keyboard users can access all functionality
- Semantic HTML: Uses proper list semantics (
<ul>or<ol>) inherited fromListBasic
Best Practices
-
Use memoized callbacks: To prevent unnecessary re-renders, memoize your action handlers with
useCallback:const handleEdit = useCallback((index: number) => {
editItem(index);
}, []);
const actions = [
{ id: 'edit', label: 'Edit', onClick: handleEdit },
]; -
Provide aria labels: Always include
ariaLabelfor icon-only actions:{ id: 'delete', label: <TrashIcon />, ariaLabel: 'Delete item', onClick: handleDelete } -
Keep actions concise: Limit the number of actions per item (3-4 max) to avoid cluttering the UI
-
Mobile-first: Remember that actions are always visible on mobile, so design them to be touch-friendly
-
Use appropriate spacing: Choose spacing based on content density:
sm: For compact lists or when space is limitedmd: For standard content (default)lg: For better readability with rich content
-
Consider type prop:
- Use
type="ordered"for sequential steps or ranked items - Use
type="unordered"for feature lists or options - Omit
typefor clean, marker-free lists
- Use
Performance
The component is optimized for performance:
- React.memo: Prevents re-renders when props haven't changed
- useMemo: Memoizes computed class names
- useCallback: Memoizes action click handlers
To maximize performance in your application, ensure your actions array and items array references remain stable between renders.