{
  "$type": "site.standard.document",
  "content": {
    "$type": "site.standard.content.markdown",
    "text": "I’ve been writing CSS for a long time and it’s really exciting to see the fast improvements to the language that we’ve received over that time. In this post, I’ll demonstrate two ideas that I always wanted to do natively with CSS alone.\n\nFirst, a warm-up before we get to the wild stuff.\n\n## Grid centered line-height\n\nI know of plenty of designers who want to maintain a [vertical rhythm](https://zellwk.com/blog/why-vertical-rhythms/) for all content on the page. They’ll painstakingly adjust border widths and line-heights to ensure all of the little elements on the page align to some vertical grid such that there’s a rhythm to reading downward.\n\nAs the person who wrote [Gridless Design](https://gridless.design/), you can imagine how I feel about this. In my opinion, the time spent trying to get this alignment would be more worthwhile on other things; say… improving the user experience as an example. However, now that we have some new CSS in browsers, getting the vertical rhythm to align with your grid can happen at any font size very easily.\n\nWe’re going to need a bit of math for this. Let’s say that you have an [`8px` grid](https://notadesigner.io/p/8px-grid). This means elements on your composition should be sized in multiples of `8px`. Let’s also say that your `line-height` for paragraphs should be `1.4`, meaning the resulting height of each line should be 1.4 times the size of the font so the space between lines helps readability. If your paragraph font is `18px`, this means the paragraph `line-height` would result in each line of text being `25.2px` high.\n\n18∗1.4\\=25.218\\*1.4=25.218∗1.4\\=25.2\n\nClearly, this doesn’t align to our `8px` grid. Historically, we’d curate the `line-height` each time we introduce a new `font-size`. In this case, we have to hardcode the `line-height` knowing the `font-size` such that it remains on the `8px` grid.\n\n```\n.paragraph {\n    font-size: 18px;\n    line-height: var(--paragraph-lineheight);\n}\n```\n\n### Rounding to the nearest grid\n\nIn order to determine the correct number here, we’ll need to round 25.2 to the nearest 8. Doing this in JavaScript would look like this, assuming the numbers are entered without units:\n\n```\nfunction lineHeight(fontSize = 18, lhTarget = 1.4, gridUnit = 8) {\n    return Math.ceil((fontSize * lhTarget) / gridUnit) * gridUnit;\n}\n```\n\nThe messy part is rounding to the nearest grid unit. We have to divide 25.2 by 8, then round that to the nearest integer and then multiply by 8 to get 32. This means that the `--paragraph-lineheight` should be set as `32px` when the font size is `18px` and the target line height is `1.4`.\n\nAs you can see, this is very cumbersome. We’d need to know the exact `font-size` value, the grid unit, and the target `line-height` for this kind of element to determine the final result.\n\nLuckily, the new `round()` CSS function can really help us out.\n\n```\n.paragraph {\n    line-height: round(up, 1.4em, var(--grid-unit, 8px));\n}\n```\n\nThis is effectively the same calculation that was demonstrated earlier in JavaScript. The [`round()` function](https://developer.mozilla.org/en-US/docs/Web/CSS/round) is [fairly well supported](https://developer.mozilla.org/en-US/docs/Web/CSS/round#browser_compatibility) and can take three arguments:\n\n1.  The first can determine the rounding strategy, in this case we want to round up in the same way we used `Math.ceil()` before.\n2.  The next argument is the number we’re going to be rounding. This will represent the target `line-height`; basically the minimum `line-height` allowed. Unfortunately, we can’t use the unitless `line-height` here but we can use the `em` version instead which would result in the same final pixel amount.\n3.  Finally, the amount to round by. This part of the function is **awesome**. It avoids all of the weird division and multiplication to round by an amount in the JavaScript. Here, we want to round to the nearest grid unit. In this example, I simply set the default as `8px` in the second argument of the `var()`, and then it can accept any update in the page and fallback to `8px`. You can also use any value with units here instead.\n\nYou’ll also notice I’m not explicitly setting the `font-size` here. This is because this `line-height` value will respect whatever `font-size` this element is because it’s referencing `em` units, even if it is inherited from a parent! If your target `line-height` is meant to be tighter, as is normally the case for headings, you can adjust the target to be smaller like `1.2em`.\n\n```\n.heading {\n    line-height: round(up, 1.2em, var(--grid-unit, 8px));\n}\n```\n\nHere is a codepen demonstrating the technique. The gray lines behind the text represent the grid unit intervals and the red background on the text shows how much space the text takes up. You’ll see that the gray lines will never be cut-off at the bottom of the container because the `line-height` for this element is *always* on grid. The span is not important to the CSS, it’s there just to visualize the height of the text.\n\n<p>See the Pen <a href=\"https://codepen.io/fauxserious/pen/wBwdwNa\"> Line Height on grid</a> by Donnie D’Amato (<a href=\"https://codepen.io/fauxserious\">@fauxserious</a>) on <a href=\"https://codepen.io\">CodePen</a>.</p>\n\nOk, that was a good warm-up. Now it’s time to get wild!\n\n## Dynamic letter-spacing\n\nLike `line-height`, I always felt that tweaking the `letter-spacing` was a waste of time. Especially as more custom fonts become normal, I felt setting the `letter-spacing` should be embedded in the font file itself. However, I saw [a post by Mathieu Badimon](https://www.linkedin.com/posts/badimon_theres-this-strange-perception-effect-where-activity-7267563944435757056-BhJv/#), formerly leading efforts for the design system at Adobe, sharing a way to do this *dynamically*. The gist is that he’s developed a scale where the `letter-spacing` of the text will get tighter as the `font-size` increases. Immediately, I wanted to try to do this in CSS alone. This took much longer than I hoped.\n\n### The easy parts\n\nThe first thing is that there is an upper and lower limit. This is pretty easy in CSS today using the `clamp()` function. So we’ll start with that using the values he recommends in the post; Sizes `25px` and below should be `0` and `-4%` for sizes above about `145px`. Since, `letter-spacing` doesn’t accept `%` units, we’ll use `em` instead which can be thought of as a percent of the `font-size`. Remember that the lowest value goes first, that’s why `-0.04em` is first and `0em` is last.\n\n```\n.paragraph {\n    letter-spacing: clamp(\n        -0.04em,\n        var(--ems) * 1em,\n        0em\n    );\n}\n```\n\nOk, so far so good. Now all we need is the rate of change. You’ll see that there’s a graph included in his post, but he mentions that the graph is actually inaccurate. That’s fine, we’re going to use the numbers [he provided later in a comment](https://www.linkedin.com/feed/update/urn:li:ugcPost:7267563941390684160?commentUrn=urn%3Ali%3Acomment%3A%28ugcPost%3A7267563941390684160%2C7268482417206689792%29&replyUrn=urn%3Ali%3Acomment%3A%28ugcPost%3A7267563941390684160%2C7268561421481738240%29&dashCommentUrn=urn%3Ali%3Afsd_comment%3A%287268482417206689792%2Curn%3Ali%3AugcPost%3A7267563941390684160%29&dashReplyUrn=urn%3Ali%3Afsd_comment%3A%287268561421481738240%2Curn%3Ali%3AugcPost%3A7267563941390684160%29).\n\n![Dynamic Letter Spacing Chart](/images/dynamic-letter-spacing-chart.png)\n\nWhat we want is the rate of change between to points, this can be thought of as finding the slope of a line between points. If we think of the `font-size` as the `y` and the resulting percent as `x`, we can find the slope (`m`) in the following way:\n\nm\\=(y2−y1)/(x2−x1)m=(y\\_2-y\\_1)/(x\\_2-x\\_1)m\\=(y2​−y1​)/(x2​−x1​)\n\nPlugging in some numbers from the chart, we get the following:\n\nm\\=(28−25)/(−.1−0)\\=−30m=(28-25)/(-.1-0)=-30m\\=(28−25)/(−.1−0)\\=−30\n\nThis shouldn’t be a surprise from Mathieu’s post:\n\n![Letter spacing steps](/images/letter-spacing-steps.jpg)\n\nThe `-30` is related to the `30px` every `1%` decrease. The next thing we need is the formula that produces all of the `x` values based on a given `y`. You might remember this formula from school:\n\ny\\=mx+by=mx+by\\=mx+b\n\nHowever, we want to solve for `x`, so we need to rearrange the terms:\n\nx\\=(y−b)/mx=(y-b)/mx\\=(y−b)/m\n\nIn this formula, `y` is our `font-size`, and `m` is the slope we got earlier of `-30`. So what does `b` represent? It’s where the slope crosses the x-axis. We could find this by moving terms around again, but Mathieu has already provided this number; `25` (for `25px` where this calculation starts). Our formula now looks like this:\n\npercent\\=(fontsize−25)/−30percent=(fontsize-25)/-30percent\\=(fontsize−25)/−30\n\nThis doesn’t look too intimidating yet, but here’s where it get challenging. We need to turn this into CSS. Let’s try just making some variables for this first:\n\n```\n.paragraph {\n    --b: 25px; /** starting size */\n    --m: -30; /** rate of change */\n    --x: calc(1em - var(--b)) / var(--m)); /** percent? */\n    letter-spacing: clamp(\n        -0.04em,\n        var(--ems) * 1em,\n        0em\n    );\n}\n```\n\nOk, so far all of this *should* work. We’re still missing the `--ems` assignment but we can subtract unit values from each other, and we can divide a unit number by an unitless number. When we subtract units like this, the result is resolved into a pixel amount. This’ll be important later.\n\n### The hard parts\n\nWe’re about to begin using super new CSS properties in a very hacky way. Consider yourself warned.\n\nThe first problem is that `--x` *isn’t* a unitless value, it’s a pixel amount. Setting this as the `--ems` isn’t correct because in Mathieu’s formula, we want a *percent* of the `1em`. So we want `--x` to have the pixel units removed.\n\nThere’s [a recent trick made popular by Jane Ori](https://dev.to/janeori/css-type-casting-to-numeric-tanatan2-scalars-582j) which demonstrates how we might hack CSS to give us unitless values using `atan2()`. We’ll need to add a few things:\n\n```\n@property --x {\n  syntax: \"<length>\";\n  initial-value: 0px;\n  inherits: false;\n}\n\n.paragraph {\n    --b: 25px; /** starting size */\n    --m: -30; /** rate of change */\n    --x: calc(1em - var(--b)) / var(--m)); /** percent? */\n    letter-spacing: clamp(\n        -0.04em,\n        var(--ems) * 1em,\n        0em\n    );\n}\n```\n\nThe first thing we need is to effectively *force* the `--x` to give us a pixel amount. It might seem weird that we need to do this since we know it’s resolving as a pixel, but it’ll be important so we can use `atan2()`.\n\n```\n@property --x {\n  syntax: \"<length>\";\n  initial-value: 0px;\n  inherits: false;\n}\n\n.paragraph {\n    --b: 25px; /** starting size */\n    --m: -30; /** rate of change */\n    --x: calc(1em - var(--b)) / var(--m)); /** percent? */\n    --ems: tan(atan2(var(--x), 16px); /** percent! */\n    letter-spacing: clamp(\n        -0.04em,\n        var(--ems) * 1em,\n        0em\n    );\n}\n```\n\nYou’ll see I’ve added the `--ems` variable by using the approach in the blog post. You can essentially look at this as returning `--x` in terms of `16px` and return the unitless result. What we have will work, but there’s one small problem. I’ve explicitly set `16px` here which is assuming base font size as a fixed pixel amount. Entering `1rem` in here doesn’t work (and admittedly I’m not entirely sure why even when updating the `@property` defaults).\n\nWhat we’ll need is `1rem` in terms of pixels. Luckily, [Jane has that example](https://codepen.io/propjockey/pen/WNLLLWy) in her post. We’ll use another `atan2()` to get the *unitless* number of pixels per `rem` (`16`) and expect to replace the `16px` with this result. This actually needs a number with a pixel unit so we can do that quickly by multiplying the unitless result of the new `atan2()` by `1px` to finally resolve to `16px`. Here’s the whole working declaration, followed by the codepen:\n\n```\n@property --x {\n  syntax: \"<length>\";\n  initial-value: 0px;\n  inherits: false;\n}\n\n@property --rem {\n  syntax: \"<length>\";\n  initial-value: 0px;\n  inherits: false;\n}\n\n.paragraph {\n    --rem: 1rem;\n    --b: 25px; /** starting size */\n    --m: -30; /** rate of change */\n    --x: calc(1em - var(--b)) / var(--m)); /** percent? */\n    --ems: tan(atan2(\n        var(--x),\n        calc(tan(atan2(var(--rem), 1px))) * 1px)\n    ); /** percent! */\n    letter-spacing: clamp(\n        -0.04em,\n        var(--ems) * 1em,\n        0em\n    );\n}\n```\n\n<p>See the Pen <a href=\"https://codepen.io/fauxserious/pen/ByByMeN\"> Dynamic letter spacing test</a> by Donnie D’Amato (<a href=\"https://codepen.io/fauxserious\">@fauxserious</a>) on <a href=\"https://codepen.io\">CodePen</a>.</p>\n\nI do wonder if there’s a setup that allows us to remove the second `atan2()` usage and insert the `1rem` directly here but I haven’t found it yet.\n\nWhew, what a ride! Maybe one day, we’ll get a supported way of dividing values with units to return a unitless value. But for now, we can wish upon this proof-of-concept. Thanks to Mathieu and Jane for their resources!",
    "version": "1.0"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreiad6v4hqhwqrcvtun2myvdv7t3f644hdt76wehcdbnywe5myj47r4"
    },
    "mimeType": "image/png",
    "size": 342302
  },
  "description": "Modern CSS unlocks some wishful visual treatments.",
  "path": "/posts/two-typographic-tricks",
  "publishedAt": "2024-12-19T00:00:00.000Z",
  "site": "https://blog.damato.design",
  "tags": [
    "typography",
    "css"
  ],
  "textContent": "I’ve been writing CSS for a long time and it’s really exciting to see the fast improvements to the language that we’ve received over that time. In this post, I’ll demonstrate two ideas that I always wanted to do natively with CSS alone.\n\nFirst, a warm-up before we get to the wild stuff.\n\nGrid centered line-height\n\nI know of plenty of designers who want to maintain a vertical rhythm for all content on the page. They’ll painstakingly adjust border widths and line-heights to ensure all of the little elements on the page align to some vertical grid such that there’s a rhythm to reading downward.\n\nAs the person who wrote Gridless Design, you can imagine how I feel about this. In my opinion, the time spent trying to get this alignment would be more worthwhile on other things; say… improving the user experience as an example. However, now that we have some new CSS in browsers, getting the vertical rhythm to align with your grid can happen at any font size very easily.\n\nWe’re going to need a bit of math for this. Let’s say that you have an  grid. This means elements on your composition should be sized in multiples of . Let’s also say that your  for paragraphs should be , meaning the resulting height of each line should be 1.4 times the size of the font so the space between lines helps readability. If your paragraph font is , this means the paragraph  would result in each line of text being  high.\n\n18∗1.4\\=25.218\\1.4=25.218∗1.4\\=25.2\n\nClearly, this doesn’t align to our  grid. Historically, we’d curate the  each time we introduce a new . In this case, we have to hardcode the  knowing the  such that it remains on the  grid.\n\nRounding to the nearest grid\n\nIn order to determine the correct number here, we’ll need to round 25.2 to the nearest 8. Doing this in JavaScript would look like this, assuming the numbers are entered without units:\n\nThe messy part is rounding to the nearest grid unit. We have to divide 25.2 by 8, then round that to the nearest integer and then multiply by 8 to get 32. This means that the  should be set as  when the font size is  and the target line height is .\n\nAs you can see, this is very cumbersome. We’d need to know the exact  value, the grid unit, and the target  for this kind of element to determine the final result.\n\nLuckily, the new  CSS function can really help us out.\n\nThis is effectively the same calculation that was demonstrated earlier in JavaScript. The  function is fairly well supported and can take three arguments:\nThe first can determine the rounding strategy, in this case we want to round up in the same way we used  before.\nThe next argument is the number we’re going to be rounding. This will represent the target ; basically the minimum  allowed. Unfortunately, we can’t use the unitless  here but we can use the  version instead which would result in the same final pixel amount.\nFinally, the amount to round by. This part of the function is awesome. It avoids all of the weird division and multiplication to round by an amount in the JavaScript. Here, we want to round to the nearest grid unit. In this example, I simply set the default as  in the second argument of the , and then it can accept any update in the page and fallback to . You can also use any value with units here instead.\n\nYou’ll also notice I’m not explicitly setting the  here. This is because this  value will respect whatever  this element is because it’s referencing  units, even if it is inherited from a parent! If your target  is meant to be tighter, as is normally the case for headings, you can adjust the target to be smaller like .\n\nHere is a codepen demonstrating the technique. The gray lines behind the text represent the grid unit intervals and the red background on the text shows how much space the text takes up. You’ll see that the gray lines will never be cut-off at the bottom of the container because the  for this element is always on grid. The span is not important to the CSS, it’s there just to visualize the height of the text.\n\nSee the Pen  Line Height on grid by Donnie D’Amato (@fauxserious) on CodePen.\n\nOk, that was a good warm-up. Now it’s time to get wild!\n\nDynamic letter-spacing\n\nLike , I always felt that tweaking the  was a waste of time. Especially as more custom fonts become normal, I felt setting the  should be embedded in the font file itself. However, I saw a post by Mathieu Badimon, formerly leading efforts for the design system at Adobe, sharing a way to do this dynamically. The gist is that he’s developed a scale where the  of the text will get tighter as the  increases. Immediately, I wanted to try to do this in CSS alone. This took much longer than I hoped.\n\nThe easy parts\n\nThe first thing is that there is an upper and lower limit. This is pretty easy in CSS today using the  function. So we’ll start with that using the values he recommends in the post; Sizes  and below should be  and  for sizes above about . Since,  doesn’t accept  units, we’ll use  instead which can be thought of as a percent of the . Remember that the lowest value goes first, that’s why  is first and  is last.\n\nOk, so far so good. Now all we need is the rate of change. You’ll see that there’s a graph included in his post, but he mentions that the graph is actually inaccurate. That’s fine, we’re going to use the numbers he provided later in a comment.\n\nWhat we want is the rate of change between to points, this can be thought of as finding the slope of a line between points. If we think of the  as the  and the resulting percent as , we can find the slope () in the following way:\n\nm\\=(y2−y1)/(x2−x1)m=(y\\2-y\\1)/(x\\2-x\\1)m\\=(y2​−y1​)/(x2​−x1​)\n\nPlugging in some numbers from the chart, we get the following:\n\nm\\=(28−25)/(−.1−0)\\=−30m=(28-25)/(-.1-0)=-30m\\=(28−25)/(−.1−0)\\=−30\n\nThis shouldn’t be a surprise from Mathieu’s post:\n\nThe  is related to the  every  decrease. The next thing we need is the formula that produces all of the  values based on a given . You might remember this formula from school:\n\ny\\=mx+by=mx+by\\=mx+b\n\nHowever, we want to solve for , so we need to rearrange the terms:\n\nx\\=(y−b)/mx=(y-b)/mx\\=(y−b)/m\n\nIn this formula,  is our , and  is the slope we got earlier of . So what does  represent? It’s where the slope crosses the x-axis. We could find this by moving terms around again, but Mathieu has already provided this number;  (for  where this calculation starts). Our formula now looks like this:\n\npercent\\=(fontsize−25)/−30percent=(fontsize-25)/-30percent\\=(fontsize−25)/−30\n\nThis doesn’t look too intimidating yet, but here’s where it get challenging. We need to turn this into CSS. Let’s try just making some variables for this first:\n\nOk, so far all of this should work. We’re still missing the  assignment but we can subtract unit values from each other, and we can divide a unit number by an unitless number. When we subtract units like this, the result is resolved into a pixel amount. This’ll be important later.\n\nThe hard parts\n\nWe’re about to begin using super new CSS properties in a very hacky way. Consider yourself warned.\n\nThe first problem is that  isn’t a unitless value, it’s a pixel amount. Setting this as the  isn’t correct because in Mathieu’s formula, we want a percent of the . So we want  to have the pixel units removed.\n\nThere’s a recent trick made popular by Jane Ori which demonstrates how we might hack CSS to give us unitless values using . We’ll need to add a few things:\n\nThe first thing we need is to effectively force the  to give us a pixel amount. It might seem weird that we need to do this since we know it’s resolving as a pixel, but it’ll be important so we can use .\n\nYou’ll see I’ve added the  variable by using the approach in the blog post. You can essentially look at this as returning  in terms of  and return the unitless result. What we have will work, but there’s one small problem. I’ve explicitly set  here which is assuming base font size as a fixed pixel amount. Entering  in here doesn’t work (and admittedly I’m not entirely sure why even when updating the  defaults).\n\nWhat we’ll need is  in terms of pixels. Luckily, Jane has that example in her post. We’ll use another  to get the unitless* number of pixels per  () and expect to replace the  with this result. This actually needs a number with a pixel unit so we can do that quickly by multiplying the unitless result of the new  by  to finally resolve to . Here’s the whole working declaration, followed by the codepen:\n\nSee the Pen  Dynamic letter spacing test by Donnie D’Amato (@fauxserious) on CodePen.\n\nI do wonder if there’s a setup that allows us to remove the second  usage and insert the  directly here but I haven’t found it yet.\n\nWhew, what a ride! Maybe one day, we’ll get a supported way of dividing values with units to return a unitless value. But for now, we can wish upon this proof-of-concept. Thanks to Mathieu and Jane for their resources!",
  "title": "Two Typographic Tricks",
  "updatedAt": "2026-06-13T19:13:49.330Z"
}