Skip to content

Instantly share code, notes, and snippets.

@jasonday
Created March 14, 2025 22:16
Show Gist options
  • Save jasonday/7239eb243efd70cbc65448b0f4a5ed1c to your computer and use it in GitHub Desktop.
Save jasonday/7239eb243efd70cbc65448b0f4a5ed1c to your computer and use it in GitHub Desktop.
ImageCard component for Astro, using Astro's built in <Image/>. Derived from Incluud's component Card
---
/**
* ImageCard Component (copied from accessible components Card, to use <Image>)
*
* @description A versatile card component with image, title, and content areas
*/
interface Props extends astroHTML.JSX.HTMLAttributes {
/**
* Additional classes to apply to the card
*/
class?: string
/**
* Card's title
* @default "Default title"
*/
title?: string
/**
* URL for the card's image
* @default "https://fakeimg.pl/640x360"
*/
img?: string
/**
* URL for the card's link
* @default "#"
*/
url?: string
/**
* HTML tag to use for the title
* @default "h3"
*/
tagName?: string
/**
* Footer content
* @default ""
*/
footer?: string
/**
* Alt text for the image
* @default ""
*/
alt?: string
/**
* Widths for the image
* @default [300, 600]
*/
widths?: number[]
}
const {
class: className,
title = 'Default title',
img = 'https://fakeimg.pl/640x360',
url = '#',
tagName = 'h2',
footer = '',
alt = '',
widths = [300, 600],
...rest
} = Astro.props
const Tag = tagName
import { Image, Picture } from 'astro:assets';
import type { ImageMetadata } from 'astro';
const cardImage: ImageMetadata = {
src: img,
alt: alt,
widths: widths,
}
const images = import.meta.glob<{ default: ImageMetadata }>('/src/assets/img/*.{jpeg,jpg,png,gif}');
if (!images[img]) throw new Error(`"${img}" does not exist in glob: "src/assets/img/*.{jpeg,jpg,png,gif}"`);
---
<article class:list={['card', className]} {...rest}>
<div class="image">
<Image src={images[img]()} alt={alt} widths={widths}/>
</div>
<div class="content">
<div class="title leading-none *:leading-none">
<Tag>
<a href={url}>{title}</a>
</Tag>
</div>
<div class="meta"><slot name="meta" /></div>
<p class="description"><slot /></p>
{footer && <small class="footer">{footer}</small>}
</div>
</article>
<style>
:where(.card) {
--transition-duration: 0.3s;
--transition-easing: cubic-bezier(0.165, 0.84, 0.44, 1);
display: flex;
position: relative;
flex-direction: column;
border: 2px solid light-dark(hsl(0 0% 10%), hsl(0 0% 90%));
border-radius: 0.5rem;
max-inline-size: 60ch;
block-size: 100%;
overflow: hidden;
@media (prefers-reduced-motion: no-preference) {
transition: box-shadow var(--transition-duration) var(--transition-easing);
}
}
:where(.card:hover),
:where(.card:focus-within) {
box-shadow: 0 0 0 0.25rem;
}
:where(.card:focus-within) a:focus {
outline: none;
box-shadow: none;
text-decoration: none;
}
:where(.image) {
block-size: 250px;
overflow: clip;
}
:where(.image img) {
inline-size: 100%;
block-size: 100%;
object-fit: cover;
@media (prefers-reduced-motion: no-preference) {
transition: transform var(--transition-duration) var(--transition-easing);
}
}
:where(.content) {
display: flex;
flex-grow: 1;
flex-direction: column;
gap: 0.5rem;
padding-inline: 1rem;
padding-block: 1rem;
}
a {
color: currentColor;
font-size: 1.5rem;
text-decoration: none;
}
a:where(:hover, :focus-visible) {
text-decoration: underline;
text-underline-offset: 4px;
}
:where(.card:has(a:hover, a:focus-visible)) .image img {
transform: scale(1.05);
}
a::after {
position: absolute;
inset: 0;
content: '';
}
:where(.meta) {
order: -1;
margin-block-start: 0.5rem;
}
.meta :global(span) {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
.meta:empty {
display: none;
}
.description {
font-size: 1rem;
}
.description:empty {
display: none;
}
.footer {
margin-block-start: auto;
padding-block-start: 1rem;
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment