Skip to main content

Overview

The CardContainer component is a lightweight wrapper that provides a consistent card-style container for grouping related content. It features optional click handling, making it perfect for both static content displays and interactive elements. Source: src/shared/ui/CardContainer/CardContainer.tsx

Basic Usage

import CardContainer from '@/shared/ui/CardContainer/CardContainer';

function Example() {
  return (
    <CardContainer>
      <h2>Card Title</h2>
      <p>Card content goes here</p>
    </CardContainer>
  );
}

Props

children
React.ReactNode
required
The content to display inside the card container.Can include any React elements: text, images, buttons, forms, or other components.
onClick
(...args: any[]) => void
Optional click handler function.When provided, the entire card becomes clickable. Useful for navigation, selection, or triggering actions.

Static Card

Basic card without click interaction:
import CardContainer from '@/shared/ui/CardContainer/CardContainer';
import Typography from '@/shared/ui/Typography/Typography';

<CardContainer>
  <Typography as="h3" weight="medium">
    Static Card
  </Typography>
  <Typography as="p" size="text-sm">
    This card displays information without any interaction.
  </Typography>
</CardContainer>

Clickable Card

Card with click handler for interactive use cases:
import CardContainer from '@/shared/ui/CardContainer/CardContainer';

function InteractiveCard() {
  const handleClick = () => {
    console.log('Card clicked!');
    // Navigate, open modal, etc.
  };
  
  return (
    <CardContainer onClick={handleClick}>
      <h3>Click me!</h3>
      <p>This entire card is clickable.</p>
    </CardContainer>
  );
}

Real-World Examples

Auction Item Card

import CardContainer from '@/shared/ui/CardContainer/CardContainer';
import Typography from '@/shared/ui/Typography/Typography';
import { Button } from '@/shared/ui/button/Button';

function AuctionItemCard({ item }) {
  return (
    <CardContainer>
      <img 
        src={item.imageUrl} 
        alt={item.title}
        style={{ width: '100%', borderRadius: '8px', marginBottom: '12px' }}
      />
      
      <Typography as="h3" weight="medium">
        {item.title}
      </Typography>
      
      <Typography as="p" size="text-sm" style={{ color: '#666', margin: '8px 0' }}>
        {item.description}
      </Typography>
      
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <div>
          <Typography as="p" size="text-xs" weight="light">
            Current Bid
          </Typography>
          <Typography as="p" weight="bold" style={{ fontSize: '20px', color: '#2563eb' }}>
            ${item.currentBid}
          </Typography>
        </div>
        
        <Button variant="primary" size="sm">
          Place Bid
        </Button>
      </div>
    </CardContainer>
  );
}

Clickable Category Card

import CardContainer from '@/shared/ui/CardContainer/CardContainer';
import Typography from '@/shared/ui/Typography/Typography';
import { useRouter } from 'next/router';
import { Package } from 'lucide-react';

function CategoryCard({ category }) {
  const router = useRouter();
  
  const handleClick = () => {
    router.push(`/auctions/category/${category.slug}`);
  };
  
  return (
    <CardContainer onClick={handleClick}>
      <div style={{ 
        display: 'flex', 
        alignItems: 'center', 
        gap: '12px',
        cursor: 'pointer'
      }}>
        <Package size={32} color="#2563eb" />
        
        <div>
          <Typography as="h4" weight="medium">
            {category.name}
          </Typography>
          <Typography as="p" size="text-xs" style={{ color: '#666' }}>
            {category.itemCount} active auctions
          </Typography>
        </div>
      </div>
    </CardContainer>
  );
}

Dashboard Stats Card

import CardContainer from '@/shared/ui/CardContainer/CardContainer';
import Typography from '@/shared/ui/Typography/Typography';
import { TrendingUp, TrendingDown } from 'lucide-react';

function StatsCard({ title, value, change, isPositive }) {
  return (
    <CardContainer>
      <Typography as="p" size="text-sm" weight="light" style={{ color: '#666' }}>
        {title}
      </Typography>
      
      <div style={{ display: 'flex', alignItems: 'baseline', gap: '8px', margin: '8px 0' }}>
        <Typography as="p" style={{ fontSize: '32px', fontWeight: 'bold' }}>
          {value}
        </Typography>
        
        <div style={{ 
          display: 'flex', 
          alignItems: 'center', 
          gap: '4px',
          color: isPositive ? '#22c55e' : '#ef4444'
        }}>
          {isPositive ? <TrendingUp size={16} /> : <TrendingDown size={16} />}
          <Typography as="p" size="text-sm" weight="medium">
            {change}%
          </Typography>
        </div>
      </div>
      
      <Typography as="p" size="text-xs" style={{ color: '#666' }}>
        vs. last month
      </Typography>
    </CardContainer>
  );
}

// Usage
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px' }}>
  <StatsCard title="Total Bids" value="1,234" change="12.5" isPositive={true} />
  <StatsCard title="Active Auctions" value="89" change="5.2" isPositive={true} />
  <StatsCard title="Won Auctions" value="23" change="-2.1" isPositive={false} />
</div>

Profile Card

import CardContainer from '@/shared/ui/CardContainer/CardContainer';
import Typography from '@/shared/ui/Typography/Typography';
import { Button } from '@/shared/ui/button/Button';
import { User, Mail, MapPin } from 'lucide-react';

function ProfileCard({ user }) {
  return (
    <CardContainer>
      <div style={{ display: 'flex', gap: '16px', marginBottom: '16px' }}>
        <img
          src={user.avatarUrl}
          alt={user.name}
          style={{ width: '64px', height: '64px', borderRadius: '50%' }}
        />
        
        <div>
          <Typography as="h3" weight="bold">
            {user.name}
          </Typography>
          <Typography as="p" size="text-sm" style={{ color: '#666' }}>
            Member since {user.joinDate}
          </Typography>
        </div>
      </div>
      
      <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
          <Mail size={16} color="#666" />
          <Typography as="p" size="text-sm">
            {user.email}
          </Typography>
        </div>
        
        <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
          <MapPin size={16} color="#666" />
          <Typography as="p" size="text-sm">
            {user.location}
          </Typography>
        </div>
      </div>
      
      <Button variant="outline" block>
        View Full Profile
      </Button>
    </CardContainer>
  );
}

Form Card

import CardContainer from '@/shared/ui/CardContainer/CardContainer';
import Typography from '@/shared/ui/Typography/Typography';
import { TextField } from '@/shared/ui/TextField/TextField';
import { Button } from '@/shared/ui/button/Button';

function LoginCard() {
  return (
    <CardContainer>
      <Typography as="h2" weight="bold" style={{ marginBottom: '8px' }}>
        Welcome Back
      </Typography>
      
      <Typography as="p" size="text-sm" style={{ color: '#666', marginBottom: '24px' }}>
        Sign in to your account to continue
      </Typography>
      
      <form style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
        <TextField
          label="Email"
          type="email"
          placeholder="[email protected]"
        />
        
        <TextField
          label="Password"
          type="password"
          placeholder="Enter your password"
        />
        
        <Button type="submit" variant="primary" block>
          Sign In
        </Button>
        
        <Typography as="p" size="text-xs" style={{ textAlign: 'center', color: '#666' }}>
          Don't have an account? <a href="/signup">Sign up</a>
        </Typography>
      </form>
    </CardContainer>
  );
}

Card Grid Layout

import CardContainer from '@/shared/ui/CardContainer/CardContainer';
import Typography from '@/shared/ui/Typography/Typography';

function CardGrid({ items }) {
  return (
    <div style={{
      display: 'grid',
      gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
      gap: '24px',
      padding: '24px'
    }}>
      {items.map((item) => (
        <CardContainer key={item.id} onClick={() => handleItemClick(item.id)}>
          <Typography as="h3" weight="medium">
            {item.title}
          </Typography>
          <Typography as="p" size="text-sm" style={{ color: '#666' }}>
            {item.description}
          </Typography>
        </CardContainer>
      ))}
    </div>
  );
}

TypeScript Types

type CardContainerProps = {
  children: React.ReactNode;
  onClick?: (...args: any[]) => void;
};

Styling

The CardContainer uses CSS Modules for styling (CardContainer.module.css). The default styles provide:
  • Consistent padding and border radius
  • Box shadow for depth
  • Hover effects (when onClick is provided)
  • Responsive behavior

Custom Styling

You can wrap the CardContainer and apply custom styles:
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
  <CardContainer>
    <h3>Centered Card</h3>
    <p>This card is centered with a max width.</p>
  </CardContainer>
</div>

Accessibility

When using onClick, consider adding proper keyboard support and ARIA attributes for better accessibility. The current implementation uses a <div> with onClick, which may not be keyboard accessible by default.
For clickable cards, consider wrapping in a button or link for better accessibility:
<button 
  onClick={handleClick}
  style={{ all: 'unset', cursor: 'pointer', width: '100%' }}
>
  <CardContainer>
    <h3>Accessible Clickable Card</h3>
  </CardContainer>
</button>

Best Practices

Cards should be scannable. Avoid cramming too much content into a single card. If content is lengthy, consider splitting into multiple cards or using a different layout.
If a card has an onClick handler, ensure it has visual affordances (hover effects, cursor change) to indicate it’s interactive.
Use consistent gap spacing when displaying multiple cards in a grid or list. This creates a cleaner, more professional appearance.

Layout Patterns

Single Column

<div style={{ maxWidth: '600px', margin: '0 auto', display: 'flex', flexDirection: 'column', gap: '16px' }}>
  <CardContainer>Card 1</CardContainer>
  <CardContainer>Card 2</CardContainer>
  <CardContainer>Card 3</CardContainer>
</div>

Grid Layout

<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', gap: '16px' }}>
  <CardContainer>Card 1</CardContainer>
  <CardContainer>Card 2</CardContainer>
  <CardContainer>Card 3</CardContainer>
  <CardContainer>Card 4</CardContainer>
</div>

Sidebar + Main Content

<div style={{ display: 'grid', gridTemplateColumns: '300px 1fr', gap: '24px' }}>
  <CardContainer>
    {/* Sidebar content */}
  </CardContainer>
  
  <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
    <CardContainer>{/* Main content 1 */}</CardContainer>
    <CardContainer>{/* Main content 2 */}</CardContainer>
  </div>
</div>

Typography

Use Typography for consistent text styling inside cards

Button

Add action buttons to cards for user interactions

TextField

Combine with TextField for form-based cards

Build docs developers (and LLMs) love