npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

payload-better-preview

v2.0.0

Published

Better live preview for Payload CMS — hover highlighting, bi-directional admin ↔ preview sync, and click-to-focus

Readme

payload-better-preview

Better live preview for Payload CMS — hover highlighting with block identification, bi-directional admin/preview sync, and smooth transitions.

Features

Hover Highlighting

Blue overlay marks the block under the cursor with a label badge showing block type, index, and name. Nested blocks get a dashed parent overlay with breadcrumb labels.

Hover Highlighting

Admin → Preview Sync

Click a block row in the admin editor — the preview scrolls to that block and highlights it with a flash effect.

Admin to Preview Sync

Preview → Admin Sync

Click a block in the preview — the admin editor scrolls to the corresponding block row, expands it if collapsed (including all ancestor rows for nested blocks), and highlights it.

Preview to Admin Sync

Other

  • Draft-only — Zero impact on published pages
  • Scroll/Resize tracking — Overlay follows block position smoothly
  • Infinite nesting — Bi-directional sync works at any block nesting depth

Installation

pnpm add payload-better-preview
# or
npm install payload-better-preview

1. Register the plugin

// payload.config.ts
import { betterPreview } from 'payload-better-preview'

export default buildConfig({
  plugins: [
    betterPreview({
      accentColor: '#3b82f6', // optional
      scrollAlign: 'start',   // optional
      scrollOffset: 128,      // optional
    }),
  ],
  admin: {
    livePreview: {
      collections: ['pages'],
      url({ data }) {
        return `/${data.id}`
      },
    },
  },
})

2. Add data attributes to block wrappers

Each rendered block must have three data attributes:

| Attribute | Description | |---|---| | data-block | Block type slug, e.g. "hero", "text" | | data-block-index | 0-based index within the blocks field | | data-block-field | Field path — see below | | data-block-name | Optional display name shown in the overlay label |

The data-block-field value must match the Payload field name so the plugin can map blocks between the admin and the preview:

export function BlockRenderer({ block, index, field }) {
  return (
    <div
      data-block={block.blockType}
      data-block-index={index}
      data-block-field={field}
      data-block-name={block.name} // optional
    >
      {/* block content */}
    </div>
  )
}

export function RenderBlocks({ blocks, field }) {
  return blocks.map((block, index) => (
    <BlockRenderer key={block.id} block={block} index={index} field={field} />
  ))
}
// pass the exact Payload field name
<RenderBlocks blocks={page.content} field="content" />
<RenderBlocks blocks={page.sidebar} field="sidebar" />

3. Render <BetterPreview /> in your page

import { BetterPreview } from 'payload-better-preview/client'

export default async function Page() {
  const { isEnabled: draft } = await draftMode()

  return (
    <>
      {draft && <LivePreviewListener />}
      {draft && <BetterPreview />}
      {/* ... rest of page */}
    </>
  )
}

Nested blocks

For blocks that contain other blocks, construct the field prop by concatenating the parent field, parent index, and child field name with hyphens. This matches the ID pattern Payload generates in the admin and enables sync at any depth:

export function NestedBlockRenderer({ block, index, field }) {
  return (
    <div
      data-block={block.blockType}
      data-block-index={index}
      data-block-field={field}
    >
      <RenderBlocks
        blocks={block.content}
        field={`${field}-${index}-content`}
        {/* e.g. "content-0-content", "content-0-content-1-sidebar" */}
      />
    </div>
  )
}

Plugin options

| Option | Type | Default | Description | |---|---|---|---| | disabled | boolean | false | Disable the plugin entirely | | accentColor | string | '#3b82f6' | Highlight color for overlays and flash effects in the admin | | scrollAlign | 'start' \| 'center' \| 'end' | 'start' | Block alignment when scrolling in admin | | scrollOffset | number | 128 | Top offset in px when scrolling in admin — accounts for the sticky admin header |

<BetterPreview /> props

| Prop | Type | Default | Description | |---|---|---|---| | accentColor | string | '#3b82f6' | Highlight color for overlays and flash effects in the preview | | scrollAlign | 'start' \| 'center' \| 'end' | 'start' | Block alignment when scrolling in the preview | | scrollOffset | number | 0 | Top offset in px when scrolling in the preview — useful for fixed headers | | showToggle | boolean | true | Show the built-in toggle button | | toggleComponent | React.ComponentType<ToggleProps> | — | Replace the built-in toggle with a custom component |

Scroll offset in the preview

The preview uses window.scrollTo internally to avoid propagating scroll to the parent admin frame. Because of this, scroll-margin-top has no effect. Use scrollOffset instead:

<BetterPreview scrollOffset={80} />

Custom toggle component

import type { ToggleProps } from 'payload-better-preview/client'

function MyToggle({ enabled, onToggle }: ToggleProps) {
  return (
    <button onClick={onToggle} style={{ position: 'fixed', top: 16, right: 16 }}>
      {enabled ? 'Sync on' : 'Sync off'}
    </button>
  )
}

<BetterPreview toggleComponent={MyToggle} />

How it works

  • Admin → Preview: clicking anywhere inside a block row in the admin sends a scroll-to-block message to the preview iframe. The preview scrolls to the matching [data-block-field][data-block-index] element and shows a highlight overlay.
  • Preview → Admin: clicking a block in the preview sends a focus-block message to the admin. The admin expands all ancestor rows (for nested blocks), then scrolls to and highlights the target block row.
  • All communication is via window.postMessage. No network requests, no shared state.

<BetterPreview /> is a 'use client' component that renders null (no React DOM output). It injects 3 absolutely-positioned DOM elements into document.body:

  1. Overlay — Primary block highlight (solid blue border)
  2. Parent Overlay — For nested blocks (dashed border, subtle)
  3. Label — Info badge with block type and index

All interaction is handled via event delegation on document, so it survives DOM updates from live preview re-renders.

Contributors