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

react-trellis-gallery

v1.0.4

Published

High-performance trellis/gallery layout for React

Readme

react-trellis-gallery

CI npm version bundle size license

High-performance trellis/gallery layout for React with paginated grids, virtualized scrolling, and floating detail panels.

Live Playground — try every option interactively.

Features

  • Two display modes — paginated grid or virtualized infinite scroll (powered by react-window)
  • Responsive auto layout — computes rows and columns from minimum item dimensions
  • Manual layout — fixed rows × columns for precise control
  • Floating detail panels — open panels from the built-in corner triangle trigger, optional custom activation callback, and imperative ref controls (powered by Zag.js)
  • Viewport or container panel boundaries — choose full-screen movement bounds or constrain to a specific container via panelBoundary
  • Top-layer panel rendering — panels are rendered in a full-screen portal layer so consumer app z-index/stacking contexts do not cover them
  • Controlled & uncontrolled pagination — manage page state externally or let the component handle it
  • Custom pagination UI — provide your own render function, use the built-in default, or hide controls entirely
  • Draggable pagination overlay — reposition the pagination bar by dragging
  • Go to item by callback — navigate to a target match and highlight all callback matches across pages or in scroll mode, with configurable highlight color and auto-clear duration
  • Keyboard accessible — corner trigger is keyboard-focusable; custom activation callbacks can include keyboard combos
  • SSR-safe — uses isomorphic layout effects for server-side rendering compatibility
  • Fully typed — written in TypeScript with all types exported
  • Tree-shakable — ships ESM + CJS with no side effects

Installation

bun add react-trellis-gallery

or:

npm install react-trellis-gallery

Peer dependencies: react >= 19 and react-dom >= 19.

Quick Start

import { TrellisGallery } from 'react-trellis-gallery'

const items = Array.from({ length: 20 }, (_, i) => ({
  id: i,
  title: `Item ${i + 1}`,
}))

function App() {
  return (
    <div style={{ width: 800, height: 600 }}>
      <TrellisGallery
        items={items}
        mode="pagination"
        layout={{ type: 'auto', minItemWidth: 180, minItemHeight: 140 }}
        pagination={{ mode: 'uncontrolled' }}
        renderItem={(item) => <div>{item.title}</div>}
        renderExpandedItem={(item) => <div><h2>{item.title}</h2><p>Detail view</p></div>}
      />
    </div>
  )
}

The container must have explicit width and height — the gallery fills its parent.

Usage Examples

Virtualized Scroll Mode

For large datasets, switch to scroll mode which virtualizes rows via react-window:

<TrellisGallery
  items={items}
  mode="scroll"
  layout={{ type: 'auto', minItemWidth: 200, minItemHeight: 150 }}
  overscanCount={2}
  renderItem={(item) => <div>{item.title}</div>}
  renderExpandedItem={(item) => <div>{item.title}</div>}
/>

Manual Layout

Set an exact grid of 3 columns × 2 rows per page:

<TrellisGallery
  items={items}
  mode="pagination"
  layout={{ type: 'manual', rows: 2, cols: 3 }}
  gap={8}
  pagination={{ mode: 'uncontrolled' }}
  renderItem={(item) => <div>{item.title}</div>}
  renderExpandedItem={(item) => <div>{item.title}</div>}
/>

Controlled Pagination

Manage page state yourself:

const [page, setPage] = useState(0)

<TrellisGallery
  items={items}
  mode="pagination"
  layout={{ type: 'auto', minItemWidth: 200, minItemHeight: 150 }}
  pagination={{
    mode: 'controlled',
    page,
    onPageChange: setPage,
    position: 'bottom',
    align: 'center',
  }}
  renderItem={(item) => <div>{item.title}</div>}
  renderExpandedItem={(item) => <div>{item.title}</div>}
/>

Custom Pagination Controls

Replace the built-in pagination UI with your own:

<TrellisGallery
  items={items}
  mode="pagination"
  layout={{ type: 'auto', minItemWidth: 200, minItemHeight: 150 }}
  pagination={{
    mode: 'uncontrolled',
    renderControl: (vm) => (
      <div>
        <button onClick={vm.prev} disabled={!vm.hasPrev}>Prev</button>
        <span>{vm.currentPage + 1} / {vm.totalPages}</span>
        <button onClick={vm.next} disabled={!vm.hasNext}>Next</button>
      </div>
    ),
  }}
  renderItem={(item) => <div>{item.title}</div>}
  renderExpandedItem={(item) => <div>{item.title}</div>}
/>

Pass renderControl: false to hide pagination controls entirely.

Custom Panel Headers

Customize the floating panel header for each item:

<TrellisGallery
  items={items}
  mode="pagination"
  layout={{ type: 'auto', minItemWidth: 200, minItemHeight: 150 }}
  pagination={{ mode: 'uncontrolled' }}
  panelTitle={(item) => item.title}
  renderPanelHeader={(item, api) => (
    <div>
      <span {...api.titleProps}>{item.title}</span>
      <button onClick={api.togglePin}>{api.isPinned ? 'Unpin' : 'Pin'}</button>
      <button onClick={api.close}>Close</button>
    </div>
  )}
  renderItem={(item) => <div>{item.title}</div>}
  renderExpandedItem={(item) => <div>{item.title}</div>}
/>

Panel Boundary and Layering

By default, panels use viewport boundaries and render in a top-level portal layer:

<TrellisGallery
  items={items}
  mode="scroll"
  layout={{ type: 'auto', minItemWidth: 200, minItemHeight: 150 }}
  panelBoundary="viewport" // default
  renderItem={(item) => <div>{item.title}</div>}
  renderExpandedItem={(item) => <div>{item.title}</div>}
/>

To constrain panel drag/maximize bounds to a specific container:

const containerRef = useRef<HTMLDivElement | null>(null)

<div ref={containerRef} style={{ width: 900, height: 600 }}>
  <TrellisGallery
    items={items}
    mode="pagination"
    layout={{ type: 'auto', minItemWidth: 200, minItemHeight: 150 }}
    pagination={{ mode: 'uncontrolled' }}
    panelBoundary={containerRef}
    renderItem={(item) => <div>{item.title}</div>}
    renderExpandedItem={(item) => <div>{item.title}</div>}
  />
</div>

panelBoundary only controls movement/maximize boundaries. Panel DOM is still rendered in a full-screen portal layer for topmost stacking.

Custom Cell Activation

By default there is no keyboard/mouse shortcut on the cell body. Pass cellActivation to define your own logic:

<TrellisGallery
  items={items}
  mode="pagination"
  layout={{ type: 'auto', minItemWidth: 200, minItemHeight: 150 }}
  pagination={{ mode: 'uncontrolled' }}
  cellActivation={(event) => {
    if (event.type === 'dblclick') return true          // double-click
    return event.type === 'click' && event.shiftKey     // shift + click
  }}
  renderItem={(item) => <div>{item.title}</div>}
  renderExpandedItem={(item) => <div>{item.title}</div>}
/>

Cell Indicator (border + corner triangle)

The cell indicator is enabled by default. Disable it or customize styles:

// Disable the indicator entirely
<TrellisGallery cellIndicator={false} {...props} />

// Customize indicator styles
<TrellisGallery
  {...props}
  cellIndicator={{
    borderColor: 'rgba(34, 197, 94, 0.45)',
    triangleColor: 'rgba(34, 197, 94, 0.2)',
    triangleSize: 26,
  }}
/>

Imperative Panel Control (ref)

Use the ref handle when you want to open/close panels from external UI or route state:

import { useRef } from 'react'
import { TrellisGallery, type TrellisGalleryHandle } from 'react-trellis-gallery'

function GalleryWithExternalActions({ items }) {
  const galleryRef = useRef<TrellisGalleryHandle<typeof items[number]> | null>(null)

  return (
    <>
      <button onClick={() => galleryRef.current?.panels.open((item) => item.title.includes('Featured'))}>
        Open Featured
      </button>
      <button onClick={() => galleryRef.current?.panels.close((item) => item.archived)}>
        Close Archived
      </button>
      <TrellisGallery ref={galleryRef} items={items} {...otherProps} />
    </>
  )
}

Go To and Highlight by Callback

Pass a callback to match items, navigate to the target match, and highlight all matches. Works in both pagination and scroll modes.

Call goToItem on the ref handle:

import { useRef } from 'react'
import { TrellisGallery, type TrellisGalleryHandle } from 'react-trellis-gallery'

function SearchableGallery({ items }) {
  const galleryRef = useRef<TrellisGalleryHandle<typeof items[number]> | null>(null)

  const handleSearch = (query: string) => {
    const result = galleryRef.current?.goToItem(
      (item) => item.title.toLowerCase().includes(query.toLowerCase()),
      { highlightDuration: 5000 },
    )
    console.log(result) // { found, page, matchCount, matchIndices, targetIndex }
  }

  return (
    <>
      <input onChange={(e) => handleSearch(e.target.value)} placeholder="Search..." />
      <button onClick={() => galleryRef.current?.clearHighlights()}>Clear</button>
      <TrellisGallery ref={galleryRef} items={items} {...otherProps} />
    </>
  )
}

Customizing the highlight appearance:

<TrellisGallery
  highlightColor="#10b981"
  highlightClassName="my-highlight"
  {...otherProps}
/>

You can also override the CSS custom properties or target the data-rtg-highlighted attribute directly:

[data-rtg-cell][data-rtg-highlighted]::before {
  box-shadow: 0 0 0 3px hotpink;
}

Headless Usage

Use the hooks directly for full control over rendering:

import { useTrellisGallery, useCellInteraction } from 'react-trellis-gallery'

function Cell({ item, onOpen }) {
  const interaction = useCellInteraction({
    onActivate: onOpen,
    activationCallback: (event) => event.type === 'dblclick',
  })

  return <div {...interaction}>{item.title}</div>
}

function CustomGallery({ items }) {
  const { containerRef, layout, pagination, panels } = useTrellisGallery({
    items,
    mode: 'pagination',
    layout: { type: 'auto', minItemWidth: 200, minItemHeight: 150 },
    pagination: { mode: 'uncontrolled' },
  })

  const pageItems = items.slice(pagination.startIndex, pagination.endIndex)

  return (
    <div ref={containerRef} style={{ width: '100%', height: '100%' }}>
      <div style={{
        display: 'grid',
        gridTemplateColumns: `repeat(${layout.cols}, ${layout.cellWidth}px)`,
        gridTemplateRows: `repeat(${layout.rows}, ${layout.cellHeight}px)`,
      }}>
        {pageItems.map((item, i) => (
          <Cell
            key={i}
            item={item}
            onOpen={() => panels.open(pagination.startIndex + i)}
          />
        ))}
      </div>
    </div>
  )
}

API Reference

<TrellisGallery>

The main component. Renders a grid with optional pagination and floating panels.

| Prop | Type | Default | Description | | --- | --- | --- | --- | | items | T[] | — | Array of data items to display | | mode | 'pagination' \| 'scroll' | — | Display mode | | layout | LayoutConfig | — | Layout strategy (see below) | | gap | number | 0 | Gap between cells in pixels | | overscanCount | number | 1 | Extra rows rendered outside the viewport (scroll mode) | | pagination | PaginationConfig | — | Pagination options (required for pagination mode) | | panelDefaults | Partial<FloatingPanelDefaults> | { size: { width: 600, height: 400 }, minSize: { width: 300, height: 180 } } | Default floating panel dimensions | | panelBoundary | 'viewport' \| RefObject<HTMLElement \| null> | 'viewport' | Panel drag/maximize boundary. Panels still render in a full-screen portal layer | | renderItem | (item: T, index: number) => ReactNode | — | Renders each grid cell | | renderExpandedItem | (item: T, index: number) => ReactNode | — | Renders the content inside a floating panel | | panelTitle | (item: T, index: number) => ReactNode | — | Panel title text | | renderPanelHeader | (item: T, api: PanelHeaderAPI) => ReactNode | — | Custom panel header (overrides default) | | onPanelOpen | (item: T, index: number) => void | — | Called when a panel opens | | onPanelClose | (item: T, index: number) => void | — | Called when a panel closes | | cellIndicator | false \| CellIndicatorConfig | Enabled | Shows per-cell border + corner triangle trigger; pass false to disable | | cellActivation | (event: CellActivationEvent) => boolean | — | Optional callback to open panels from custom click/double-click/keyboard combos | | highlightColor | string | '#0078d4' | CSS color for the highlight border | | highlightDuration | number | 3600 | Auto-clear delay in ms. 0 = keep indefinitely | | highlightClassName | string | — | Additional CSS class applied to highlighted cells | | ref | Ref<TrellisGalleryHandle<T>> | — | Imperative handle for panels and go-to-item | | className | string | — | CSS class for the container | | style | CSSProperties | — | Inline styles for the container |

CellIndicatorConfig

{
  borderColor?: string
  triangleColor?: string
  triangleSize?: number
}

TrellisGalleryHandle

Exposed from ref on <TrellisGallery>:

{
  panels: {
    open(callback: (item: T) => boolean): void
    close(callback: (item: T) => boolean): void
    closeAll(): void
    closeUnpinned(): void
    isOpen(callback: (item: T) => boolean): boolean
    openPanels: PanelState[]
  }
  goToItem(callback: (item: T) => boolean, options?: GoToItemOptions): GoToItemResult
  clearHighlights(): void
}

GoToItemOptions

{
  highlightDuration?: number  // ms before auto-clear. 0 = keep indefinitely. Overrides the component prop.
  target?: 'first' | 'last' | number  // which match to navigate to (default: 'first')
}

GoToItemResult

{
  found: boolean        // whether any items matched
  page: number          // page index of the target item
  matchIndices: number[] // indices of all matching items
  matchCount: number    // total number of matches
  targetIndex: number   // index of the item navigated to (-1 if not found)
}

Layout Config

Auto layout — calculates the grid from minimum item dimensions:

{ type: 'auto', minItemWidth: 200, minItemHeight: 150 }

Manual layout — fixed grid dimensions:

{ type: 'manual', rows: 3, cols: 4 }

Pagination Config

Uncontrolled — the component manages page state internally:

{
  mode: 'uncontrolled',
  defaultPage?: number,        // initial page (0-indexed)
  onPageChange?: (page) => {}, // notified on change
  renderControl?: (vm) => {},  // custom UI, or false to hide
  position?: 'top' | 'bottom', // default: 'bottom'
  align?: 'start' | 'center' | 'end', // default: 'center'
  draggable?: boolean,         // make the overlay draggable
  label?: ReactNode | (vm) => ReactNode,
}

Controlled — you own the page state:

{
  mode: 'controlled',
  page: number,                // current page (0-indexed)
  onPageChange?: (page) => {}, // update your state here
  // ...same options as uncontrolled
}

PaginationVM

The view model passed to renderControl and available via useTrellisPaginationContext():

| Property | Type | Description | | --- | --- | --- | | currentPage | number | Current page index (0-based) | | totalPages | number | Total number of pages | | totalItems | number | Total number of items | | itemsPerPage | number | Items fitting on one page | | startIndex | number | First visible item index | | endIndex | number | Index after last visible item | | hasNext | boolean | Whether a next page exists | | hasPrev | boolean | Whether a previous page exists | | goToPage(page) | function | Navigate to a specific page | | next() | function | Go to the next page | | prev() | function | Go to the previous page |

PanelHeaderAPI

Passed to renderPanelHeader for controlling individual panels:

| Property | Type | Description | | --- | --- | --- | | close() | function | Close the panel | | pin() / unpin() / togglePin() | function | Pin/unpin the panel | | titleProps | Record<string, unknown> | Props to spread on your title element to keep aria-labelledby wiring intact in custom headers | | isPinned | boolean | Whether the panel is pinned | | minimize() / maximize() / restore() | function | Window state controls | | isMinimized / isMaximized | boolean | Current window state |

PanelManagerAPI

Returned by useTrellisPanels() and available on useTrellisGallery().panels:

| Property | Type | Description | | --- | --- | --- | | openPanels | PanelState[] | Currently open panels | | open(itemIndex) | function | Open a panel for an item | | activate(id) | function | Bring a panel to front | | close(id) | function | Close a panel by ID | | closeAll() | function | Close all panels | | closeUnpinned() | function | Close all unpinned panels | | togglePin(id) | function | Toggle pin state | | isOpen(itemIndex) | function | Check if an item's panel is open |

Hooks

| Hook | Description | | --- | --- | | useTrellisGallery(options) | All-in-one hook returning containerRef, containerSize, layout, pagination, and panels | | useTrellisLayout(containerSize, itemCount, config, gap?) | Computes grid layout from container dimensions | | useTrellisPagination(layout, totalItems, config) | Manages pagination state and navigation | | useTrellisPanels(callbacks?) | Manages floating panel open/close/pin state | | useTrellisHighlight(options) | Manages highlight state, match scanning, navigation, and auto-clear. Accepts items, itemsPerPage, navigate, and defaultDuration. | | useCellInteraction({ onActivate, activationCallback }) | Returns cell props for optional activation. When activationCallback is omitted it returns an empty prop object. | | useContainerSize(ref) | Tracks element dimensions via ResizeObserver | | useTrellisPaginationContext() | Reads pagination state from context (for child components of TrellisGallery) |

Utility Functions

| Function | Description | | --- | --- | | calculateLayout(width, height, itemCount, config, gap) | Pure function that computes grid layout. Returns LayoutResult with rows, cols, cellWidth, cellHeight, itemsPerPage, totalPages. | | fitGrid(itemCount, maxRows, maxCols) | Finds the smallest grid (rows × cols) that fits itemCount items within bounds. Prefers fewer rows, then fewer columns. | | clampPage(page, totalPages) | Clamps a page index to the valid range [0, totalPages - 1]. |

CSS Custom Properties

| Property | Default | Description | | --- | --- | --- | | --rtg-border-radius | 10px | Border radius for cells and highlight | | --rtg-highlight-color | #0078d4 | Highlight border color (also settable via highlightColor prop) | | --rtg-highlight-duration-ms | 1200ms | CSS transition speed for the highlight fade in/out | | --rtg-cell-border-color | rgba(0, 0, 0, 0.15) | Hover border color on cells | | --rtg-overlay-gradient-start | rgba(0, 0, 0, 0.15) | Pagination overlay gradient start color | | --rtg-bg | #ffffff | Floating panel background color | | --rtg-fg | #1a1a1a | Floating panel text color | | --rtg-shadow | 0 4px 20px rgba(0, 0, 0, 0.08) | Floating panel shadow | | --rtg-border-color | rgba(0, 0, 0, 0.12) | Panel/button border color | | --rtg-header-border-color | rgba(0, 0, 0, 0.08) | Panel header bottom border color | | --rtg-button-hover-bg | rgba(0, 0, 0, 0.06) | Header icon button hover background | | --rtg-button-active-bg | rgba(0, 0, 0, 0.1) | Header icon button active background | | --rtg-button-disabled-opacity | 0.35 | Header icon button disabled opacity |

Browser Support

Requires browsers with ResizeObserver support (all modern browsers). The library uses useIsomorphicLayoutEffect for SSR safety.

License

MIT