Skip to content

Instantly share code, notes, and snippets.

@beacrea
Last active March 24, 2026 19:22
Show Gist options
  • Select an option

  • Save beacrea/62df2bad7000606525ce518a59e99268 to your computer and use it in GitHub Desktop.

Select an option

Save beacrea/62df2bad7000606525ce518a59e99268 to your computer and use it in GitHub Desktop.
Button Shape Ratio — Interactive Sandbox (DSYS)
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Button Shape Ratio — Interactive Sandbox</title>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #ffffff;
--bg-alt: #f6f7f9;
--bg-card: #ffffff;
--bg-code: #f0f1f3;
--fg: #1a1a2e;
--fg-muted: #6b7280;
--fg-accent: #4f46e5;
--border: #e5e7eb;
--border-focus: #4f46e5;
--green: #059669;
--amber: #d97706;
--red: #dc2626;
--shadow: 0 1px 3px rgba(0, 0, 0, .08);
--font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--mono: 'JetBrains Mono', 'Fira Code', monospace;
--btn-bg: #4f46e5;
--btn-fg: #fff;
}
[data-theme="dark"] {
--bg: #0f1117;
--bg-alt: #1a1d2e;
--bg-card: #1e2132;
--bg-code: #252840;
--fg: #e2e8f0;
--fg-muted: #94a3b8;
--fg-accent: #818cf8;
--border: #2d3148;
--border-focus: #818cf8;
--shadow: 0 1px 3px rgba(0, 0, 0, .3);
--btn-bg: #6366f1;
--btn-fg: #fff;
}
html {
scroll-behavior: smooth;
}
body {
font-family: var(--font);
background: var(--bg);
color: var(--fg);
line-height: 1.6;
font-size: 16px;
transition: background .2s, color .2s;
}
.container {
max-width: 1100px;
margin: 0 auto;
padding: 0 24px;
}
header {
padding: 48px 0 32px;
border-bottom: 1px solid var(--border);
}
section {
padding: 40px 0;
border-bottom: 1px solid var(--border);
}
section:last-of-type {
border-bottom: none;
}
h1 {
font-size: 1.75rem;
font-weight: 700;
letter-spacing: -0.02em;
margin-bottom: 4px;
}
h2 {
font-size: 1.25rem;
font-weight: 600;
letter-spacing: -0.01em;
margin-bottom: 16px;
}
.subtitle {
font-size: 0.95rem;
color: var(--fg-muted);
margin-bottom: 0;
}
code {
font-family: var(--mono);
font-size: 0.82em;
background: var(--bg-code);
padding: 2px 6px;
border-radius: 4px;
}
nav {
position: sticky;
top: 0;
z-index: 100;
border-bottom: 1px solid var(--border);
padding: 10px 0;
backdrop-filter: blur(12px);
background: color-mix(in srgb, var(--bg) 85%, transparent);
}
nav .container {
display: flex;
align-items: center;
gap: 12px;
}
nav span {
font-size: 0.8rem;
font-weight: 600;
color: var(--fg-muted);
}
.nav-spacer {
flex: 1;
}
#theme-toggle {
background: none;
border: 1px solid var(--border);
border-radius: 6px;
padding: 4px 10px;
cursor: pointer;
font-size: 0.8rem;
color: var(--fg-muted);
}
#theme-toggle:hover {
border-color: var(--fg-muted);
}
/* ─── Controls ─── */
.controls {
display: flex;
flex-wrap: wrap;
gap: 24px;
align-items: end;
padding: 20px;
background: var(--bg-alt);
border-radius: 10px;
border: 1px solid var(--border);
margin-bottom: 32px;
}
.control-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.control-group label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--fg-muted);
}
.control-group input[type="range"] {
width: 240px;
accent-color: var(--fg-accent);
}
.ratio-display {
font-family: var(--mono);
font-size: 1.5rem;
font-weight: 700;
color: var(--fg-accent);
min-width: 4ch;
min-width: 80px;
}
.toggle-group {
display: flex;
gap: 4px;
}
.toggle-btn {
font-size: 0.75rem;
font-weight: 600;
padding: 5px 12px;
border-radius: 6px;
border: 1px solid var(--border);
background: var(--bg);
color: var(--fg-muted);
cursor: pointer;
transition: all .15s;
}
.toggle-btn.active {
background: var(--fg-accent);
color: #fff;
border-color: var(--fg-accent);
}
.toggle-btn:hover:not(.active) {
border-color: var(--fg-muted);
}
/* ─── Vertical Comparison ─── */
.comparison-table {
width: 100%;
border-collapse: collapse;
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
}
.comparison-table th {
background: var(--bg-alt);
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--fg-muted);
padding: 10px 16px;
border-bottom: 1px solid var(--border);
text-align: left;
}
.comparison-table th.col-current {
text-align: right;
}
.comparison-table th.col-ratio {
text-align: left;
}
.comparison-table td {
border-bottom: 1px solid var(--border);
vertical-align: middle;
}
.comparison-table tr:last-child td {
border-bottom: none;
}
.size-meta {
padding: 20px 16px;
background: var(--bg-alt);
width: 100px;
border-right: 1px solid var(--border);
}
.size-name {
font-size: 1rem;
font-weight: 700;
color: var(--fg);
}
.size-detail {
font-family: var(--mono);
font-size: 0.7rem;
color: var(--fg-muted);
margin-top: 2px;
line-height: 1.4;
}
.btn-cell {
padding: 20px 24px;
text-align: right;
}
.btn-cell .specimen-meta {
text-align: right;
}
.btn-cell-ratio {
border-left: 1px dashed var(--border);
text-align: left;
}
.btn-cell-ratio .specimen-meta {
text-align: left;
}
.btn-specimen {
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--btn-bg);
color: var(--btn-fg);
font-weight: 600;
white-space: nowrap;
border: none;
transition: border-radius .1s ease;
line-height: 1;
}
.btn-specimen svg {
flex-shrink: 0;
}
.specimen-meta {
font-family: var(--mono);
font-size: 0.65rem;
color: var(--fg-muted);
margin-top: 8px;
line-height: 1.4;
}
.specimen-meta .val {
font-weight: 700;
color: var(--fg);
}
.specimen-meta .delta {
font-weight: 600;
}
.delta-pos {
color: var(--green);
}
.delta-neg {
color: var(--red);
}
.delta-zero {
color: var(--fg-muted);
}
/* ─── Formula Preview ─── */
.formula-preview {
background: var(--bg-code);
border-radius: 8px;
padding: 16px 20px;
font-family: var(--mono);
font-size: 0.8rem;
line-height: 1.8;
overflow-x: auto;
margin-top: 24px;
border: 1px solid var(--border);
}
.formula-line {
white-space: pre;
}
.token-ref {
color: var(--fg-accent);
}
.computed {
color: var(--green);
font-weight: 600;
}
.formula-header {
margin-bottom: 8px;
color: var(--fg-muted);
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* ─── Summary Table ─── */
.summary-table {
width: 100%;
border-collapse: collapse;
font-size: 0.82rem;
margin-top: 16px;
}
.summary-table th {
text-align: left;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--fg-muted);
padding: 8px 12px;
border-bottom: 2px solid var(--border);
}
.summary-table td {
padding: 8px 12px;
border-bottom: 1px solid var(--border);
font-family: var(--mono);
font-size: 0.8rem;
}
.summary-table tr:last-child td {
border-bottom: none;
}
.match {
color: var(--green);
font-weight: 600;
}
.diff {
color: var(--amber);
font-weight: 600;
}
</style>
</head>
<body>
<nav>
<div class="container">
<span>Button Shape Ratio Sandbox</span>
<span style="color:var(--fg-muted);font-size:0.75rem">ratio-based border-radius exploration</span>
<div class="nav-spacer"></div>
<button id="theme-toggle">Toggle theme</button>
</div>
</nav>
<div class="container">
<header>
<h1>Shape Ratio Explorer</h1>
<p class="subtitle">
<code>border-radius = round(fontSize &times; ratio, snapGrid)</code>
&mdash; replacing hand-tuned shape tokens with a single ratio.
</p>
</header>
<section>
<div class="controls">
<div class="control-group">
<label>Shape Ratio</label>
<input type="range" id="ratio-slider" min="0.30" max="1.00" step="0.01" value="0.45">
</div>
<div class="control-group">
<label>&nbsp;</label>
<span class="ratio-display" id="ratio-value">0.45</span>
</div>
<div class="control-group">
<label>Snap Grid</label>
<div class="toggle-group" id="snap-toggles">
<button class="toggle-btn" data-snap="0">None</button>
<button class="toggle-btn active" data-snap="1">1px</button>
<button class="toggle-btn" data-snap="2">2px</button>
<button class="toggle-btn" data-snap="4">4px</button>
</div>
</div>
<div class="control-group">
<label>Presets</label>
<div class="toggle-group" id="preset-toggles">
<button class="toggle-btn" data-ratio="0.45">0.45</button>
<button class="toggle-btn" data-ratio="0.50">0.50</button>
<button class="toggle-btn" data-ratio="0.55">0.55</button>
<button class="toggle-btn active" data-ratio="0.60">0.60</button>
<button class="toggle-btn" data-ratio="0.667">2:3</button>
</div>
</div>
</div>
<h2>Visual Comparison</h2>
<table class="comparison-table" id="comparison-table">
<thead>
<tr>
<th>Size</th>
<th class="col-current">Current (hand-tuned)</th>
<th class="col-ratio">Ratio-based</th>
</tr>
</thead>
<tbody id="comparison-body"></tbody>
</table>
<div class="formula-preview" id="formula-preview"></div>
</section>
<section>
<h2>Value Summary</h2>
<table class="summary-table">
<thead>
<tr>
<th>Size</th>
<th>Typescale</th>
<th>Font px</th>
<th>Current Token</th>
<th>Current px</th>
<th>Raw Computed</th>
<th>Snapped</th>
<th>Delta</th>
</tr>
</thead>
<tbody id="summary-body"></tbody>
</table>
</section>
</div>
<script>
var SCALE_RATIO = 1.067;
var BASE_PX = 16;
function typescalePx(step) {
return BASE_PX * Math.pow(SCALE_RATIO, step - 12);
}
// All geometry from canonical button tokens
var SIZES = [
{ name: 'xs', step: 10, currentToken: 'shape.md-lg', currentPx: 6, padX: 12, padY: 6, gapIcon: 2 },
{ name: 'sm', step: 12, currentToken: 'shape.lg', currentPx: 8, padX: 14, padY: 8, gapIcon: 4 },
{ name: 'md', step: 15, currentToken: 'shape.xl', currentPx: 12, padX: 16, padY: 10, gapIcon: 4 },
{ name: 'lg', step: 18, currentToken: 'shape.xl', currentPx: 12, padX: 20, padY: 12, gapIcon: 6 },
{ name: 'xl', step: 21, currentToken: 'shape.2xl', currentPx: 16, padX: 20, padY: 12, gapIcon: 8 },
{ name: '2xl', step: 23, currentToken: 'shape.2xl', currentPx: 16, padX: 24, padY: 12, gapIcon: 10 },
{ name: '3xl', step: 26, currentToken: 'shape.2xl', currentPx: 16, padX: 24, padY: 12, gapIcon: 10 },
];
SIZES.forEach(function (s) { s.fontPx = typescalePx(s.step); });
var ratio = 0.60;
var snapGrid = 1;
function snap(value, grid) {
if (grid === 0) return value;
return Math.round(value / grid) * grid;
}
function computeRadius(size) {
var raw = size.fontPx * ratio;
var snapped = snap(raw, snapGrid);
return { raw: raw, snapped: snapped };
}
// ─── SVG icon factories ───
// Lucide icon factory — stroke-based, no fill
function makeLucideIcon(size, paths) {
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', String(Math.round(size)));
svg.setAttribute('height', String(Math.round(size)));
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('fill', 'none');
svg.setAttribute('stroke', 'currentColor');
svg.setAttribute('stroke-width', '2');
svg.setAttribute('stroke-linecap', 'round');
svg.setAttribute('stroke-linejoin', 'round');
svg.style.flexShrink = '0';
paths.forEach(function (d) {
var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', d);
svg.appendChild(path);
});
return svg;
}
// Lucide "star" — leading icon
function makeLeadingIcon(size) {
return makeLucideIcon(size, [
'M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z'
]);
}
// Lucide "chevron-right" — trailing icon
function makeTrailingIcon(size) {
return makeLucideIcon(size, [
'm9 18 6-6-6-6'
]);
}
// ─── Build a button specimen ───
function makeButton(s, borderRadius) {
var btn = document.createElement('div');
btn.className = 'btn-specimen';
btn.style.fontSize = s.fontPx + 'px';
btn.style.borderRadius = borderRadius + 'px';
btn.style.padding = s.padY + 'px ' + s.padX + 'px';
btn.style.gap = s.gapIcon + 'px';
btn.appendChild(makeLeadingIcon(s.fontPx));
var label = document.createElement('span');
label.textContent = 'Click me';
btn.appendChild(label);
btn.appendChild(makeTrailingIcon(s.fontPx));
return btn;
}
// ─── Render comparison ───
function renderComparison() {
var tbody = document.getElementById('comparison-body');
tbody.replaceChildren();
SIZES.forEach(function (s) {
var result = computeRadius(s);
var raw = result.raw;
var snapped = result.snapped;
var delta = snapped - s.currentPx;
var deltaClass = delta > 0 ? 'delta-pos' : delta < 0 ? 'delta-neg' : 'delta-zero';
var deltaStr = delta > 0 ? ('+' + delta) : delta === 0 ? '0' : String(delta);
var tr = document.createElement('tr');
// Size meta cell
var tdMeta = document.createElement('td');
tdMeta.className = 'size-meta';
var nameDiv = document.createElement('div');
nameDiv.className = 'size-name';
nameDiv.textContent = s.name;
tdMeta.appendChild(nameDiv);
var detailDiv = document.createElement('div');
detailDiv.className = 'size-detail';
detailDiv.textContent = 'ts-' + s.step + ' \u2022 ' + s.fontPx.toFixed(1) + 'px';
tdMeta.appendChild(detailDiv);
tr.appendChild(tdMeta);
// Current button cell
var tdCurrent = document.createElement('td');
tdCurrent.className = 'btn-cell';
tdCurrent.appendChild(makeButton(s, s.currentPx));
var metaCurrent = document.createElement('div');
metaCurrent.className = 'specimen-meta';
var valSpan = document.createElement('span');
valSpan.className = 'val';
valSpan.textContent = s.currentPx + 'px';
metaCurrent.appendChild(valSpan);
metaCurrent.appendChild(document.createTextNode(' \u2022 ' + s.currentToken));
tdCurrent.appendChild(metaCurrent);
tr.appendChild(tdCurrent);
// Ratio button cell
var tdRatio = document.createElement('td');
tdRatio.className = 'btn-cell btn-cell-ratio';
tdRatio.appendChild(makeButton(s, snapped));
var metaRatio = document.createElement('div');
metaRatio.className = 'specimen-meta';
var valSpan2 = document.createElement('span');
valSpan2.className = 'val';
valSpan2.textContent = snapped + 'px';
metaRatio.appendChild(valSpan2);
var rawSpan = document.createTextNode(' (' + raw.toFixed(1) + ') ');
metaRatio.appendChild(rawSpan);
var deltaSpan = document.createElement('span');
deltaSpan.className = 'delta ' + deltaClass;
deltaSpan.textContent = deltaStr + 'px';
metaRatio.appendChild(deltaSpan);
tdRatio.appendChild(metaRatio);
tr.appendChild(tdRatio);
tbody.appendChild(tr);
});
}
// ─── Render formula preview ───
function renderFormula() {
var container = document.getElementById('formula-preview');
container.replaceChildren();
var snapLabel = snapGrid === 0 ? 'none' : snapGrid + 'px';
var header = document.createElement('div');
header.className = 'formula-header';
header.textContent = 'Formula: math({shape.ratio.button} * {typography.scale.typescale-N}) | snap: ' + snapLabel;
container.appendChild(header);
SIZES.forEach(function (s) {
var result = computeRadius(s);
var raw = result.raw;
var snapped = result.snapped;
var line = document.createElement('div');
line.className = 'formula-line';
var ref1 = document.createElement('span');
ref1.className = 'token-ref';
ref1.textContent = '{shape.ratio.button}';
line.appendChild(ref1);
line.appendChild(document.createTextNode(' (' + ratio + ') \u00d7 '));
var ref2 = document.createElement('span');
ref2.className = 'token-ref';
ref2.textContent = '{typography.scale.typescale-' + s.step + '}';
line.appendChild(ref2);
line.appendChild(document.createTextNode(' (' + s.fontPx.toFixed(2) + 'px) = ' + raw.toFixed(2)));
if (snapGrid > 0) {
line.appendChild(document.createTextNode(' \u2192 snap(' + snapGrid + ') \u2192 '));
} else {
line.appendChild(document.createTextNode(' \u2192 '));
}
var comp = document.createElement('span');
comp.className = 'computed';
comp.textContent = (snapGrid > 0 ? snapped : raw.toFixed(2)) + 'px';
line.appendChild(comp);
line.appendChild(document.createTextNode(' // ' + s.name));
container.appendChild(line);
});
}
// ─── Render summary table ───
function renderSummary() {
var tbody = document.getElementById('summary-body');
tbody.replaceChildren();
SIZES.forEach(function (s) {
var result = computeRadius(s);
var raw = result.raw;
var snapped = result.snapped;
var delta = snapped - s.currentPx;
var deltaClass = delta === 0 ? 'match' : 'diff';
var deltaStr = delta > 0 ? ('+' + delta + 'px') : delta === 0 ? '0' : (delta + 'px');
var tr = document.createElement('tr');
var cells = [
{ text: s.name, style: 'font-weight:600;color:var(--fg)' },
{ text: 'typescale-' + s.step },
{ text: s.fontPx.toFixed(2) },
{ text: s.currentToken, style: 'color:var(--fg-muted)' },
{ text: String(s.currentPx) },
{ text: raw.toFixed(2) },
{ text: String(snapped), style: 'font-weight:700' },
{ text: deltaStr, cls: deltaClass },
];
cells.forEach(function (c) {
var td = document.createElement('td');
td.textContent = c.text;
if (c.style) td.setAttribute('style', c.style);
if (c.cls) td.className = c.cls;
tr.appendChild(td);
});
tbody.appendChild(tr);
});
}
function render() {
var display = ratio === 0.667 ? ratio.toFixed(3) : ratio.toFixed(2);
document.getElementById('ratio-value').textContent = display;
renderComparison();
renderFormula();
renderSummary();
}
// ─── Event handlers ───
document.getElementById('ratio-slider').addEventListener('input', function (e) {
ratio = parseFloat(e.target.value);
document.querySelectorAll('#preset-toggles .toggle-btn').forEach(function (b) {
b.classList.toggle('active', parseFloat(b.dataset.ratio) === ratio);
});
render();
});
document.getElementById('snap-toggles').addEventListener('click', function (e) {
var btn = e.target.closest('.toggle-btn');
if (!btn) return;
snapGrid = parseInt(btn.dataset.snap);
document.querySelectorAll('#snap-toggles .toggle-btn').forEach(function (b) { b.classList.remove('active'); });
btn.classList.add('active');
render();
});
document.getElementById('preset-toggles').addEventListener('click', function (e) {
var btn = e.target.closest('.toggle-btn');
if (!btn) return;
ratio = parseFloat(btn.dataset.ratio);
document.getElementById('ratio-slider').value = ratio;
document.querySelectorAll('#preset-toggles .toggle-btn').forEach(function (b) { b.classList.remove('active'); });
btn.classList.add('active');
render();
});
document.getElementById('theme-toggle').addEventListener('click', function () {
var html = document.documentElement;
html.dataset.theme = html.dataset.theme === 'dark' ? 'light' : 'dark';
});
render();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment