@get-set/gs-flexogrid
v0.0.27
Published
Get-Set FlexoGrid
Maintainers
Readme
GSFlexoGrid
A dependency-free, responsive masonry/grid layout engine available in two flavours from one codebase:
- Native / vanilla JS — a
window.GSFlexoGrid(element, params)factory (also exposed as a jQuery plugin and anHTMLElement.prototypemethod). - React — a
<GSFlexoGrid />component.
Both share the exact same engine (actions/, constants/, helpers/, types/), so behaviour is identical across the two.
Features
- Three layout modes:
masonry(shortest-column packing),uniform(fixed-height tile grid),justified(rows stretched to fill the width, Flickr / Google-Photos style, honouringdata-araspect-ratio hints) - Live sort (
sortByOptions) and filter (filterByOptions) driven by your own buttons, with reset handlers - Relayout animation personalities:
ease,spring,linear,none— with tunableduration - Staggered entrance reveal:
fade,scale,fade-scale,slide,none— with per-itemstaggercascade - Hover-lift (
hoverLift) — lift + deepen the shadow on hover - Design tokens:
gap,radius,shadow(built-in token shadow or a custombox-shadowstring) - Theming:
light,dark,autocolour themes via CSS custom properties - Shimmering loading skeleton (
loading+skeletonCount) - Responsive breakpoints (
responsive) — per-breakpoint param overrides, auto-sorted - Automatic relayout via
ResizeObserveron the grid and on each item - Lifecycle hooks (
beforeInit,afterInit,afterPositionAnimating) - All animations and the skeleton shimmer respect
prefers-reduced-motion: reduce - React-only
gsxprop for per-instance scoped styles
Compatibility
| Target | Requirement |
|---|---|
| React (the <GSFlexoGrid> component) | React 16.8+ (Hooks are required), and 17 / 18 / 19. React is an optional peer dependency — you only need it for the component. |
| Native / vanilla (window.GSFlexoGrid) | No framework. Any modern evergreen browser (uses ResizeObserver). Optional jQuery ($(...).GSFlexoGrid(...)) and HTMLElement.prototype.GSFlexoGrid(...) adapters are registered automatically by the bundle. |
| TypeScript | First-class — type declarations (.d.ts) ship in the package. |
| SSR / Next.js | Safe to import server-side (no DOM access at module load). The grid itself is browser-only, so render it inside a Client Component ('use client'). |
The peer range is
^16.8.0 || ^17.0 || ^18.0 || ^19.0, withreact/react-dommarked optional so the native build has zero React dependency.
Installation
npm i @get-set/gs-flexogridThe package ships both targets:
- React / npm entry:
dist/components/GSFlexoGrid.js(main/module, types atdist/components/GSFlexoGrid.d.ts) - Native bundle (
require/ CDN):dist-js/bundle.js - Native stylesheet:
styles/GSFlexoGrid.css
Project layout
GSFlexoGrid.ts # native entry (webpack -> dist-js/bundle.js, window global)
components/GSFlexoGrid.tsx # React component (tsc -> dist/, npm entry)
actions/ # shared engine (init, calculatePositions, sort, filter, theme, skeleton, destroy)
constants/ # shared constants (defaultParams, layoutTokens, propType)
helpers/uihelpers.ts # shared helpers
types/ # Params / Ref / Window augmentation
components/styles/ # compiled CSS + CSS-as-TS (runtime injection for React)
styles/ # compiled CSS (for <link> use by the native build)Build
npm install
npm run build # builds both targets
npm run build:js # native bundle -> dist-js/bundle.js
npm run build:react # React + types -> dist/Tests
Unit tests use Vitest + jsdom:
npm test # run once
npm run test:watchUsage — Native JS
Include the stylesheet and the bundle, then call new GSFlexoGrid(element, params). Each direct child of the grid becomes a positioned item; tag items with data-* attributes to drive sorting/filtering.
<link rel="stylesheet" href="styles/GSFlexoGrid.css" />
<div class="grid">
<div data-price="120" data-category="new"><img src="1.jpg" /></div>
<div data-price="80" data-category="sale"><img src="2.jpg" /></div>
<div data-price="200" data-category="new"><img src="3.jpg" /></div>
</div>
<script src="dist-js/bundle.js"></script>
<script>
new GSFlexoGrid(document.querySelector('.grid'), {
count: 4,
gap: '10px',
layout: 'masonry',
animation: 'spring',
entrance: 'fade-scale',
stagger: 40,
hoverLift: true,
theme: 'dark',
radius: '12px',
shadow: true,
resetSortHandler: '.btn-reset-sort',
resetFilterHandler: '.btn-reset-filter',
sortByOptions: [
{ handler: document.querySelector('.btn-sort-date'), prop: 'date', type: 'string' },
{ handler: document.querySelector('.btn-sort-price'), prop: 'price', type: 'number' },
],
filterByOptions: [
{ handler: document.querySelector('.btn-filter-new'), prop: 'category', value: 'new' },
],
responsive: [
{ windowSize: 991, params: { count: 2 } },
{ windowSize: 767, params: { count: 1 } },
],
afterInit: () => console.log('ready'),
});
</script>Equivalent jQuery / element forms (registered by the bundle when it loads):
// jQuery — initialises every matched element
$('.grid').GSFlexoGrid({ count: 3 });
// HTMLElement.prototype
document.querySelector('.grid').GSFlexoGrid({ count: 3 });The registry & instance methods
Every instance is registered on the global window.GSFlexoGridConfigue registry under its reference key (auto-generated if you don't pass one). Look an instance up and call its methods:
new GSFlexoGrid(document.querySelector('.grid'), { reference: 'gallery' });
const instance = window.GSFlexoGridConfigue.instance('gallery');
instance.refresh(); // re-read children + recalculate layout
instance.destroy(); // disconnect observers, clear timers, strip inline styles, unregisterwindow.GSFlexoGridConfigue has the shape:
{
references: Array<{ key: string; ref: Ref }>;
instance: (key: string) => Ref | undefined; // returns undefined for unknown keys
}The returned Ref exposes grid, currentParams, reference, data, originaldata, and the refresh() / destroy() methods.
Usage — React
import GSFlexoGrid from '@get-set/gs-flexogrid';
import { useRef } from 'react';
export default function Gallery() {
const sortBtnRef = useRef(null);
const filterBtnRef = useRef(null);
const resetRef = useRef(null);
return (
<>
<button ref={sortBtnRef}>Sort by price</button>
<button ref={filterBtnRef}>Show new</button>
<button ref={resetRef}>Reset</button>
<GSFlexoGrid
count={4}
gap="10px"
layout="masonry"
animation="spring"
entrance="fade-scale"
stagger={40}
hoverLift
theme="dark"
radius="12px"
shadow
resetFilterHandler={resetRef}
sortByOptions={[
{ handler: sortBtnRef, prop: 'price', type: 'number' },
]}
filterByOptions={[
{ handler: filterBtnRef, prop: 'category', value: 'new' },
]}
responsive={[
{ windowSize: 991, params: { count: 2 } },
{ windowSize: 767, params: { count: 1 } },
]}
>
<div data-price="120" data-category="new"><img src="1.jpg" /></div>
<div data-price="80" data-category="sale"><img src="2.jpg" /></div>
</GSFlexoGrid>
</>
);
}The static stylesheet is injected into <head> automatically — no CSS import required. Each real child is tagged with the gs-flexogrid-item class so the radius / shadow / hover-lift tokens apply without extra markup. The dynamic options (layout, rowHeight, animation, duration, entrance, stagger, hoverLift, theme, radius, shadow, loading, skeletonCount, gap, count) are re-applied automatically whenever they change.
Next.js (App Router)
The grid is browser-only, so use it from a Client Component:
'use client';
import GSFlexoGrid from '@get-set/gs-flexogrid';
export default function Gallery() {
return (
<GSFlexoGrid count={4} gap="12px" layout="masonry">
<div><img src="/1.jpg" /></div>
<div><img src="/2.jpg" /></div>
<div><img src="/3.jpg" /></div>
</GSFlexoGrid>
);
}gsx — scoped styles (React only)
gsx is a React-only prop that injects per-instance, scoped CSS for this grid. It is a nested CSS-in-JS object (NestedCSS): each nested key becomes a descendant selector and the leaf values are CSSProperties. Rules are scoped to this instance via its generated data-key, so they never leak to other grids, and the injected <style> is removed automatically when the component unmounts.
<GSFlexoGrid
count={3}
gsx={{
background: '#111',
padding: '12px',
'.gs-flexogrid-item': {
borderRadius: '8px',
overflow: 'hidden',
},
'img': {
display: 'block',
width: '100%',
},
}}
>
{/* items */}
</GSFlexoGrid>The example above injects (scoped to [data-key='...']):
[data-key='...'] { background: #111; padding: 12px; }
[data-key='...'] .gs-flexogrid-item { border-radius: 8px; overflow: hidden; }
[data-key='...'] img { display: block; width: 100%; }Ref / imperative API (GSFlexoGridHandle)
The component is a forwardRef. Attach a ref to call instance methods directly:
import { useRef } from 'react';
import GSFlexoGrid, { GSFlexoGridHandle } from '@get-set/gs-flexogrid';
const gridRef = useRef<GSFlexoGridHandle>(null);
// <GSFlexoGrid ref={gridRef} count={4}>…</GSFlexoGrid>
gridRef.current?.refresh(); // re-read items + recalculate layout
gridRef.current?.destroy(); // tear down observers/timers + strip inline styles| Method | Description |
|---|---|
| refresh() | Re-reads the grid's children and recalculates the layout (e.g. after items change). |
| destroy() | Disconnects resize observers, clears pending layout timers, removes inline styles + token classes, and unregisters the instance. |
Options
Every option below is optional. Defaults come from constants/defaultParams.ts.
| Option | Type | Default | Description |
|---|---|---|---|
| reference | string | '' (auto GUID) | Unique key used by the window.GSFlexoGridConfigue registry. Auto-generated if omitted. |
| count | number | 3 | Number of columns. |
| gap | string | '' | CSS gap value, e.g. '10px' or '10px 20px' (row / column). |
| className | string | '' | Extra CSS class added to the grid element. |
| layout | 'masonry' \| 'uniform' \| 'justified' | 'masonry' | Layout algorithm. See Layout modes. |
| rowHeight | number | 0 | Forced cell height (uniform) / target row height (justified) in px. 0 = auto (defaults to 220px internally). Ignored by masonry. |
| animation | 'ease' \| 'spring' \| 'linear' \| 'none' | 'ease' | Relayout easing personality. See Animation modes. |
| duration | number | 400 | Relayout transition duration in ms. |
| entrance | 'fade' \| 'scale' \| 'fade-scale' \| 'slide' \| 'none' | 'fade-scale' | Reveal applied to items as they are first placed. See Entrance modes. |
| stagger | number | 0 | Per-item delay (ms) for the entrance cascade. 0 = all at once. |
| hoverLift | boolean | false | Lift + deepen the shadow on hover (adds gs-flexogrid--hover-lift). |
| theme | 'light' \| 'dark' \| 'auto' | 'light' | Colour theme for the design-token variables. See Themes. |
| radius | string | '' | Item corner-radius token, e.g. '12px' → sets --gs-flexo-radius. |
| shadow | boolean \| string | false | true = built-in token shadow; a string = custom box-shadow; false = none. See Shadow. |
| loading | boolean | false | Show the shimmering skeleton placeholder layer. |
| skeletonCount | number | 0 | Number of skeleton tiles while loading. 0 = derive from count (count × 3). |
| resetSortHandler | string \| HTMLElement \| RefObject | '' | Selector, element or React ref of a button that resets the active sort. |
| resetFilterHandler | string \| HTMLElement \| RefObject | '' | Selector, element or React ref of a button that resets the active filter. |
| sortByOptions | SortByOption[] | [] | Sort button definitions. See SortByOption. |
| filterByOptions | FilterByOption[] | [] | Filter button definitions. See FilterByOption. |
| responsive | ResponsiveOption[] | [] | Per-breakpoint param overrides (auto-sorted by windowSize descending). See Responsive. |
| beforeInit | () => void | – | Callback fired before each position calculation. |
| afterInit | () => void | – | Callback fired after init. |
| afterPositionAnimating | () => void | – | Callback fired after the relayout transition ends. |
| gsx | NestedCSS | – | React only — scoped, nested CSS-in-JS styles for this instance. See gsx. |
SortByOption
| Prop | Type | Description |
|---|---|---|
| handler | string \| HTMLElement \| RefObject | Button to attach the sort onclick to. |
| prop | string | The data-* attribute name read off each item. |
| type | 'string' \| 'number' | Determines the sort comparison ('number' sorts numerically). |
| value | any | Optional — reserved; not required for sorting. |
FilterByOption
| Prop | Type | Description |
|---|---|---|
| handler | string \| HTMLElement \| RefObject | Button to attach the filter onclick to. |
| prop | string | The data-* attribute name read off each item. |
| value | any | Value to match against data-[prop]; matching items stay visible. |
| type | 'string' \| 'number' | Optional type of the prop. |
ResponsiveOption
| Prop | Type | Description |
|---|---|---|
| windowSize | number | Max viewport width (px) at which params apply. |
| params | Partial<Params> | Option overrides merged on top of the base params when window.innerWidth <= windowSize. |
Handler types
resetSortHandler, resetFilterHandler, and each handler in sortByOptions / filterByOptions all accept any of these three forms interchangeably:
// CSS selector string — vanilla JS
resetSortHandler: '.btn-reset-sort'
// HTMLElement — vanilla JS
resetSortHandler: document.querySelector('.btn-reset-sort')
// React ref
resetSortHandler: myResetRef // useRef<HTMLButtonElement>(null)Variant catalogs
Layout modes
| layout | Description |
|---|---|
| masonry | Classic shortest-column packing — each item drops into the currently shortest column. Items keep their natural height. (default) |
| uniform | Every item snaps to a fixed cell height (rowHeight, default 220px) so the grid reads as an even tile grid. |
| justified | Rows of items, each row stretched to fill the container width (Flickr / Google-Photos style). Honours rowHeight as the target row height and item data-ar (aspect-ratio) hints. |
Animation modes
The relayout transition's timing function is keyed by animation:
| animation | Timing function | Feel |
|---|---|---|
| ease | cubic-bezier(0.22, 0.61, 0.36, 1) | Smooth, decelerating (default). |
| spring | cubic-bezier(0.34, 1.56, 0.64, 1) | Bouncy overshoot. |
| linear | linear | Constant speed. |
| none | linear (with 0 duration) | No animated movement; items snap into place. |
Entrance modes
Applied once, to items as they are first placed (and cleared afterwards). Disabled automatically under prefers-reduced-motion.
| entrance | Description |
|---|---|
| fade | Fade in from opacity 0. |
| scale | Scale up into place. |
| fade-scale | Fade + scale together (default). |
| slide | Slide in (translate) + fade. |
| none | No entrance; items appear immediately. |
Themes
theme sets data-theme on the grid, which selects the design-token palette:
| theme | Description |
|---|---|
| light | Light palette (default). |
| dark | Dark palette. |
| auto | Follows the user's prefers-color-scheme. |
Shadow
| shadow value | Result |
|---|---|
| false | No shadow (default). |
| true | Built-in token shadow 0 6px 20px -6px rgba(15, 23, 42, 0.25) (sets --gs-flexo-shadow). |
| string | Used verbatim as the box-shadow value (sets --gs-flexo-shadow). |
Layout modes, animations & theming — combined example
<GSFlexoGrid
count={4}
gap="14px"
layout="justified" // 'masonry' | 'uniform' | 'justified'
rowHeight={220} // target row height for justified / forced height for uniform
animation="spring" // 'ease' | 'spring' | 'linear' | 'none'
duration={500}
entrance="slide" // 'fade' | 'scale' | 'fade-scale' | 'slide' | 'none'
stagger={40} // cascade the entrance, 40ms between items
hoverLift // lift + shadow on hover
theme="dark" // 'light' | 'dark' | 'auto'
radius="12px"
shadow // or shadow="0 8px 24px rgba(0,0,0,.3)"
loading={isLoading} // shimmering skeleton while data loads
>
{/* items */}
</GSFlexoGrid>Identical in vanilla JS:
new GSFlexoGrid(el, {
count: 4,
layout: 'justified',
rowHeight: 220,
animation: 'spring',
entrance: 'slide',
stagger: 40,
hoverLift: true,
theme: 'dark',
radius: '12px',
shadow: true,
loading: true,
});Design tokens
The design tokens are CSS custom properties on the grid element. Override them via gsx, the radius / shadow options, or your own stylesheet:
| Token | Set by | Purpose |
|---|---|---|
| --gs-flexo-radius | radius | Item corner radius. |
| --gs-flexo-shadow | shadow | Item resting shadow. |
| --gs-flexo-shadow-hover | theme / CSS | Item shadow on hover (with hoverLift). |
| --gs-flexo-lift | theme / CSS | Translate distance for the hover lift. |
| --gs-flexo-bg | theme | Grid background. |
| --gs-flexo-item-bg | theme | Item background. |
| --gs-flexo-skeleton-base | theme | Skeleton tile base colour. |
| --gs-flexo-skeleton-shine | theme | Skeleton shimmer highlight. |
| --gs-flexo-skeleton-cols | count | Skeleton column count. |
Responsive
responsive entries override params below a given viewport width. Entries are auto-sorted by windowSize descending and applied when window.innerWidth <= windowSize, so smaller breakpoints win:
responsive: [
{ windowSize: 991, params: { count: 2 } },
{ windowSize: 767, params: { count: 1, gap: '6px' } },
]The grid re-resolves its params and relayouts automatically on resize via a ResizeObserver.
License
ISC.
