Mixed Tile Mosaic

About this block

Mixed Tile MosaicPRO

A six-slot mosaic under a parenthetical eyebrow and a large heading, mixing full-bleed photo tiles with flat text tiles on muted and inverted surfaces, where the treatment belongs to the slot and every tile advances one story through a larger article pool on a timer, staggered 100 milliseconds apart so the refresh reads as a wave running left to right, pausing on hover, focus, hidden tabs, and reduced motion.

Blog51: Mixed Tile Mosaic

A blog listing section that lays a rolling newsroom into a fixed six-tile mosaic mixing full-bleed photo tiles with flat text tiles on muted and inverted surfaces. The treatment belongs to the slot rather than to the article, so on every tick each tile advances one story through a larger pool while the geometry stays put. The tiles do not turn over together: each one starts 100 ms after the tile before it, so the refresh reads as a wave running left to right.

Upgrade to Pro

Pro blocks install through the shadcn CLI with your license key and ship their full source. Docs and live previews stay open to everyone, so you can read every block's details first.

Upgrade Now

Installation

Swap YOUR_EMAIL and YOUR_KEY for the email and license key on your account. Find your license key on your account page.

Radix flavor

bash
npx shadcn add "https://ui.beste.co/r/blog51?email=YOUR_EMAIL&license_key=YOUR_KEY"

Base UI flavor

bash
npx shadcn add "https://ui.beste.co/r-base/blog51?email=YOUR_EMAIL&license_key=YOUR_KEY"

This installs the block to components/beste/block/blog51.tsx and its dependencies, including the Badge7 component it renders.

Quick start

The installed file exports blog51Demo alongside the block: the exact props behind the preview above, including its pool of eighteen articles. Spread it to get a working mosaic in one line.

tsx
import { Blog51, blog51Demo } from "@/components/beste/block/blog51";

export default function Page() {
  return <Blog51 {...blog51Demo} />;
}

Then replace the demo with your own props. Pass more articles than there are tiles so the mosaic has somewhere to rotate to; six tiles against eighteen stories is what the demo uses.

tsx
import { Blog51 } from "@/components/beste/block/blog51";

export default function Page() {
  return (
    <Blog51
      badge={{ label: "Press releases" }}
      heading="From the Northwind newsroom"
      description="Results, appointments, and reports from across the group."
      intervalMs={5000}
      items={[
        {
          image: { src: "/press/aragon.jpg", alt: "Battery cabinets in a storage hall" },
          category: "Corporate",
          title: "Aragon storage campus enters commercial operation",
          date: "March 4, 2026",
          author: { name: "Maria Santos", title: "Tech Lead" },
          href: "/press/aragon-storage",
        },
        {
          image: { src: "/press/q4.jpg", alt: "Quarterly figures on a screen" },
          category: "Markets",
          title: "Fourth-quarter revenue climbs 12 percent",
          date: "February 26, 2026",
          author: { name: "David Chen", title: "Backend Engineer" },
          href: "/press/q4-revenue",
        },
        // ...more stories; the pool is what the tiles cycle through
      ]}
    />
  );
}

Props

PropTypeDefaultDescription
badgeBadge–Badge7 parenthetical eyebrow above the heading
headingstring–Section heading
descriptionstring–Muted supporting paragraph under the heading
itemsArticle[][]The story pool the six tiles cycle through
intervalMsnumber–Milliseconds between ticks; falls back to 5000 when omitted
classNamestring–Extra classes merged onto the outer section element
ts
type Badge = {
  label: string;
};

type Article = {
  image: ArticleImage;
  category: string;
  title: string;
  date: string;
  author: Author;
  href: string;
};

type ArticleImage = {
  src: string;
  alt: string;
};

type Author = {
  name: string;
  title?: string;
  avatar?: { src: string; alt: string };
  href?: string;
};

Only author.name is rendered, in the tile byline. The optional title, avatar, and href fields on the author exist for shape parity with the other blog blocks and are not displayed here.

Behavior notes

  • Tile treatment is a property of the slot, never of the article. Slot 0 is a col-span-2 row-span-2 photo tile, slots 2 and 3 are single-cell photo tiles and slot 5 is a wide col-span-2 photo tile, while slot 1 is a tall row-span-2 bg-muted text tile and slot 4 is a wide col-span-2 inverted bg-foreground text tile. Because the articles move and the slots do not, a given story is rendered as a photograph in one position and as a flat text tile a few ticks later.
  • The six spans tile a four-column, three-row grid exactly (4 + 2 + 1 + 1 + 2 + 2 = 12 cells), so the mosaic never leaves a gap at the end of the last row. Only the first six slots are rendered; the rest of items is the pool waiting its turn.
  • Each tick advances every tile by one position in the pool, a rigid (index + 1) % poolSize translation. Slots that start out distinct therefore stay distinct, so the same story can never occupy two tiles at once and no per-slot bookkeeping is needed. With the demo's eighteen articles the mosaic walks the whole pool and returns to its starting arrangement after eighteen ticks.
  • The turn-over is staggered rather than simultaneous. Slot i begins its fade at i * 100 ms and commits its new article 300 ms after that, so a full wave takes 800 ms and finishes well inside the default 5000 ms interval. Every timeout id is collected in one array that the effect cleanup clears, and because the effect re-runs on each tick that cleanup also fires before a fresh wave is queued, so an intervalMs shorter than the wave cannot orphan a timer or strand a tile at zero opacity.
  • Rotation is skipped entirely, rather than started and stopped, when the user prefers reduced motion. The preference is read from window.matchMedia("(prefers-reduced-motion: reduce)") and kept live through a change listener. The same guard suppresses rotation while the tab is hidden (tracked via visibilitychange), while a pointer is over the grid, and while any tile holds focus, so tabbing through the links pauses the mosaic for as long as focus stays inside.
  • The initial slot map is the plain identity mapping from a lazy state initializer, with no shuffle and no randomness, so the server-rendered markup and the first client render agree and only later ticks change the arrangement. If the caller swaps in a pool of a different size, the render falls back to a fresh identity map on the same pass the mismatch appears, so a stale index never blanks out a tile for even one frame.
  • Text tiles carry no image, so an image supplied on an article currently sitting in slot 1 or 4 is not displayed. Give every article one anyway, since the rotation will move it into a photo slot.
  • Row heights are fixed by the grid (auto-rows-[11rem], md:auto-rows-[13rem]), so a swap can never reflow the layout, and titles are clamped per slot so a long headline cannot overflow its tile. No min-height reservations are needed for that reason.
  • Every tile is a Next.js Link wrapping the whole tile with a visible focus ring, and the grid carries aria-live="off" so rotating content is never announced mid-tick. Photos have no hover treatment; the only hover motion is the ArrowUpRight nudge on the text tiles, guarded by motion-safe.

More Blog blocks

View all Blog
PRO

blog50

Magazine Mosaic

PRO

blog56

Category Mosaic

PRO

blog53

Mixed Release Masonry

Get Pro access