Sharing a simple lightweight HTML snippet that can generate a searchable, sortable table and allow users to download the displayed data as a CSV file, without using any external plugins or complicated Bubble workflows.
This is useful when you want to display database records in a clean table format inside a Bubble HTML element, especially for admin pages, reports, internal dashboards, submissions, logs, or exported lists.
What it does:
It automatically reads JSON data, builds table columns from the available keys, displays the records in a responsive scrollable table, adds a search box, allows column sorting, and includes an Export CSV button.
Best use case:
This works best when the data is loaded from Bubble’s database and converted into JSON using Format as text .
For example, you can create a custom state or dynamic expression that outputs a list of things in JSON format like this:
[
{"id":1,"name":"Ada Lovelace","role":"Engineer","active":true,"score":98.5},
{"id":2,"name":"Alan Turing","role":"Research","active":true,"score":99.1},
{"id":3,"name":"Grace Hopper","role":"Engineer","active":false,"score":97.0}
]
Then paste your Bubble dynamic JSON expression inside the script tag below:
<script type="application/json" id="bwt-data">
PASTE YOUR BUBBLE JSON DATA HERE
</script>
The rest of the snippet handles the table, search, sorting, loading state, empty state, and CSV export automatically.
How to use in Bubble:
- Add an HTML element to the page.
- Paste the full snippet into the HTML element.
- Replace the sample JSON inside the
#bwt-datascript tag with your Bubble dynamic JSON expression. - Use Bubble’s Format as text on your database search/list to generate valid JSON.
- Preview the page and the table should load automatically.
Important note:
Make sure the JSON is valid. If Bubble outputs broken JSON, missing commas, or unescaped quotes, the table will not load. The snippet includes a fallback parser, but clean JSON will always give the best result.
This approach is especially helpful when you want a fast table view and simple CSV export without adding plugins, backend workflows, API calls, or complex repeating group logic.
Script
<!-- Bubble: paste your Page State JSON expression between the #bwt-data script tags below (replace the sample). -->
<div id="bwt-app">
<div id="bwt-bar" class="bwt-bar" hidden>
<input id="bwt-search" class="bwt-search" type="text" placeholder="Search…" autocomplete="off">
<span id="bwt-count" class="bwt-count"></span>
<button id="bwt-export" class="bwt-btn" type="button">Export CSV</button>
</div>
<div id="bwt-scroll" class="bwt-scroll" hidden>
<table class="bwt-table"><thead id="bwt-thead"></thead><tbody id="bwt-tbody"></tbody></table>
</div>
<div id="bwt-empty" class="bwt-empty" hidden>No data to display.</div>
<div id="bwt-loading" class="bwt-loading"><div class="bwt-spinner"></div><div>Loading data…</div></div>
</div>
<script type="application/json" id="bwt-data">
[
{"id":1,"name":"Ada Lovelace","role":"Engineer","active":true,"score":98.5},
{"id":2,"name":"Alan Turing","role":"Research","active":true,"score":99.1},
{"id":3,"name":"Grace Hopper","role":"Engineer","active":false,"score":97.0}
]
</script>
<style>
#bwt-app{--fg:#1f2328;--mut:#6b7280;--line:#e5e7eb;--head:#f7f8fa;--alt:#fafbfc;--hov:#eef4ff;--acc:#2563eb;--r:8px;font:14px -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;color:var(--fg);display:flex;flex-direction:column;height:100%;min-height:0;width:100%}
#bwt-app *,#bwt-app *::before,#bwt-app *::after{box-sizing:border-box}
#bwt-app [hidden]{display:none!important}
#bwt-app .bwt-bar{display:flex;align-items:center;gap:10px;padding:8px 2px;flex:0 0 auto}
#bwt-app .bwt-search{flex:1 1 auto;min-width:0;padding:8px 12px;border:1px solid var(--line);border-radius:var(--r);font-size:14px;outline:none;background:#fff;color:var(--fg)}
#bwt-app .bwt-search:focus{border-color:var(--acc);box-shadow:0 0 0 3px rgba(37,99,235,.12)}
#bwt-app .bwt-count{color:var(--mut);font-variant-numeric:tabular-nums;white-space:nowrap}
#bwt-app .bwt-btn{padding:8px 16px;border:1px solid var(--acc);background:var(--acc);color:#fff;border-radius:var(--r);font-size:14px;font-weight:600;cursor:pointer;white-space:nowrap}
#bwt-app .bwt-btn:hover{filter:brightness(.94)}
#bwt-app .bwt-btn:disabled{opacity:.5;cursor:not-allowed}
#bwt-app .bwt-scroll{flex:1 1 auto;min-height:0;overflow:auto;border:1px solid var(--line);border-radius:var(--r);background:#fff;-webkit-overflow-scrolling:touch}
#bwt-app .bwt-table{border-collapse:collapse;width:100%;font-size:13.5px}
#bwt-app .bwt-table thead th{position:sticky;top:0;z-index:2;background:var(--head);text-align:left;font-weight:600;padding:10px 12px;border-bottom:2px solid var(--line);white-space:nowrap;cursor:pointer;user-select:none}
#bwt-app .bwt-table thead th:hover{color:var(--acc)}
#bwt-app .bwt-si{color:var(--acc);font-size:11px;margin-left:4px}
#bwt-app .bwt-ix{color:var(--mut);text-align:right;font-variant-numeric:tabular-nums;width:1%;white-space:nowrap}
#bwt-app th.bwt-ix,#bwt-app th.bwt-ix:hover{cursor:default;color:var(--mut)}
#bwt-app .bwt-table tbody td{padding:8px 12px;border-bottom:1px solid var(--line);max-width:420px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#bwt-app .bwt-table tbody tr:nth-child(even){background:var(--alt)}
#bwt-app .bwt-table tbody tr:hover{background:var(--hov)}
#bwt-app .bwt-empty{padding:24px;text-align:center;color:var(--mut)}
#bwt-app .bwt-loading{flex:1 1 auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;padding:48px;color:var(--mut);min-height:140px;font-size:13.5px}
#bwt-app .bwt-spinner{width:36px;height:36px;border:3px solid var(--line);border-top-color:var(--acc);border-radius:50%;animation:bwt-spin .8s linear infinite}
@keyframes bwt-spin{to{transform:rotate(360deg)}}
@media(prefers-reduced-motion:reduce){#bwt-app .bwt-spinner{animation-duration:1.6s}}
</style>
<script>
(function(){"use strict";
var $=function(id){return document.getElementById(id)},root=$("bwt-app");if(!root)return;
var E={search:$("bwt-search"),count:$("bwt-count"),exp:$("bwt-export"),scroll:$("bwt-scroll"),thead:$("bwt-thead"),tbody:$("bwt-tbody"),empty:$("bwt-empty"),raw:$("bwt-data"),bar:$("bwt-bar"),load:$("bwt-loading")};
var rows0=[],cols=[],view=[],sortKey=null,dir=1,term="",MAX=8000,POLL=250,t0=0,timer=null;
function dec(s){var t=document.createElement("textarea");t.innerHTML=s;return t.value}
function parse(){var x=(E.raw?E.raw.textContent:"")||"";x=x.trim();if(!x)return[];var d;try{d=JSON.parse(x)}catch(e){try{d=JSON.parse(dec(x))}catch(e2){console.error("[bwt] parse:",e2);return null}}if(d==null)return[];if(Array.isArray(d))return d;if(typeof d==="object")return[d];return[]}
function buildCols(r){var seen={},c=[];for(var i=0;i<r.length;i++){var o=r[i];if(o&&typeof o==="object"&&!Array.isArray(o))for(var k in o)if(Object.prototype.hasOwnProperty.call(o,k)&&!seen[k]){seen[k]=1;c.push(k)}}if(!c.length)c.push("value");return c}
function cell(o,k){if(o==null)return"";if(typeof o!=="object"||Array.isArray(o))return k==="value"?o:"";return Object.prototype.hasOwnProperty.call(o,k)?o[k]:""}
function disp(v){if(v==null)return"";if(typeof v==="object"){try{return JSON.stringify(v)}catch(e){return String(v)}}return String(v)}
function esc(s){return s.replace(/[&<>"']/g,function(c){return{"&":"&","<":"<",">":">",'"':""","'":"'"}[c]})}
function cmp(a,b){if(a==null&&b==null)return 0;if(a==null)return 1;if(b==null)return-1;var na=typeof a==="number"?a:parseFloat(a),nb=typeof b==="number"?b:parseFloat(b),an=typeof a==="number"||(a!==""&&!isNaN(na)),bn=typeof b==="number"||(b!==""&&!isNaN(nb));if(an&&bn)return(na-nb)*dir;return String(a).localeCompare(String(b),undefined,{numeric:true,sensitivity:"base"})*dir}
function build(){var r=rows0;if(term){var q=term.toLowerCase();r=r.filter(function(o){for(var i=0;i<cols.length;i++)if(disp(cell(o,cols[i])).toLowerCase().indexOf(q)!==-1)return true;return false})}else r=r.slice();if(sortKey!=null)r.sort(function(a,b){return cmp(cell(a,sortKey),cell(b,sortKey))});view=r}
function head(){var h='<tr><th class="bwt-ix">#</th>';for(var i=0;i<cols.length;i++){var k=cols[i],ind=k===sortKey?'<span class="bwt-si">'+(dir===1?"\u25B2":"\u25BC")+"</span>":"";h+='<th data-key="'+esc(k)+'" title="'+esc(k)+'">'+esc(k)+ind+"</th>"}E.thead.innerHTML=h+"</tr>"}
function body(){var n=view.length,p=new Array(n);for(var i=0;i<n;i++){var o=view[i],c="";for(var j=0;j<cols.length;j++){var d=disp(cell(o,cols[j]));c+='<td title="'+esc(d)+'">'+esc(d)+"</td>"}p[i]='<tr><td class="bwt-ix">'+(i+1)+"</td>"+c+"</tr>"}E.tbody.innerHTML=p.join("")}
function count(){var t=rows0.length,s=view.length;E.count.textContent=s===t?t.toLocaleString()+(t===1?" row":" rows"):s.toLocaleString()+" of "+t.toLocaleString()+" rows"}
function draw(){build();head();body();count();E.scroll.scrollTop=0}
function csvE(v){var s=disp(v);return/[",\r\n]/.test(s)?'"'+s.replace(/"/g,'""')+'"':s}
function exportCSV(){if(!view.length)return;var L=[cols.map(csvE).join(",")];for(var i=0;i<view.length;i++){var o=view[i],c=new Array(cols.length);for(var j=0;j<cols.length;j++)c[j]=csvE(cell(o,cols[j]));L.push(c.join(","))}var b=new Blob(["\uFEFF"+L.join("\r\n")],{type:"text/csv;charset=utf-8;"}),u=URL.createObjectURL(b),a=document.createElement("a"),d=new Date(),p=function(x){return(x<10?"0":"")+x};a.href=u;a.download="export_"+d.getFullYear()+p(d.getMonth()+1)+p(d.getDate())+"_"+p(d.getHours())+p(d.getMinutes())+p(d.getSeconds())+".csv";document.body.appendChild(a);a.click();document.body.removeChild(a);setTimeout(function(){URL.revokeObjectURL(u)},1500)}
var st;E.search.addEventListener("input",function(){clearTimeout(st);st=setTimeout(function(){term=E.search.value.trim();draw()},150)});
E.thead.addEventListener("click",function(e){var th=e.target.closest&&e.target.closest("th");if(!th)return;var k=th.getAttribute("data-key");if(k==null)return;if(sortKey===k)dir=-dir;else{sortKey=k;dir=1}draw()});
E.exp.addEventListener("click",exportCSV);
function mode(m){E.load.hidden=m!=="loading";E.bar.hidden=m!=="ready";E.scroll.hidden=m!=="ready";E.empty.hidden=m!=="empty"&&m!=="error"}
function stop(){if(timer){clearInterval(timer);timer=null}}
function tryLoad(){var p=parse();if(p===null){stop();E.empty.textContent="Could not parse the JSON. Check the data passed in.";mode("error");return true}var r=p.filter(function(x){return x!==undefined});if(r.length){stop();rows0=r;cols=buildCols(r);E.exp.disabled=false;mode("ready");draw();return true}if(Date.now()-t0>=MAX){stop();E.empty.textContent="No data to display.";mode("empty");return true}return false}
function init(){mode("loading");t0=Date.now();var s=false,go=function(){if(s)return;s=true;if(!tryLoad())timer=setInterval(tryLoad,POLL)};requestAnimationFrame(go);setTimeout(go,60)}
init();
})();
</script>