Override file items, prompts, summaries, and the list wrapper via render callbacks
mode="structural")This page covers mode 2 of 3 — the framework owns when and what to render, you provide the HTML structure via callbacks. The hard-switch mode="structural" attribute is required: setting a render callback alone does nothing in the default mode="bulk".
<web-dropzone mode="structural" display-mode="detailed" id="x"></web-dropzone>
<script>
const el = document.getElementById('x')
el.renderPromptCallback = () => '...' // dropzone prompt
el.renderFileItemCallback = (file, ctx) => '...' // one file row
el.renderListWrapperCallback = (rows) => '...' // wraps the rows
el.renderSummaryCallback = (files) => '...' // compact-mode summary
</script>
mode="bulk" (default) — you want the working UI fast; style via CSS variables.mode="structural" — you want custom HTML shape (this page).mode="headless" — you want pure events + own renderer (see Architecture).renderPromptCallback)Replaces the inner content of the dropzone prompt area.
document.getElementById('prompt-cloud').renderPromptCallback = () => `
<div style="display:flex;flex-direction:column;align-items:center;gap:0.5rem;">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#667eea"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
<strong>Upload to cloud</strong>
<small style="color:#6b7280">Drag and drop or click to browse</small>
</div>
`
renderFileItemCallback)Each call receives the FileState and a context object describing the render position. Return a string or an HTMLElement.
el.renderFileItemCallback = (file, ctx) => `
<div class="receipt-row" data-file-id="${file.id}">
<span class="receipt-icon">🧾</span>
<span class="receipt-name">${file.name}</span>
<span class="receipt-date">${new Date(file.file.lastModified).toLocaleDateString()}</span>
<span class="receipt-size">${(file.size/1024).toFixed(1)} KB</span>
<button data-action="remove" data-file-id="${file.id}">×</button>
</div>
`
data-file-id="${file.id}" on the root element so internal patching can target the row.data-action="remove" and data-file-id="${file.id}" — click handlers are auto-attached.HTMLElementIf you'd rather build with the DOM API than concatenate strings, return an element directly. Useful for binding event listeners or attaching data.
el.renderFileItemCallback = (file) => {
const row = document.createElement('div')
row.className = 'dz__file-item dz__file-item--list'
row.dataset.fileId = file.id
const name = document.createElement('a')
name.textContent = file.name
name.href = URL.createObjectURL(file.file)
name.target = '_blank'
name.style.flex = '1'
name.style.color = '#667eea'
name.style.textDecoration = 'underline'
row.appendChild(name)
const btn = document.createElement('button')
btn.textContent = '×'
btn.dataset.action = 'remove'
btn.dataset.fileId = file.id
btn.className = 'dz__file-item__remove'
row.appendChild(btn)
return row
}
renderSummaryCallback)Replaces the inline summary line that triggers the compact-mode popover. Required: include an element with class dz__summary__line so the popover click handler still binds correctly.
el.renderSummaryCallback = (files) => {
const total = files.reduce((s, f) => s + f.size, 0)
return `
<div class="dz__summary__line" tabindex="0" role="button">
<span class="pill">${files.length}</span>
<span>files · ${(total/1024).toFixed(1)} KB</span>
</div>
`
}
el.customStylesCallback = () => `
.pill {
display: inline-block;
background: #667eea; color: white;
border-radius: 999px;
padding: 0.1rem 0.55rem;
font-weight: 600; font-size: 0.85em;
}
`
renderListWrapperCallback)Replaces the <div class="dz__file-list"> shell around the rows. Pairs with renderFileItemCallback to build table layouts. The wrapper receives the rows as an HTML string and the files array, returns the surrounding HTML.
el.renderListWrapperCallback = (rowsHtml) => `
<table class="receipt-table">
<thead><tr>
<th>Name</th><th>Type</th><th>Size</th><th></th>
</tr></thead>
<tbody>${rowsHtml}</tbody>
</table>
`
el.renderFileItemCallback = (file) => `
<tr data-file-id="${file.id}">
<td>${file.name}</td>
<td>${file.type || '—'}</td>
<td>${(file.size/1024).toFixed(1)} KB</td>
<td><button data-action="remove" data-file-id="${file.id}">×</button></td>
</tr>
`
data-file-id on each row so internal patching can target it.file-row-update)When renderFileItemCallback returns an HTMLElement, the framework preserves it across state changes — instead of re-rendering, it dispatches a file-row-update CustomEvent on the row. Listeners attached during the initial render survive every tick, so you can wire complex per-row behavior without redoing it on every progress event.
data-action="row-action" convention auto-wires them.
el.renderFileItemCallback = (file) => {
const row = document.createElement('div')
row.dataset.fileId = file.id
row.innerHTML = `
<span class="star">☆</span>
<span class="name">${file.name}</span>
<progress class="bar" max="100" value="0"></progress>
<span class="pct">0%</span>
`
row.querySelector('.star').addEventListener('click', e => {
e.currentTarget.textContent =
e.currentTarget.textContent === '☆' ? '⭐' : '☆'
})
row.addEventListener('file-row-update', e => {
const { file } = e.detail
row.querySelector('.bar').value = file.progress
row.querySelector('.pct').textContent = Math.round(file.progress) + '%'
})
return row
}
The progress-throttle="120" attribute caps per-file event emission and DOM patching at one every 120 ms. Without it, a 20-file upload at the default 50 ms tick rate would fire 400 file-row-update events / sec.