← All gadgets

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 file

Every 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>