Article List
Article listing grid with cards for blog and documentation pages.
Installation
This component is part of the Blog block. Install the full block to use it:
CLI
npx shadcn@latest add "https://ui.xaclabs.dev/r/blog.json"Usage
import { ArticleList } from "@/registry/blocks/blog/components/blog-article-list"View component source
blog-article-list.tsx
tsximport {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { cn } from "@/lib/utils"
export interface ArticleItem {
title: string
description: string
date: Date
href: string
cover?: string
author?: {
name: string
avatar?: string
}
tags?: string[]
}
export interface BlogArticleListProps {
articles: ArticleItem[]
className?: string
}
export function BlogArticleList({ articles, className }: BlogArticleListProps) {
return (
<div
className={cn(
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4",
className,
)}
>
{articles.map((article) => (
<a key={article.href} href={article.href} className="block group">
<Card className="h-full overflow-hidden transition-all duration-300 hover:scale-105 hover:shadow-lg dark:hover:shadow-primary/5 border-muted/60">
{article.cover && (
<div className="aspect-video w-full overflow-hidden border-b border-muted/60">
<img
src={article.cover}
alt={article.title}
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
</div>
)}
<CardHeader className="gap-2 p-5">
<div className="flex items-center justify-between gap-2">
<time
dateTime={article.date.toISOString()}
className="text-xs font-medium text-muted-foreground/80"
>
{new Intl.DateTimeFormat("en-US", {
month: "long",
day: "numeric",
year: "numeric",
}).format(article.date)}
</time>
{article.tags && article.tags.length > 0 && (
<div className="flex gap-1.5">
{article.tags.slice(0, 2).map((tag) => (
<span
key={tag}
className="inline-flex items-center rounded-sm bg-secondary px-1.5 py-0.5 text-[10px] font-medium text-secondary-foreground ring-1 ring-inset ring-secondary-foreground/10"
>
{tag}
</span>
))}
{article.tags.length > 2 && (
<span className="text-[10px] text-muted-foreground">
+{article.tags.length - 2}
</span>
)}
</div>
)}
</div>
<CardTitle className="line-clamp-2 text-lg font-semibold tracking-tight group-hover:text-primary transition-colors">
{article.title}
</CardTitle>
<CardDescription className="line-clamp-3 text-sm text-muted-foreground/90">
{article.description}
</CardDescription>
</CardHeader>
{article.author && (
<CardContent className="p-5 pt-0 mt-auto">
<div className="flex items-center gap-2.5 pt-4 border-t border-muted/40">
{article.author.avatar ? (
<img
src={article.author.avatar}
alt={article.author.name}
className="h-6 w-6 rounded-full object-cover ring-1 ring-border"
/>
) : (
<div className="h-6 w-6 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold text-muted-foreground ring-1 ring-border">
{article.author.name.charAt(0)}
</div>
)}
<span className="text-xs font-medium text-foreground/80">
{article.author.name}
</span>
</div>
</CardContent>
)}
</Card>
</a>
))}
</div>
)
}