Skip to content

Components / Sections

Blog Article

The page of one blog post: a header with the date, the reading time, the title, the author, the tags and the cover, then the body in a measured column that a beam fills as it is read, beside a contents list that follows the reader. The body is your rendered Markdown or MDX, and the contents list comes from the headings Astro returns with it. No script of its own: the beam is TracingBeam, the list is ReadingToc.

Live demo

All posts

Sample post: a page built for reading

This post is sample text. It describes the page it sits on, so every sentence can be checked against what the page does.

What follows you as you read

Two things on this page follow the reader: the line beside the text and the list of headings in the side column.

The beam

The line fills as the article scrolls past. It runs on the article's own view timeline, so the browser draws it and no script counts pixels.

The contents list

The list marks the heading you are reading. Its links are plain anchors, and a jump stops below the sticky bar.

Components a post brings

A post imports what it uses: a marker, code tabs, a gallery and a link card.

npx astro add moonarc
pnpm astro add moonarc
Sample picture 1, a gradient studySample picture 2, a gradient studySample picture 3, a gradient study

The list beside this text is Reading TocReading TocA table of contents that marks the heading you are reading., and the line is its sibling.

Without JavaScript

The line is full and still, the list is a list of links, the pictures link to their full size, and the card is a plain link.

Live, from the library itself, at full size: scroll, hover, navigate away and back. The demo is never gated.

Variants · each row writes the attribute the prop writes; the section's own CSS answers

tone
density

Measured

JavaScript of its own

0 B

Uses ReadingToc and the shared runtime (1.9 kB raw, once per site). With those included: 2.6 kB raw.

CSS 15.8 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.

Browser support

Limited availability

Chrome 115 · not Firefox · Safari 26; needs scroll-timeline, property. Elsewhere: Firefox shows the beam as a full, static line with its dot at the end; the contents list still marks the current heading, and the post reads the same..

Install Pro

The demo above is never gated; the source is. With a key in components.json (see account), the registry serves it like any other item:

terminal
npx shadcn@latest add @moonarc-pro/blog-article
src/pages/index.astro
---
import BlogArticle from '@/components/moonarc/pro/BlogArticle.astro';
import { getEntry, render } from 'astro:content';
// src/pages/blog/[slug].astro: the post from your collection, rendered; headings are the ids the Markdown pipeline wrote
const post = await getEntry('posts', Astro.params.slug!);
const { Content, headings } = await render(post!);
---
<BlogArticle
  title={post.data.title}
  date={post.data.pubDate}
  updated={post.data.updated}
  author={{ name: 'Ada Park', role: 'Engineering' }}
  tags={post.data.tags}
  minutes={6}
  cover={{ src: post.data.cover.src, alt: post.data.cover.alt }}
  headings={headings}
  offset={64}
  back={{ href: '/blog/', label: 'All posts' }}
  next={{ href: '/blog/next-post/', title: 'The next post' }}
>
  <Content />
</BlogArticle>

<!-- A post imports what it uses itself (HighlightMarker, CodeTabs, Lightbox, LinkPreview): they are the post's, not the
     section's, so a post without them ships none of their JavaScript. Code fences go through Shiki at build; pass
     markdown.shikiConfig the readableCode() transformer so their colours keep 4.5 : 1. -->

Without a key the registry answers 401 with an upgrade message. Everything this section composes is free: tracing-beam, reading-toc, page-transition.

Props

PropTypeDefaultDescription
titlestringnoneThe post's title: the h1 unless `level` says 2. Required.
level1 | 21Heading level of the title: 1 on the post page, 2 under another h1 (a demo). Write the body's headings from h2 either way.
datestring | DatenonePublication day. A string prints as given and is the datetime; a Date (a collection's z.coerce.date()) prints through Intl.DateTimeFormat in UTC, the day the file names, with that day in ISO form as the datetime. BlogGrid prints the same day the same way.
updatedstring | DatenoneDay of the last real change, printed after the date through updatedLabel as a second <time>.
localestringunset (the runtime's)Intl locale a Date prints in.
dateStyle'full' | 'long' | 'medium' | 'short''medium'Intl date style of a Date (medium: Mar 2, 2026).
updatedLabelstring'Updated {date}'The updated line; {date} becomes the <time>. Without {date} the build fails.
author{ name, role?, avatar? }noneWho wrote it. avatar is a URL or an imported image (its src, width and height are used); it is decorative (alt="") because the name is printed beside it.
tagsstring[][]Tags, printed as a list under the author.
minutesnumbernoneReading time in minutes, rounded, at least 1. Compute it at build from the body's word count.
minutesLabelstring'{n} min read'The reading-time text; {n} is the number. Without {n} the build fails.
cover{ src, alt }noneThe cover image: a URL or an imported image. Its transition name is ma-cover- and the page's path, the name BlogGrid gives the same post's card, so the router morphs one into the other. alt describes the picture, or is empty for a decorative one.
headings{ depth, slug, text }[][]Astro's headings from render(): depth 2 and 3 become the contents list, linking to the ids the Markdown pipeline wrote.
tocbooleantrueShow the contents list. It is rendered only when two headings or more qualify.
tocTitlestring'On this page'The contents list's visible title and the name of its nav.
offsetnumber80Height of the page's sticky bar in px. The side column sticks below it, a jump to a heading stops below it (scroll-margin), and ReadingToc counts a heading as current from that line down.
back{ href, label }noneA link back above the title (the blog index); its arrow turns in a right-to-left document.
prev{ href, title }noneThe previous post, linked under the body with rel="prev".
next{ href, title }noneThe next post, linked under the body with rel="next".
prevLabelstring'Previous'The small line above the previous post's title.
nextLabelstring'Next'The small line above the next post's title.
pagerLabelstring'More posts'The name of the nav that holds the two links.
tone'surface' | 'muted' | 'ink' | 'brand'unset (paints nothing)The ground: the page surface, a muted band, an always-dark ink panel, or a deep brand panel. Each paints --background, --ma-tone-muted (over --muted, with a --border hairline), --ma-tone-ink or --ma-tone-brand (over --primary) with the matching text. With theme.css, a painted tone also re-declares --card, --border, --input and --ring inside the section. The beam is the theme's primary on an unset, surface or muted ground and the tone's text on ink and brand; the contents list's rule and mark follow the text.
density'compact' | 'cozy' | 'roomy''cozy'Block padding and the main gaps ×0.8 / ×1 / ×1.25 through --ma-density.

Theming and overrides

The section reads the theme contract (shadcn's variable names with the base tokens as fallback), so a pasted theme or one --ma-brand recolours it. Its class names are the override surface and stay stable; a named slot replaces the matching prop with your own markup.

Classes

.ma-article.ma-article__arrow.ma-article__author.ma-article__avatar.ma-article__back.ma-article__beam.ma-article__cover.ma-article__footer.ma-article__head.ma-article__inner.ma-article__layout.ma-article__layout--side.ma-article__meta.ma-article__name.ma-article__pager.ma-article__prose.ma-article__role.ma-article__side.ma-article__step.ma-article__step--next.ma-article__step-label.ma-article__step-title.ma-article__tags.ma-article__title.ma-article__toc.ma-article__who.ma-toc__link

Slots

slot="aside"slot="footer"

Reduced motion

The beam is a full, still line, the contents list moves its mark without easing, and the back arrow does not travel. The list still marks the current heading.

With ClientRouter

ReadingToc binds through the runtime's onMount and drops its scroll listener before every swap, so a post reached by a ClientRouter navigation has one listener. The cover's transition name pairs it with BlogGrid's card for the same post.

Why it is built this way

Replaces: Blog post layouts (Tailwind Plus) · Article pages (shadcnblocks) · TracingBeam post layouts (Aceternity). See the migration table.

npx astro add moonarc
pnpm astro add moonarc
bunx astro add moonarc

A code block with tabs (for example npm, pnpm, bun) and a copy button per panel.

every browserclick

A highlighter sweeps across the phrases you marked, one after another, when the sentence scrolls into view.

every browserscroll
Study 1Study 2Study 3

click · grows from its thumbnail · ← →

A thumbnail gallery whose images open full size: the image grows out of its own thumbnail on a same-document view transition and returns to it on close, and ← → move through the set.

newly · Chrome 111 · Firefox 144 · Safari 18click

A link that shows a small card (image, title, one line, domain) after a moment on hover or focus, above everything, flipping when there is no room.

newly · Chrome 125 · Firefox 147 · Safari 26hover

A table of contents that lights up as you read.

every browserscroll

A line beside an article fills as it is read, with a dot at its head.

Chrome 115 · not Firefox · Safari 26scroll
Release

Endpoint, Homescreen, Estate, Practice and Stay, templates for developer products, mobile apps, estate agents, practices and places to stay, and thirty sections

Endpoint, a template for a product with an HTTP API: a console on the home page that sends a request and streams the response back with its status, its time and its region, an API reference written at build from your OpenAPI document, a quickstart and four guides, usage pricing with a slider, a status page, the regions on a map, a changelog with a feed, search and a contact page for sales.

Shows blog posts as cards, with the first post featured across two columns.

newly · Chrome 111 · Firefox 144 · Safari 18scrollhoverclick

Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown