Query Results
A database query types itself into an editor with its SQL colour-coded. Run is pressed, a spinner ticks for a moment, and the results table fills in row by row, each new row glowing briefly, with the row count and timing above.
| name | city | mrr |
|---|---|---|
| Northwind | Leeds | 4,820 |
| Bluebird Co | Bristol | 3,975 |
| Fieldnote | Glasgow | 3,140 |
| Paper Lane | Cardiff | 2,610 |
| Kestrel | York | 2,385 |
How to use it
- Paste the snippet just before
</head>. - Add
data-query-resultsto a container holding a<table>, and put the SQL indata-query. The table’s first row is the header.
Options
Add any of these attributes to the same element.
data-query | The SQL to type. |
data-title | The tab name above the editor. Default query.sql. |
data-time | The milliseconds shown in the status line. Default 12. |
data-speed | Typing time per character. Default 34 ms. |
data-stagger | Gap between rows arriving. Default 160 ms. |
data-loop | Clear and run the query again after a pause. |
data-hold | How long the results stay before looping. Default 2600 ms. |
The table stays a real table, so screen readers can read the results. The query box keeps its finished height from the start.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Query Results: a database query types into an editor, runs with a
// spinner, and the results table fills in row by row with a row count and
// timing.
// Usage:
// <div data-query-results data-query="SELECT name, plan FROM users LIMIT 3;">
// <table>
// <tr><th>name</th><th>plan</th></tr>
// <tr><td>Ada</td><td>pro</td></tr>
// <tr><td>Linus</td><td>free</td></tr>
// </table>
// </div>
// The first row of the table is the header; the rest are results.
// Optional: data-query (the SQL to type, default "SELECT * FROM results;"),
// data-title (tab name, default "query.sql"), data-time (ms shown in the
// status line, default 12), data-speed (ms per character, default 34),
// data-stagger (ms between rows, default 160), data-loop (clear and run
// again), data-hold (ms the results stay before looping, default 2600).
var DEFAULTS = { query: 'SELECT * FROM results;', title: 'query.sql', time: 12, speed: 34, stagger: 160, hold: 2600 };
var SPIN = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
var KEYWORDS = /^(select|from|where|and|or|order|by|group|limit|as|join|left|right|inner|on|desc|asc|count|sum|avg|having|insert|into|values|update|set|delete|distinct|not|in|is|null|like)$/i;
var css =
'.qry{background:#101015;color:#e4e4e7;border-radius:12px;overflow:hidden;text-align:left;box-shadow:0 18px 40px rgba(10,10,20,.28)}' +
'.qry-bar{display:flex;align-items:center;gap:0.6em;padding:0.55em 0.8em;background:#1b1b22;font-size:0.85em;color:#a1a1aa}' +
'.qry-tab{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}' +
'.qry-run{flex:none;display:inline-flex;align-items:center;gap:0.4em;padding:0.3em 0.8em;border-radius:6px;background:#22c55e;color:#06260f;font-weight:700}' +
'.qry-run i{width:0;height:0;border-style:solid;border-width:0.35em 0 0.35em 0.55em;border-color:transparent transparent transparent currentColor}' +
'.qry-sql{margin:0;padding:0.8em 1em;min-height:1.6em;line-height:1.6;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre-wrap;word-break:break-word;border-bottom:1px solid #26262e}' +
'.qry-k{color:#c084fc;font-weight:600}.qry-s{color:#86efac}.qry-n{color:#fbbf24}.qry-p{color:#71717a}' +
'.qry-status{display:flex;align-items:center;gap:0.5em;padding:0.45em 1em;font-size:0.82em;color:#8a8a93;min-height:1.5em;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}' +
'.qry-spin{color:#c084fc}.qry-ok{color:#4ade80}' +
'.qry-out{padding:0 0.6em 0.7em;overflow:hidden}' +
'.qry-out table{width:100%;border-collapse:separate;border-spacing:0;table-layout:fixed;font-variant-numeric:tabular-nums}' +
'.qry-out th,.qry-out td{padding:0.4em 0.5em;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;border-bottom:1px solid #22222a}' +
'.qry-out th{font-weight:600;color:#a1a1aa;font-size:0.85em;text-transform:none;border-bottom-color:#33333d}' +
'.qry-out tr.qry-new td{background:rgba(192,132,252,.16)}' +
'.qry-out td{transition:background-color .7s}' +
'.qry-caret{display:inline-block;width:0.55em;height:1.15em;vertical-align:-0.2em;background:#e4e4e7;margin-left:1px}' +
'.qry-caret.qry-blink{animation:qry-blink 1.06s steps(1) infinite}@keyframes qry-blink{50%{opacity:0}}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }
function opt(d, k) { return d[k] !== undefined && d[k] !== '' ? d[k] : DEFAULTS[k]; }
function esc(t) { return t.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
// A tiny SQL colourer: keywords, strings, numbers and punctuation.
function colour(src) {
return src.replace(/('[^']*'?)|(\b\d+(?:\.\d+)?\b)|([A-Za-z_]+)|([(),;*=<>])|([^'A-Za-z_\d(),;*=<>]+)/g, function (m, s, n, w, p, rest) {
if (s) return '<span class="qry-s">' + esc(s) + '</span>';
if (n) return '<span class="qry-n">' + n + '</span>';
if (w) return KEYWORDS.test(w) ? '<span class="qry-k">' + w + '</span>' : w;
if (p) return '<span class="qry-p">' + esc(p) + '</span>';
return esc(rest);
});
}
function setup(el) {
var d = el.dataset;
var table = el.querySelector('table');
var rows = table ? Array.from(table.querySelectorAll('tr')) : [];
var head = rows.shift();
el.textContent = '';
el.classList.add('qry');
var bar = document.createElement('div');
bar.className = 'qry-bar';
bar.setAttribute('aria-hidden', 'true');
bar.innerHTML = '<span class="qry-tab"></span><span class="qry-run"><i></i>Run</span>';
bar.firstChild.textContent = opt(d, 'title');
var sql = document.createElement('pre');
sql.className = 'qry-sql';
var status = document.createElement('div');
status.className = 'qry-status';
status.setAttribute('aria-live', 'off');
var out = document.createElement('div');
out.className = 'qry-out';
if (table) {
// Rebuild a clean table so the original markup's own styles don't fight ours.
var t = document.createElement('table');
var thead = t.createTHead();
if (head) thead.appendChild(head);
var tbody = t.createTBody();
rows.forEach(function (r) { tbody.appendChild(r); });
out.appendChild(t);
}
el.appendChild(bar);
el.appendChild(sql);
el.appendChild(status);
el.appendChild(out);
var caret = document.createElement('span');
caret.className = 'qry-caret';
caret.setAttribute('aria-hidden', 'true');
return { el: el, query: opt(d, 'query'), sql: sql, status: status, run: bar.querySelector('.qry-run'), head: head, rows: rows, caret: caret, visible: false, wake: null, spinT: 0 };
}
function statusDone(s) {
var n = s.rows.length;
s.status.innerHTML = '<span class="qry-ok">✓</span><span></span>';
s.status.lastChild.textContent = n + (n === 1 ? ' row' : ' rows') + ' · ' + opt(s.el.dataset, 'time') + ' ms';
}
function finished(s) {
s.sql.innerHTML = colour(s.query);
statusDone(s);
}
function blank(s) {
s.sql.textContent = '';
s.status.textContent = '';
if (s.head) anime.utils.set(s.head, { opacity: 0 });
s.rows.forEach(function (r) { anime.utils.set(r, { opacity: 0 }); r.classList.remove('qry-new'); });
}
function whenVisible(s) {
return s.visible ? Promise.resolve() : new Promise(function (r) { s.wake = r; });
}
function type(s, speed) {
return new Promise(function (done) {
var n = 0;
s.caret.classList.remove('qry-blink');
(function tick() {
s.sql.innerHTML = colour(s.query.slice(0, n));
s.sql.appendChild(s.caret);
if (n++ >= s.query.length) { done(); return; }
var ch = s.query.charAt(n - 1);
setTimeout(tick, speed * (0.5 + Math.random()) + (ch === ' ' ? speed : 0));
})();
});
}
function play(s) {
var d = s.el.dataset;
var stagger = +opt(d, 'stagger');
blank(s);
sleep(400).then(function () {
return type(s, +opt(d, 'speed'));
}).then(function () {
s.caret.classList.add('qry-blink');
return sleep(450);
}).then(function () {
anime.animate(s.run, { scale: [1, 0.88, 1], duration: 260, ease: 'inOut(2)' });
s.caret.remove();
s.status.innerHTML = '<span class="qry-spin"></span><span>Running query…</span>';
var spin = s.status.firstChild, i = 0;
spin.textContent = SPIN[0];
s.spinT = setInterval(function () { spin.textContent = SPIN[++i % SPIN.length]; }, 80);
return sleep(900 + Math.random() * 300);
}).then(function () {
clearInterval(s.spinT);
statusDone(s);
if (s.head) anime.animate(s.head, { opacity: [0, 1], duration: 300, ease: 'out(2)' });
s.rows.forEach(function (r, i) {
setTimeout(function () {
r.classList.add('qry-new');
anime.animate(r, { opacity: [0, 1], duration: 260, ease: 'out(2)' });
anime.animate(r.children, { y: [-6, 0], duration: 320, ease: 'out(3)' });
setTimeout(function () { r.classList.remove('qry-new'); }, 350);
}, 200 + i * stagger);
});
return sleep(200 + s.rows.length * stagger + 400);
}).then(function () {
if (d.loop === undefined || d.loop === 'false') return;
return sleep(+opt(d, 'hold')).then(function () { return whenVisible(s); }).then(function () {
var all = s.rows.slice();
if (s.head) all.unshift(s.head);
anime.animate(all.concat([s.sql, s.status]), { opacity: 0, duration: 300, ease: 'in(2)' });
return sleep(330);
}).then(function () {
anime.utils.set([s.sql, s.status], { opacity: 1 });
play(s);
});
});
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var s = e.target._qry;
s.visible = e.isIntersecting;
if (s.visible && s.wake) { var w = s.wake; s.wake = null; w(); }
});
}, { threshold: 0.3 });
document.querySelectorAll('[data-query-results]').forEach(function (el) {
if (el._qry) return;
var s = (el._qry = setup(el));
finished(s);
if (reduced()) return;
// Keep the finished query's height so the window never grows while it types.
var fs = parseFloat(getComputedStyle(s.sql).fontSize) || 16;
if (s.sql.offsetHeight) s.sql.style.minHeight = (s.sql.offsetHeight / fs) + 'em';
io.observe(el);
whenVisible(s).then(function () { play(s); });
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-query-results data-query="SELECT name, plan FROM users;">
<table>
<tr><th>name</th><th>plan</th></tr>
<tr><td>Ada</td><td>pro</td></tr>
<tr><td>Linus</td><td>free</td></tr>
</table>
</div>