A Pen by Tom Hermans on CodePen.
Created
July 11, 2025 10:14
-
-
Save tomhermans/22cc3c6884e80e246cbf52c39e0815d8 to your computer and use it in GitHub Desktop.
Typescale Calculator with Clamp (added easy COPY to clipboard)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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