demo D138
HTML in cells, safely
Templates escape by default; the opt-out and what it opens up
allowUnsafeTemplates: true
The configuration
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.27.0/lattice-grid.min.css">
<div id="grid"></div>
<script type="module">
import React from 'https://esm.sh/react@18';
import { createRoot } from 'https://esm.sh/react-dom@18/client';
import { createGrid } from 'https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.27.0/lattice-grid.esm.min.js';
import createLatticeGrid from 'https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.27.0/modules/react.esm.min.js';
const LatticeGrid = createLatticeGrid({ React, createGrid });
// Each row stores a value that happens to contain markup.
const rows = [
{ id: 1, service: 'edge-router', tier: 'gold', label: '<b>10G</b> uplift' },
{ id: 2, service: 'billing-api', tier: 'gold', label: '<span style="color:#b45309">pending</span> review' },
{ id: 3, service: 'legacy-vpn', tier: 'bronze', label: '<em>ceased</em> on 2026-07-31' },
{ id: 4, service: 'cost-report', tier: 'silver', label: 'cost < budget, so no action' },
{ id: 5, service: 'dr-replica', tier: 'silver', label: '<b>prod</b> & <b>dr</b>' },
];
const columns = [
{ field: 'service', title: 'Service', layout: { width: 150, pin: 'start' } },
// No template at all: the plain text path, which never parses markup.
{ id: 'raw', field: 'label', title: 'The stored value', layout: { width: 260 },
cell: { wrap: true } },
// Double braces: escaped even with the opt-out set, because the opt-out is
// per interpolation, not per grid.
{ id: 'escaped', field: 'label', title: '{{value}}', layout: { width: 260 },
cell: { template: '{{value}}', wrap: true } },
// Triple braces: rendered as elements, because the flag below is set.
{ id: 'unescaped', field: 'label', title: '{{{value}}}', layout: { width: 260 },
cell: { template: '{{{value}}}', wrap: true } },
// A render function returning an element never needs the opt-out: built with
// createElement and textContent, so a value containing markup is text by
// construction rather than by a flag someone can turn off later.
{ id: 'fromElement', title: 'render to element', layout: { flex: 1, min: 220 },
cell: {
render: (p) => {
const span = document.createElement('span');
const tier = document.createElement('b');
tier.textContent = String(p.data?.tier ?? '');
span.append(tier, document.createTextNode(` · ${String(p.data?.label ?? '')}`));
return span;
},
wrap: true,
} },
];
function App() {
return (
<LatticeGrid
rowKey="id"
autoHeight
// The explicit opt-out, and the whole subject of this page. It means the
// interpolated value is trusted, so set it only when the data cannot be
// authored by anyone you would not trust with your users' sessions.
allowUnsafeTemplates
columns={columns}
rows={rows}
style={{ height: '540px' }}
/>
);
}
createRoot(document.getElementById('grid')).render(<App />);
</script>
Rendering trusted markup with allowUnsafeTemplates
By default, Lattice Grid escapes every value it puts into a cell template, so a string containing <span> or & lands on screen as literal text rather than markup. That default is what keeps a JavaScript data grid safe against XSS when a template pulls in user-submitted or third-party data: a name field, a comment, an imported spreadsheet cell. Setting allowUnsafeTemplates: true on the grid config lifts that escaping for every template column at once, so a developer reaches for it when the data is already trusted, or already sanitised upstream, and the cell genuinely needs to render markup rather than describe it: inline badges built from a template string, a highlighted substring, a small icon plus label composed in one interpolation rather than a full custom renderer. The option is grid-wide rather than per-column, so mixing untrusted and markup-bearing columns in the same grid means sanitising the untrusted values yourself before they reach the template. Turning the option on does not change how the grid virtualises rows: templates still compile once at initialisation and run at the same per-cell cost during scroll, whether their output is escaped or not. Screen readers read whatever text content the rendered markup exposes, so a badge or icon inserted this way needs its own accessible label if the visual marker is not decorative.
How do I render HTML inside a Lattice Grid cell?
Set allowUnsafeTemplates: true in the grid configuration, then include markup directly in a column’s cell.template string. The grid stops escaping template output for every column once this is set, so only enable it when the underlying data is trusted or has already been sanitised, since it removes the default protection against injected markup.