---
title: Table-of-content (TOC)
slug: experience/table-of-content-toc
docTags: 
createdAt: 2025-08-15T08:05:09.136Z
---

:::hint{type="danger"}
In earlier versions of PXP, TOCs have been configured via a toolbar. The TOC field in the toolbar is **deprecated&#x20;**&#x6E;ow in favour of the configuration below. It allow greater flexibility.&#x20;
:::



This guide presents a fully configurable and flexible method for implementing a Table of Contents (TOC) in **PXP**.
Depending on the structure of your app, you may configure the button to appear in different places in your app.&#x20;
The most common places are the App Bar and the navigation bar.&#x20;

&#x20;**Reference App:&#x20;**[https://staging.purplemanager.com/#appDetail;id=9e5a0da6-c284-4720-9b59-d942b2c48bf7](https://staging.purplemanager.com/#appDetail;id=9e5a0da6-c284-4720-9b59-d942b2c48bf7)&#x20;

# Content Body Component (APP + WEB)

## 1. TOC Button

### 1. 1. Overview

Create a custom TOC button using an **HTML component**.
This approach allows full control over styling, icon selection, and functionality.

### 1. 2.  JSON Configuration

**1. 2. 1  Views.json**

```json
{
  "tag": "img",
  "type": "html",
  "tap": {
    "type": "setContextVariables",
    "variables": [
      {
        "key": "tocOpened",
        "value": "$functions.updateToc($context.tocOpened)"
      }
    ]
  },
  "attributes": {
    "src": "resource://dynamic/storefront/assets/images/icons/toc.svg",
  },
  "class": "toc-icon"
}
```

**1. 2. 2 custom.server.js**

```javascript
updateToc:(tocOpened) => {
        return !tocOpened;
}
```

**Explanation:**

- **tag**: "img" → Specifies the element type (an image in this case).
- **type**: "html" → Renders as a raw HTML component.
- **tap**: Sets the tocOpened context variable by calling the updateToc function. This toggles the TOC list open/closed. It also sets a context variable accordingly via custom.server.js. .&#x20;
- **attributes.src**: Path to the TOC icon image. In this example, the icon is stored in **dynamic resources**.
- **class**: "toc-icon" → Apply custom CSS for styling.

## 2. TOC Panel

This section explains how to configure a fully functional TOC panel that displays a list of content items, allows navigation to each, and provides a close action.

### 2. 1. JSON Configuration

**2. 1. 1 TOC Data Source**

```json
{
  "type": "content",
  "contextKey": "toc-content",
  "filter": {
    "properties": {
      "key": "slug",
      "value": ":bundleSlug"
    }
  },
  "fetchOptions": {
    "includeBundledContent": true
  }
}
```

**Explanation:**

- **Purpose:** This data source should be added to the **data** field of the view. It provides the content list for the TOC Panel.
- **Content Type:** In this example, a **bundle** type is requested.
- **includeBundledContent:** Ensures that post data within the bundle is included in the response.
- **Alternative:** The same logic can be implemented in the URL resolver:



```javascript
const content = await dataResolver.findContentById(id, { includeBundledContent: true });

```

**2. 1.2 List Configuration**

**2. 1.2.1 Views.json**

```json
{
  "content": [
    {
      "tag": "div",
      "type": "html",
      "content": "[]",
      "class": "toc-backface",
      "tap": {
        "type": "setContextVariables",
        "variables": [
          {
            "key": "tocOpened",
            "value": "$functions.updateToc($context.tocOpened)"
          }
        ]
      },
      "condition": {
        "value": "$context.tocOpened",
        "compareValue": "true"
      }
    },
    {
      "content": [
        {
          "content": [
            {
              "content": [
                {
                  "tag": "h2",
                  "type": "html",
                  "content": "Toc title"
                },
                {
                  "content": [
                    {
                      "tag": "button",
                      "type": "html",
                      "class": "icon icon-close",
                      "tap": {
                        "type": "setContextVariables",
                        "variables": [
                          {
                            "key": "tocOpened",
                            "value": "$functions.updateToc($context.tocOpened)"
                          }
                        ]
                      }
                    }
                  ],
                  "type": "section",
                  "tag": "div",
                  "class": "toc-actions"
                }
              ],
              "type": "section",
              "class": "toc-header"
            },
            {
              "content": [
                {
                  "content": {
                    "content": [
                      {
                        "tag": "img",
                        "type": "html",
                        "content": "",
                        "attributes": {
                          "src": "$context.context.thumbnails.default"
                        },
                        "class": "toc-image"
                      },
                      {
                        "content": [
                          {
                            "tag": "h2",
                            "type": "html",
                            "content": "$context.context.name",
                            "class": "toc-title"
                          },
                          {
                            "tag": "p",
                            "type": "html",
                            "content": "$context.context.description",
                            "class": "toc-description"
                          }
                        ],
                        "type": "section",
                        "class": "toc-content"
                      }
                    ],
                    "type": "section",
                    "tap": {
                      "type": "navigate",
                      "path": "read/$context['toc-content'][0].properties.slug/$context.context.properties.slug/"
                    },
                      "class": [
                          "toc-card",
                           {
                              "value": "active",
                              "condition": {
                              "compareValue": "$context.pathParams.postSlug",
                              "value": "$context.context.properties.slug"
                              }
                           }
                      ]                  

                    "class": "toc-card"
                  },
                  "dataSource": {
                    "data": "$functions.getBundleList($context['toc-content'][0])",
                    "type": "context"
                  },
                  "type": "list"
                }
              ],
              "type": "section",
              "class": "toc-content"
            }
          ],
          "type": "section",
          "class": "toc collapsible"
        }
      ],
      "type": "section",
      "condition": {
        "compareValue": "true",
        "value": "$context.tocOpened"
      }
    }
  ],
  "type": "section",
  "class": "toc-container"
}
```

**2.1.2.2 custom.server.js**

```javascript
getBundleList:(bundle) => {
        return bundle.contents.map(content => content.post)
}
```

**2.1.2.3 custom.css**

```css
.toc-icon {
  filter: invert(1);
}

.toc-container .toc-backface {
  position: fixed;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  background-color: rgba(0, 0, 0, 0.6);
}

.toc {
  padding: 0 28px 24px 28px;
  font-family: var(--contentFont);
  border-radius: 4px;
  display: grid;
  justify-content: end;
}

.toc.collapsible {
  position: fixed;
  top: 56px;
  z-index: 1;
  width: 100%;
  max-width: 384px;
  right: 0;
  bottom: 50px;
  height: auto;
  overflow: auto;
  background-color: white;
  -webkit-overflow-scrolling: touch;
}

.toc.collapsible .toc-header button {
  display: block;
}

.toc .toc-actions {
  display: flex;
  justify-content: end;
  -webkit-justify-content: flex-end;
  align-items: flex-end;
  -webkit-align-items: flex-end;
}

.toc .toc-actions button.icon {
  margin-right: 0.2em;
}

.toc .toc-header {
  text-transform: uppercase;
  border-bottom: 2px solid black;
  padding-bottom: 12px;
  padding-top: 35px;
  display: flex;
  background-color: white;
  z-index: 1;
  justify-content: space-between;
  position: -webkit-sticky;
  position: sticky;
  top: 0;
  height: 70px;
}

.toc .toc-header h2 {
  letter-spacing: 0.3px;
  font-size: 16px;
  line-height: 22px;
  font-weight: 500;
}

.toc .toc-header button {
  display: none;
  color: black;
  font-size: 16px;
}

.toc .toc-content {
  display: grid;
}

.toc-card {
  display: flex;
  align-items: center;
  background: #fff;
  border-radius: 12px;
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
  padding: 16px;
  margin-bottom: 12px;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.toc-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
}
.toc-card.active {
  font-weight: 700;
  background-color: #f0f4ff; /* light highlight */
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
  border: 1px solid #3366ff; /* optional accent */
}

.toc-image {
  width: 64px;
  height: 64px;
  border-radius: 50%;
  object-fit: cover;
  margin-right: 16px;
}

.toc-content {
  flex: 1;
}

.toc-title {
  font-size: 1.2rem;
  margin: 0;
  font-weight: 600;
  color: #333;
}

.toc-description {
  font-size: 0.9rem;
  margin-top: 4px;
  color: #666;
}

```

### 2.2. Component Breakdown

1. **toc-backface**
   - Displays a gray overlay behind the TOC Panel when it’s open.
   - Clicking it closes the TOC Panel using setContextVariables.
   - Visibility is controlled by the condition: "$context.tocOpened" === true.
2. **toc-header**
   - Displays the TOC title.
   - Contains the **toc-actions** section with a close button to hide the TOC Panel.
3. **toc-content** (Main List)
   - Displays the list of TOC items.
   - Uses the toc-content data source from **1.1**.
   - Uses the helper function getBundleList to normalize data into a list of posts.
   - Works with any source type (bundle, dossier, collection) as long as the result is a post list.
4. **TOC Item (toc-card)**
   - Contains:
     - **Thumbnail** (toc-image)
     - **Post title** (toc-title)
     - **Description** (toc-description)
   - Clicking a TOC item navigates to the post using:

```json
read/$context['toc-content'][0].properties.slug/$context.context.properties.slug/

```

- **Post slug:** $context.context.properties.slug
- **Bundle slug:** $context\['toc-content']\[0].properties.slug
- **active CSS** **class** is applied to a TOC card when its content slug matches the post slug in the current URL.

### Notes

- You can customize layout, styles, and actions to fit project needs.

# Post Swiper (APP)

## 1. TOC Button

same as the content body component.

## 2. TOC Panel

### 2. 1. JSON Configuration

**2. 1. 1 TOC Data Source**

Not needed.

**2. 1.2 List Configuration**

**2. 1.2.1 Views.json**

Similar to the content body component with some updates:

**1. Tap action**: Executes two sequential actions:

- **Navigate** → Uses a URL that reflects the current view’s path. A query parameter from the swiper component is appended to this URL, representing the current post being displayed.
- **Close TOC** → Explicitly closes the table of contents (since it is not closed automatically for the Post Swiper).

```json
"tap": {
  {
    "actions": [
      {
        "type": "navigate",
        "path": "readBundle/$context.content.properties.slug",
        "params": {
          "swiper-id": "$context.context.id"
        }
      },
      {
        "type": "setContextVariables",
        "variables": [
          {
            "key": "tocOpened",
            "value": "$functions.updateToc($context.tocOpened)"
          }
        ]
      }
    ],
    "type": "multiAction"
  }
}
```

**2. Active class for toc-card:&#x20;**&#x54;he condition for applying the **active class** has been updated:

- It now uses **id** instead of **slug**.
- The swiper-id is used as the reference for the currently displayed post.

```json
"class": [
    "toc-card", {
        "value": "active",
        "condition": {
           "compareValue": "$context['swiper-id']",
            "value": "$context.context.id"
        }
    }
]
```

**3. TOC panel data source:&#x20;**&#x54;hanks to the URL resolver, the **bundle** is available under content.

&#x20;in the context, making it accessible for rendering.

```json
"dataSource": {
      "data": "$functions.getBundleList($context.content)",
      "type": "context"
}
```

**Final output**

```json
{
  "content": [
    {
      "content": [
        {
          "content": [
            {
              "tag": "h2",
              "type": "html",
              "content": "Toc title"
            },
            {
              "content": [
                {
                  "tag": "button",
                  "type": "html",
                  "class": "icon icon-close",
                  "tap": {
                    "type": "setContextVariables",
                    "variables": [
                      {
                        "key": "tocOpened",
                        "value": "$functions.updateToc($context.tocOpened)"
                      }
                    ]
                  }
                }
              ],
              "type": "section",
              "tag": "div",
              "class": "toc-actions"
            }
          ],
          "type": "section",
          "class": "toc-header"
        },
        {
          "content": [
            {
              "content": {
                "content": [
                  {
                    "tag": "img",
                    "type": "html",
                    "content": "",
                    "attributes": {
                      "src": "$context.context.thumbnails.default"
                    },
                    "class": "toc-image"
                  },
                  {
                    "content": [
                      {
                        "tag": "h2",
                        "type": "html",
                        "content": "$context.context.name",
                        "class": "toc-title"
                      },
                      {
                        "tag": "p",
                        "type": "html",
                        "content": "$context.context.description",
                        "class": "toc-description"
                      }
                    ],
                    "type": "section",
                    "class": "toc-content"
                  }
                ],
                "type": "section",
                "tap": {
                  "type": "navigate",
                  "path": "readBundle/$context.content.properties.slug",
                  "params": {
                    "swiper-id": "$context.context.id"
                  }
                },
                "class": [
                  "toc-card",
                  {
                    "value": "active",
                    "condition": {
                      "compareValue": "$context['swiper-id']",
                      "value": "$context.context.id"
                    }
                  }
                ]
              },
              "dataSource": {
                "data": "$functions.getBundleList($context.content)",
                "type": "context"
              },
              "type": "list",
              "attributes": {
                "data": {
                  "sample": "$context['swiper-id']"
                }
              }
            }
          ],
          "type": "section",
          "class": "toc-content"
        }
      ],
      "type": "section",
      "class": "toc collapsible"
    }
  ],
  "type": "section",
  "condition": {
    "compareValue": "true",
    "value": "$context.tocOpened"
  }
}
```

****
