masonix
Examples

Scroll Seek Placeholders

Render lightweight placeholders while a virtual feed scrolls quickly.

Scroll seek is useful when real cards are expensive. While velocity is high, Masonix renders placeholders that keep the same layout size.

Example

import { useRef } from 'react';

type FeedItem = {
  id: string;
  author: string;
  handle: string;
  topic: string;
  body: string;
  note?: string;
  tags: string[];
  metric: string;
  gradient: string;
};

function FeedCard({ item }: { item: FeedItem }) {
  return (
    <article
      className={clsx(
        'overflow-hidden',
        'rounded-xl border',
        'border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-950',
      )}
    >
      <div className="p-4" style={{ background: item.gradient }}>
        <div className="flex items-center justify-between gap-3">
          <span className="rounded-full bg-white/85 px-2 py-1 text-xs font-medium text-zinc-900">
            {item.topic}
          </span>
          <span className="text-xs font-medium text-white/80">
            {item.metric}
          </span>
        </div>
        <p className="mt-8 text-xs font-medium leading-5 text-white/80">
          {item.tags.join(' / ')}
        </p>
      </div>
      <div className="p-4">
        <div>
          <h3 className="text-sm font-semibold text-zinc-950 dark:text-zinc-50">
            {item.author}
          </h3>
          <p className="text-xs text-zinc-500">{item.handle}</p>
        </div>
        <p className="mt-3 text-sm leading-6 text-zinc-600 dark:text-zinc-400">
          {item.body}
        </p>
        {item.note ? (
          <p className="mt-3 rounded-lg bg-zinc-100 p-3 text-xs leading-5 text-zinc-600 dark:bg-zinc-900 dark:text-zinc-400">
            {item.note}
          </p>
        ) : null}
        <div className="mt-4 flex flex-wrap gap-2">
          {item.tags.map((tag) => (
            <span
              key={tag}
              className="rounded-full border border-zinc-200 px-2 py-1 text-xs text-zinc-500 dark:border-zinc-800"
            >
              {tag}
            </span>
          ))}
        </div>
      </div>
    </article>
  );
}

function FeedSkeleton({
  height,
}: MasonryRenderProps<FeedItem> & { height: number }) {
  return (
    <div
      className={clsx(
        'flex flex-col overflow-hidden',
        'rounded-xl border',
        'border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-950',
      )}
      style={{ height }}
    >
      <div className="h-24 shrink-0 animate-pulse bg-zinc-100 dark:bg-zinc-900" />
      <div className="space-y-3 p-4">
        <div className="h-3 w-1/2 rounded-full bg-zinc-100 dark:bg-zinc-900" />
        <div className="h-3 w-5/6 rounded-full bg-zinc-100 dark:bg-zinc-900" />
        <div className="h-3 w-2/3 rounded-full bg-zinc-100 dark:bg-zinc-900" />
        <div className="h-3 w-1/3 rounded-full bg-zinc-100 dark:bg-zinc-900" />
      </div>
    </div>
  );
}

export function FastFeed({ items }: { items: FeedItem[] }) {
  const scrollContainerRef = useRef<HTMLDivElement>(null);

  return (
    <div
      ref={scrollContainerRef}
      className={clsx(
        'min-w-0 overflow-x-hidden overflow-y-auto overscroll-contain',
        'h-96 p-3',
        'rounded-lg border',
        'border-zinc-200 dark:border-zinc-800',
      )}
    >
      <MasonryVirtual
        items={items}
        columns={{ 0: 1, 600: 2 }}
        gap={12}
        estimatedItemHeight={280}
        scrollContainer={scrollContainerRef}
        scrollSeek={{
          velocityThreshold: 900,
          placeholder: FeedSkeleton,
        }}
        itemKey={(item) => item.id}
        render={({ data }) => <FeedCard item={data} />}
      />
    </div>
  );
}

When to use

Use this when cards include expensive media, charts, syntax highlighting, or heavy interactive controls.

Notes

  • Placeholder height should be used directly.
  • Keep placeholders visually quiet.
  • Real item keys, positions, and measurements remain intact.

On this page