Cards

Blog Card

A reusable card component for displaying a summary of a blog post, designed to be flexible by showing or hiding elements based on the provided props.

Default Usage

This example shows the BlogCard with all props utilized, including an image, category, read time, author, and date, inside a responsive grid.

The Future of AI in Web Development
Tech5 min read

The Future of AI in Web Development

Exploring how artificial intelligence is reshaping the landscape of modern web and application development, from automated coding to design.

Written by

Alex Johnson

Posted on

Aug 15, 2025

Minimal Cards

The component is flexible. Here, the imageUrl, author, and date props are omitted to create a more compact, text-only card.

Frontend2 min read

Quick Guide to CSS Grid

A brief overview of the most powerful features of CSS Grid for modern layouts.

Description Truncation

Use the truncateLines prop to control text overflow. This is useful for creating a uniform layout when descriptions have varying lengths.

UI/UX

Truncated to 3 Lines

This is a very long description designed to showcase the line-clamping functionality of the BlogCard component. By setting the truncateLines prop, we can effectively limit the text to a specified number of lines, followed by an ellipsis. This is crucial for maintaining a consistent and clean UI.

Written by

Jane Doe

Installation

CLI

Installation via the CLI is coming soon. For now, please follow the manual installation instructions below.

Manual

  1. This component relies on the Card and Badge components. Ensure you have installed them first:

    npx shadcn-ui@latest add card
    npx shadcn-ui@latest add badge
  2. Copy and paste the utility functions from the Utilities section below into your lib/utils.ts file.

  3. Copy and paste the following component code into your project.

    components/ui/blog-card.tsx
    import { Badge } from '@/components/ui/badge';
    import {
      Card,
      CardContent,
      CardFooter,
      CardHeader,
    } from '@/components/ui/card';
    import { cn, formatPostDate, formatReadTime } from '@/lib/utils';
    import Image from 'next/image';
    
    interface BlogCardProps {
      /** The main title of the blog post. */
      title: string;
      /** A short summary or excerpt from the blog post. */
      description: string;
      /** The URL for the main image of the post. */
      imageUrl?: string;
      /** The category or tag associated with the post. */
      category?: string;
      /** The estimated reading time in seconds. */
      readTime?: number;
      /** The name of the post's author. */
      author?: string;
      /** The publication date of the post. */
      date?: Date;
      /** The number of lines to truncate the description to before showing an ellipsis. */
      truncateLines?: number;
    }
    
    /**
     * A reusable card component for displaying a summary of a blog post.
     * It's designed to be flexible, showing or hiding elements based on the provided props.
     */
    export const BlogCard: React.FC<BlogCardProps> = ({
      imageUrl,
      category,
      readTime,
      title,
      description,
      author,
      date,
      truncateLines,
    }) => {
      const showMetadata = category || readTime;
      const showFooter = author || date;
    
      return (
        <Card className='max-w-sm flex flex-col w-full overflow-hidden shadow-lg border p-3 rounded-3xl gap-3'>
          {imageUrl && (
            <CardHeader className='p-0'>
              <div className='relative w-full h-56'>
                <Image
                  src={imageUrl}
                  alt={title}
                  layout='fill'
                  objectFit='cover'
                  className='rounded-2xl'
                />
              </div>
            </CardHeader>
          )}
    
          <CardContent className='p-3 flex-grow'>
            {showMetadata && (
              <div className='flex items-center text-sm text-muted-foreground mb-4'>
                {category && (
                  <Badge className='text-sm bg-muted text-muted-foreground px-3 py-1 rounded-full'>
                    {category}
                  </Badge>
                )}
                {category && readTime && <span className='mx-2'>•</span>}
                {readTime && <span>{formatReadTime(readTime)}</span>}
              </div>
            )}
    
            <h2 className='text-2xl font-bold text-card-foreground mb-2 leading-tight'>
              {title}
            </h2>
    
            <p
              className={cn('text-muted-foreground', {
                'overflow-hidden text-ellipsis [-webkit-box-orient:vertical] [display:-webkit-box]':
                  truncateLines && truncateLines > 0,
              })}
              style={{
                WebkitLineClamp: truncateLines,
              }}
            >
              {description}
            </p>
          </CardContent>
    
          {showFooter && (
            <CardFooter className='flex justify-between items-center p-3'>
              {author && (
                <div>
                  <p className='text-sm text-muted-foreground'>Written by</p>
                  <p className='font-semibold text-muted-foreground'>
                    {author}
                  </p>
                </div>
              )}
              {date && (
                <div className={author ? 'text-right' : ''}>
                  <p className='text-sm text-muted-foreground'>Posted on</p>
                  <p className='font-semibold text-muted-foreground'>
                    {formatPostDate(date)}
                  </p>
                </div>
              )}
            </CardFooter>
          )}
        </Card>
      );
    };

Utilities

The BlogCard component uses the following helper functions for formatting dates and read times. Make sure to add them to your lib/utils.ts file.

lib/utils.ts
/**
 * Formats a Date object into a string like "Apr 14, 2025".
 * @param date - The Date object to format.
 * @returns A formatted date string in the format "Month Day, Year".
 *
 * @example
 * const myDate = new Date('2025-04-14T12:00:00Z');
 * formatPostDate(myDate); // Returns "Apr 14, 2025"
 */
export const formatPostDate = (date: Date): string => {
  // 'en-US' locale is used to ensure the month-day-year order.
  // You can change this to other locales if needed.
  return new Intl.DateTimeFormat('en-US', {
    month: 'short', // e.g., "Apr"
    day: 'numeric', // e.g., "14"
    year: 'numeric', // e.g., "2025"
  }).format(date);
};

/**
 * Converts seconds into a human-readable string like "6 min read".
 * @param seconds - The total number of seconds for the estimated read time.
 * @returns A formatted string estimating the read time in minutes.
 *
 * @example
 * formatReadTime(350); // Returns "6 min read"
 * formatReadTime(360); // Returns "6 min read"
 */
export const formatReadTime = (seconds: number): string => {
  // Calculate the minutes and round up to the nearest whole number
  const minutes = Math.ceil(seconds / 60);
  return `${minutes} min read`;
};

Usage

Here are a couple of examples demonstrating how to use the BlogCard.

import { BlogCard } from '@/components/ui/blog-card';

const BlogPostPage = () => {
  const longDescription =
    'This is a much longer description that is intended to show how the truncation works. By setting the truncateLines prop, we can limit the amount of text shown before an ellipsis appears, creating a cleaner and more uniform look in a list of posts.';

  return (
    <div className='flex flex-wrap justify-center gap-8 p-4'>
      {/* Example 1: Full-featured card */}
      <BlogCard
        title='Exploring the Alps'
        description="A comprehensive guide to hiking and sightseeing in one of the world's most beautiful mountain ranges."
        imageUrl='https://source.unsplash.com/random/800x600?mountain'
        category='Travel'
        readTime={300} // 5 minutes
        author='Jane Doe'
        date={new Date('2025-08-15')}
      />

      {/* Example 2: Minimal card with description truncation */}
      <BlogCard
        title='A Quick Guide to React Hooks'
        description={longDescription}
        category='Programming'
        readTime={180} // 3 minutes
        truncateLines={3}
      />
    </div>
  );
};

export default BlogPostPage;

Props

PropTypeDefaultDescription
titlestringReqThe main title of the blog post.
descriptionstringReqA short summary or excerpt from the blog post.
imageUrlstring-The URL for the main image of the post.
categorystring-The category or tag associated with the post.
readTimenumber-The estimated reading time in seconds.
authorstring-The name of the post's author.
dateDate-The publication date of the post.
truncateLinesnumber-The number of lines to truncate the description to before showing an ellipsis.