Card

Flexible container for content and actions.

zweiundeins <sb-card> container panel tile Since 2026-09-21 MIT licence 1.2 kB0 Open in playground Edit on GitHub

Playground

A cold, quiet world with excellent stargazing.

A surface with optional media, heading, body and footer. Sections that get no content collapse (whitespace doesn't count), and href makes the whole card one link: its heading, stretched over the card.

Examples

A violet planet rising over pixel hills A cold, quiet world with excellent stargazing. Visit Details
<sb-card heading="Planet X-9" style="inline-size: 18rem">
  <img slot="media" src="/art/landscape.svg" alt="A violet planet rising over pixel hills" width="256" height="144">
  A cold, quiet world with excellent stargazing.
  <sb-button slot="footer" size="sm">Visit</sb-button>
  <sb-button slot="footer" size="sm" variant="ghost">Details</sb-button>
</sb-card>

Linked card

Props, codecs, slots and scoping, straight from the source.
<sb-card heading="Read the Rocket reference" href="https://data-star.dev/reference/rocket" variant="glow" style="inline-size: 18rem">
  Props, codecs, slots and scoping, straight from the source.
</sb-card>

Styling

Style it from your page's CSS, without changing the component or importing anything into it. Custom properties, inherited properties and ::part() all reach into its shadow root.

  • Size: it fills the width it is given (a grid cell, say) and tightens its padding when that is under 14rem; in a flex row or any other fit-content layout it takes the width of its content. Set max-inline-size on the card to cap it. Cards in one grid row stretch to the same height, with their footers at the bottom.
  • Body: normal flow, like a <div>: inline markup stays in its line, and block children (paragraphs, lists) keep their own margins.
  • Media: scaled to the card's width and clipped to its corners. Nothing else is clipped, so tooltips and badges can reach outside the card. Raster pixel art that should stay crisp when scaled needs image-rendering: pixelated on your <img>.
  • Fonts: the heading and the body use your page's font.
  • Colours: the surface is --sb-surface-card (--sb-surface-inset for variant="inset") with a --sb-border edge; the heading is --sb-text-1 and the body --sb-text-2. --sb-brand tints the border of the glow variant and of a linked card on hover and keyboard focus; keyboard focus also rings the card with --sb-focus-ring. Corners are --sb-radius-lg.
  • Motion: a linked card lifts by 2px on hover and focus, except with prefers-reduced-motion.
  • Parts: card (the frame), media, heading, body and footer. Your page's ::part() rules win over the component's own, without !important.
A cold, quiet world with excellent stargazing.
<style>
  .my-card { max-inline-size: 20rem; --sb-surface-card: var(--sb-brand-subtle); --sb-radius-lg: 0; }
  .my-card::part(body) { padding: 1.5rem; }
  .my-card::part(heading) { font-size: 1.25rem; }
</style>
<sb-card class="my-card" heading="Planet X-9">A cold, quiet world with excellent stargazing.</sb-card>

Accessibility

A linked card renders one real <a> in the heading and stretches its click area over the card, so screen readers hear a single link; without a heading there is no link. Keyboard focus rings the whole card. <a> and <button> elements placed directly in the body, and everything in the footer, stay clickable on their own; give other interactive content in the body of a linked card (an <sb-button>, a link inside a paragraph) position: relative, so it sits above the card's link. Always give media images alt text.

Installation

Add Datastar with Rocket and the Starbase autoloader once per page, then use the tag: the autoloader imports each component the first time its tag appears, including tags a Datastar morph adds later. The Datastar here is v1.0.4 with Starbase's fixes until they are released; the components also run on the official release.

<script type="importmap">
  { "imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/autoloader.js"></script>

<sb-card heading="Planet X-9">A cold, quiet world with excellent stargazing.</sb-card>

No flash of undefined elements: put class="sb-cloak" on <html> and add .sb-cloak :not(:defined) { visibility: hidden } to your CSS. The autoloader removes the class once the page's components are defined.

Load just this component, pinned to this version: all its modules in one minified file with its integrity hash, so the browser refuses it if a single byte changes.

<script type="importmap">
  {
    "imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
    "integrity": {
      "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5"
    }
  }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/card@abe5f1868935/card.bundle.min.js" integrity="sha384-yer9K1Wzvw8FO/laJTbrjanP+AczNO7kGc7L1OKCvtYrrh3d9adJ4GeoYUTqT89y"></script>

<sb-card heading="Planet X-9">A cold, quiet world with excellent stargazing.</sb-card>

Or the minified module files (one file, which the browser requests as it finds their imports), each pinned in the import map:

<script type="importmap">
  {
    "imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
    "integrity": {
      "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5",
      "https://starbase.zweiundeins.gmbh/c/card@abe5f1868935/card.min.js": "sha384-wLH9/At4W/ZFt9c/rQlOjQXaIBhupuvfmY53uq+7JTQIixd7PpPGmUggn4sJYVZ3"
    }
  }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/card@abe5f1868935/card.min.js" integrity="sha384-wLH9/At4W/ZFt9c/rQlOjQXaIBhupuvfmY53uq+7JTQIixd7PpPGmUggn4sJYVZ3"></script>

<sb-card heading="Planet X-9">A cold, quiet world with excellent stargazing.</sb-card>

In production, pin today's whole catalog: its autoloader and an import map with integrity hashes, so the browser refuses any file that changed. This map covers Datastar and this component; importmap.json has the hashes of every component, to merge into yours.

<script type="importmap">
  {
    "imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
    "integrity": {
      "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5",
      "https://starbase.zweiundeins.gmbh/c/card@abe5f1868935/card.bundle.min.js": "sha384-yer9K1Wzvw8FO/laJTbrjanP+AczNO7kGc7L1OKCvtYrrh3d9adJ4GeoYUTqT89y"
    }
  }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/@339249aca48f/autoloader.js" integrity="sha384-ZNpMayI1XShztIEX0iDHyCDDbhT8H+t+zEESUq4BCrN4b+7m931LQEl5x0vbY+Vo"></script>

<sb-card heading="Planet X-9">A cold, quiet world with excellent stargazing.</sb-card>

Components import only datastar and their own files, so copying is enough: save each component's one file (and a file it loads on first use, keeping its folder), and point the import map at your own copy of datastar-rocket.js (the /js/ paths stand for yours).

  • <sb-card>
    • card.bundle.min.js one file 1.2 kB brotli
<script type="importmap">
  { "imports": { "datastar": "/js/datastar-rocket.js" } }
</script>
<script type="module" src="/js/card/card.bundle.min.js"></script>

<sb-card heading="Planet X-9">A cold, quiet world with excellent stargazing.</sb-card>

Or copy the module files, keeping each component's folder, and load each component's module:

<script type="importmap">
  { "imports": { "datastar": "/js/datastar-rocket.js" } }
</script>
<script type="module" src="/js/card/card.min.js"></script>

<sb-card heading="Planet X-9">A cold, quiet world with excellent stargazing.</sb-card>

Size

A page loads this component as one file, its bundle, compressed the way it is served (gzip -9, brotli -11): brotli is what it downloads. Datastar and Rocket are shared by every component and not counted.

FileOriginalgzipbrotli
card.bundle.min.js 2.9 kB1.4 kB1.2 kB
As module files: one request, 1.2 kB minified with brotli

For a page that loads the module files as they are, requesting each one as it finds its import, or bundles them itself. Each file compressed on its own; the minified one is what the page downloads, and the readable source is always there too.

FileOriginalgzipbrotliminified
card.js 4.8 kB2.2 kB1.8 kB1.2 kB

API reference

Props

AttributeTypeDefaultDescription
headingstring""Card title.
hrefstring""Makes the whole card a link: the heading is the link, so it needs `heading`.
variant"default" | "inset" | "glow""default"Surface style.

Slots

NameDescription
mediaImage or illustration on top, edge to edge.
defaultBody content.
footerActions or metadata at the bottom.