ContentCard

Card

{% card %} is a generic content card: a bordered surface with an optional media zone, a body, and an optional footer. It carries no knowledge of the registry or $item — it's a plain presentational component you can drop into prose, or feed from a collection body template.

There is one generic card (named by shape, not by entity) — refrakt deliberately has no article-card / recipe-card / per-entity card runes. A designed list item is {% card %} fed by entity fields, not a bespoke card per type.

{% card href="/runes/collection" %}
---
### Collection rune
The plural counterpart to `ref` and `expand` — query the registry and render the results.

---

Core rune · see also `expand`
{% /card %}
<div data-rune="card" data-rune-fields="{&quot;media-position&quot;:&quot;top&quot;}">
  <div data-name="body">
    <h3 id="collection-rune" data-name="title">Collection rune</h3>
    <p>
      The plural counterpart to
      <code>ref</code>
      and
      <code>expand</code>
      — query the registry and render the results.
    </p>
  </div>
  <footer data-name="footer">
    <p>
      Core rune · see also
      <code>expand</code>
    </p>
  </footer>
  <a data-name="link" href="/runes/collection" aria-hidden="true" tabindex="-1"></a>
</div>

Collection rune

The plural counterpart to ref and expand — query the registry and render the results.

Core rune · see also expand

<div class="rf-card" data-media-position="top" data-rune="card" data-density="full">
  <div data-name="content" class="rf-card__content">
    <div data-name="body" class="rf-card__body" data-section="body">
      <h3 id="collection-rune" data-name="title" class="rf-card__title">Collection rune</h3>
      <p>
        The plural counterpart to 
        <code>ref</code>
         and 
        <code>expand</code>
         — query the registry and render the results.
      </p>
    </div>
    <footer data-name="footer" class="rf-card__footer">
      <p>
        Core rune · see also 
        <code>expand</code>
      </p>
    </footer>
  </div>
  <a data-name="link" href="/runes/collection" aria-hidden="true" tabindex="-1" class="rf-card__link"></a>
</div>

Zones

The body divides on --- (a horizontal rule) into up to three zones — here all three, with a real cover image:

{% card href="/runes/learning/recipe" media-position="start" %}
![A tequila sunrise cocktail](https://assets.refrakt.md/tequila-sunrise.png)

---

Brunch classic

### Tequila Sunrise
A bright, layered cocktail — five minutes, no shaker.

---

Cocktail · Easy
{% /card %}
<div data-rune="card" data-rune-fields="{&quot;media-position&quot;:&quot;start&quot;}">
  <div data-section="media" data-name="media">
    <img src="https://assets.refrakt.md/tequila-sunrise.png" alt="A tequila sunrise cocktail">
  </div>
  <p data-name="eyebrow">Brunch classic</p>
  <div data-name="body">
    <h3 id="tequila-sunrise" data-name="title">Tequila Sunrise</h3>
    <p>A bright, layered cocktail — five minutes, no shaker.</p>
  </div>
  <footer data-name="footer">
    <p>Cocktail · Easy</p>
  </footer>
  <a data-name="link" href="/runes/learning/recipe" aria-hidden="true" tabindex="-1"></a>
</div>
A tequila sunrise cocktail

Brunch classic

Tequila Sunrise

A bright, layered cocktail — five minutes, no shaker.

Cocktail · Easy

<div class="rf-card" data-media-position="start" data-rune="card" data-density="full">
  <div data-section="media" data-name="media" class="rf-card__media" data-guest-fit="clip" data-guest-posture="presentational">
    <img src="https://assets.refrakt.md/tequila-sunrise.png" alt="A tequila sunrise cocktail" />
  </div>
  <div data-name="content" class="rf-card__content">
    <p data-name="eyebrow" class="rf-card__eyebrow">Brunch classic</p>
    <div data-name="body" class="rf-card__body" data-section="body">
      <h3 id="tequila-sunrise" data-name="title" class="rf-card__title">Tequila Sunrise</h3>
      <p>A bright, layered cocktail — five minutes, no shaker.</p>
    </div>
    <footer data-name="footer" class="rf-card__footer">
      <p>Cocktail · Easy</p>
    </footer>
  </div>
  <a data-name="link" href="/runes/learning/recipe" aria-hidden="true" tabindex="-1" class="rf-card__link"></a>
</div>
  • media (optional, leading zone) — laid out beside the body on wide screens and as a full-bleed header on small screens.
  • body — the main content.
  • footer (optional, trailing zone) — a muted meta row.

Zones are assigned by count: 1 zone → body; 2 → media + body; 3 → media + body + footer. To have a footer with no media, lead with an empty zone (--- body --- footer).

In the body zone, a leading paragraph immediately followed by a heading is treated as an eyebrow — the small uppercase kicker above the title (the same pattern as page-section and recipe). Above, Brunch classic is the eyebrow over the Tequila Sunrise heading.

The media zone isn't just images

The media zone holds any content — it's ordinary markdown, transformed in place. An image is the common case, but a {% codegroup %}, a {% sandbox %}, a chart, or a video embed work equally well:

{% card %}
{% codegroup %}
```js
export const sum = (a, b) => a + b;
```
```py
def sum(a, b): return a + b
```
{% /codegroup %}

---

### Tiny utilities
A starter kit of one-liners.
{% /card %}
<div data-rune="card" data-rune-fields="{&quot;media-position&quot;:&quot;top&quot;}">
  <div data-section="media" data-name="media">
    <section data-rune="code-group">
      <div role="tablist" data-name="tabs">
        <button data-name="tab" role="tab">
          <span>JavaScript</span>
        </button>
        <button data-name="tab" role="tab">
          <span>Python</span>
        </button>
      </div>
      <div data-name="panels">
        <div role="tabpanel" data-name="panel">
          <div class="rf-codeblock">
            <pre data-language="js">
              <code data-language="js">export const sum = (a, b) =&gt; a + b;
</code>
            </pre>
          </div>
        </div>
        <div role="tabpanel" data-name="panel">
          <div class="rf-codeblock">
            <pre data-language="py">
              <code data-language="py">def sum(a, b): return a + b
</code>
            </pre>
          </div>
        </div>
      </div>
    </section>
  </div>
  <div data-name="body">
    <h3 id="tiny-utilities" data-name="title">Tiny utilities</h3>
    <p>A starter kit of one-liners.</p>
  </div>
</div>
export const sum = (a, b) => a + b;
def sum(a, b): return a + b

Tiny utilities

A starter kit of one-liners.

<div class="rf-card" data-media-position="top" data-rune="card" data-density="full">
  <div data-section="media" data-name="media" class="rf-card__media" data-guest-fit="clip">
    <section class="rf-codegroup rf-codegroup--scroll" data-overflow="scroll" data-elevation="flat" data-rune="code-group" data-density="compact" data-code-host="true">
      <div role="tablist" data-name="tabs" class="rf-codegroup__tabs">
        <button data-name="tab" role="tab" class="rf-codegroup__tab">
          <span>JavaScript</span>
        </button>
        <button data-name="tab" role="tab" class="rf-codegroup__tab">
          <span>Python</span>
        </button>
      </div>
      <div data-name="panels" class="rf-codegroup__panels">
        <div role="tabpanel" data-name="panel" class="rf-codegroup__panel">
          <div class="rf-codeblock">
            <pre data-language="js"><code data-language="js">export const sum = (a, b) =&gt; a + b;
</code></pre>
          </div>
        </div>
        <div role="tabpanel" data-name="panel" class="rf-codegroup__panel">
          <div class="rf-codeblock">
            <pre data-language="py"><code data-language="py">def sum(a, b): return a + b
</code></pre>
          </div>
        </div>
      </div>
    </section>
  </div>
  <div data-name="content" class="rf-card__content">
    <div data-name="body" class="rf-card__body" data-section="body">
      <h3 id="tiny-utilities" data-name="title" class="rf-card__title">Tiny utilities</h3>
      <p>A starter kit of one-liners.</p>
    </div>
  </div>
</div>

href makes the entire card clickable. It's rendered as a stretched-link overlay, so real links inside the body or footer stay clickable too (no invalid nested <a>).

{% card href="/guide/getting-started/" %}
### Getting started
Read the five-minute guide.
{% /card %}

Media guests in a linked card

A linked card is one interaction target, so its media guest is presentational. An interactive guest in the media zone — a codegroup, tabs, a live map — is demoted: its controls go pointer-events: none (clicks fall through to the card link) and it renders its static fallback rather than live chrome. The demotion is scoped to the media zone only — a button or link in the body/footer stays clickable. Authoring an interactive guest in a linked card emits a build warning; drop href or the interactivity. In cover mode the media is always an inert backdrop, linked or not. See the interaction-posture contract for the full model.

Layout attributes

card shares the media+content layout vocabulary with bento-cell and every other media-bearing rune. The body splits on --- into media → body → footer (media-first, in source order); media-position decides where the media sits visually, independently of source order.

AttributeTypeDefaultDescription
media-positionstringtopWhere the media sits: top, bottom, start (left), end (right), or cover (media fills the card, content overlays — see below)
media-ratiostringMedia zone's share of the row when beside content (start/end): 1/3, 2/5, 1/2, 3/5, 2/3
valignstringCross-axis alignment when media is beside content: top, center, bottom, stretch
collapsestringBreakpoint at which beside layouts collapse to a stack: sm, md, lg, never
content-placestringautoCover only. Where the overlaid content anchors: <block> <inline> (each start/center/end), or auto to adapt to orientation
heightstringIntrinsic card height (named scale sm/md/lg/xl) — gives a cover or bg-only card a poster shape
aspectstringIntrinsic card aspect ratio (e.g. 16/9, 3/4) — the proportional alternative to height
hrefstringOptional whole-card link target

Elevation & frame

A card exposes two decorable surfaces (surface model): the self surface (the card box) takes elevation — a box-shadow that floats the whole tile — and the media surface (the [data-section="media"] zone) takes frame chrome: aspect, crop anchor, a silhouette drop-shadow, displacement, and oversize. The two never collide — elevation is the card's z-shadow, frame-shadow is the photo's silhouette.

{% card elevation="raised" frame-aspect="16/9" frame-anchor="top left" %}
![Dashboard](https://picsum.photos/seed/carddash/800/450)
---
### Framed media
`elevation` floats the card; `frame-*` presents the image in its media zone.
{% /card %}
<div data-rune="card" data-rune-fields="{&quot;media-position&quot;:&quot;top&quot;}" elevation="raised">
  <div data-section="media" data-name="media">
    <img src="https://picsum.photos/seed/carddash/800/450" alt="Dashboard">
  </div>
  <div data-name="body">
    <h3 id="framed-media" data-name="title">Framed media</h3>
    <p>
      <code>elevation</code>
      floats the card;
      <code>frame-*</code>
      presents the image in its media zone.
    </p>
  </div>
  <meta data-field="frame-aspect" content="16/9">
  <meta data-field="frame-anchor" content="top left">
</div>
Dashboard

Framed media

elevation floats the card; frame-* presents the image in its media zone.

<div class="rf-card" data-media-position="top" data-elevation="raised" data-rune="card" data-density="full">
  <div data-section="media" data-name="media" class="rf-card__media" data-guest-fit="clip" style="--frame-aspect: 16/9; --frame-anchor: top left">
    <img src="https://picsum.photos/seed/carddash/800/450" alt="Dashboard" />
  </div>
  <div data-name="content" class="rf-card__content">
    <div data-name="body" class="rf-card__body" data-section="body">
      <h3 id="framed-media" data-name="title" class="rf-card__title">Framed media</h3>
      <p>
        <code>elevation</code>
         floats the card; 
        <code>frame-*</code>
         presents the image in its media zone.
      </p>
    </div>
  </div>
</div>

elevation floats the card box (none/sm/md/lg); frame (or a named preset from theme/project config) decorates the media zone. Because the media zone is a clipping host, a displaced or oversized guest is cropped into a peek — frame-anchor picks the focal point. See surfaces for the full facet list.

Cover mode

media-position="cover" is the poster layout: the media well fills the card interior and the body overlays it. It's a one-attribute switch from top/bottom/start/end — the same content, restacked. The media stays a media guest, so the thin edge and --rf-radius-media are preserved; nothing else about the card changes.

{% card href="/runes/learning/recipe" media-position="cover" scrim-type="frost" scrim-blur="md" height="lg" %}
![A tequila sunrise cocktail](https://assets.refrakt.md/tequila-sunrise.png)

---

Brunch classic

### Tequila Sunrise
A bright, layered cocktail — five minutes, no shaker.
{% /card %}
<div data-rune="card" data-rune-fields="{&quot;media-position&quot;:&quot;cover&quot;,&quot;height&quot;:&quot;lg&quot;}">
  <div data-section="media" data-name="media">
    <img src="https://assets.refrakt.md/tequila-sunrise.png" alt="A tequila sunrise cocktail">
  </div>
  <p data-name="eyebrow">Brunch classic</p>
  <div data-name="body">
    <h3 id="tequila-sunrise" data-name="title">Tequila Sunrise</h3>
    <p>A bright, layered cocktail — five minutes, no shaker.</p>
  </div>
  <a data-name="link" href="/runes/learning/recipe" aria-hidden="true" tabindex="-1"></a>
  <meta data-field="scrim-type" content="frost">
  <meta data-field="scrim-blur" content="md">
</div>
A tequila sunrise cocktail

Brunch classic

Tequila Sunrise

A bright, layered cocktail — five minutes, no shaker.

<div class="rf-card rf-card--cover" data-media-position="cover" data-height="lg" data-scrim-type="frost" data-scrim-blur="md" data-rune="card" data-density="full" data-cover-scope="full">
  <div data-section="media" data-name="media" class="rf-card__media" data-guest-fit="clip" data-guest-posture="presentational">
    <img src="https://assets.refrakt.md/tequila-sunrise.png" alt="A tequila sunrise cocktail" />
  </div>
  <div data-name="content" class="rf-card__content" data-color-scheme="dark">
    <p data-name="eyebrow" class="rf-card__eyebrow">Brunch classic</p>
    <div data-name="body" class="rf-card__body" data-section="body">
      <h3 id="tequila-sunrise" data-name="title" class="rf-card__title">Tequila Sunrise</h3>
      <p>A bright, layered cocktail — five minutes, no shaker.</p>
    </div>
  </div>
  <a data-name="link" href="/runes/learning/recipe" aria-hidden="true" tabindex="-1" class="rf-card__link"></a>
</div>

A cover card has no natural height (there's no side-by-side content to set it), so give it one with height (the named scale sm/md/lg/xl) or aspect (e.g. aspect="3/4"). When the card sits in an external grid track (a bento cell, a collection row), the track wins; otherwise height/aspect set the shape, falling back to a portrait default. height/aspect are also the standalone analog of a bento row-span for a bg-only card — a card with no media at all, just a background fill, needs an intrinsic height to show.

Placing the overlay — content-place

content-place anchors the overlaid content. It's a two-axis logical value — <block> <inline>, each start / center / end — mapping to align/justify. The default, auto, adapts to the card's own orientation via a container query: a portrait card drops the content to the block end (a caption band), a landscape card pulls it to the inline start (a side panel). Pin it with an explicit value, e.g. content-place="center center" for a centred hero.

content-place is inert outside cover mode — there's no overlay to anchor — and the engine warns if you set it on a non-cover card.

The cover scrim

Overlaying text on an arbitrary photo is a legibility footgun, so cover mode turns on a default scrim on the media surface — a gradient weighted toward the content edge (it follows content-place, and you can pin it with an explicit scrim="top|bottom|left|right"). The example above instead uses the frost treatment (scrim-type="frost", with scrim-blur on the none/sm/md/lg scale): a frosted-glass blur banded behind the text rather than a gradient. Either way the scrim is masked to the content edge so it never blurs or darkens the whole image, and the foreground reads against the darkened media automatically (scrim-tone controls the polarity — a dark scrim yields light text). Opt out with scrim="none", or set a tint for a bespoke overlay colour.

The scheme is scoped to the overlaid text, not the whole card: the card's own surface — the padded edge framing the media well — keeps the page palette (light in light mode, dark in dark mode), while only the text sitting on the image flips to stay legible.

In a recipe, cover mode uses header scope: only the title block overlays the image (a poster header), and the ingredients/steps flow below on the page palette — the same media-position="cover" switch, scoped to the part that should sit on the image.

Feeding a card from a collection

Because the card is plain, a collection body template wires entity fields into it — the card neither knows nor cares that it's in a collection:

{% collection type="page" filter="url:/blog/*" sort="date-desc" layout="grid" %}
{% card href=$item.url %}
### {% $item.data.title %}
{% date($item.data.date) %} — {% $item.data.description %}
{% /card %}
{% /collection %}

See collection → per-item templates for the $item contract.

In a grid, every card in a row shares the height of the tallest one, and the body stretches to fill that height — so the footers line up along a common baseline regardless of how much body each card has.

Output contract

<div class="rf-card" data-rune="card" data-media-position="top">
  <div data-section="media" data-name="media">…</div>
  <div data-name="content">
    <div data-name="body">
      <p data-name="eyebrow">…</p>  <!-- leading paragraph before a heading -->
      <h3 data-name="title">…</h3>  <!-- the body's leading heading -->

    </div>
    <footer data-name="footer">…</footer>
  </div>
  <!-- only when href is set -->
  <a data-name="link" href="…" aria-hidden="true" tabindex="-1"></a>
</div>

The media / content split, responsive collapse, and mobile full-bleed media header all come from the shared layouts/split.css (keyed off data-layout / data-section="media" / data-media-position), so card ships only its box chrome.

In cover mode the root carries data-media-position="cover" plus data-cover-scope="full" (and rf-card--cover); content-place emits data-content-place and the --cover-place-block/--cover-place-inline custom properties, height emits data-height, and aspect an inline aspect-ratio. The default scrim is added unless scrim="none" or a tint opts out; scrim-type="frost" adds data-scrim-type/data-scrim-blur (the scrim renders on the media well's ::after, never the self-surface bg layer). The overlay foreground scheme (data-color-scheme) lands on the [data-name="content"] overlay — not the root — so the card box surface keeps the page palette.

Attributes

AttributeTypeRequiredDescription
hrefstringOptional link target; makes the whole card clickable.
content-placestringCover overlay anchor: "<block> <inline>" (e.g. "end start") or "auto"
height"sm" | "md" | "lg" | "xl"Intrinsic card height (named scale) for cover / bg-only cards
aspectstringIntrinsic card aspect ratio (e.g. "16/9", "3/4") for cover / bg-only cards

Inherited from the split layout preset

AttributeTypeRequiredDescription
media-position"top" | "bottom" | "start" | "end" | "cover"Where the media zone sits relative to the content: above (top), below (bottom), or beside (start/end)
media-ratio"1/3" | "2/5" | "1/2" | "3/5" | "2/3"Media zone’s share of the row width when media is beside content (start/end)
valign"top" | "center" | "bottom" | "stretch"Cross-axis alignment when media is beside content (start/end); applies to the shorter zone
collapse"sm" | "md" | "lg" | "never"Breakpoint at which side-by-side layouts collapse to a single stacked column

Universal attributes

bg

The background layer (SPEC-088): image, video, gradient, flat overlay wash and legibility scrim, in a single injected layer behind the rune's content.

AttributeTypeRequiredDescription
bgstringBackground preset applied to this block
bg-fromstringGradient start colour — a semantic token name (→ var(--rf-color-*))
bg-gradient"to-t" | "to-b" | "to-l" | "to-r" | "to-tr" | "to-br" | "to-bl" | "to-tl"Gradient direction (bounded named set)
bg-gradient-type"linear" | "radial" | "conic"Gradient type
bg-tostringGradient end colour — a semantic token name
bg-viastringOptional middle gradient stop — a semantic token name
scrim"top" | "bottom" | "left" | "right" | "none"Scrim direction (heaviest edge); presence turns the scrim on, "none" opts out of the default cover scrim
scrim-blur"none" | "sm" | "md" | "lg"Frost scrim blur amount
scrim-strength"sm" | "md" | "lg"Gradient scrim strength
scrim-tone"dark" | "light"Whether the scrim darkens (for light text) or lightens (for dark text)
scrim-type"gradient" | "frost"Scrim treatment: gradient (default) or frost (backdrop blur)
dropcap

Per-instance drop-cap opt-in (SPEC-108).

AttributeTypeRequiredDescription
dropcapbooleanStyle the opening letter of a prose body as a drop cap (SPEC-108). Honoured only when the body reads as prose; ignored otherwise.
elevation

The chrome/depth ladder (SPEC-107). The skin maps each rung to a chrome bundle by attribute, so there is no BEM class.

AttributeTypeRequiredDescription
elevation"sunken" | "flush" | "flat" | "raised" | "floating" | "overlay" | "none" | "sm" | "md" | "lg"Surface depth on the SPEC-107 ladder (sunken→overlay); none/sm/md/lg are deprecated aliases
frame

Surface chrome (SPEC-086): a named preset from the theme registry with inline `frame-*` overrides layered on top.

AttributeTypeRequiredDescription
framestringNamed frame preset presenting this block's media surface
frame-anchorstringCrop focal point when the guest is cut (object-position)
frame-aspectstringAspect ratio of the framed media, e.g. "16/9"
frame-displace"top" | "bottom" | "end" | "bottom-end" | "top-end"Edge/corner the framed guest moves toward
frame-displace-mode"peek" | "bleed"How displacement renders: `peek` (default) translates the guest visually inside its frame target; `bleed` uses negative margin on the media zone so following layout pulls up — extends past a section like a hero without leaving a gap above
frame-offset"none" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl"Displacement distance (non-linear named scale: sm…xl ride the block-spacing tokens, 2xl/3xl/4xl step up to section-spacing for bleed-mode displacements that clear a section edge)
frame-overflow"clip" | "bleed"How the frame handles a guest whose content is wider than it: `clip` (default) keeps it inside the rounded frame; `bleed` runs an overflowing guest out to the layout edge on a narrow viewport so it reads as cropped by the screen (a bleed host only — hero/feature)
frame-oversizestringHow far the guest exceeds its slot (scale factor)
frame-placestringGuest-box alignment in the slot (e.g. "left top")
frame-shadow"none" | "sm" | "md" | "lg"Silhouette drop-shadow strength for the framed media
inset

Internal padding override.

AttributeTypeRequiredDescription
inset"flush" | "tight" | "default" | "loose" | "breathe"Inner padding of this block
motion

Scroll-reveal entrance (SPEC-105). The author declares the character, the theme owns the choreography, a behaviour owns the timing.

AttributeTypeRequiredDescription
reveal"none" | "fade" | "slide" | "scale" | "blur"Scroll-reveal entrance character (none|fade|slide|scale|blur); the theme owns the choreography
staggerbooleanCascade this block's items in as it reveals (no-op on single-child runes)
reading

Editorial register for body text (SPEC-108). The author picks the register; the theme owns the magnitude.

AttributeTypeRequiredDescription
reading"fine" | "ui" | "prose"Reading register for this block’s body (SPEC-108): fine | ui | prose. The theme owns the editorial treatment.
spacing

Block-level rhythm override.

AttributeTypeRequiredDescription
spacing"flush" | "tight" | "default" | "loose" | "breathe"Vertical spacing above and below this block
substrate

Generated pattern fills (SPEC-087). Markers only — the engine sets the attributes and cell/opacity custom properties, CSS draws the pattern.

AttributeTypeRequiredDescription
substrate"dots" | "grid" | "lines" | "cross" | "checker" | "none"Generated surface pattern
substrate-fill"inherit" | "inset"Surface fill the pattern sits on (full colour stays with tint)
substrate-opacity"sm" | "md" | "lg"Pattern ink strength
substrate-size"sm" | "md" | "lg"Pattern cell size
substrate-target"self" | "media"Which surface the pattern fills (overrides the rune/theme default)
tint

Per-rune colour override (SPEC-053): a named tint from the theme registry, with inline per-token overrides layered on top.

AttributeTypeRequiredDescription
tintstringColor tint preset applied to this block
tint-mode"auto" | "dark" | "light"Whether the tint adapts to auto, dark, or light mode
width

The track a block rune occupies.

AttributeTypeRequiredDescription
width"compact" | "narrow" | "content" | "wide" | "full"Maximum width constraint for this block
Not availableWhy
prominencethis rune has no page-section header

See also

  • collection — feeds cards from registry entities via a body template.
  • The recipe rune is built on the same split-layout skeleton (media zone + delimited content).