The GridLayout control from PnP SPFx React Controls is used to render a responsive grid inside a SharePoint Framework web part.
It is useful when a web part needs to display a collection of visual items such as news, documents, articles, cards, links, resources, or dashboard entries.
Instead of manually calculating columns, widths, wrapping, and compact behavior, the GridLayout control handles the layout logic and calls a custom render function for each item.
SPFx GridLayout Control with Fluent UI DocumentCard
Introduction
The GridLayout control from PnP SPFx React Controls is used to render a responsive grid inside a SharePoint Framework web part.
It is useful when a web part needs to display a collection of visual items such as news, documents, articles, cards, links, resources, or dashboard entries.
Instead of manually calculating columns, widths, wrapping, and compact behavior, the GridLayout control handles the layout logic and calls a custom render function for each item.
Official documentation:
Why GridLayout is useful in SPFx
In SharePoint Online, web parts can be placed in different page layouts:
- Full-width sections
- One-column sections
- Two-column sections
- Three-column sections
- Narrow mobile screens
A fixed layout may work in one section but break in another. GridLayout solves this problem by automatically adapting the number of visible columns based on the available width.
This makes it especially useful for SPFx web parts because the same component can be reused across different SharePoint page layouts without rewriting CSS grid logic manually.
Install the PnP SPFx Controls package
npm install @pnp/spfx-controls-react --save
If this is a new or restored SPFx solution, also run:
npm install
Run the local workbench/server with Heft:
heft start
Component file
Create or update the component file, for example:
New-Item -Path ".\src\webparts\gridLayoutWp\components\GridLayoutWp.tsx" -ItemType File -Force
Complete React component
import * as React from 'react';import { GridLayout } from '@pnp/spfx-controls-react/lib/GridLayout';import { DocumentCard, DocumentCardActivity, DocumentCardPreview, DocumentCardDetails, DocumentCardTitle, DocumentCardLocation, DocumentCardType, IDocumentCardPreviewProps, ImageFit, ISize} from '@fluentui/react';interface IGridLayoutItem { thumbnail: string; title: string; name: string; profileImageSrc: string; location: string; activity: string;}const items: IGridLayoutItem[] = [ { thumbnail: 'https://pixabay.com/get/57e9dd474952a414f1dc8460825668204022dfe05555754d742e7bd6/hot-air-balloons-1984308_640.jpg', title: 'Adventures in SPFx', name: 'Perry Losselyong', profileImageSrc: 'https://robohash.org/blanditiisadlabore.png?size=50x50&set=set1', location: 'SharePoint', activity: '3/13/2019' }, { thumbnail: 'https://pixabay.com/get/55e8d5474a52ad14f1dc8460825668204022dfe05555754d742d79d0/autumn-3804001_640.jpg', title: 'The Wild, Untold Story of SharePoint!', name: 'Ebonee Gallyhaock', profileImageSrc: 'https://robohash.org/delectusetcorporis.bmp?size=50x50&set=set1', location: 'SharePoint', activity: '6/29/2019' }, { thumbnail: 'https://pixabay.com/get/57e8dd454c50ac14f1dc8460825668204022dfe05555754d742c72d7/log-cabin-1886620_640.jpg', title: 'Low Code Solutions: PowerApps', name: 'Seward Keith', profileImageSrc: 'https://robohash.org/asperioresautquasi.jpg?size=50x50&set=set1', location: 'PowerApps', activity: '12/31/2018' }, { thumbnail: 'https://pixabay.com/get/55e3d445495aa514f1dc8460825668204022dfe05555754d742b7dd5/portrait-3316389_640.jpg', title: "Not Your Grandpa's SharePoint", name: 'Sharona Selkirk', profileImageSrc: 'https://robohash.org/velnammolestiae.png?size=50x50&set=set1', location: 'SharePoint', activity: '11/20/2018' }, { thumbnail: 'https://pixabay.com/get/57e6dd474352ae14f1dc8460825668204022dfe05555754d742a7ed1/faucet-1684902_640.jpg', title: 'Get with the Flow', name: 'Boyce Batstone', profileImageSrc: 'https://robohash.org/nulladistinctiomollitia.jpg?size=50x50&set=set1', location: 'Flow', activity: '5/26/2019' }];const onRenderGridItem = ( item: IGridLayoutItem, finalSize: ISize, isCompact: boolean): JSX.Element => { const previewProps: IDocumentCardPreviewProps = { previewImages: [ { previewImageSrc: item.thumbnail, imageFit: ImageFit.cover, height: 130 } ] }; return ( <div data-is-focusable={true} role="listitem" aria-label={item.title} > <DocumentCard type={isCompact ? DocumentCardType.compact : DocumentCardType.normal} onClick={() => alert(`You clicked on ${item.title}`)} > <DocumentCardPreview {...previewProps} /> {!isCompact && ( <DocumentCardLocation location={item.location} /> )} <DocumentCardDetails> <DocumentCardTitle title={item.title} shouldTruncate={true} /> <DocumentCardActivity activity={item.activity} people={[ { name: item.name, profileImageSrc: item.profileImageSrc } ]} /> </DocumentCardDetails> </DocumentCard> </div> );};const GridLayoutWp: React.FC = () => { return ( <GridLayout ariaLabel="List of content, use right and left arrow keys to navigate, arrow down to access details." items={items} onRenderGridItem={( item: IGridLayoutItem, finalSize: ISize, isCompact: boolean ) => onRenderGridItem(item, finalSize, isCompact)} /> );};export default GridLayoutWp;
Technical explanation
The main import is:
import { GridLayout } from '@pnp/spfx-controls-react/lib/GridLayout';
This imports the PnP reusable grid control.
The Fluent UI imports are used to render each grid item as a DocumentCard:
import { DocumentCard, DocumentCardActivity, DocumentCardPreview, DocumentCardDetails, DocumentCardTitle, DocumentCardLocation, DocumentCardType, IDocumentCardPreviewProps, ImageFit, ISize} from '@fluentui/react';
The GridLayout itself does not decide what each item looks like. It only controls the responsive grid behavior. The visual structure is created by the onRenderGridItem callback.
Data model
The interface defines the shape of each item:
interface IGridLayoutItem { thumbnail: string; title: string; name: string; profileImageSrc: string; location: string; activity: string;}
This is better than using any because TypeScript can validate the object structure. If a property is missing or misspelled, the compiler can detect it.
Each item contains:
thumbnail: image used byDocumentCardPreviewtitle: card titlename: author or person nameprofileImageSrc: profile imagelocation: category or sourceactivity: date or activity text
Rendering each grid item
The function below is the most important part:
const onRenderGridItem = ( item: IGridLayoutItem, finalSize: ISize, isCompact: boolean): JSX.Element => {
The GridLayout calls this function for each item.
The parameters are:
item: the current item from theitemsarrayfinalSize: the calculated size available for the itemisCompact: tells if the grid is currently rendering in compact mode
The isCompact parameter is very important. It allows the component to simplify the card when there is not enough space.
In this example:
type={isCompact ? DocumentCardType.compact : DocumentCardType.normal}
When the layout is compact, the card uses DocumentCardType.compact.
Also, this line hides the location in compact mode:
{!isCompact && ( <DocumentCardLocation location={item.location} />)}
This is good responsive design. In narrow layouts, the card shows only the most important information.
Accessibility
The wrapper div uses accessibility attributes:
<div data-is-focusable={true} role="listitem" aria-label={item.title}>
data-is-focusable={true} allows the item to participate in keyboard focus behavior.
role="listitem" tells assistive technologies that each card is an item in a list.
aria-label={item.title} gives a readable label to screen readers.
The GridLayout also receives an ariaLabel:
ariaLabel="List of content, use right and left arrow keys to navigate, arrow down to access details."
This gives screen reader users instructions about how to navigate the grid.
GridLayout usage
The final usage is simple:
<GridLayout ariaLabel="List of content, use right and left arrow keys to navigate, arrow down to access details." items={items} onRenderGridItem={( item: IGridLayoutItem, finalSize: ISize, isCompact: boolean ) => onRenderGridItem(item, finalSize, isCompact)}/>
The required properties are:
itemsonRenderGridItem
The ariaLabel is optional but strongly recommended.
Useful GridLayout properties
The control also supports additional layout properties:
<GridLayout ariaLabel="List of content, use right and left arrow keys to navigate, arrow down to access details." items={items} itemPadding={16} itemMinWidth={220} itemMaxWidth={320} compactThreshold={480} rowsPerPage={3} onRenderGridItem={onRenderGridItem}/>
itemPadding
Controls the gap between grid items.
itemMinWidth
Defines the minimum width of each item before the layout recalculates.
itemMaxWidth
Defines the maximum width of each item.
compactThreshold
Defines the width below which compact rendering is activated.
rowsPerPage
Limits the number of rows displayed per page.
Why DocumentCard works well with GridLayout
DocumentCard is a good match for GridLayout because it is already designed to display rectangular content blocks.
A card can contain:
- Preview image
- Title
- Location
- Activity
- Person metadata
- Click behavior
This makes the combination useful for content discovery scenarios in SharePoint.
Examples:
- News cards
- Training material cards
- Document recommendations
- Knowledge base articles
- Project resources
- Power Platform examples
- Internal portal links
Common mistake: using any everywhere
Your original code works with any, but for a technical SPFx project it is better to define an interface.
Instead of this:
const _onRenderGridItem = (item: any, finalSize: ISize, isCompact: boolean): JSX.Element => {
Use this:
const onRenderGridItem = ( item: IGridLayoutItem, finalSize: ISize, isCompact: boolean): JSX.Element => {
This makes the component safer and easier to maintain.
Common mistake: ignoring compact mode
A common error is to render the same heavy card in every layout.
This can make the web part look bad in a narrow SharePoint column.
The correct approach is to use isCompact:
type={isCompact ? DocumentCardType.compact : DocumentCardType.normal}
And optionally hide secondary content:
{!isCompact && ( <DocumentCardLocation location={item.location} />)}
Common mistake: missing accessibility labels
Because grid items are interactive, they should be understandable by keyboard and screen reader users.
At minimum, keep:
data-is-focusable={true}role="listitem"aria-label={item.title}
And also keep the ariaLabel on the GridLayout.
Conclusion
The GridLayout control is a practical and powerful PnP SPFx React Control for building responsive card-based web parts.
It separates layout responsibility from rendering responsibility:
GridLayoutmanages responsiveness.onRenderGridItemcontrols the visual representation.- Fluent UI
DocumentCardprovides a professional SharePoint-like card UI.
This pattern is ideal for SharePoint Framework solutions because it works across modern page sections, adapts to mobile layouts, supports accessibility, and keeps the rendering logic clean and reusable.
