Create SPFx Dynamic Accordion Webpart Using PnP Controls React

In this article, we will learn how to create a dynamic SPFx accordion Webpart that takes data from a SharePoint list and the accordion control from the @pnp/spfx-controls-react library. We are using the React framework to build this solution.

Let’s take a look at the SPFx dynamic accordion example. In the web part properties, if I change the “Number of items to display”, the accordion will show that number of items. This data is retrieved directly from a SharePoint list.

Creating Dynamic Accordion Control using PnP SPFx Controls React

Additionally, I will explain how to:

  • Display all items from the SharePoint list in the SPFx Accordion control
  • Display only the latest 10 items
  • Display items based on the number entered in the SPFx web part properties

Before implementing the webpart, let’s understand the properties of the accordion control.

Accordion Properties in @pnp/spfx-controls-react Library

The Accordion control from @pnp/spfx-controls-react comes with properties like title, defaultCollapsed, etc Here’s a basic usage example:

<Accordion title={item.Question} defaultCollapsed={true} className={"itemCell"} key={index}>
  <div className={"itemContent"}>
    <div className={"itemResponse"}>{item.Reponse}</div>
    <div className={"itemIndex"}>{`Langue :  ${item.Langue.Nom}`}</div>
  </div>
</Accordion>

Let’s take a look at the detailed information about the accordion control.

PropertyTypeDescriptionRequired
titlestringyesThe title in the accordion is to be displayed as a question.
defaultCollapsedbooleannoIs the accordion collapsed by default?
classNamestringnoTo add class names for further uses, like applying styles, etc.
collapsedIconstringnoAn optional custom icon when the accordion is collapsed.
expandedIconstringnoAn optional custom icon when the accordion is expanded.

Dynamic SPFx Accordion Example: Display SharePoint List Items in a Web Part

To create a dynamic accordion example, we first need to create a SharePoint list. In my scenario, I created a SharePoint list named ‘FAQsLeaveManagement’, which will store all the FAQs for the leave management application.

How To Use PnP React Accordion Control In SPFx

By this time, I hope you are familiar with setting up the environment and creating an SPFx web part using the React framework. If not, follow the links below!

After creating the SPFx webpart with the React framework, follow the steps below!

  1. Run the below command either in the Terminal pane of VS Code or in Windows Command Prompt within the SPFx solution.
npm install @pnp/spfx-controls-react --save --save-exact

To use the accordion control from the pnp/spfx-controls-react library, we have to install the above dependency in our project.

  1. Then, import the statement below into the accordionwebpart.tsx file.
import { Accordion } from "@pnp/spfx-controls-react/lib/Accordion";

Note: To interact with SharePoint, we are using the PnP JS library. So, we need to install and import the PnP JS into our project.

  1. Run the command below in the terminal pane of VS Code or in the Windows Command Prompt within the solution.
npm install @pnp/sp --save

This will install the PNP JS library into your project.

  1. To import the PnP JS library into the solution, add the following statements in the .ts file.
import { spfi, SPFx } from "@pnp/sp";
import { SPFI } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
  1. PnP needs to be initialized with the SharePoint context to connect properly to the SharePoint site. So, update your onInit() as below.
  protected onInit(): Promise<void> {
    return this._getEnvironmentMessage().then(message => {
      this._environmentMessage = message;
      this._sp = spfi().using(SPFx(this.context));
    });
  }
  1. To hold the instance of the SPFI, create a private variable within the export default class in the .ts file.
  private _sp: SPFI
  1. Also, update the render() function within the .ts file, which will take this._sp as a prop.
public render(): void {
    const element: React.ReactElement<IFabricComboWpProps> = React.createElement(
      FabricComboWp,
      {
        description: this.properties.description,
        isDarkTheme: this._isDarkTheme,
        environmentMessage: this._environmentMessage,
        hasTeamsContext: !!this.context.sdks.microsoftTeams,
        userDisplayName: this.context.pageContext.user.displayName,
        sp: this._sp,
      }
    );

    ReactDom.render(element, this.domElement);
  }
spfx accordion web part react
  1. We also need to include the sp as a prop in the Props.ts file.
import { SPFI } from "@pnp/sp";

export interface IFabricComboWpProps {
  description: string;
  isDarkTheme: boolean;
  environmentMessage: string;
  hasTeamsContext: boolean;
  userDisplayName: string;
  sp: SPFI;
}
  1. Create an interface in the .tsx file to specify the data type of state. Here, I created a state named ‘ISpfxAccordianState’, which contains an array of objects named faqItems with properties’ title’ and ‘answer’.
export interface ISpfxAccordianState {
  faqItems: { title: string; answer: string }[];
}

Place this code at the top of the export default class.

  1. Then, initialize the state with the default value using the constructor() within the export default class in the .tsx file.
constructor(props: ISpfxAccordianProps) {
  super(props);
  this.state = {
  faqItems: []
      };
}
  1. Add the below function in the .tsx file within the export default class to load SharePoint list items using PnPJS.
 private async fetchFAQItems(): Promise<void>{
  try {
    const items = await this.props.sp.web.lists.getByTitle("FAQsLeaveManagement").items.select("Title", "Answer")();
    const formattedItems = items.map((item: any) => ({
      title: item.Title,
      answer: item.Answer
    }));

    this.setState({ faqItems: formattedItems });
  } catch (error) {
    console.error("Error fetching FAQ items: ", error);
  }
 }
Accordion in spfx

Here,

  • await this.props.sp.web.lists.getByTitle(“FAQsLeaveManagement”).items.select(“Title”, “Answer”)(); = This line retrieves all items from the SharePoint list and stores them in the items variable.
  • Using map(), we extract the Title and Answer fields’ data from the items variable for each record and assign it to the title and answer properties of an object. Then, the objects are collected in the formattedItems array.
  • this.setState({ faqItems: formattedItems }); = Using setState(), we are updating the state with collected objects.
  1. To call the above function whenever the webpart is loaded, add the below code into the .tsx file within the export default class.
public async componentDidMount(): Promise<void> {
 await this.fetchFAQItems();
}
  1. Add the code below to the render() function in the .tsx file.
return (
      <section className={`${styles.samplereactweb}`}>
        <div>
          <h1>Leave Management FAQ's</h1>
        </div>
       <div>
       {this.state.faqItems.map((item, index) => (
           <Accordion
             title={item.title}
             defaultCollapsed={true}
             key={index}
             collapsedIcon="Rocket" 
             expandedIcon="InkingTool"    
             className={styles.itemCell}
          >
          <p>{item.answer}</p>
          </Accordion>
        ))}
       </div>
      </section>
    );
  1. Add the below styles to the .scss file in the project.
.itemCell {
  text-align: left;
  margin-bottom: 12px;

  .ms-Button-icon {
    float: left;
    margin-right: 8px;
  }

  .accordion-title {
    font-weight: 600;
    padding-left: 28px; 
  }
}
  1. Here is the final code:
import * as React from 'react';
import styles from './Samplereactweb.module.scss';
import type { ISamplereactwebProps } from './ISamplereactwebProps';
import { Accordion } from "@pnp/spfx-controls-react/lib/Accordion";
export interface ISpfxAccordianState {
  faqItems: { title: string; answer: string }[];
}
export default class Samplereactweb extends React.Component<ISamplereactwebProps, ISpfxAccordianState> {
    constructor(props: ISamplereactwebProps) {
    super(props);
    this.state = {
      faqItems: []
    };
  }
public async componentDidMount(): Promise<void> {
 await this.fetchFAQItems();
}

 private async fetchFAQItems(): Promise<void>{
  try {
    const items = await this.props.sp.web.lists.getByTitle("FAQsLeaveManagement").items.select("Title", "Answer")();
    const formattedItems = items.map((item: any) => ({
      title: item.Title,
      answer: item.Answer
    }));

    this.setState({ faqItems: formattedItems });
  } catch (error) {
    console.error("Error fetching FAQ items: ", error);
  }
 }
  public render(): React.ReactElement<ISamplereactwebProps> {
    return (
      <section className={`${styles.samplereactweb}`}>
        <div>
          <h1>Leave Management FAQ's</h1>
        </div>
        <div>
         {this.state.faqItems.map((item, index) => (
             <Accordion
               title={item.title}
               defaultCollapsed={true}
               key={index}
               collapsedIcon="Rocket" 
               expandedIcon="InkingTool"    
               className={styles.itemCell}
             >
          <p>{item.answer}</p>
          </Accordion>
        ))}
       </div>
      </section>
    );
  }
}
  1. Then, save the changes and run the command below to test it in the local workbench.
gulp serve
  1. The workbench URL will be like below:
https://<tenantname>.sharepoint.com/sites/RetailManagementSite/_layouts/15/workbench.aspx

Once you add the web part to the local workbench, you’ll see the accordion displaying all the items from your SharePoint list.

how to display sharepoint list items in spfx accordion example

This way, you can display all items from the SharePoint list in the SPFX Accordion Webpart using the accordion control.

Dynamic SPFx Accordion: Display Latest 10 SharePoint List Items

In this section, I will explain how to display the latest 10 SharePoint list items in an SPFX Accordion Web Part. Look at the example below; it displays only the latest 10 records.

Create spfx accordion dynamically retrieve data from sharepoint list

To achieve this, update the fetchFAQItems() function in the .tsx file with the below code.

 private async fetchFAQItems(): Promise<void>{
  try {
    const items = await this.props.sp.web.lists.getByTitle("FAQsLeaveManagement").items.select("Title", "Answer", "Created").orderBy("Created", false).top(10)();
    const formattedItems = items.map((item: any) => ({
      title: item.Title,
      answer: item.Answer
    }));

    this.setState({ faqItems: formattedItems });
  } catch (error) {
    console.error("Error fetching FAQ items: ", error);
  }
 }

In the above fetchFAQItems function, I retrieve items from the SharePoint list “FAQsLeaveManagement”. To get only the latest items, I added two methods to the query:

  • orderBy(“Created”, false) = This sorts the list items by their creation date in descending order, so the newest items come first. false indicates descending order.
  • top(10) = this limits the results to only the top 10 items.

After fetching these items, I map them to a simpler format with just the title and answer fields, and then update the component state to render them in the accordion.

This way, we can fetch only the latest top 10 records from the SharePoint list and display them in the accordion control within the web part.

Dynamic SPFx Accordion: Display SharePoint List Items Based on User-Defined Count

So far, we have seen how to display all SharePoint list items and the top 10 latest items in the accordion control. However, you may also need to adjust the number of items displayed in the accordion web part at times.

At that time, changing the code was not an ideal solution, so we added a property to the web part, which allows you to change the number of items you want to display in the accordion web part. By default, we can set a number, and later you can change it according to your requirements.

Creating Dynamic Accordion Control using PnP SPFx Controls React

Follow the steps below to achieve this!

  1. Run the below command in the terminal pane of the current solution or Windows Command Prompt.
npm install @pnp/spfx-property-controls --save --save-exact

This command installs the @pnp/spfx-property-controls package, which provides additional property controls not included by default in SPFx.

We specifically need this package to utilize the PropertyFieldNumber control, which enables us to add a numeric input field in the web part properties.

  1. Add the below import statement in the .ts file.
import { PropertyFieldNumber } from '@pnp/spfx-property-controls/lib/PropertyFieldNumber';
  1. To add a new property for the web part, start by updating the Props.ts file.
import { SPFI } from "@pnp/sp";
export interface ISpfxAccordianProps {
  description: string;
  numberValue: number;
  isDarkTheme: boolean;
  environmentMessage: string;
  hasTeamsContext: boolean;
  userDisplayName: string;
   sp: SPFI;
}

Here, we defined a new property named numberValue with the data type number.

  1. Next, include this property in the webpart’s main properties interface by adding the numberValue to the ISpfxAccordionWebPartProps interface in the .ts file.
export interface ISpfxAccordionWebPartProps {
  description: string;
  numberValue: number;
}

This step makes the numberValue property available for use in the webpart configuration and rendering.

  1. Update your render() method with the below one in the .ts file.
  public render(): void {
    const element: React.ReactElement<ISpfxAccordianProps> = React.createElement(
      SpfxAccordian,
      {
        description: this.properties.description,
        isDarkTheme: this._isDarkTheme,
        environmentMessage: this._environmentMessage,
        hasTeamsContext: !!this.context.sdks.microsoftTeams,
        userDisplayName: this.context.pageContext.user.displayName,
        sp: this._sp,
        numberValue: this.properties.numberValue
      }
    );

    ReactDom.render(element, this.domElement);
  }

I added numberValue to the render() method to pass the user-set number from the web part settings to the React component, allowing it to display that many items dynamically.

  1. Then, add the number field control to the webpart properties by updating the getPropertyPaneConfiguration() method in your code with the one below.
  protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: strings.PropertyPaneDescription
          },
          groups: [
            {
              groupName: strings.BasicGroupName,
              groupFields: [
                PropertyPaneTextField('description', {
                  label: "Description"
                })
              ]
            },
            {
              groupName: "Property Controls Test",
              groupFields: [
                 PropertyFieldNumber('numberValue', {
                 key: "numberValue",
                 label: "Number of items to display",
                 description: "Number field description",
                 value: this.properties.numberValue,
                 maxValue: 50,
                 minValue: 1,
                disabled: false
              })]}
          ]
        }
      ]
    };
  }

In the above method, we added this group, which included a new property for the web part.

{
  groupName: "Property Controls Test",
   groupFields: [
       PropertyFieldNumber('numberValue', {
       key: "numberValue",
       label: "Number of items to display",
       description: "Number field description",
       value: this.properties.numberValue,
       maxValue: 50,
       minValue: 1,
       disabled: false
       })
  ]
}

Let’s understand the PropertyFieldNumber control properties:

PropertyTypeRequiredDescription
keystringyesA unique key that indicates the identity of this control.
labelstringyesThe property field label is displayed on top.
descriptionstringnoThe number field input description.
valuenumbernoValue to be displayed in the number field.
maxValuenumbernoThe maximum number that can be inserted.
minValuenumbernoMinimum number that can be inserted.
disabledbooleannoSpecify if the control needs to be disabled.
  1. Then, in the .tsx file, update the fetchFAQItems() method with the below one.
 private async fetchFAQItems(): Promise<void>{
  try {
    const items = await this.props.sp.web.lists.getByTitle("FAQsLeaveManagement").items.select("Title", "Answer", "Created").orderBy("Created", false).top(this.props.numberValue)();
    const formattedItems = items.map((item: any) => ({
      title: item.Title,
      answer: item.Answer
    }));

    this.setState({ faqItems: formattedItems });
  } catch (error) {
    console.error("Error fetching FAQ items: ", error);
  }
 }

Here, I added this.props.numberValue to the .top() method, which will retrieve the number of items from the web part property.

  1. Also, add the componentDidUpdate() method in the .tsx file within the export default class.
public async componentDidUpdate(prevProps: Readonly<ISpfxAccordianProps>): Promise<void >{
  if (prevProps.numberValue !== this.props.numberValue){
    await this.fetchFAQItems();
  }
}

I added componentDidUpdate() to reload the items whenever the user changes the number, so the accordion updates immediately without needing a full reload.

  1. To add a default value for the number of items to display, update the code below within the “preconfiguredEntries” array in the manifest.json file.
    "properties": {
      "description": "SPFXAccordian",
      "numberValue":5
    }

I set five as the default value; you can provide a number according to your specific requirements.

  1. Here is the final .tsx file code.
import * as React from 'react';
import styles from './SpfxAccordian.module.scss';
import type { ISpfxAccordianProps } from './ISpfxAccordianProps';
import { escape } from '@microsoft/sp-lodash-subset';
import { Accordion } from "@pnp/spfx-controls-react/lib/Accordion";
export interface ISpfxAccordianState {
  faqItems: { title: string; answer: string }[];
}

export default class SpfxAccordian extends React.Component<ISpfxAccordianProps,ISpfxAccordianState> {
    constructor(props: ISpfxAccordianProps) {
    super(props);
    this.state = {
      faqItems: []
    };
  }
public async componentDidMount(): Promise<void> {
 await this.fetchFAQItems();
}
public async componentDidUpdate(prevProps: Readonly<ISpfxAccordianProps>): Promise<void >{
  if (prevProps.numberValue !== this.props.numberValue){
    await this.fetchFAQItems();
  }
}
 private async fetchFAQItems(): Promise<void>{
  try {
    const items = await this.props.sp.web.lists.getByTitle("FAQsLeaveManagement").items.select("Title", "Answer", "Created").orderBy("Created", false).top(this.props.numberValue)();
    const formattedItems = items.map((item: any) => ({
      title: item.Title,
      answer: item.Answer
    }));

    this.setState({ faqItems: formattedItems });
  } catch (error) {
    console.error("Error fetching FAQ items: ", error);
  }
 }
  public render(): React.ReactElement<ISpfxAccordianProps> {
    const {
      hasTeamsContext,
      userDisplayName,   
    } = this.props;

    return (
      <section className={`${styles.spfxAccordian} ${hasTeamsContext ? styles.teams : ''}`}>
        <div className={styles.welcome}>
          <h2>Well done, {escape(userDisplayName)}!</h2>    
          <h4>Leave Management FAQs, {this.props.numberValue}!</h4>       
        <div>
       {this.state.faqItems.map((item, index) => (
           <Accordion
             title={item.title}
             defaultCollapsed={true}
             key={index}
             collapsedIcon="Rocket" 
             expandedIcon="InkingTool"    
             className={styles.itemCell}
          >
          <p>{item.answer}</p>
          </Accordion>
        ))}
       </div>
        </div> 
      </section>
    );
  }
}
  1. And this is the final .ts file code.
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
  type IPropertyPaneConfiguration,
  PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { IReadonlyTheme } from '@microsoft/sp-component-base';

import * as strings from 'SpfxAccordianWebPartStrings';
import SpfxAccordian from './components/SpfxAccordian';
import { ISpfxAccordianProps } from './components/ISpfxAccordianProps';
import { spfi, SPFx } from "@pnp/sp";
import { SPFI } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import { PropertyFieldNumber } from '@pnp/spfx-property-controls/lib/PropertyFieldNumber';
export interface ISpfxAccordianWebPartProps {
  description: string;
  numberValue: number;
}
export default class SpfxAccordianWebPart extends BaseClientSideWebPart<ISpfxAccordianWebPartProps> {

  private _isDarkTheme: boolean = false;
  private _environmentMessage: string = '';
  private _sp: SPFI;

  public render(): void {
    const element: React.ReactElement<ISpfxAccordianProps> = React.createElement(
      SpfxAccordian,
      {
        description: this.properties.description,
        isDarkTheme: this._isDarkTheme,
        environmentMessage: this._environmentMessage,
        hasTeamsContext: !!this.context.sdks.microsoftTeams,
        userDisplayName: this.context.pageContext.user.displayName,
        sp: this._sp,
        numberValue: this.properties.numberValue
      }
    );

    ReactDom.render(element, this.domElement);
  }

  protected onInit(): Promise<void> {
    return this._getEnvironmentMessage().then(message => {
      this._environmentMessage = message;
       this._sp = spfi().using(SPFx(this.context));
    });
  }



  private _getEnvironmentMessage(): Promise<string> {
    if (!!this.context.sdks.microsoftTeams) { // running in Teams, office.com or Outlook
      return this.context.sdks.microsoftTeams.teamsJs.app.getContext()
        .then(context => {
          let environmentMessage: string = '';
          switch (context.app.host.name) {
            case 'Office': // running in Office
              environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOffice : strings.AppOfficeEnvironment;
              break;
            case 'Outlook': // running in Outlook
              environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOutlook : strings.AppOutlookEnvironment;
              break;
            case 'Teams': // running in Teams
            case 'TeamsModern':
              environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentTeams : strings.AppTeamsTabEnvironment;
              break;
            default:
              environmentMessage = strings.UnknownEnvironment;
          }

          return environmentMessage;
        });
    }

    return Promise.resolve(this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentSharePoint : strings.AppSharePointEnvironment);
  }

  protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
    if (!currentTheme) {
      return;
    }

    this._isDarkTheme = !!currentTheme.isInverted;
    const {
      semanticColors
    } = currentTheme;

    if (semanticColors) {
      this.domElement.style.setProperty('--bodyText', semanticColors.bodyText || null);
      this.domElement.style.setProperty('--link', semanticColors.link || null);
      this.domElement.style.setProperty('--linkHovered', semanticColors.linkHovered || null);
    }

  }

  protected onDispose(): void {
    ReactDom.unmountComponentAtNode(this.domElement);
  }

  protected get dataVersion(): Version {
    return Version.parse('1.0');
  }

  protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: strings.PropertyPaneDescription
          },
          groups: [
            {
              groupName: strings.BasicGroupName,
              groupFields: [
                PropertyPaneTextField('description', {
                  label: "Description"
                })
              ]
            },
            {
              groupName: "Property Controls Test",
              groupFields: [
                 PropertyFieldNumber('numberValue', {
                 key: "numberValue",
                 label: "Number of items to display",
                 description: "Number field description",
                 value: this.properties.numberValue,
                 maxValue: 50,
                 minValue: 1,
                disabled: false
              })]}
          ]
        }
      ]
    };
  }
}

This way, you can dynamically display SharePoint list items based on a user-defined count in the Accordion Web Part.

Now it’s time to deploy the client-side web part to SharePoint Online so that we can use it on the SharePoint site.

Deploy SPFx Accordion Webpart

To deploy the SPFx client-side web part to your SharePoint Online app catalog site or site collection app catalog, run the commands below, which will create the .sppkg file under the SharePoint folder.

gulp bundle --ship

gulp package-solution --ship

I hope you found this article helpful! In this article, I have provided three different examples of displaying SharePoint list items in the SharePoint Framework Accordion Web Part. Follow this article if you are also trying to display SharePoint list items in the accordion control.

Also, you may like:

Power Apps functions free pdf

30 Power Apps Functions

This free guide walks you through the 30 most-used Power Apps functions with real business examples, exact syntax, and results you can see.

Download User registration canvas app

DOWNLOAD USER REGISTRATION POWER APPS CANVAS APP

Download a fully functional Power Apps Canvas App (with Power Automate): User Registration App