Circular Text
TextA ring of text turning slowly around an icon, a logo, or nothing.
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.
A letter every Friday
One idea, one example and one thing to try, every Friday morning.
Sitename writer
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
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
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.
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:
npx shadcn@latest add @moonarc-pro/hero-bento---
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.
| Prop | Type | Default | Description |
|---|---|---|---|
eyebrow | string | none | A short line above the heading, with a dot before it. Slot "eyebrow" wins. |
headline | string | none | The heading. Slot "headline" wins and may hold an <em>, set in the emphasis face and colour. |
lede | string | none | A line under the heading. |
primary | { href: string; label: string } | none | The main action, filled, with an arrow. |
secondary | { href: string; label: string } | none | The second action, outlined. |
person | { name: string; role?: string; photo?: string } | none | The 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. |
badge | string | none | Words 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? } | none | The 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 } | none | The 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 }[] } | none | Where 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. |
externalLabel | string | '(another site)' | What a screen reader hears after a link to another site in the links tile. |
block | 1 | 2 | 3 | 4 | 5 | 5 | The 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. |
level | 1 | 2 | 1 | Heading 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. |
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.
.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
slot="eyebrow"slot="headline"
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.
ClientRouterThe 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.
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.
Counts a number up from zero to its value when it scrolls into view, with thousands separators, decimals, prefix and suffix.
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.
A drawn portrait for a slot that has no photograph yet: a speaker, an artist, a host, a member of a team.
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.
A row of bars drawn from a word, like the shape of an episode's sound.
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.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown