Lattice Grid Buy a licence

demo D41

Images, avatars and QR codes in htmx

Media in cells, including a QR generated with no dependency

type: 'image' · render: 'qrcode'

This grid draws media in cells: product thumbnails, circular avatars, and QR codes generated on the fly from a row's own data with no extra dependency. It lets a table carry the picture that identifies a record, not just its text.

Building…
Loading a live grid…

This is the htmx version. A server-rendered table is upgraded in place, so the page is readable before any script runs and gains the full grid when the module loads, which suits a progressively enhanced app.

The configuration

<link rel="stylesheet" href="https://cdn.latticegrid.dev/lattice-grid@1.107.0/lattice-grid.min.css">

<!-- A real, server-rendered table: readable with JavaScript off. data-field on
     each header names the column that cell becomes, and data-id on each row is
     the key the grid carries it under. -->
<table id="images-and-qr-table" data-lattice-grid>
  <thead>
    <tr>
      <th data-field="avatar"></th>
      <th data-field="owner">Owner</th>
      <th data-field="logo">Mark</th>
      <th data-field="service">Service</th>
      <th data-field="ticket">QR</th>
      <th data-field="url">Runbook</th>
    </tr>
  </thead>
  <tbody>
    <tr data-id="0"><td>data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2040%2040%22%20width%3D%2240%22%20height%3D%2240%22%3E%3Ccircle%20cx%3D%2220%22%20cy%3D%2220%22%20r%3D%2220%22%20fill%3D%22%232f6fed%22%2F%3E%3Ctext%20x%3D%2220%22%20y%3D%2226%22%20text-anchor%3D%22middle%22%20fill%3D%22%23ffffff%22%20font-family%3D%22system-ui%2C-apple-system%2CSegoe%20UI%2Csans-serif%22%20font-size%3D%2215%22%20font-weight%3D%22600%22%3EAL%3C%2Ftext%3E%3C%2Fsvg%3E</td><td>Ken Lamport</td><td>data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2040%2040%22%20width%3D%2240%22%20height%3D%2240%22%3E%3Crect%20width%3D%2240%22%20height%3D%2240%22%20rx%3D%229%22%20fill%3D%22%230891b2%22%2F%3E%3Cg%20transform%3D%22rotate(75%2020%2020)%22%20fill%3D%22%23ffffff%22%20fill-opacity%3D%220.92%22%3E%3Crect%20x%3D%2210%22%20y%3D%2210%22%20width%3D%228%22%20height%3D%228%22%20rx%3D%221.5%22%2F%3E%3Crect%20x%3D%2222%22%20y%3D%2210%22%20width%3D%228%22%20height%3D%228%22%20rx%3D%221.5%22%20fill-opacity%3D%220.6%22%2F%3E%3Crect%20x%3D%2210%22%20y%3D%2222%22%20width%3D%228%22%20height%3D%228%22%20rx%3D%221.5%22%20fill-opacity%3D%220.6%22%2F%3E%3Crect%20x%3D%2222%22%20y%3D%2222%22%20width%3D%228%22%20height%3D%228%22%20rx%3D%221.5%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E</td><td>fraud-edge</td><td>LG-1000</td><td>https://example.com/services/fraud-edge</td></tr>
    <tr data-id="1"><td>data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2040%2040%22%20width%3D%2240%22%20height%3D%2240%22%3E%3Ccircle%20cx%3D%2220%22%20cy%3D%2220%22%20r%3D%2220%22%20fill%3D%22%230f9d76%22%2F%3E%3Ctext%20x%3D%2220%22%20y%3D%2226%22%20text-anchor%3D%22middle%22%20fill%3D%22%23ffffff%22%20font-family%3D%22system-ui%2C-apple-system%2CSegoe%20UI%2Csans-serif%22%20font-size%3D%2215%22%20font-weight%3D%22600%22%3EGH%3C%2Ftext%3E%3C%2Fsvg%3E</td><td>Tony Liskov</td><td>data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2040%2040%22%20width%3D%2240%22%20height%3D%2240%22%3E%3Crect%20width%3D%2240%22%20height%3D%2240%22%20rx%3D%229%22%20fill%3D%22%2365a30d%22%2F%3E%3Cg%20transform%3D%22rotate(90%2020%2020)%22%20fill%3D%22%23ffffff%22%20fill-opacity%3D%220.92%22%3E%3Crect%20x%3D%2210%22%20y%3D%2210%22%20width%3D%228%22%20height%3D%228%22%20rx%3D%221.5%22%2F%3E%3Crect%20x%3D%2222%22%20y%3D%2210%22%20width%3D%228%22%20height%3D%228%22%20rx%3D%221.5%22%20fill-opacity%3D%220.6%22%2F%3E%3Crect%20x%3D%2210%22%20y%3D%2222%22%20width%3D%228%22%20height%3D%228%22%20rx%3D%221.5%22%20fill-opacity%3D%220.6%22%2F%3E%3Crect%20x%3D%2222%22%20y%3D%2222%22%20width%3D%228%22%20height%3D%228%22%20rx%3D%221.5%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E</td><td>payments-stream</td><td>LG-1001</td><td>https://example.com/services/payments-stream</td></tr>
    <tr data-id="2"><td>data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2040%2040%22%20width%3D%2240%22%20height%3D%2240%22%3E%3Ccircle%20cx%3D%2220%22%20cy%3D%2220%22%20r%3D%2220%22%20fill%3D%22%23c2410c%22%2F%3E%3Ctext%20x%3D%2220%22%20y%3D%2226%22%20text-anchor%3D%22middle%22%20fill%3D%22%23ffffff%22%20font-family%3D%22system-ui%2C-apple-system%2CSegoe%20UI%2Csans-serif%22%20font-size%3D%2215%22%20font-weight%3D%22600%22%3EAT%3C%2Ftext%3E%3C%2Fsvg%3E</td><td>Leslie Thompson</td><td>data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2040%2040%22%20width%3D%2240%22%20height%3D%2240%22%3E%3Crect%20width%3D%2240%22%20height%3D%2240%22%20rx%3D%229%22%20fill%3D%22%23a16207%22%2F%3E%3Cg%20transform%3D%22rotate(105%2020%2020)%22%20fill%3D%22%23ffffff%22%20fill-opacity%3D%220.92%22%3E%3Crect%20x%3D%2210%22%20y%3D%2210%22%20width%3D%228%22%20height%3D%228%22%20rx%3D%221.5%22%2F%3E%3Crect%20x%3D%2222%22%20y%3D%2210%22%20width%3D%228%22%20height%3D%228%22%20rx%3D%221.5%22%20fill-opacity%3D%220.6%22%2F%3E%3Crect%20x%3D%2210%22%20y%3D%2222%22%20width%3D%228%22%20height%3D%228%22%20rx%3D%221.5%22%20fill-opacity%3D%220.6%22%2F%3E%3Crect%20x%3D%2222%22%20y%3D%2222%22%20width%3D%228%22%20height%3D%228%22%20rx%3D%221.5%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E</td><td>checkout-edge</td><td>LG-1002</td><td>https://example.com/services/checkout-edge</td></tr>
    <!-- the rest of the rows, rendered by your server -->
  </tbody>
</table>

<script type="module">
  import { hydrateTable } from 'https://cdn.latticegrid.dev/lattice-grid@1.107.0/modules/htmx.esm.min.js';

  // The markup carries the rows; a server-rendered table's column types are
  // declared in the config hydrateTable takes. The grid replaces the table
  // where it stood.
  const grid = hydrateTable(document.getElementById('images-and-qr-table'), {
    density: 'comfortable',
    selection: 'multiple',
    toolPanel: {
      side: 'left',
      panels: ['columns', 'filters', 'views', 'quick'],
      actions: ['undo', 'redo', 'export', 'restore', 'maximise'],
      exportName: 'lattice-demo',
    },
    columns: [
      { field: 'avatar', title: '', type: 'image', layout: { pin: 'start', width: 64 },
        sort: { enabled: false }, filter: { enabled: false },
        // The alt text is the row's own name. "Image" repeated down a column
        // tells a screen reader nothing.
        cell: { props: { shape: 'circle', alt: (p) => String(p?.data?.owner ?? '') } } },
      { field: 'owner', title: 'Owner', layout: { width: 220 },
        cell: { render: 'twoline', props: { secondary: 'email' } } },
      { field: 'logo', title: 'Mark', type: 'image', layout: { width: 74 },
        sort: { enabled: false }, filter: { enabled: false },
        cell: { props: { shape: 'rounded', alt: 'Service mark' } } },
      { field: 'service', title: 'Service', layout: { width: 190 } },
      // Generated from the cell's value with no dependency and no network.
      // Short payloads on purpose: 30 pixels of cell will not carry a long
      // URL as a scannable symbol.
      { field: 'ticket', title: 'QR', layout: { width: 90 },
        sort: { enabled: false }, cell: { render: 'qrcode', props: { size: 36, margin: 2, level: 'M' } } },
      { field: 'url', title: 'Runbook', layout: { flex: 1, min: 220 },
        cell: { render: 'link', props: { target: '_blank' } } },
    ],
  });
</script>

Rendering images, avatars and QR codes in a grid cell

An image renderer draws a picture rather than text in a cell: a product thumbnail, a user avatar, a status icon, or a QR code generated on the fly from the row’s own data, a ticket reference, a URL, a serial number. A developer reaches for it wherever a column’s value is more useful shown than read, a catalogue grid where a thumbnail confirms the row faster than a SKU, or an admin table where a QR code lets someone scan a row straight from the screen rather than retype an ID. Lattice Grid sets this with type: 'image' for pictures and render: 'qrcode' for codes generated from cell content, and the QR code is drawn with no external dependency, so a grid full of them carries no extra library weight and no network request per code. Images load lazily as their row scrolls into view, so a column of a thousand avatars costs nothing for the rows never scrolled to, which matters in a JavaScript data grid where the row count is unbounded and the renderer has to stay cheap per cell rather than per dataset. Every image and generated code carries an accessible text alternative drawn from the underlying value, so a screen reader announces the row’s identity even where the cell shows only a picture.

How do I show a QR code or an image in a data grid cell?

Set render: 'qrcode' on a column to generate a QR code from each cell’s value with no external dependency, or type: 'image' to draw a picture from a URL or data field. Images load lazily as rows scroll into view, and both renderer types include a text alternative for the underlying value, so the column stays accessible even when the visible content is graphical.