Card

Installation

Add the following Soul components

The card component uses the skeleton component. Make sure you have added it to your project.

Install the following dependencies

npm install clsx lucide-react

Copy and paste the following code into your project

primitives/card/index.tsx

import { clsx } from 'clsx';import { ArrowUpRight } from 'lucide-react';import Image from 'next/image';import Link from 'next/link';import * as Skeleton from '@/vibes/soul/primitives/skeleton';export interface CardContent {  title: string;  image?: { src: string; alt: string };  href: string;}export interface CardProps extends CardContent {  className?: string;  textColorScheme?: 'light' | 'dark';  iconColorScheme?: 'light' | 'dark';  aspectRatio?: '5:6' | '3:4' | '1:1';  imageSizes?: string;  textPosition?: 'inside' | 'outside';  textSize?: 'small' | 'medium' | 'large' | 'x-large';  showOverlay?: boolean;}/** * This component supports various CSS variables for theming. Here's a comprehensive list, along * with their default values: * * ```css * :root { *   --card-focus: var(--primary); *   --card-light-offset: var(--background); *   --card-light-text: var(--foreground); *   --card-light-icon: var(--foreground); *   --card-light-background: var(--contrast-100); *   --card-dark-offset: var(--foreground); *   --card-dark-text: var(--background); *   --card-dark-icon: var(--background); *   --card-dark-background: var(--contrast-500); *   --card-font-family: var(--font-family-body); *   --card-border-radius: 1rem; * } * ``` */export function Card({  className,  title,  image,  href,  textColorScheme = 'light',  iconColorScheme = 'light',  aspectRatio = '5:6',  imageSizes = '(min-width: 42rem) 25vw, (min-width: 32rem) 33vw, (min-width: 28rem) 50vw, 100vw',  textPosition = 'outside',  textSize = 'small',  showOverlay = true,}: CardProps) {  return (    <article      className={clsx(        'group @container relative flex w-full max-w-md min-w-0 cursor-pointer flex-col gap-2 rounded-(--card-border-radius,1rem) font-(family-name:--card-font-family,var(--font-family-body))',        {          small: 'gap-2',          medium: 'gap-3',          large: 'gap-4',          'x-large': 'gap-5',        }[textSize],        className,      )}    >      <ArrowUpRight        className={clsx(          'absolute top-5 right-5 z-10 transition-transform duration-700 ease-out group-hover:translate-x-1.5 group-hover:-translate-y-1.5',          {            light: 'text-(--card-light-icon,var(--foreground))',            dark: 'text-(--card-dark-icon,var(--background))',          }[iconColorScheme],        )}        strokeWidth={1.5}      />      <div        className={clsx(          'relative overflow-hidden rounded-[inherit] group-focus:ring-(--card-focus,var(--primary)) group-focus-visible:ring-2',          {            light: 'bg-(--card-light-background,var(--contrast-100))',            dark: 'bg-(--card-dark-background,var(--contrast-500))',          }[textColorScheme],          {            '5:6': 'aspect-[5/6]',            '3:4': 'aspect-[3/4]',            '1:1': 'aspect-square',          }[aspectRatio],        )}      >        {image != null ? (          <Image            alt={image.alt}            className={clsx(              'w-full scale-100 object-cover transition-transform duration-500 ease-out select-none group-hover:scale-110',              {                light: 'bg-(--card-light-background,var(--contrast-100))',                dark: 'bg-(--card-dark-background,var(--contrast-500))',              }[textColorScheme],            )}            fill            sizes={imageSizes}            src={image.src}          />        ) : (          <div            className={clsx(              'pt-5 pl-5 text-4xl leading-[0.8] font-bold tracking-tighter break-words opacity-25 transition-transform duration-500 ease-out group-hover:scale-105 @xs:text-7xl',              {                light: 'text-(--card-light-text,var(--foreground))',                dark: 'text-(--card-dark-text,var(--background))',              }[textColorScheme],            )}          >            {title}          </div>        )}        {textPosition === 'inside' && (          <div            className={clsx(              'absolute inset-0 flex items-end p-6 @xs:p-8',              showOverlay &&                'from-foreground/0 via-foreground/0 to-foreground/50 bg-gradient-to-b from-50% via-50% to-100%',            )}          >            <h3              className={clsx(                'leading-tight font-medium',                {                  small: 'text-lg tracking-normal @xs:text-xl',                  medium: 'text-xl tracking-normal @xs:text-2xl',                  large: 'text-2xl tracking-tight @xs:text-3xl',                  'x-large': 'text-3xl tracking-tight @xs:text-4xl',                }[textSize],                {                  light: 'text-(--card-light-text,var(--foreground))',                  dark: 'text-(--card-dark-text,var(--background))',                }[textColorScheme],              )}            >              {title}            </h3>          </div>        )}      </div>      {textPosition === 'outside' && (        <h3          className={clsx(            'line-clamp-1 leading-tight font-medium',            {              small: 'text-lg tracking-normal @xs:text-xl',              medium: 'text-xl tracking-normal @xs:text-2xl',              large: 'text-2xl tracking-tight @xs:text-3xl',              'x-large': 'text-3xl tracking-tight @xs:text-4xl',            }[textSize],            {              light: 'text-(--card-light-text,var(--foreground))',              dark: 'text-(--card-dark-text,var(--background))',            }[textColorScheme],          )}        >          {title}        </h3>      )}      <Link        className={clsx(          'absolute inset-0 rounded-(--card-border-radius,1rem) focus:outline-hidden focus-visible:ring-2 focus-visible:ring-(--card-focus,var(--primary)) focus-visible:ring-offset-4',          {            light: 'ring-offset-(--card-light-offset,var(--background))',            dark: 'ring-offset-(--card-dark-offset,var(--foreground))',          }[textColorScheme],        )}        href={href}      >        <span className="sr-only">View product</span>      </Link>    </article>  );}export function CardSkeleton({  aspectRatio = '5:6',  className,}: Pick<CardProps, 'aspectRatio' | 'className'>) {  return (    <div className={clsx('@container', className)}>      <Skeleton.Box        className={clsx(          'rounded-(--card-border-radius,1rem)',          {            '5:6': 'aspect-[5/6]',            '3:4': 'aspect-[3/4]',            '1:1': 'aspect-square',          }[aspectRatio],        )}      />      <div className="mt-3">        <Skeleton.Text characterCount={10} className="rounded-sm text-lg" />      </div>    </div>  );}

Usage

import { Card } from '@/vibes/soul/primitives/card';function Usage() {  return (    <Card href="#" title="Partial shade" image={{ src: 'https://storage.googleapis.com/s.mkswft.com/RmlsZTpmNjJhNTMyOC1hNzMwLTQxYjQtODE5Ny05ZDdlYWViMjJhMDQ=/pink-caladium.jpeg', alt: 'Low Maintenance' }} />  );}

API Reference

CardProps

PropTypeDefault
className
string
title*
string
image
{ src: string; alt: string }
href*
string
textColorScheme
'light' | 'dark'
'light'
iconColorScheme
'light' | 'dark'
'light'
aspectRatio
'5:6' | '3:4' | '1:1'
'5:6'
borderRadius
'none' | 'small' | 'medium' | 'large'
'large'
imageSizes
string
'(min-width: 42rem) 25vw, (min-width: 32rem) 33vw, (min-width: 28rem) 50vw, 100vw'
textPosition
'inside' | 'outside'
'outside'
textSize
'small' | 'medium' | 'large' | 'x-large'
'small'
showOverlay
boolean
true

CSS Variables

This component supports various CSS variables for theming. Here's a comprehensive list.

:root {  --card-focus: var(--primary);  --card-light-offset: var(--background);  --card-light-text: var(--foreground);  --card-light-icon: var(--foreground);  --card-light-background: var(--contrast-100);  --card-dark-offset: var(--foreground);  --card-dark-text: var(--background);  --card-dark-icon: var(--background);  --card-dark-background: var(--contrast-500);  --card-font-family: var(--font-family-body);  --card-border-radius: 1rem;}