Card
A flexible card component for displaying content in a contained, organized format. Supports images, headers, footers, and various interactive states. Built for Pulse Framework with full reactivity support.
Import
import { Card, Pulse } from '@odyssee/components';Basic Usage
With Image
Add images to cards with flexible positioning.
Variants
The Card component supports multiple variants: default, bordered, and shadow.
Sizes
Three size options control padding: sm, md, and lg.
With Header and Footer
Add custom headers and footers to cards.
Horizontal Layout
Display cards in horizontal layout with image on the side.
Clickable Cards
Make entire cards clickable with hover effects.
const clickableCard = (
<Card
image="https://images.unsplash.com/photo-1680868543815-b8666dba60f7"
title="Clickable Card"
clickable={true}
href="/articles/123"
hoverEffect="shadow"
>
Click anywhere on this card to navigate
</Card>
);Hover Effects
Apply different hover effects to interactive cards.
Centered Content
Center align all content within the card.
Image Overlay
Display content over the image as an overlay.
Scrollable Content
Make card content scrollable with a fixed height.
const scrollableCard = (
<Card
title="Scrollable Content"
scrollable={true}
scrollHeight="h-64"
>
<p>Long content that will be scrollable...</p>
<p>More content...</p>
<p>Even more content...</p>
<p>This continues...</p>
</Card>
);Empty State
Display an empty state with optional icon and message.
With Actions
Add action buttons to card footers.
const cardWithActions = (
<Card
title="Article Title"
subtitle="Published 2 days ago"
>
<p>Article preview text goes here...</p>
<div class="mt-4 flex gap-2">
<Button variant="solid" color="primary" size="sm">
Read More
</Button>
<Button variant="outline" color="secondary" size="sm">
Share
</Button>
</div>
</Card>
);Reactive Content
Use Pulse signals to create dynamic cards.
import { Card, Button, Pulse } from '@odyssee/components';
const DynamicCard = () => {
const likes = Pulse.signal(42);
const isLiked = Pulse.signal(false);
const handleLike = () => {
if (isLiked()) {
likes(likes() - 1);
} else {
likes(likes() + 1);
}
isLiked(!isLiked());
};
return (
<Card
image="https://images.unsplash.com/photo-1680868543815-b8666dba60f7"
title="Interactive Card"
>
<p>Click the button to like this content</p>
<div class="mt-4 flex items-center gap-3">
<Button
variant={isLiked() ? 'solid' : 'outline'}
color="danger"
size="sm"
icon="❤️"
onClick={handleLike}
>
{likes()} Likes
</Button>
</div>
</Card>
);
};Complete Example
Here's a comprehensive example combining multiple features:
import { Card, Badge, Button, Avatar, Pulse } from '@odyssee/components';
const BlogPostCard = () => {
const post = Pulse.signal({
title: 'Getting Started with Pulse Framework',
excerpt: 'Learn how to build reactive applications with Pulse Framework...',
image: 'https://images.unsplash.com/photo-1680868543815-b8666dba60f7',
author: {
name: 'John Doe',
avatar: 'https://images.unsplash.com/photo-1568602471122-7832951cc4c5'
},
category: 'Tutorial',
readTime: '5 min read',
publishedAt: '2 days ago',
likes: 124,
comments: 18
});
const isBookmarked = Pulse.signal(false);
return (
<Card
image={post().image}
imageAlt={post().title}
hoverEffect="shadow"
clickable={true}
>
{/* Header with category badge */}
<div class="flex items-center justify-between mb-3">
<Badge variant="soft" color="primary">
{post().category}
</Badge>
<span class="text-xs text-gray-500">
{post().readTime}
</span>
</div>
{/* Title and excerpt */}
<h3 class="text-xl font-bold mb-2 text-gray-800 dark:text-white">
{post().title}
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-4">
{post().excerpt}
</p>
{/* Author info */}
<div class="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-gray-700">
<div class="flex items-center gap-2">
<Avatar
src={post().author.avatar}
alt={post().author.name}
size="sm"
/>
<div>
<p class="text-sm font-medium">{post().author.name}</p>
<p class="text-xs text-gray-500">{post().publishedAt}</p>
</div>
</div>
{/* Stats and actions */}
<div class="flex items-center gap-3">
<span class="text-sm text-gray-500">
❤️ {post().likes}
</span>
<span class="text-sm text-gray-500">
💬 {post().comments}
</span>
<Button
variant="ghost"
size="sm"
icon={isBookmarked() ? '🔖' : '📑'}
onClick={(e) => {
e.preventDefault();
isBookmarked(!isBookmarked());
}}
/>
</div>
</div>
</Card>
);
};Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | - | Card title |
subtitle | string | - | Card subtitle |
children | string | HTMLElement | Array | - | Card body content |
header | string | HTMLElement | - | Custom header content |
headerBordered | boolean | false | Add border to header |
footer | string | HTMLElement | - | Custom footer content |
footerBordered | boolean | false | Add border to footer |
image | string | - | Image URL |
imageAlt | string | "Card Image" | Image alt text |
imagePosition | "top" | "bottom" | "top" | Image position |
imageOverlay | boolean | false | Display content over image |
imageRounded | boolean | true | Round image corners |
variant | "default" | "bordered" | "shadow" | "default" | Card variant style |
size | "sm" | "md" | "lg" | "md" | Padding size |
horizontal | boolean | false | Horizontal layout |
centered | boolean | false | Center align content |
clickable | boolean | false | Make card clickable |
href | string | - | Link URL (makes card an anchor) |
onClick | (event: Event) => void | - | Click handler |
scrollable | boolean | false | Enable scrolling |
scrollHeight | string | "h-80" | Height for scrollable content |
emptyState | boolean | false | Show empty state |
emptyStateIcon | string | - | Empty state icon |
emptyStateText | string | "No data to show" | Empty state message |
hoverEffect | "none" | "shadow" | "scale" | "none" | Hover animation |
className | string | - | Additional CSS classes |
id | string | - | HTML id attribute |
style | string | CSSStyleDeclaration | - | Inline styles |
Accessibility
The Card component follows accessibility best practices:
- ✅ Semantic HTML structure
- ✅ Proper heading hierarchy
- ✅ Keyboard navigation for clickable cards
- ✅ Focus indicators
- ✅ Screen reader friendly content
- ✅ Proper alt text for images
Best Practices
✅ Do
- Use appropriate image sizes and optimize for web
- Provide meaningful titles and descriptions
- Use consistent card sizes in grids
- Add loading states for async content
- Include proper alt text for images
// Good: Well-structured card with all needed info
const goodCard = (
<Card
image="optimized-image.jpg"
imageAlt="Descriptive alt text"
title="Clear, Descriptive Title"
subtitle="Additional context"
>
Meaningful content that adds value
</Card>
);❌ Don't
- Don't use cards for everything (use appropriate components)
- Don't nest cards too deeply
- Don't forget to handle loading states
- Don't use very large unoptimized images
// Bad: Poor structure and no context
const badCard = (
<Card image="huge-image.png">
Text
</Card>
);Use Cases
Product Cards
const ProductCard = ({ product }) => (
<Card
image={product.image}
imageAlt={product.name}
clickable={true}
href={`/products/${product.id}`}
hoverEffect="shadow"
>
<div class="flex justify-between items-start mb-2">
<h3 class="font-bold">{product.name}</h3>
<Badge color="success">{product.discount}% OFF</Badge>
</div>
<p class="text-gray-600 text-sm mb-3">{product.description}</p>
<div class="flex items-center justify-between">
<div>
<span class="text-2xl font-bold">${product.price}</span>
<span class="text-sm text-gray-500 line-through ml-2">
${product.originalPrice}
</span>
</div>
<Button size="sm" color="primary">Add to Cart</Button>
</div>
</Card>
);User Profile Cards
const UserProfileCard = ({ user }) => (
<Card centered={true}>
<Avatar
src={user.avatar}
size="xl"
className="mb-4"
/>
<h3 class="text-xl font-bold mb-1">{user.name}</h3>
<p class="text-gray-500 mb-4">{user.title}</p>
<div class="flex gap-2">
<Badge variant="soft" color="primary">{user.followers} Followers</Badge>
<Badge variant="soft" color="success">{user.posts} Posts</Badge>
</div>
</Card>
);Dashboard Stats
const StatsCard = ({ title, value, change, icon }) => (
<Card size="sm">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">{title}</p>
<p class="text-2xl font-bold mt-1">{value}</p>
<p class={`text-sm mt-1 ${change > 0 ? 'text-green-600' : 'text-red-600'}`}>
{change > 0 ? '↑' : '↓'} {Math.abs(change)}%
</p>
</div>
<div class="text-4xl">{icon}</div>
</div>
</Card>
);Styling & Theming
All card styles use Tailwind CSS classes and support dark mode automatically.
Custom Styling
const customCard = (
<Card
className="border-l-4 border-blue-500 hover:border-blue-600 transition-colors"
title="Custom Styled Card"
>
Additional custom styling with Tailwind classes
</Card>
);TypeScript
Full TypeScript support with complete type definitions:
import type { CardProps } from '@odyssee/components';
const props: CardProps = {
title: 'TypeScript Card',
variant: 'shadow',
size: 'md',
onClick: (e: Event) => {
console.log('Card clicked!');
}
};
const card = <Card {...props}>Content</Card>;Related Components
- Avatar - Display user avatars in cards
- Badge - Add status badges to cards
- Button - Add action buttons to cards
Version: 1.0.0
Last Updated: January 2025