SharePoint Approval Status Hover Card (PnP Pattern) — A Publish-Ready Guide + Variations

This article shows how to implement the PnP “generic-approval-status-hover-card” pattern to transform SharePoint’s built-in Approval Status into a clean icon + label with a rich hover card (callout). Then we’ll build multiple variations you can copy/paste and adapt.

This is 100% SharePoint Column Formatting JSON (display-only). It does not modify item data—only how the field renders in the list view. (Microsoft Learn)


Why this pattern works so well

When you enable Content Approval in a list/library, SharePoint tracks an internal moderation state that formatting can read using:

  • [$_ModerationStatus] (Draft / Pending / Approved / Rejected)

The PnP sample uses that value to:

  1. Render a smart status label in the cell.
  2. Show a custom “details” panel using customCardProps on hover.

Microsoft documents custom hover cards via customCardProps + openOnEvent: "hover" and how to build the card markup with elmType, children, style, txtContent, and Excel-like expressions. (Microsoft Learn)


Prerequisites (don’t skip this)

1) Turn on Content Approval

This pattern assumes your list/library is using approval (moderation). Without it, [$_ModerationStatus] will not behave as expected.

2) Make sure the right fields exist (and are usable in the view)

At minimum, you typically want:

  • Approval Status → used as [$_ModerationStatus]
  • Modified → used as [$Modified] for “since” / “last updated” text

The PnP sample explicitly calls out Content Approval and depends on these columns for the experience. (Microsoft Adoption)


How the hover card works (the mental model)

In SharePoint column formatting, the cell is a JSON “tree”:

  • elmType: the HTML-like element (div, span, etc.)
  • children: nested elements
  • attributes: things like iconName, title, class
  • style: inline styles
  • txtContent: text (supports expressions)

The hover card is attached via customCardProps:

  • customCardProps.formatter: the card body JSON
  • openOnEvent: "hover" or "click"
  • directionalHint: where the callout opens
  • isBeakVisible: callout pointer visibility

All of that is documented in Microsoft’s syntax reference. (Microsoft Learn)


Baseline: Minimal “Approval Status + Hover Summary” (stable starting point)

This is the baseline I recommend you validate first. It keeps the hover card simple so you can confirm the mechanics, then expand safely.

Apply this to the Approval Status column using Format this column → Advanced mode.

{
"$schema": "https://developer.microsoft.com/json-schemas/sp/v2/column-formatting.schema.json",
"elmType": "div",
"attributes": {
"title": "='Approval: ' + [$_ModerationStatus]"
},
"children": [
{
"elmType": "span",
"attributes": {
"iconName": "=if([$_ModerationStatus]=='Draft','Flow',if([$_ModerationStatus]=='Pending','WorkFlow',if([$_ModerationStatus]=='Approved','StatusCircleCheckmark','StatusCircleBlock')))",
"class": "='ms-fontColor-' + if([$_ModerationStatus]=='Approved','green',if([$_ModerationStatus]=='Pending','orange',if([$_ModerationStatus]=='Rejected','red','neutralSecondaryAlt')))"
},
"style": { "padding-right": "6px" }
},
{
"elmType": "span",
"txtContent": "=[$_ModerationStatus]"
}
],
"customCardProps": {
"formatter": {
"elmType": "div",
"style": { "padding": "12px 14px", "width": "320px" },
"children": [
{
"elmType": "div",
"style": { "font-size": "16px", "font-weight": "600", "margin-bottom": "10px" },
"txtContent": "Approval details"
},
{
"elmType": "div",
"style": { "font-size": "12px", "margin-bottom": "6px" },
"txtContent": "='Status: ' + [$_ModerationStatus]"
},
{
"elmType": "div",
"style": { "font-size": "12px" },
"txtContent": "='Last update: ' + toLocaleDateString([$Modified])"
}
]
},
"openOnEvent": "hover",
"directionalHint": "bottomCenter",
"isBeakVisible": true
}
}

Why this baseline is “safe”:

  • It uses only documented building blocks (customCardProps, toLocaleDateString, expressions). (Microsoft Learn)
  • It references only [$_ModerationStatus] and [$Modified].

Variation 1: “Pending SLA” hover card (days pending)

If the item is Pending, show how many days it has been pending based on Modified. This uses @now math in milliseconds plus floor() and toString(), which are part of the formatting expression set. (Microsoft Learn)

{
"$schema": "https://developer.microsoft.com/json-schemas/sp/v2/column-formatting.schema.json",
"elmType": "div",
"children": [
{
"elmType": "span",
"attributes": {
"iconName": "=if([$_ModerationStatus]=='Pending','Clock',if([$_ModerationStatus]=='Approved','StatusCircleCheckmark','CircleStopSolid'))",
"class": "='ms-fontColor-' + if([$_ModerationStatus]=='Pending','orange',if([$_ModerationStatus]=='Approved','green','red'))"
},
"style": { "padding-right": "6px" }
},
{ "elmType": "span", "txtContent": "=[$_ModerationStatus]" }
],
"customCardProps": {
"formatter": {
"elmType": "div",
"style": { "padding": "12px 14px", "width": "340px" },
"children": [
{
"elmType": "div",
"style": { "font-size": "16px", "font-weight": "600", "margin-bottom": "10px" },
"txtContent": "Approval SLA"
},
{
"elmType": "div",
"style": { "font-size": "12px", "margin-bottom": "6px" },
"txtContent": "='Status: ' + [$_ModerationStatus]"
},
{
"elmType": "div",
"style": {
"font-size": "12px",
"font-weight": "600"
},
"attributes": {
"class": "=if([$_ModerationStatus] != 'Pending','', if((@now - [$Modified]) > 432000000,'ms-fontColor-red', if((@now - [$Modified]) > 172800000,'ms-fontColor-orange','ms-fontColor-green')))"
},
"txtContent": "=if([$_ModerationStatus] != 'Pending','Not pending','Pending for ' + toString(floor((@now - [$Modified]) / 86400000)) + ' day(s)')"
},
{
"elmType": "div",
"style": { "font-size": "12px", "margin-top": "8px" },
"txtContent": "='Last updated: ' + toLocaleDateString([$Modified])"
}
]
},
"openOnEvent": "hover",
"directionalHint": "bottomCenter",
"isBeakVisible": true
}
}

Variation 2: “Compact pill in the cell, rich hover card”

This makes the list view cleaner: the cell becomes a “pill badge”, while the hover card carries the full detail.

{
"$schema": "https://developer.microsoft.com/json-schemas/sp/v2/column-formatting.schema.json",
"elmType": "div",
"style": {
"display": "inline-block",
"padding": "2px 10px",
"border-radius": "999px",
"font-size": "12px",
"font-weight": "600"
},
"attributes": {
"class": "=if([$_ModerationStatus]=='Approved','ms-bgColor-green ms-fontColor-white',if([$_ModerationStatus]=='Pending','ms-bgColor-yellow ms-fontColor-neutralPrimary',if([$_ModerationStatus]=='Rejected','ms-bgColor-red ms-fontColor-white','ms-bgColor-neutralLight ms-fontColor-neutralPrimary')))",
"title": "='Approval: ' + [$_ModerationStatus]"
},
"txtContent": "=[$_ModerationStatus]",
"customCardProps": {
"formatter": {
"elmType": "div",
"style": { "padding": "12px 14px", "width": "320px" },
"children": [
{
"elmType": "div",
"style": { "font-size": "16px", "font-weight": "600", "margin-bottom": "10px" },
"txtContent": "Approval Summary"
},
{ "elmType": "div", "style": { "font-size": "12px", "margin-bottom": "6px" }, "txtContent": "='Status: ' + [$_ModerationStatus]" },
{ "elmType": "div", "style": { "font-size": "12px" }, "txtContent": "='Modified: ' + toLocaleString([$Modified])" }
]
},
"openOnEvent": "hover",
"directionalHint": "bottomCenter",
"isBeakVisible": true
}
}

Variation 3: “Outcome-focused messaging” (Approved vs Rejected vs Pending)

This variation is for teams that want the hover card to explain what to do next.

{
"$schema": "https://developer.microsoft.com/json-schemas/sp/v2/column-formatting.schema.json",
"elmType": "div",
"children": [
{
"elmType": "span",
"attributes": {
"iconName": "=if([$_ModerationStatus]=='Approved','LikeSolid',if([$_ModerationStatus]=='Rejected','DislikeSolid','Sync'))",
"class": "='ms-fontColor-' + if([$_ModerationStatus]=='Approved','green',if([$_ModerationStatus]=='Rejected','red','orange'))"
},
"style": { "padding-right": "6px" }
},
{ "elmType": "span", "txtContent": "=[$_ModerationStatus]" }
],
"customCardProps": {
"formatter": {
"elmType": "div",
"style": { "padding": "12px 14px", "width": "360px" },
"children": [
{
"elmType": "div",
"style": { "font-size": "16px", "font-weight": "600", "margin-bottom": "10px" },
"txtContent": "Approval Outcome"
},
{
"elmType": "div",
"style": { "font-size": "12px", "margin-bottom": "8px" },
"txtContent": "=if([$_ModerationStatus]=='Pending','Waiting for an approver decision.',if([$_ModerationStatus]=='Approved','Approved: this item passed moderation.','Rejected: update the content and resubmit for approval.'))"
},
{
"elmType": "div",
"style": { "font-size": "12px" },
"txtContent": "='Last update: ' + toLocaleDateString([$Modified])"
}
]
},
"openOnEvent": "hover",
"directionalHint": "bottomCenter",
"isBeakVisible": true
}
}

Debugging rules (the 90% fixes)

These come directly from how the syntax behaves:

  1. Expression errors are usually missing ) or quotes '...' inside =if(...).
  2. Blank output often means a referenced field isn’t present or isn’t available in context.
  3. Validate with a minimal baseline first, then expand.
  4. Keep Microsoft Learn’s syntax reference open while editing expressions. (Microsoft Learn)

Microsoft Learn + PnP references (the sources you should keep bookmarked)


Final Tables

Implementation Steps (copy/paste workflow)

StepActionWhat you’re validatingExpected result
1Enable Content ApprovalModeration existsApproval Status behaves (Draft/Pending/Approved/Rejected)
2Ensure Modified is availableDate output worksHover shows “Last update”
3Apply the Baseline JSONHover card plumbingIcon + label + hover panel works
4Switch to Variation 1Date math + SLAShows “Pending for X day(s)”
5Switch to Variation 2Cleaner UIPill badge + rich hover
6Switch to Variation 3Guidance messagingHover explains next steps

JSON “Cheat Sheet” (what controls what)

JSON PartWhat it doesKey properties
Cell renderingWhat you see in the list rowelmType, children, txtContent, attributes.iconName, attributes.class
Tooltip (simple hover text)Browser-style titleattributes.title
Hover cardCallout panel contentcustomCardProps.formatter
Hover behaviorWhen/where it opensopenOnEvent, directionalHint, isBeakVisible
Date formattingFriendly date displaytoLocaleDateString(), toLocaleString()
SLA mathAge since Modified@now, milliseconds math, floor(), toString()

Edvaldo Guimrães Filho Avatar

Published by