DEV Community

Cover image for Svelte Capsize Styling: Typography Tooling
Rodney Lab
Rodney Lab

Posted on • Originally published at rodneylab.com

Svelte Capsize Styling: Typography Tooling

📐 Capsize Typography Tooling

In this post we will see how you can add Svelte Capsize styling to your SvelteKit app. Although we use SvelteKit you should be able easily to adapt the code for Astro or Slinkity. Before that though we should take a look at what Capsize is and the problems it solves. Capsize is a tool which helps you to design text layouts in a predictable way. Typically to adjust how big a piece of text appears in the browser window, we change the CSS font-size property. This works but the spacing above and below the text as it appears in the browser is not easy to determine. The space below the baseline and above the text is unpredictable and changes from font to font even with fixed font size.

In summary, if you set a font size of 16px or even 1em, you will not know predictably what the height of any displayed character will be. This is quite different to, for example, how img or other block elements are defined — we can reliably and predictably set height for these. Capsize lets us define a cap height to adjust the size of rendered characters. Further, it gives us parameters needed to trim a snug bounding box. This is fantastic for creating predictable text layouts.

🧱 Svelte Capsize Styling: Building

Svelte Capsize Styling: Typography Tooling: Screenshot shows two charts side by side. Each chart is a mockup os an optometrist test chart with rows of letters growing smaller with each row.  The left chart shows output with capsize applied, while the right, shows without. The capsize letters are snug to the background but the without capsize letters have noticeable gaps above and below letters.

In the image above we can see Capsize makes it possible to generate snug text elements. Capsize does this by generating a styles object similar to those created by CSS in JS frameworks. To sveltify these styles objects, we will use Svelte actions and custom properties. Interested to hear if you have another method of attack! Drop a comment below if you do.

Rather than build out an app, in this post we will look at how to set up Capsize and then see a way to apply the generated styles in Svelte. Although we won’t build out an app, there is a full demo on the Rodney Lab GitHub page if you want a closer look at how the implementation works. We will look at SvelteKit code here, though the code should work in Astro or Slinkity.

⚙️ Capsize Setup

The only setup for Capsize is to install a couple of packages:

pnpm add -D @capsizecss/core @capsizecss/metrics
Enter fullscreen mode Exit fullscreen mode

📏 Font Metrics

To get a better understanding of Capsize inputs and outputs, here is a screenshot from a tool on the Capsize site. It shows metrics for the Courier New font which we use in the demo code.

Svelte Capsize Styling: Typography Tooling: Capsize Metrics: diagram shows Courier font letters Hg. Cap Height matches the height of uppercase H.

The Em square is a fundamental unit which originates from printing. It is a square with sides set equal to the width of an uppercase ‘M’ printed using the typeface. For us, it represents the space the element takes up on screen and is used to set font size.

We can specify font size or cap height as a Capsize input. Lets say we need tighter control over the height of each row in our comparison chart (above). Setting cap height gives us the control and predictability we a looking for; if we want capital letters to be 48px high, we can easily have that by setting cap height to that value. If we were replicating a figma or penpot design with font heights set we might instead opt for setting font size.

🚤 Using Capsize in Svelte

Let’s start by seeing what Capsize creates for us. The second row of the With Capsize / Wihtout Capsize comparison charts has cap height set to 48px. We’ll use that as a concrete value to examine Capsize inputs and outputs. Since the metrics vary from font to font, we need to specify our font as an input. Here’s the Svelte code for the 48px cap height example:

<script>
  import { createStyleObject } from '@capsizecss/core';
  import courierPrimeMetrics from '@capsizecss/metrics/courierPrime';

  const styles = createStyleObject({
    capHeight: 48,
    lineGap: 24,
    fontMetrics: courierPrimeMetrics,
  });
</script>
Enter fullscreen mode Exit fullscreen mode

We import Courier Prime font metrics from @capsizecss/metrics/courierPrime in line 3. These are then used to generate the styles (lines 59). Let’s take a peek at these metrics:

{
  familyName: "Courier Prime",
  capHeight: 1187,
  ascent: 1600,
  descent: -700,
  lineGap: 0,
  unitsPerEm: 2048,
  xHeight: 924
}
Enter fullscreen mode Exit fullscreen mode

These match the data from the screenshot of the tool on the Capsize size above.
The first question you will ask is probably how these numbers relate to the font size. For our precise use case we want to set cap height so we know how high our upper case letters in the chart will be. The unitsPerEm field in the metrics is just the height of the em square. So the ratio of em square height to cap height is 2048 / 1187 = 1.725. This means if we set capHeight to 48px when using Capsize, we want a 48px * 1.725 = 82.8px font for the without Capsize comparison.

You can examine the precise font measurements using FontForge.

Svelte Capsize Styling: Typography Tooling: FontForge Courier Prime Font: Screenshot shows a large letter E with various dimensions marked on the FontForge window

It also provides another route to the metrics above.

Svelte Capsize Styling: Typography Tooling: FontForge metrics: screenshot shows various measurements for a letter in FontForge.

We won’t go into more detail on metrics here, though there is an excellent Deep dive on font metrics by Vincent De Oliveira. Google Fonts also provide an alternative explanation of em square.

In the rest of the article we focus on the Svelte implementation.

💄 Styles object

The styles object generated for our example looks like this:

{
  fontSize: "82.8172px",
  lineHeight: "72px",
  "::before": {
    content: "''",
    marginBottom: "-0.0748em",
    display: "table"
  },
  "::after": {
    content: "''",
    marginTop: "-0.215em",
    display: "table"
  }
}
Enter fullscreen mode Exit fullscreen mode

You will notice we have ::before and ::after pseudo elements in there. Svelte has style directives (we will see an example shortly) which can set styles for an element itself, but not for pseudo elements. Using Svelte actions, it is possible to set custom CSS properties (often also called CSS variables). Our plan of attack is to use Svelte actions to define all the CSS values we need (pseudo and actual element). We will define these on a parent wrapper element, so that they are in scope on the actual lines we want to use them in. So markup is roughly something like this:

<span class="container">
  <div class="line with-capsize-line"><slot /></div>
</span>
Enter fullscreen mode Exit fullscreen mode

with scoped CSS

  .with-capsize-line { 
    font-size: var(--font-size);
    line-height: var(--line-height);
  }

  .with-capsize-line::before {
    content: var(--before-content);
    margin-bottom: var(--before-margin-bottom);
    display: var(--before-display);
  }

  .with-capsize-line::after {
    content: var(--after-content);
    margin-top: var(--after-margin-top);
    display: var(--after-display);
  }
Enter fullscreen mode Exit fullscreen mode

🎬 Styles Action

Svelte lets you define CSS custom properties on an element. It takes care of making them available within CSS for you. With that helping hand, effectively we need to transform the container span above into something like:

<span --font-size={X} --line-height={Y} --before-content={Z} class="container">
  <div class="line with-capsize-line"><slot /></div>
</span>
Enter fullscreen mode Exit fullscreen mode

That is what we will use Svelte actions for. Svelte actions have the node they are applied to as the first parameter. There can also be a second parameter which takes the value we set on the node. We will use that to pass the styles object in. Then the action’s job is to split the styles object up into custom properties we want added to the node. Here is the code:

/**
 * @param {HTMLElement} node - action element
 * @param {{fontSize: string; lineHeight?: string; '::before'?: {content?: string; marginBottom?: string; display?: string}; '::after'?: {content?: string; marginTop?: string; display?: string}}} styles - element styles
 */
function setCustomProperties(node, styles) {
  const { fontSize, lineHeight, '::before': before, '::after': after } = styles;

  fontSize && node.style.setProperty('--font-size', fontSize);
  lineHeight && node.style.setProperty('--line-height', lineHeight);

  if (before) {
    const { content, marginBottom, display } = before;
    content && node.style.setProperty('--before-content', content);
    marginBottom && node.style.setProperty('--before-margin-bottom', marginBottom);
    display && node.style.setProperty('--before-display', display);
  }
  if (after) {
    const { content, marginTop, display } = after;
    content && node.style.setProperty('--after-content', content);
    marginTop && node.style.setProperty('--after-margin-top', marginTop);
    display && node.style.setProperty('--after-display', display);
  }
}

/**
 * @param {HTMLElement} node - action element
 * @param {{fontSize: string; lineHeight: string; '::before': {content: string; marginBottom: string; display: string}; '::after': {content: string; marginTop: string; display: string}}} styles - element styles object
 * @returns{{update: (styles: {fontSize: string; lineHeight?: string; '::before'?: {content?: string; marginBottom?: string; display?: string}; '::after'?: {content?: string; marginTop?: string; display?: string}}) => void}} - 
 */
function styles(node, styles) {
  setCustomProperties(node, styles);
  return {
    update(styles) {
      setCustomProperties(node, styles);
    },
  };
}

export default styles;
Enter fullscreen mode Exit fullscreen mode

🧩 Using the Action: Line Component

The structure of the app is that we create each of the two charts using a Chart Svelte component. Then each line in a chart is a line Svelte component. The line components are where we use Capsize. Here is a possible implementation of the line component, using the new action:

<script>
  import { createStyleObject } from '@capsizecss/core';
  import courierPrimeMetrics from '@capsizecss/metrics/courierPrime';
  import capsizeStyles from '$lib/shared/actions/styles';

  /** @type {number} */
  export let capHeight;

  const styles = createStyleObject({
    capHeight,
    lineGap: 24,
    fontMetrics: courierPrimeMetrics,
  });

</script>

<span use:capsizeStyles={styles} class="container">
  <div class="line with-capsize-line"><slot /></div>
</span>
Enter fullscreen mode Exit fullscreen mode

You see how we use the action in code in line 17.

💄 Chart Component Styling: Svelte Style Props

As an aside the two charts have different background colours. In the code where we consume them, we can set the CSS custom property for the background right on the element:

<!-- ...TRUNCATED> -->
<Chart --background-colour="hsl(340deg 70% 70%)" withCapsize={false} />
Enter fullscreen mode Exit fullscreen mode

Then in the chart component itself, we use this in the CSS

<style>
  .container {
    padding: var(--spacing-12) var(--spacing-4);
    background-color: var(--background-colour);
    width: var(--max-width-full);
    height: 100%;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

These are called style props. You can read more on style props in the Svelte docs.

⏱ Final Line Component

For completeness, here is the final Line component code:

<script>
  import { createStyleObject } from '@capsizecss/core';
  import courierPrimeMetrics from '@capsizecss/metrics/courierPrime';
  import capsizeStyles from '$lib/shared/actions/styles';

  /** @type {number} */
  export let capHeight;

  /** @type {boolean} */
  export let withCapsize;

  const styles = createStyleObject({
    capHeight,
    lineGap: 24,
    fontMetrics: courierPrimeMetrics,
  });

  const { capHeight: capHeightEm, unitsPerEm } = courierPrimeMetrics;
  const fontSizeToCapHeight = unitsPerEm / capHeightEm;
  const fontSize = `${capHeight * fontSizeToCapHeight}px`;
</script>

{#if withCapsize}
  <span use:capsizeStyles={styles} class="container">
    <div class="line with-capsize-line"><slot /></div>
  </span>
{:else}
  <span class="container">
    <div style:font-size={fontSize} class="line">
      <slot />
    </div>
  </span>
{/if}

<style>
  .line {
    background-color: var(--colour-light);
    color: var(--colour-dark);
    margin: var(--spacing-4) auto;
    font-weight: var(--font-weight-bold);
    font-size: var(--font-size);
    text-align: center;
    width: fit-content;
    line-height: calc(1 / 1.2);
  }

  .with-capsize-line {
    line-height: var(--line-height);
  }

  .with-capsize-line::before {
    content: var(--before-content);
    margin-bottom: var(--before-margin-bottom);
    display: var(--before-display);
  }

  .with-capsize-line::after {
    content: var(--after-content);
    margin-top: var(--after-margin-top);
    display: var(--after-display);
  }
</style>
Enter fullscreen mode Exit fullscreen mode

The promised style directive example comes in line 29. We set font-size on the div. We use a style directive here because we have a dynamic property. This value overrides the one set in the style block.

🙌🏽 Svelte Capsize Styling: Wrapup

In this post we looked at:

  • how to use Capsize with Svelte for more predictable text layouts,
  • adding CSS custom properties to a Svelte element using Svelte actions,
  • how you can add style props to Svelte components, setting CSS custom properties (CSS variables).

I do hope there is at least one thing in this article which you can use in your work or a side project. Also let me know if you feel more explanation of the config is needed.

You can see the full SvelteKit code for this project on the Rodney Lab Git Hub repo. If you run into any issues, you can drop a comment below as well as reach out for a chat on Element. Also Twitter @mention if you have suggestions for improvements or questions.

🙏🏽 Feedback

If you have found this video useful, see links below for further related content on this site. I do hope you learned one new thing from the video. Let me know if there are any ways I can improve on it. I hope you will use the code or starter in your own projects. Be sure to share your work on Twitter, giving me a mention so I can see what you did. Finally be sure to let me know ideas for other short videos you would like to see. Read on to find ways to get in touch, further below. If you have found this post useful, even though you can only afford even a tiny contribution, please consider supporting me through Buy me a Coffee.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on Twitter and also askRodney on Telegram. Also, see further ways to get in touch with Rodney Lab. I post regularly on SvelteKit as well as Astro among other topics. Also subscribe to the newsletter to keep up-to-date with our latest projects.

Top comments (0)