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:
Make sure you check out the Tiles component to see how to lay cards out in equal-height, container-responsive columns. (See below and this site’s own blog index for examples.)
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 |
Card.astro |
The Astro component | Github |
Tags.astro, Avatar.astro |
Composed components (dependencies) | Github |
| Tiles | Equal-height, container-responsive list layout | 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, on a Tiles list. Tiles is container-responsive: columns appear as the column the list sits in crosses the size’s container thresholds, so this site’s blog index gets its 4-up laptop layout purely by widening the page column (--layout-content-width), no per-list tuning:
<ul class="tiles"> <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 Rollins</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. |
Tiles
Lists of cards go in a Tiles component: a ul whose items stretch, so cards in a row share the same height, with a container-responsive column count. See its page for the sizes, thresholds, and container rules.
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>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 Tiles from '../components/Tiles.astro';---<Tiles> <li> <Card href="/blog/my-post" headingLevel={3} flushImage imageSrc={cover} title="Designing with cascade layers" tags={["css", "learn"]} author="Sonny Rollins" date={new Date("2026-05-02")} > <p>Why layer order beats specificity wars.</p> </Card> </li></Tiles>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>