Sure. Just create a Custom Widget Builder, and put the following in the HTML and the Javascript pages.
<!DOCTYPE html>
<meta charset="utf-8" />
<script src="https://docs.getgrist.com/grist-plugin-api.js"></script>
<style>
:root { color-scheme: light dark; }
html, body { height: 100%; margin: 0; }
body {
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, "Helvetica Neue", Arial, sans-serif;
display: grid; place-items: center;
background: canvas; color: canvastext;
}
#wrap { position: relative; width: 100%; height: 100%; }
#img {
max-width: 100%;
max-height: 100%;
object-fit: cover;
object-position: center;
display: none;
}
</style>
<div id="wrap">
<img id="img" alt="" />
<div id="msg">Select a row and map an attachment column in the widget settings…</div>
</div>
// Request mapping for one Attachments-type column.
grist.ready({
requiredAccess: 'read table',
columns: [{
name: 'Attachment', // logical name used inside the widget
title: 'Attachment column', // label shown in the mapping UI
type: 'Attachments',
optional: false
}]
});
const img = document.getElementById('img');
const msg = document.getElementById('msg');
async function buildAttachmentUrl(attId) {
// Short-lived, safe URL parts for the current document
const { token, baseUrl } = await grist.docApi.getAccessToken({ readOnly: true });
return `${baseUrl}/attachments/${attId}/download?auth=${token}`;
}
function showMessage(text) {
img.removeAttribute('src');
img.style.display = 'none';
msg.textContent = text;
}
async function render(record) {
const mapped = grist.mapColumnNames(record) || {};
const list = mapped.Attachment;
// Attachment cells are arrays of IDs; show the first if present.
const attId = Array.isArray(list) && list.length ? list[0] : null;
if (!attId) {
showMessage('X'); //whatever you want to show if nothing
return;
}
try {
const url = await buildAttachmentUrl(attId);
img.src = url;
img.alt = `Attachment ${attId}`;
img.style.display = 'block';
msg.textContent = '';
} catch (err) {
console.error('Attachment load error:', err);
showMessage('Unable to load image (check access or file type).');
} finally {
// Nudge height after image settles
queueMicrotask(() => grist.setHeight(document.body.scrollHeight));
}
}
// Update when the selected record changes.
grist.onRecord(render);
// Keep height sensible on resizes and image events.
window.addEventListener('resize', () => grist.setHeight(document.body.scrollHeight));
img.addEventListener('load', () => grist.setHeight(document.body.scrollHeight));
img.addEventListener('error', () => grist.setHeight(document.body.scrollHeight));
Once you have this all you need to do is specify the attachment field.