Table-of-content (TOC)
In earlier versions of PXP, TOCs have been configured via a toolbar. The TOC field in the toolbar is deprecated now in favour of the configuration below. It allow greater flexibility.
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. The most common places are the App Bar and the navigation bar.
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
{
"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
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. .
- 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
{
"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:
const content = await dataResolver.findContentById(id, { includeBundledContent: true });
2. 1.2 List Configuration
2. 1.2.1 Views.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
getBundleList:(bundle) => {
return bundle.contents.map(content => content.post)
}2.1.2.3 custom.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
- 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.
- toc-header
- Displays the TOC title.
- Contains the toc-actions section with a close button to hide the TOC Panel.
- 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.
- TOC Item (toc-card)
- Contains:
- Thumbnail (toc-image)
- Post title (toc-title)
- Description (toc-description)
- Clicking a TOC item navigates to the post using:
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).
"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: The 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.
"class": [
"toc-card", {
"value": "active",
"condition": {
"compareValue": "$context['swiper-id']",
"value": "$context.context.id"
}
}
]3. TOC panel data source: Thanks to the URL resolver, the bundle is available under content.
in the context, making it accessible for rendering.
"dataSource": {
"data": "$functions.getBundleList($context.content)",
"type": "context"
}Final output
{
"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"
}
}