Skip to content

Instantly share code, notes, and snippets.

@tomhermans
Created July 11, 2025 10:14
Show Gist options
  • Save tomhermans/22cc3c6884e80e246cbf52c39e0815d8 to your computer and use it in GitHub Desktop.
Save tomhermans/22cc3c6884e80e246cbf52c39e0815d8 to your computer and use it in GitHub Desktop.
Typescale Calculator with Clamp (added easy COPY to clipboard)
<div class="wrap">
<aside>
<h1>Typescale Calculator <br> with Clamp
<span>and easy copy to clipboard</span></h1>
<div class="controls">
<div class="control-group">
<label for="baseSize">Base Font Size (rem):</label>
<input type="number" id="baseSize" value="1" step="0.01" min="0.5" max="2">
</div>
<div class="control-group">
<label for="ratio">Type Scale Ratio:</label>
<select id="ratio">
<option value="1.067">1.067 – Minor Second</option>
<option value="1.125" selected>1.125 – Major Second</option>
<option value="1.200">1.200 – Minor Third</option>
<option value="1.250">1.250 – Major Third</option>
<option value="1.333">1.333 – Perfect Fourth</option>
<option value="1.414">1.414 – Augmented Fourth</option>
<option value="1.500">1.500 – Perfect Fifth</option>
<option value="1.618">1.618 – Golden Ratio</option>
</select>
</div>
<div class="control-group">
<label for="mobileScale">Mobile Scale Factor:</label>
<input type="number" id="mobileScale" value="0.8" step="0.05" min="0.5" max="1">
<small>Reduces all sizes on mobile (0.8 = 80% of desktop size)</small>
</div>
<div class="control-group">
<label for="vwFactor">Viewport Width Scaling:</label>
<input type="number" id="vwFactor" value="0.5" step="0.1" min="0" max="2">
<small>Controls how much fonts scale with viewport (higher = more scaling)</small>
</div>
</div>
<div class="output" id="output"></div>
</aside>
<div class="preview" id="preview"></div>
</div>
function calculateTypescale() {
const baseSize = parseFloat(document.getElementById("baseSize").value);
const ratio = parseFloat(document.getElementById("ratio").value);
const mobileScale = parseFloat(document.getElementById("mobileScale").value);
const vwFactor = parseFloat(document.getElementById("vwFactor").value);
// Calculate sizes for each level
const sizes = [];
// Small sizes (negative steps)
sizes.push({
name: "--font-size-1",
level: -2,
size: baseSize / Math.pow(ratio, 2)
});
sizes.push({
name: "--font-size-2",
level: -1,
size: baseSize / ratio
});
// Base and positive steps
for (let i = 0; i <= 8; i++) {
sizes.push({
name: `--font-size-${i + 3}`,
level: i,
size: baseSize * Math.pow(ratio, i)
});
}
// Display sizes (extra large)
sizes.push({
name: "--font-size-display-1",
level: 9,
size: baseSize * Math.pow(ratio, 9)
});
sizes.push({
name: "--font-size-display-2",
level: 10,
size: baseSize * Math.pow(ratio, 10)
});
sizes.push({
name: "--font-size-display-3",
level: 11,
size: baseSize * Math.pow(ratio, 11)
});
// Generate CSS
let css = "/* Typescale: " + ratio + " */\n";
let previewHTML = "";
sizes.forEach((item) => {
const desktopSize = item.size;
const mobileSize = item.size * mobileScale;
const vwScaling = (desktopSize - mobileSize) * vwFactor;
css += `${item.name}: clamp(${mobileSize.toFixed(
3
)}rem, ${mobileSize.toFixed(3)}rem + ${vwScaling.toFixed(
2
)}vw, ${desktopSize.toFixed(3)}rem);\n`;
// Add to preview
previewHTML += `<div class="font-sample" contenteditable style="font-size: var(${
item.name
})">
Sample Text <strong>Bold</strong> (${item.name})
<span class="font-info">Mobile: ${mobileSize.toFixed(
2
)}rem, Desktop: ${desktopSize.toFixed(2)}rem</span>
</div>`;
});
document.getElementById("output").textContent = css;
// document.getElementById("output").addEventListener('click', (e) => {
// console.log('click')
// })
document.getElementById("output").addEventListener('click', async (e) => {
try {
const textContent = e.target.textContent || e.target.innerText;
await navigator.clipboard.writeText(textContent);
console.log('Content copied to clipboard');
// Optional: Provide visual feedback
const originalText = e.target.style.backgroundColor;
e.target.style.backgroundColor = '#4CAF50';
setTimeout(() => {
e.target.style.backgroundColor = originalText;
}, 200);
} catch (err) {
console.error('Failed to copy text: ', err);
// Fallback for older browsers
fallbackCopyTextToClipboard(e.target.textContent || e.target.innerText);
}
});
function fallbackCopyTextToClipboard(text) {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
textArea.style.left = "-999999px";
textArea.style.top = "-999999px";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
console.log('Fallback: Content copied to clipboard');
} catch (err) {
console.error('Fallback: Unable to copy', err);
}
document.body.removeChild(textArea);
}
// Update CSS custom properties for preview
const root = document.documentElement;
sizes.forEach((item) => {
const desktopSize = item.size;
const mobileSize = item.size * mobileScale;
const vwScaling = (desktopSize - mobileSize) * vwFactor;
root.style.setProperty(
item.name,
`clamp(${mobileSize}rem, ${mobileSize}rem + ${vwScaling}vw, ${desktopSize}rem)`
);
});
document.getElementById("preview").innerHTML =
"<h3>Live Preview:</h3>" + previewHTML;
}
// Initial calculation
calculateTypescale();
// Update on input change
document.querySelectorAll("input, select").forEach((element) => {
element.addEventListener("input", calculateTypescale);
});
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto+Condensed:ital,wght@0,100..900;1,100..900&display=swap');
body {
font-family: "Roboto Condensed", system-ui, -apple-system, sans-serif;
font-weight: 400;
// font-family: 'whirly birdie';
// font-variation-settings: 'wdth' 60, 'wght' 79;
// max-width: 800px;
margin: 0 auto;
padding: 2rem;
line-height: 1.6;
}
.controls {
background: #f5f5f5;
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 2rem;
}
.control-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
}
input,
select {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.output {
background: #f9f9f9;
padding: 1.5rem;
border-radius: 8px;
font-family: "Courier New", monospace;
white-space: pre-line;
margin-bottom: 2rem;
position: relative;
&::before {
content: 'COPY';
position: absolute;
top: 2px;
right: 4px;
border-radius: 12px;
padding: 4px;
width: 64px;
height: 24px;
background: blue;
color: white;
display: grid;
place-content: center;
cursor: pointer;
&:hover {
background: green;
}
}
}
.preview {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1.5rem;
width: 80vw;
}
.font-sample {
margin-bottom: 0.5rem;
border-bottom: 1px solid #eee;
padding-bottom: 0.5rem;
}
.font-info {
font-size: 0.8rem;
color: #666;
margin-left: 1rem;
}
.wrap {
display: flex;
> * {
&:first-child {
width: 32vw;
}
}
}
aside h1 {
line-height: 1.2;
span {
display: inline-block;
line-height: 1.2;
font-size: 65%;
font-weight: 400;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment