Type Scale
Pick a ratio, see the whole scale at once
Running live from https://fun.whitneys.co,
cross-origin and sandboxed.
Put it on your site
<iframe
src="https://fun.whitneys.co/g/type-scale"
width="340"
height="440"
title="Type Scale"
sandbox="allow-scripts allow-popups-to-escape-sandbox"
loading="lazy"
style="border:0"></iframe>
An iframe, not a script tag -- so this gadget cannot touch the page you paste it into, and neither can we. Your settings above are baked into the URL.
Optional: let it resize itself to fit
<script>
addEventListener('message', function (e) {
var d = e.data;
if (!d || d.type !== 'fwf:height') return;
if (typeof d.height !== 'number' || !isFinite(d.height)) return;
document.querySelectorAll('iframe').forEach(function (f) {
// Identity by source window, not by origin -- sandboxed frames report
// their origin as "null". Do not "fix" this into an origin check.
if (f.contentWindow !== e.source) return;
f.style.height = Math.max(40, Math.min(2000, Math.ceil(d.height))) + 'px';
});
});
</script>
The whole thing
Raw fileEvery gadget is one file, and this is it -- all 2.6 kB. Take it, change it, ship it. Just keep the credit.
Show source
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<script type="application/json" id="gadget-manifest">
{
"name": "Type Scale",
"tagline": "Pick a ratio, see the whole scale at once",
"category": "text",
"tags": ["typography", "design", "ratio"],
"author": "rwhitney",
"background": "light",
"size": { "w": 340, "h": 440, "resizable": true },
"config": {
"sample": { "type": "text", "label": "Sample", "max": 18, "default": "Handgloves" },
"ratio": { "type": "select", "label": "Ratio",
"options": ["minor third 1.200", "major third 1.250", "perfect fourth 1.333", "golden 1.618"],
"default": "perfect fourth 1.333" },
"base": { "type": "range", "label": "Base size", "min": 12, "max": 20, "step": 1, "default": 15 },
"serif": { "type": "bool", "label": "Serif", "default": false }
},
"permissions": []
}
</script>
<style>
html, body { margin:0; background:transparent; color:#2b2b2b; }
body { padding:18px 20px; font-family: ui-sans-serif, system-ui, sans-serif; }
ul { list-style:none; margin:0; padding:0; }
li { display:flex; align-items:baseline; gap:10px;
padding:5px 0; border-bottom:1px solid rgb(0 0 0 / .07); }
li:last-child { border-bottom:0; }
.px { flex:none; width:44px; font-size:10px; letter-spacing:.06em;
color:#8a8474; font-family: ui-monospace, Menlo, monospace; }
.sample { line-height:1.15; white-space:nowrap; overflow:hidden;
text-overflow:ellipsis; }
body.serif .sample { font-family: Georgia, "Times New Roman", serif; }
</style>
</head>
<body>
<ul id="scale"></ul>
<script>
(function () {
var cfg = window.gadget ? window.gadget.config : {};
var text = cfg.sample || 'Handgloves';
var base = typeof cfg.base === 'number' ? cfg.base : 15;
// The ratio label carries its own number, so parse rather than keeping a
// second lookup table in sync with the manifest.
var ratio = parseFloat(String(cfg.ratio || '').split(' ').pop()) || 1.333;
if (cfg.serif) document.body.classList.add('serif');
var list = document.getElementById('scale');
for (var step = 5; step >= -1; step--) {
var size = base * Math.pow(ratio, step);
if (size > 64) continue;
var li = document.createElement('li');
var px = document.createElement('span');
px.className = 'px';
px.textContent = size.toFixed(0) + 'px';
var s = document.createElement('span');
s.className = 'sample';
s.style.fontSize = size.toFixed(2) + 'px';
s.textContent = text; // text, never markup
li.appendChild(px);
li.appendChild(s);
list.appendChild(li);
}
})();
</script>
</body>
</html>