Building Custom Cards
This guide walks through implementing custom cards in your application. For a conceptual overview of what cards are and when to use them, see Custom Cards.
Build with AI
Create custom cards for my actions
Tip: Use⌘.to enable Plan mode in Cursor for best results
# Create Pillar Custom Cards for My Actions
Look at my existing Pillar actions and create custom confirmation card components.
## What Custom Cards Do
Instead of a generic confirmation dialog, cards let you:
- Show rich, branded UI for confirmations
- Let users edit AI-extracted data before confirming
- Display previews of what will happen
- Use your existing design system components
## Your Task
### 1. Find My Existing Actions
Look for my Pillar actions definition (usually in lib/pillar/actions.ts or similar).
### 2. Identify Actions That Need Cards
Focus on actions that:
- Modify data (create, update, delete)
- Have multiple parameters the user might want to edit
- Would benefit from a preview (e.g., "Invite 3 members as Admin")
### 3. For Each Card, Generate:
```tsx
// components/cards/[ActionName]Card.tsx
import type { CardComponentProps } from '@pillar-ai/react';
import { useState } from 'react';
// Import my design system components
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
interface ActionData {
// Match the dataSchema from my action
}
export function ActionNameCard({
data,
onConfirm,
onCancel,
}: CardComponentProps<ActionData>) {
const [formData, setFormData] = useState(data);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleConfirm = async () => {
setLoading(true);
setError(null);
try {
// Call my API if needed
onConfirm(formData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<div className="...">
{/* Editable form fields */}
{/* Preview of action */}
{error && <p className="text-red-500">{error}</p>}
<div className="flex gap-2">
<Button variant="outline" onClick={onCancel} disabled={loading}>
Cancel
</Button>
<Button onClick={handleConfirm} disabled={loading}>
{loading ? 'Processing...' : 'Confirm'}
</Button>
</div>
</div>
);
}
```
### 4. Register Cards in Provider
```tsx
import { InviteMembersCard } from '@/components/cards/InviteMembersCard';
import { ConfirmDeleteCard } from '@/components/cards/ConfirmDeleteCard';
<PillarProvider
productKey="..."
publicKey="..."
cards={{
invite_members: InviteMembersCard,
delete_item: ConfirmDeleteCard,
// ... other cards
}}
>
```
## Start Here
1. Find my Pillar actions definition
2. Look at my design system (components/ui/ or similar)
3. Identify which actions would benefit from custom cards
4. Generate card components using my design patterns
How It Works
- Define an action with
type: 'inline_ui'and acard_type - Register a React component for that
card_type - When AI suggests the action, your component renders inline
- User interacts and confirms, triggering your callback
User: "Invite [email protected] as an admin"
↓
AI extracts email and role
↓
Your InviteMembersCard renders with the data
↓
User confirms → Your API is called
Quick Start
1. Create the Card Component
examples/guides/custom-cards/invite-members-card.tsx
// components/cards/InviteMembersCard.tsximport type { CardComponentProps } from '@pillar-ai/react';import { useState } from 'react';interface InviteData {emails: string[];role: 'admin' | 'member';}export function InviteMembersCard({data,onConfirm,onCancel,}: CardComponentProps<InviteData>) {const [emails, setEmails] = useState(data.emails);const [loading, setLoading] = useState(false);const handleConfirm = async () => {setLoading(true);try {await fetch('/api/invitations', {method: 'POST',body: JSON.stringify({ emails, role: data.role }),});onConfirm({ emails, role: data.role });} finally {setLoading(false);}};return (<div className="p-4 border rounded-lg"><h3 className="font-semibold mb-3">Invite Team Members</h3><div className="flex flex-wrap gap-2 mb-4">{emails.map(email => (<span key={email} className="px-2 py-1 bg-gray-100 rounded text-sm">{email}<buttononClick={() => setEmails(e => e.filter(x => x !== email))}className="ml-2">×</button></span>))}</div><div className="flex gap-2"><button onClick={onCancel} disabled={loading}>Cancel</button><button onClick={handleConfirm} disabled={loading}>{loading ? 'Sending...' : 'Send Invites'}</button></div></div>);}
2. Register the Card
examples/guides/custom-cards/register-card.tsx
import { PillarProvider } from '@pillar-ai/react';import { InviteMembersCard } from '@/components/cards/InviteMembersCard';<PillarProviderproductKey="..."publicKey="..."cards={{invite_members: InviteMembersCard,}}>
Component Props
examples/guides/custom-cards/card-component-props.ts
interface CardComponentProps<T> {/** Data extracted by the AI */data: T;/** Call when user confirms */onConfirm: (modifiedData?: Record<string, unknown>) => void;/** Call when user cancels */onCancel: () => void;/** Report state changes */onStateChange?: (state: 'loading' | 'success' | 'error', message?: string) => void;}
Default Fallback
If no card is registered for a card_type, Pillar shows a default confirmation UI with:
- Action name as title
- Data displayed as key-value pairs
- Confirm and Cancel buttons
Best Practices
Let Users Modify Data
The AI extracts data from messages but might make mistakes. Let users edit:
examples/guides/custom-cards/modify-data.tsx
const [emails, setEmails] = useState(data.emails);// User can add/remove emails before confirming
Show Loading States
examples/guides/custom-cards/loading-states.tsx
const [loading, setLoading] = useState(false);<button disabled={loading}>{loading ? 'Processing...' : 'Confirm'}</button>
Handle Errors
examples/guides/custom-cards/handle-errors.tsx
const [error, setError] = useState<string | null>(null);{error && (<div className="text-red-500 text-sm mt-2">{error}</div>)}
Use Your Design System
Cards render inside your app, so use your existing components:
examples/guides/custom-cards/design-system.tsx
import { Button } from '@/components/ui/button';import { Badge } from '@/components/ui/badge';
Example: Confirm Delete
examples/guides/custom-cards/confirm-delete-card.tsx
export function ConfirmDeleteCard({ data, onConfirm, onCancel }) {return (<div className="p-4 border border-red-200 rounded-lg bg-red-50"><h3 className="font-semibold text-red-900">Delete {data.itemName}?</h3><p className="text-sm text-red-700 mt-1">This action cannot be undone.</p><div className="flex gap-2 mt-4"><button onClick={onCancel}>Cancel</button><buttononClick={() => onConfirm()}className="bg-red-600 text-white">Delete</button></div></div>);}
TypeScript
examples/guides/custom-cards/typescript-example.ts
import type { CardComponentProps, CardComponent } from '@pillar-ai/react';interface MyData {items: string[];count: number;}const MyCard: CardComponent<MyData> = ({ data, onConfirm, onCancel }) => {// data is typed as MyDatareturn <div>...</div>;};