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 by DocumentCardPreview
  • title: card title
  • name: author or person name
  • profileImageSrc: profile image
  • location: category or source
  • activity: 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 the items array
  • finalSize: the calculated size available for the item
  • isCompact: 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:

  • items
  • onRenderGridItem

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:

  • GridLayout manages responsiveness.
  • onRenderGridItem controls the visual representation.
  • Fluent UI DocumentCard provides 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.

Edvaldo Guimrães Filho Avatar

Published by