Skip to content

Components / Sections

Hero Bento

The first screen of one person's work, as a bento. On a strong colour block sit the headline, a line and two actions, and beside them soft tiles in the palette's tints: the person drawn (PortraitArt) or photographed, with a ring of words turning at its corner; the latest piece with its waveform or its drawing; the count of readers, rising as it comes into view (core CountUp); and the places to find them elsewhere. The tiles lean toward the pointer (core Tilt). Zero JavaScript of its own.

Live demo

A letter every Friday

Small things on the web, made with care

One idea, one example and one thing to try, every Friday morning.

Sitename writer

Latest issue

The last ten percent

2 October 2026 · 6 min read

12,480

people read it every Friday

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

density

Measured

JavaScript of its own

0 B

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

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

Browser support

Baseline · newly available

Chrome 105 · Firefox 128 · Safari 17; needs container-units, has, property, counter-style. Elsewhere: the count shows its final number at once without @property; without :has() the latest piece's link shows no focus ring; without container units the headline keeps the page's text size and the intro and the tiles stay stacked.

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/hero-bento
src/pages/index.astro
---
import HeroBento from '@/components/moonarc/pro/HeroBento.astro';
---
<HeroBento
  eyebrow="Weekly newsletter"
  lede="One essay a week on making a living from small things. Free, every Friday."
  primary={{ href: '#signup', label: 'Subscribe' }}
  secondary={{ href: '/archive/', label: 'Read the archive' }}
  person={{ name: 'Your Name', role: 'Writer and illustrator' }}
  badge="New issue every Friday · "
  latest={{ kind: 'Issue 42', title: 'The slow way to a first thousand readers', meta: '8 min read · 3 October', href: '/issues/42/' }}
  count={{ to: 12400, suffix: '+', label: 'readers every Friday' }}
  links={{ title: 'Elsewhere', items: [{ label: 'Podcast', href: 'https://example.com/podcast' }, { label: 'Courses', href: '/courses/' }] }}
>
  <span slot="headline">Making a living from <em>small things</em></span>
</HeroBento>

Without a key the registry answers 401 with an upgrade message. Everything this section composes is free: portrait-art, count-up, circular-text, tilt, motif, waveform.

Props

PropTypeDefaultDescription
eyebrowstringnoneA short line above the heading, with a dot before it. Slot "eyebrow" wins.
headlinestringnoneThe heading. Slot "headline" wins and may hold an <em>, set in the emphasis face and colour.
ledestringnoneA line under the heading.
primary{ href: string; label: string }noneThe main action, filled, with an arrow.
secondary{ href: string; label: string }noneThe second action, outlined.
person{ name: string; role?: string; photo?: string }noneThe person. The name draws the portrait (PortraitArt: the same name, the same drawing), or photo shows their photograph in its place (a path under public/ or a URL). role is the caption, a pill at the portrait's lower left with the name in bold and then the role. The portrait and the photograph are decoration, so without a role the name is not printed in this tile.
badgestringnoneWords that turn in a ring at the portrait's corner (CircularText). End them with a separator so the loop closes: "New issue every Friday · ". Decoration: say it in the lede too.
latest{ kind, title, meta?, href, look?, art? }noneThe latest piece: its kind in the mono face, its title (the link, stretched over the whole tile), a line of detail and where it is. look is wave (a waveform drawn from the title, for audio and video) or tile (a drawing, the default); art (sun, gate, flower, grid, phone, plate, wave or shapes) picks the drawing, and it is drawn from the title when unset.
count{ to: number; label: string; prefix?: string; suffix?: string; separator?: string }noneThe count that rises (CountUp, over 1600 ms, as the tile comes into view): readers, listeners, students. separator is the thousands separator, a comma by default.
links{ title?: string; items: { label: string; href: string }[] }noneWhere else to find them: a short title in the mono face, and the links as outlined pills across the foot of the tiles. A list with no items draws no tile.
externalLabelstring'(another site)'What a screen reader hears after a link to another site in the links tile.
block1 | 2 | 3 | 4 | 55The colour block the panel paints: the strong one, 5 in every palette.
tiles(1 | 2 | 3 | 4 | 5)[][1, 2, 3, 4]Four colour blocks, one per tile in order: the portrait, the latest piece, the count, the links.
level1 | 21Heading level: 1 as the page's hero, 2 under another h1.
density'compact' | 'cozy' | 'roomy''cozy'Block padding ×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-bento.ma-bento__actions.ma-bento__art.ma-bento__btn.ma-bento__btn--primary.ma-bento__btn--secondary.ma-bento__caption.ma-bento__count.ma-bento__count-label.ma-bento__eyebrow.ma-bento__headline.ma-bento__inner.ma-bento__intro.ma-bento__kind.ma-bento__latest.ma-bento__lede.ma-bento__link.ma-bento__links.ma-bento__meta.ma-bento__panel.ma-bento__photo.ma-bento__ring.ma-bento__tile.ma-bento__tile--latest.ma-bento__tile--links.ma-bento__tile--portrait.ma-bento__tiles.ma-bento__wave.ma-portrait

Slots

slot="eyebrow"slot="headline"

Reduced motion

The tiles do not lean, the ring is still, the count shows its final number without counting, the waveform and the drawing stay at rest, and the buttons and links change without their short lift.

With ClientRouter

The lean and the count are core Tilt and CountUp, bound by the runtime once per swap; the ring, the waveform and the rest are CSS.

Why it is built this way

Replaces: Bento grid hero sections (Tailwind Plus, Magic UI) · Link-in-bio landing pages (Linktree, Beacons). See the migration table.

A ring of text turning slowly around an icon, a logo, or nothing.

every browseralways

Counts a number up from zero to its value when it scrolls into view, with thousands separators, decimals, prefix and suffix.

newly · Chrome 91 · Firefox 128 · Safari 17scroll

Motif

Backgrounds

A drawn picture for a slot that has no photograph yet: a sun over the sea, a split gate, a flower, a grid of tiles, a phone going live, a plate set for dinner, waves under a moon or three shapes.

every browserhover

Portrait Art

Backgrounds

A drawn portrait for a slot that has no photograph yet: a speaker, an artist, a host, a member of a team.

every browserhover

Tilt

Pointer

A card leans toward the cursor in 3D, lifts slightly, catches a glare, and settles back on the active preset's spring when the cursor leaves.

every browserpointerhover

The opening of a colour-block site: the headline rises line by line out of its masks, its last word turns through a few in the emphasis face (RotatingText), a round badge with text running around it links further down the page, the lede and two pill actions sit under it, and a strip of tiles in the palette's blocks runs across the bottom (Marquee), each a drawing (Motif) or a picture with a caption.

every browserloadhover

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