# Custom Table of Contents for SharePoint Page Libraries -  JSON View Formatting

# Background

JSON [view customizations](https://learn.microsoft.com/en-us/sharepoint/dev/declarative-customization/formatting-syntax-reference) in SharePoint Online are usually created for local use within a list or library. The view described in this article, however, is intended for **web parts** - altering how Site Page contents are displayed across pages and templates.

![Site Page library custom Table of Content view within web part](https://cdn.hashnode.com/res/hashnode/image/upload/v1768787433439/85acf32a-15d0-41f4-825e-9c25bf0bc305.png align="center")

<center><b>Site Page Table of Contents - Example</b></center>

### Automatic Updates

Changes and additions to the Site Page library are automatically reflected *everywhere* the TOC view is currently implemented via web part.

Even when installed across hundreds of pages, updates require just a single action at the source library. This can be quite useful for wiki-style and informational sites… especially ones that will evolve and grow over time.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The TOC view is to meant to <em>augment</em> (but not replace) site and hub navigation</div>
</div>

# Implementing

> Note: Don’t let the length of this section fool you. These steps are not particularly complicated or time-consuming. I always try to include extra detail, as a fair amount of visitors stumble across this blog. 👍

## Site Page Library - *Columns*

Several custom columns are needed to define the TOC structure. These columns can be created directly within the *Site Pages* library (easiest) or within the corresponding *content type* (more flexible).

### 🔷 Sort Order

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Technically, this step is optional. You can rely on alpha sort (or another system column) for your TOC view. However, a custom sort method (1) <strong>enables indentation to work predictably</strong>. It also (2) <strong>prevents ambiguity</strong>, like the page titled <em>Home</em> showing up in the middle<em> </em>(after items A-G), for example.</div>
</div>

Create a *number* column, which will be used to define the TOC page sequence. This field is not referenced by the formatting JSON, so name it as you wish:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768761593758/6be2de78-5693-4a02-8d8b-2829ba4440a4.png align="center")

<center><b>Column setup for <i>Order</i> field</b></center>

The above settings will work fine, but you don’t have to *strictly* adhere to them. You might want to disable **enforce unique values**, for example, as it can be a minor pain later on, without much benefit.

### 🔷 Indentation

Create another *number* column, which will define the **hierarchy** within the TOC. Name it *Indent*, to **align with field references** in the formatting JSON:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768762321765/740b8d08-8ebf-4f24-8087-a7c5aee7a1d6.png align="center")

<center><b>Column setup for <i>Indent</i> field</b></center>

<div data-node-type="callout">
<div data-node-type="callout-emoji">ℹ</div>
<div data-node-type="callout-text">Setting a default value is recommended. The JSON formatting expects indentation values of <strong>0, 2, 4 or 6</strong></div>
</div>

## Site Page Library - *Custom View(s)*

### 🔷 Table of Contents (display view)

This view will (later) receive the custom JSON formatting.

1. Create a new public view (call it *TOC* or something similar)
    
2. Include these columns:
    
    1. *Indent*
        
    2. *Name (for use in forms)*
        
    3. *Title*
        
3. Set the Sort parameter to use the custom *Order* column (assuming you created it):
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768764440171/903aa2c9-ac9c-44a5-9931-dde4444396cb.png align="center")
    
4. Set the Filter criteria to only include items with an *Indent* value:
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768764522931/d6e0e28d-15d5-4c6e-8fda-4897d6e19138.png align="center")
    
    <div data-node-type="callout">
    <div data-node-type="callout-emoji">💡</div>
    <div data-node-type="callout-text">Ignore the lowercase “i” in the screenshot above - field name is <em>Indent </em>(woops)</div>
    </div>
    
    <div data-node-type="callout">
    <div data-node-type="callout-emoji">ℹ</div>
    <div data-node-type="callout-text">You can add a boolean column (or similar) if you’d like more control over item visibility in the TOC. <em>Indent </em>works fine, but if that column has a default value set, new pages will show up instantly.</div>
    </div>
    

### 🔷 Table of Contents (management view)

Optional step, but you may want to do **one** of these:

1. **Add the new library columns** to the *All Pages* system view (and make it the library default, instead of the clumsy, grouped *By Author* view).
    
2. **Create a new view** for working with TOC site page metadata (that *won’t* receive the JSON formatting)
    

![Modified All Pages system view](https://cdn.hashnode.com/res/hashnode/image/upload/v1768790186167/a356502f-b978-480c-ae4c-e8fcb5ee3691.png align="center")

<center><b>Example modified <i>All Pages</i> view</b></center>

## Site Page Library - *Bulk Edit Existing Pages*

If there are existing pages in the library, **use quick edit to assign column values** (*Order*, *Indent*) to the pages you want included in the TOC.

Also, make sure each page has a *Title* value.

### Column Usage Tips: *Indent*

The indent value affects the padding of TOC items and for the more indented ones, font size.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768762968075/e99b1db3-0c64-43af-b442-46bdfdcfb3fa.png align="center")

<center><b><i>Indent</i> value conditional formatting</b></center>

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Note: the TOC items will not display the <em>Indent </em>value, as seen above. That was added temporarily for illustration purposes.</div>
</div>

### Column Usage Tips: *Order*

Give yourself plenty of “space” when assigning *Order* values. Space them apart by counts of 10-100, to provide flexibility for reordering or adding later.

If you number each page sequentially (1,2,3…) you might need to re-number a **bunch** of Order values to insert new pages where you want them, later.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768771564891/1b8c9246-e3c3-4209-89c8-f7e3e74ab3a7.png align="center")

<center><b><i>All Pages</i> library view, showing <i>Order</i> values</b></center>

# View Formatting JSON

## Details / Notes

Overall, it’s a *decent* custom view - not a perfect one.

* Much of the styling in this JSON is done using [classes](https://zerg00s.github.io/sp-modern-classes/), but there are some fixed values in the mix (margins, e.g.)
    
* There are probably some minor opportunities to improve HTML / flexbox stuff, too
    
* The key lines for conditional styling are **<mark>20</mark>**<mark>, </mark> **<mark>39</mark>** <mark>and </mark> **<mark>45</mark>**
    
    * Some of those conditions resolve identically. Those are legacies from development, left in to allow for future customization.
        

```json
{
  "$schema": "https://developer.microsoft.com/json-schemas/sp/v2/row-formatting.schema.json",
  "schema": "https://developer.microsoft.com/json-schemas/sp/view-formatting.schema.json",
  "target": "_self",
  "debugMode": false,
  "hideColumnHeader": true,
  "hideSelection": true,
  "rowFormatter": {
    "elmType": "a",
    "attributes": {
      "class": "ms-fontColor-themeDarker ms-fontColor-themeDarker--hover",
      "href": "=@currentWeb + '/SitePages/' + [$FileLeafRef]",
      "target": "_self"
    },
    "style": {
      "text-decoration": "none",
      "border": "none",
      "cursor": "pointer",
      "margin-bottom": "2px",
      "margin-left": "=if([$indent] == 2,'12px', if([$indent] == 4, '24px', if([$indent] == 6,'36px', '0px')))",
      "padding": "2px",
      "overflow": "hidden",
              "width": "100%",
              "max-width": "280px"
    },
    "customRowAction": {
      "action": "defaultClick"
    },
    "children": [
      {
        "elmType": "div",
        "style": {
          "display": "flex",
          "flex-direction": "column",
          "align-items": "baseline",
          "flex-wrap": "wrap",
          "box-sizing": "border-box",
          "border-width": "0px",
          "border-left-width": "=if([$indent] == 0,'3px', if([$indent] == 2, '3px', '3px'))",
          "border-style": "solid",
          "padding-left": "4px",
              "width": "100%"
        },
        "attributes": {
          "class": "=if([$indent] == 0,'ms-fontSize-mPlus ms-borderColor-themeDark', if([$indent] == 2, 'ms-fontSize-mPlus ms-borderColor-themeSecondary', if([$indent] == 4,'ms-fontSize-m ms-borderColor-themeLight', 'ms-fontSize-m ms-borderColor-themeLighter'))) + ' ms-bgColor-themeLighter--hover'"
        },
        "children": [
          {
            "elmType": "div",
            "style": {
              "line-height": "1.0em",
              "margin": "2px 0",
              "text-align": "left"
            },
            "txtContent": "[$Title]"
          }
        ]
      }
    ]
  }
}
```

# Document Library Web Part

Add a **Document Library web part** onto a site page and configure it like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768773772106/886e7040-1f6d-4aeb-9262-4926cd8d2464.png align="center")

<center><b><i>Document Library</i> web part setup and config</b></center>

<div data-node-type="callout">
<div data-node-type="callout-emoji">ℹ</div>
<div data-node-type="callout-text">Keep in mind, you can also add the web part to <em>page templates. </em>This is a great way to rapidly propagate the web part and view across a site in a consistent manner.</div>
</div>

By this point, you should be in business.

If this worked out for you, or if you have any difficulties, please **drop me a note** in the comments.
