Skip to content

cminn10/react-trellis-gallery

Repository files navigation

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

About

High-performance trellis/gallery layout for React

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors