{"id":6059,"date":"2025-06-09T17:28:59","date_gmt":"2025-06-09T22:28:59","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=6059"},"modified":"2025-06-09T17:29:00","modified_gmt":"2025-06-09T22:29:00","slug":"scroll-driven-letter-grid","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/scroll-driven-letter-grid\/","title":{"rendered":"Scroll-Driven Letter Grid"},"content":{"rendered":"\n<p>I was thinking about variable fonts the other day, and how many of them that deal with a variable axis for their weight go from 100 to 900. It varies \u2014 so you can always check <a href=\"https:\/\/wakamaifondue.com\/\">wakamaifondue.com<\/a> if you have the font file. <a href=\"https:\/\/fonts.google.com\/specimen\/Jost\">Jost on Google Fonts<\/a> is a classic example. Load that sucker up and you can use whatever weight you want.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_JodOePb\" src=\"\/\/codepen.io\/anon\/embed\/JodOePb?height=450&amp;theme-id=47434&amp;slug-hash=JodOePb&amp;default-tab=css,result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed JodOePb\" title=\"CodePen Embed JodOePb\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>I was also thinking about the &#8220;simple&#8221; kind of scroll-driven animations where all it does is move a <code>@keyframe<\/code> animation from 0% to 100% while a scrolling element goes from 0% to 100% &#8220;scrolled&#8221;.  Fair warning that <a href=\"https:\/\/caniuse.com\/mdn-css_properties_scroll-timeline\">browser support isn&#8217;t great<\/a>, but it&#8217;s just a fun thing that can easily just not happen. <\/p>\n\n\n\n<p>It&#8217;s deliciously simple to use:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_azOVQmp\/a00982eef276cb1b39091140800e1fa1\" src=\"\/\/codepen.io\/anon\/embed\/azOVQmp\/a00982eef276cb1b39091140800e1fa1?height=450&amp;theme-id=47434&amp;slug-hash=azOVQmp\/a00982eef276cb1b39091140800e1fa1&amp;default-tab=css,result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed azOVQmp\/a00982eef276cb1b39091140800e1fa1\" title=\"CodePen Embed azOVQmp\/a00982eef276cb1b39091140800e1fa1\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>We can smash these things together. We should be able to map 0%-100% to 100-900 pretty easily, right?<\/p>\n\n\n\n<p>Right. <\/p>\n\n\n\n<p>Let&#8217;s made a grid of 100 letters inside a <code>&lt;div id=\"grid\"&gt;<\/code>. We could use any kind of HTML generating technology. Let&#8217;s just vanilla JavaScript here.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">generateGrid<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> grid = <span class=\"hljs-built_in\">document<\/span>.getElementById(<span class=\"hljs-string\">\"grid\"<\/span>);\n  grid.innerHTML = <span class=\"hljs-string\">\"\"<\/span>;\n\n  <span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">let<\/span> i = <span class=\"hljs-number\">0<\/span>; i &lt; <span class=\"hljs-number\">100<\/span>; i++) {\n    <span class=\"hljs-keyword\">const<\/span> div = <span class=\"hljs-built_in\">document<\/span>.createElement(<span class=\"hljs-string\">\"div\"<\/span>);\n    div.textContent = getRandomLetter();\n    grid.appendChild(div);\n  }\n}\n\ngenerateGrid();<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The lay it out as a 10\u271510:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-id\">#grid<\/span> {\n  <span class=\"hljs-attribute\">display<\/span>: grid;\n  <span class=\"hljs-attribute\">grid-template-columns<\/span>: <span class=\"hljs-built_in\">repeat<\/span>(<span class=\"hljs-number\">10<\/span>, <span class=\"hljs-number\">1<\/span>fr);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We can chew through that grid in Sass applying random weights:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"SCSS\" data-shcb-language-slug=\"scss\"><span><code class=\"hljs language-scss\"><span class=\"hljs-keyword\">@for<\/span> <span class=\"hljs-variable\">$i<\/span> from <span class=\"hljs-number\">1<\/span> through <span class=\"hljs-number\">100<\/span> {\n  <span class=\"hljs-selector-id\">#grid<\/span> <span class=\"hljs-selector-pseudo\">:nth-child<\/span>(#{<span class=\"hljs-variable\">$i<\/span>}) {\n    <span class=\"hljs-attribute\">font-weight<\/span>: <span class=\"hljs-number\">100<\/span> + math.ceil(random() * <span class=\"hljs-number\">800<\/span>);\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">SCSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">scss<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Looks like this.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_bNdYQqJ\" src=\"\/\/codepen.io\/anon\/embed\/bNdYQqJ?height=550&amp;theme-id=47434&amp;slug-hash=bNdYQqJ&amp;default-tab=result\" height=\"550\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed bNdYQqJ\" title=\"CodePen Embed bNdYQqJ\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>But scroll up and down that preview! <\/p>\n\n\n\n<p>I attached a scroll timeline to the document like:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">html<\/span> {\n  <span class=\"hljs-attribute\">scroll-timeline<\/span>: --page-scroll block;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Then use that timeline to call an animation like:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-id\">#grid<\/span> {\n  &gt; div {\n    <span class=\"hljs-attribute\">animation<\/span>: to-thin auto linear;\n    <span class=\"hljs-attribute\">animation-timeline<\/span>: --page-scroll;\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>That animation is named <code>to-thin<\/code>, but actually I made three different animations: <code>to-thick<\/code>, <code>to-thin<\/code>, and <code>to-mid<\/code>, then applied them in rotation to all the letters, so any given letter does something a bit different. <\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-keyword\">@keyframes<\/span> to-thick {\n  50% {\n    <span class=\"hljs-attribute\">font-weight<\/span>: <span class=\"hljs-number\">900<\/span>;\n  }\n}\n<span class=\"hljs-keyword\">@keyframes<\/span> to-thin {\n  50% {\n    <span class=\"hljs-attribute\">font-weight<\/span>: <span class=\"hljs-number\">100<\/span>;\n  }\n}\n<span class=\"hljs-keyword\">@keyframes<\/span> to-mid {\n  50% {\n    <span class=\"hljs-attribute\">font-weight<\/span>: <span class=\"hljs-number\">450<\/span>;\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>See how I used <code>50%<\/code> keyframes there which is a nice trick to animate to that value half way through the animation, <strong>then back.<\/strong><\/p>\n\n\n\n<p>It then occurred to me I could make a <em>secret message.<\/em> So I make a <code>@mixin<\/code> that would override certain letters in CSS to make the message. It still randomized the weight, but all the letters animate to thin while the secret message animates to thick, revealing it as you scroll down. <\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_gbpXQQy\" src=\"\/\/codepen.io\/anon\/embed\/gbpXQQy?height=900&amp;theme-id=47434&amp;slug-hash=gbpXQQy&amp;default-tab=result\" height=\"900\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed gbpXQQy\" title=\"CodePen Embed gbpXQQy\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Anyway this is sometimes how I spend my free time and it&#8217;s completely normal. <\/p>\n","protected":false},"excerpt":{"rendered":"<p>scroll-timelines go from 0 to 100. Many variable fonts axis have similar ranges, like 100 to 900. Surely that&#8217;s begging for interplay.<\/p>\n","protected":false},"author":1,"featured_media":6061,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[7,57,169],"class_list":["post-6059","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-css","tag-scroll-driven-animations","tag-variable-fonts"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/grid.jpg?fit=1600%2C900&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6059","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=6059"}],"version-history":[{"count":5,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6059\/revisions"}],"predecessor-version":[{"id":6065,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6059\/revisions\/6065"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/6061"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=6059"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=6059"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=6059"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}