A simple filled card
Natural Law, is a set of universal, non-man-made, binding and unchangeable conditions which govern the consequence of our actions.
The .card component, in its simplest form, is a container with 3 style options:
Every part is optional: header with title and subtitle, image (inset, or edge-to-edge with the flush modifier), badges, tags, byline with avatar and date, actions footer, clickable body, disabled state, and loading state with skeleton UI. The Astro component makes it easy to use all those features, and CardList lays cards out in equal-height responsive columns. This site’s own blog index is built from exactly these two components.
Natural Law, is a set of universal, non-man-made, binding and unchangeable conditions which govern the consequence of our actions.
Natural Law, is a set of universal, non-man-made, binding and unchangeable conditions which govern the consequence of our actions.
| File | Description | Source |
|---|---|---|
component.card.css |
All card styles (.card) |
Github |
component.cardList.css |
Equal-height list layout | Github |
Card.astro, CardList.astro |
The Astro components | Github |
Tags.astro, Avatar.astro |
Composed components (dependencies) | Github |
--card-* block in settings.ui.css |
Interface tokens (see below) | Github |
Category
This is where the content goes!
<article class="card">
<header class="card_header">
<div class="card_header_text">
<h2 class="card_title">Card title</h2>
<p>Category</p>
</div>
</header>
<section class="card_content">
<p>This is where the content goes!</p>
</section>
</article>| Available modifiers | Description |
|---|---|
.card |
Default style. Transparent background with border. |
.card + .card-filled |
Filled with a background color. |
.card + .card-raised |
Drop shadow, animated on hover. |
.card + .is-disabled |
Lowered opacity. Interactivity disabled. |
.card_media + .card_media-flush |
Edge-to-edge image on top of the card. |
A card carries one link: put .card_link on a real anchor (usually the title link) and it stretches over the whole card with a ::after, so screen readers hear one link named by the title. Actions stay clickable because .card_actions is positioned and comes later in the DOM; tags stay clickable through their own z-index bump. The blog-index pattern combines this with a flush image, tags, and a byline, wrapped in a .cardList over the grid system:
<ul class="cardList grid" col="1" col-md="2" col-lg="2"> <li> <article class="card"> <figure class="card_media card_media-flush"> <img class="card_image" src="cover.jpg" alt="" loading="lazy" /> </figure> <nav aria-label="Tags"><ul class="tags tags-inline card_tags"> <li><a href="/blog/tags/css">css</a></li> </ul></nav> <header class="card_header"> <div class="card_header_text"> <h3 class="card_title"><a class="card_link" href="/blog/my-post">Designing with cascade layers</a></h3> </div> </header> <div class="card_content"><p>Why layer order beats specificity wars.</p></div> <footer class="card_byline"> <!-- avatar markup, see the Avatar docs --> <div> <span class="card_author">Sonny Mueller</span> <time datetime="2026-05-02">May 2, 2026</time> </div> </footer> </article> </li></ul>The following custom properties are available in settings.ui.css:
| Property | Color |
|---|---|
--card-spacing |
General padding inside the card. |
--card-color |
Text color. |
--card-secondary-text-color |
Subtitle, date, and secondary text color. |
--card-bg-color |
Background color of the “filled” variant. |
--card-border-radius |
Border radius. |
--card-border-color |
Border color. |
--card-border-width |
Border width. |
--card-shadow |
Default drop shadow or the “raised” variant. |
--card-shadow-hover |
Hover state drop shadow. |
--card-aspect-ratio |
Image aspect ratio (defaults to --ar-golden). |
--card-loading-bg |
Loading skeleton background color. |
Badge colors come from the badge component’s --badge-* tokens.
Card
Every part is optional; render only what you pass. Badges and tags can coexist, but they compete for attention: badges are non-interactive status labels pinned top-right, tags are taxonomy links in the content flow. Prefer one or the other on a given card.
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
string |
undefined | Card’s visual style: filled, raised. |
isDisabled |
boolean |
false |
Disabled style. Disabled interactions. |
isLoading |
boolean |
false |
Loading state with skeleton UI. |
href |
string |
undefined | Makes the whole card clickable (through the title link, if any). |
title |
string |
undefined | Header title text. |
subtitle |
string |
undefined | Header subtitle text. |
headingLevel |
number |
2 |
Heading level of the title: 2, 3, or 4. |
imageSrc |
ImageMetadata | string |
undefined | Add an image to the card (imported asset or URL/path). |
imageAlt |
string |
'' |
Alt text for the image. |
aspectRatio |
string |
golden |
Image aspect ratio (available aspect ratios). |
flushImage |
boolean |
false |
Edge-to-edge image on top of the card instead of inset. |
badges |
array |
[] |
Array of objects (see example below). |
tags |
string[] |
[] |
Tag names, rendered with the Tags component. |
tagsUrl |
string |
'/blog/tags' |
Base URL for tag links. |
author |
string |
undefined | Byline name (also feeds the Avatar initials). |
avatarSrc |
ImageMetadata | string |
undefined | Byline avatar image. |
date |
Date |
undefined | Publication date, rendered as a <time> in the byline. |
locale |
string |
'en-US' |
Locale for the formatted date. |
data-testid |
string |
'card' |
Test ID for testing frameworks. |
class |
string |
undefined | Additional CSS classes, useful for helper classes. |
CardList
A ul over the grid system whose items stretch, so cards in a row share the same height. Children go in the default slot; wrap each card in an <li>.
| Prop | Type | Default | Description |
|---|---|---|---|
cols |
number |
2 |
Columns from --lg: 1, 2, or 3. |
The Card component supports the following slots for content projection:
It is recommended to use the button component in the actions slot, e.g.:
<Fragment slot="actions"> <a class="bt bt-text" href="/home">Cancel</a> <button class="bt bt-primary">Add to Cart</button></Fragment>The badges prop takes an array of objects. Each object consists of a label property which defines the badge’s text, and an optional color property that defines the badge’s background color. If no color is provided, it defaults to --badge-background-color from the badge component.
This is where the content goes. Unless you only want a bunch of badges!
<Card title="A card with too many badges" badges={[ { label: "Way", color: "#a6b65a" }, { label: "Too", color: "#86615c" }, { label: "Many", color: "#81a598" }, { label: "Badges", color: "#e3863d" }, ]}> <p>This is where the content goes. Unless you only want a bunch of badges!</p></Card>The article-card recipe: flush image, tags, stretched title link, description, and byline, in a CardList.
Why layer order beats specificity wars, and how mCSS hands the final word to your unlayered CSS.
One file of custom properties is the whole theming story.
---import Card from '../components/Card.astro';import CardList from '../components/CardList.astro';---<CardList cols={2}> <li> <Card href="/blog/my-post" headingLevel={3} flushImage imageSrc={cover} title="Designing with cascade layers" tags={["css", "learn"]} author="Sonny Mueller" date={new Date("2026-05-02")} > <p>Why layer order beats specificity wars.</p> </Card> </li></CardList>Category
This is a extremely detailed description of the product.
---import placeholder from "../../assets/images/image.png";import Card from '../../components/Card.astro';---<Card variant="raised" href="/product/123" title="Product Name" subtitle="Category" imageSrc={placeholder} imageAlt="Product image" aspectRatio="widescreen"> <p>This is a extremely detailed description of the product.</p> <Fragment slot="actions"> <a class="bt bt-md bt-text " href="/details"> View Details </a> <button class="bt bt-md bt-primary">Add to Cart</button> </Fragment></Card>This is what the card looks like without the isLoading prop.
---import placeholder from "../../assets/images/image.png";import Card from '../../components/Card.astro';---<Card imageSrc={placeholder} imageAlt="Product image" isLoading> <p>This is what the card looks like without the `isLoading` prop.</p></Card>With the isDisabled prop, the link doesn’t work anymore.
---import placeholder from "../../assets/images/image.png";import Card from '../../components/Card.astro';---<Card href="/product/123" imageSrc={placeholder} imageAlt="Product image" isDisabled> <p>With the `isDisabled` prop, the link doesn't work anymore.</p></Card>Category
This is a extremely detailed description of the product.
<article class="card card-raised" style="--card-aspect-ratio: var(--ar-golden)"> <header class="card_header"> <div class="card_header_text"> <h2 class="card_title"> <a class="card_link" href="/product/123">Product Name</a> </h2> <p>Category</p> </div> <div class="card_badges"><span class="badge card_badge">new</span></div> </header> <figure class="card_media"> <img class="card_image" src="image.jpg" alt="Product image" loading="lazy" decoding="async" /> </figure> <section class="card_content"> <p>This is a extremely detailed description of the product.</p> </section> <footer class="card_actions"> <a class="bt bt-text" href="/details">View Details</a> <button class="bt bt-primary">Add to Cart</button> </footer></article>