← Back to Examples

✨ Structural Rendering

Override file items, prompts, summaries, and the list wrapper via render callbacks

SR01 · Structural mode (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>
When to use which mode
  • 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).

SR02 · Custom Prompt (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>
`

SR03 · Custom File Item (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>
`
Required hooks
  • Keep data-file-id="${file.id}" on the root element so internal patching can target the row.
  • For remove buttons, set data-action="remove" and data-file-id="${file.id}" — click handlers are auto-attached.

SR04 · Returning a live HTMLElement

If 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
}

SR05 · Custom Compact Summary (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.

Click the summary to open the file popover.
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;
    }
`

SR06 · Custom List Wrapper (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.

~20% of files randomly fail mid-upload — click 🔄 to retry from where they left off, not from 0.
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>
`
Caveats
  • Keep data-file-id on each row so internal patching can target it.
  • Combining the wrapper with an HTMLElement-returning row callback (see next section) falls back to the string path — element identity is not preserved through wrapper composition.

SR07 · Event-driven rows (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.

Simulated upload runs on selection. ⏸ pause / ▶ resume / 🔄 retry buttons appear based on each file's status; the framework's 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
}
Why the throttle?

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.