SharePointVault ~/gotchas/lists ⌕ Search 404 pages, error messages, scripts…Ctrl K ★ Star
M↓ README.mdM↓ gallery-cards-render-from-tileprops.md
sharepoint-vault›gotchas›lists›gallery-cards-render-from-tileprops.mdhistoryedit on GitHub
● GOTCHAlists· 5 min read

Gallery cards render from tileProps, not from formatter

applies to: SharePoint Online (PnP PowerShell and plain REST / SPFx)reviewed: 2026-09-24#lists#document-library#view-formatting#gallery#pnp-powershell#rest-api#spfx

In a document library's Gallery view the card is built from a nested tileProps.formatter; the top-level formatter of the tile-formatting schema is ignored, so a hand-written tile formatter silently renders as the stock card — put your design in tileProps.formatter and switch the view with ViewType2 = "TILES".

V zobrazení Galerie v dokumentové knihovně se karta staví z vnořeného tileProps.formatter; kořenový formatter z tile-formatting schématu se ignoruje, takže ručně psaný tile formatter se tiše vykreslí jako výchozí karta – návrh patří do tileProps.formatter a zobrazení se přepíná přes ViewType2 = "TILES".

Symptom

You author tile formatting against the documented schema, push it to a document library view, and nothing you designed appears. There is no error — the formatter saves, reads back byte-identical, and the view renders the stock card (thumbnail, file name, modified date).

The failure changes shape depending on one unrelated property, which makes it read like a layout bug:

ViewType2 CustomFormatter Rendered
(empty) tile formatting JSON plain list — as if no formatter existed
TILES tile formatting JSON stock gallery card — as if no formatter existed

Chasing it through the CSS property allowlist, TabularView, or the size fields changes nothing, because none of them is the cause.

Cause

The library's Gallery layout is driven by ViewType2 = "TILES" alone — a gallery with no CustomFormatter at all renders the stock card perfectly well. That is the first surprise: the default tiles are a built-in mode, not a default template you are overriding.

The second is that the card design lives one level deeper than the schema suggests. What the in-product Document card designer writes is:

{
  "$schema": "https://developer.microsoft.com/json-schemas/sp/v2/tile-formatting.schema.json",
  "height": 250, "width": 250, "hideSelection": false, "fillHorizontally": true,
  "formatter": { /* present, valid, and never rendered */ },
  "tileProps": {
    "height": 696, "width": 254, "hideSelection": false, "fillHorizontally": true,
    "formatter": { /* THIS is the card */ }
  }
}

tileProps is not in the published tile-formatting schema, so writing to the documented top-level formatter looks correct and fails quietly. Both keys can coexist — the top-level one is simply dead weight.

Fix

Set the layout and put the design in tileProps.formatter — in two separate calls, layout first:

# 1) layout - the formatter alone will not switch a view to gallery
Set-PnPView -List $listId -Identity 'Gallery' -Values @{ ViewType2 = 'TILES' }

# 2) the card itself
Set-PnPView -List $listId -Identity 'Gallery' -Values @{ CustomFormatter = $json }

Why two calls and not one. Switching a view to TILES makes SharePoint install its own default tile template alongside the layout change. Send ViewType2 and CustomFormatter in the same request and the card you just wrote is the one that loses — you get the stock card again, with no error, which looks exactly like the tileProps mistake above and sends you back to debugging JSON that was fine. Order matters too: layout first, card second.

Same thing over plain REST (SPFx, no PnP)

ViewType2 is writable over REST as well, which is not obvious — it appears in no request sample and reads like a server-computed property. Verified live from an SPFx web part with SPHttpClient as a delegated (non-admin-consent) user:

PATCH /_api/web/lists(guid'<listId>')/views(guid'<viewId>')
Content-Type: application/json
OData-Version: 4.0          (added by SPHttpClient.configurations.v1)
X-RequestDigest: <fresh digest>
IF-MATCH: *

{ "@odata.type": "#SP.View", "TabularView": true, "ViewType2": "TILES" }

Then a second PATCH with { "@odata.type": "#SP.View", "CustomFormatter": "<json string>" }.

  • Keep TabularView: true. The native gallery has it set; with false SharePoint ignores the tile formatter and falls back to the stock card — the same silent failure by a third route.
  • CustomFormatter is a string containing the JSON, not a nested object.
  • Send it as plain JSON (Content-Type: application/json) with @odata.type, not odata=verbose with __metadata — SPHttpClient.configurations.v1 adds odata-version: 4.0, which a verbose body then collides with (HTTP 400). See metadata-body-requires-verbose.md. The annotation is accepted because of the OData-Version: 4.0 header, not because of the Content-Type: a live A/B test (2026-09-24) found @odata.type passing under both application/json and …;odata=nometadata with that header, and failing under both without it — which is what a bare fetch probe sends (POST /views takes an SP.View body).
  • Creating the view from scratch works the same way: POST /views with { "@odata.type": "#SP.View", "Title": …, "PersonalView": false, "RowLimit": …, "ViewQuery": … } under the same plain Content-Type (or, simpler, with no annotation at all — the recipe measured in the article linked above), then set columns via POST …/views(guid'…')/viewfields/removeallviewfields followed by one POST …/viewfields/addviewfield('<InternalName>') per column. Those two endpoints take no body — send the digest and Accept only; an empty JSON body with a Content-Type is rejected by some farms.
  • Wrap the ViewType2 PATCH in its own error branch. If it is refused, the row-level settings you sent in a combined call would go down with it; on its own, the view degrades to a plain list and everything else still applies.

Build the card on the product's own structure rather than from scratch — the classes carry the click target, hover, selection and keyboard behaviour, and filepreview gives a real document thumbnail that an <img> pointed at getpreview.ashx will not match:

{
  "elmType": "div",
  "attributes": { "class": "sp-card-container" },
  "children": [
    { "elmType": "div",
      "attributes": { "class": "sp-card-defaultClickButton" },
      "customRowAction": { "action": "defaultClick" } },
    { "elmType": "div",
      "attributes": { "class": "ms-bgColor-white sp-css-borderColor-neutralLight sp-card-borderHighlight sp-card-subContainer" },
      "children": [
        { "elmType": "div",
          "attributes": { "class": "sp-card-previewColumnContainer" },
          "children": [
            { "elmType": "div",
              "attributes": { "class": "sp-card-imageContainer" },
              "children": [
                { "elmType": "filepreview",
                  "attributes": { "src": "@thumbnail.512x432" },
                  "style": { "height": "170px" },
                  "filePreviewProps": {
                    "fileTypeIconClass": "sp-fileTypeIcon-cardDesigner",
                    "brandTypeIconClass": "sp-brandTypeIcon-cardDesigner" } } ] } ] }
      ] }
  ]
}

Column display names are available as [!InternalName.DisplayName], which is how the designer renders its field labels.

Notes

  • Read the product's own output before designing. Create a gallery view in the UI, edit the card once in the designer, then read CustomFormatter back over REST. That one round trip answers questions no amount of schema-reading will.
  • Column formatting does not carry into cards — a Choice column with a coloured pill formatter shows as plain text in the designer's card. Re-implement the visual inside tileProps.formatter.
  • An extra div injected as the first child of sp-card-subContainer (a coloured status stripe, say) flashes on load and then disappears once the product's own CSS settles. Put the colour on the container itself with an inline border-top-* instead — an inline style outranks the class.
  • Accent colours belong on border-top-width/-style/-color as separate properties; expression-driven shorthand is easy to get wrong and harder to debug.
  • Related: View formatting JSON can't contain < or & — the same view formatter is also subject to that restriction.
Was this helpful?ask or add your experience on GitHub →
Saved you an hour? A star on GitHub helps other SharePoint people find the vault.★ Star on GitHub
last reviewed: 2026-09-24last updated: 2026-09-24historyedit on GitHubview source