Skip to content

How to: T shirt sizing in Spectrum CSS

Larry Davis edited this page Dec 1, 2020 · 3 revisions

What is t-shirt sizing and how does that relate to scales?

In Spectrum, we have two sizing notions:

  • Scale - This is the overall size of all components on the page, it's either medium for desktop, or large for touch.
  • T-shirt size - This is the size of a specific component, basically a variant/modifier that resizes one instance of a component only. A component whose size is set with t-shirt sizing is still affected by scale.

History: how do we implement modifier classes today?

Traditionally, in CSS, we define modifier classes that change various properties of a given component. If we used this approach for t-shirt sizing, it would look something like this:

.spectrum-Button {
  /* ... */
}
.spectrum-Button--sizeM {
  height: var(--spectrum-button-primary-m-height);
  border-radius: var(--spectrum-button-primary-m-border-radius);
}
.spectrum-Button--sizeL {
  height: var(--spectrum-button-primary-l-height);
  border-radius: var(--spectrum-button-primary-l-border-radius);
}

As you can see, this would mean that we would have 5 separate modifier classes (XS, S, M, L, XL) that each re-define the same set of properties. Worse, we would have to maintain that set of properties if any of them were ever modified upstream in Spectrum Tokens; i.e. if suddenly Button's font-weight was adjusted by t-shirt sizing, we would have to manually add 5 font-weight property declarations or miss out on that change.

How do we implement t-shirt sizing?

Using the magic of CSS custom properties and a few PostCSS plugins, we can ensure that ALL possible modified tokens for t-shirt sizing are automatically reflected in the CSS, with no manual effort outside of defining the modifier classes one time. Here's how.

Spectrum Tokens provides all tokens for all t-shirt sizes of a given component, whether or not they change between sizes. Every variable is consistently named in this form:

--spectrum-COMPONENT-SIZE-VARIANT-PROPERTY-STATE

For example:

--spectrum-button-m-primary-height: var(--spectrum-alias-item-height-m);
--spectrum-button-l-primary-height: var(--spectrum-alias-item-height-l);

Now, instead of directly referencing --spectrum-button-m-primary-height in the code, we'll actually drop the size and simply reference --spectrum-button-primary-height, and we do this for all other variables as well:

.spectrum-Button {
  height: var(--spectrum-button-primary-height);
}

But wait, not only is --spectrum-button-primary-height undefined, but it's also meaningless -- it doesn't have a size associated with it. We actually define that variable on the modifier class for the t-shirt size:

.spectrum-Button--sizeM {
  --spectrum-button-primary-height: var(--spectrum-button-primary-m-height);
}
.spectrum-Button--sizeL {
  --spectrum-button-primary-height: var(--spectrum-button-primary-l-height);
}

As you can see, when you apply a t-shirt size class, the only thing that changes is the variables that drive the original class' properties. Instead of re-defining properties, we're re-defining variables! This means there's a single definition of the CSS height property for .spectrum-Button, not 5 (one for each t-shirt size).

That's nice, but how do we do this at scale?

That's where @remapvars comes in. Instead of manually writing out those .spectrum-Button--size* modifier classes, we generate them:

/* Make sure we have all the variables imported that we're going to remap */
@import "../vars/css/components/spectrum-button.css";

/* Define the modifier class just like before */
.spectrum-Button--sizeS {
  /* Define those sizeless custom properties with @remapvars */
  @remapvars {
    find: --spectrum-button-s-;
    replace: --spectrum-button-;
  }
}

/* etc for each size */
.spectrum-Button--sizeM {
  @remapvars {
    find: --spectrum-button-m-;
    replace: --spectrum-button-;
  }
}

This tells @remapvars to find all of the t-shirt sized variables for Button, and define a sizeless variable that references them. We're now automatically generating all of the variables we need to drive our component for each t-shirt size.

What if we need to override a variable?

There are two ways to override a variable:

Override for a single t-shirt size

Simply define the override under the call to @remapvars:

.spectrum-Button--sizeS {
  @remapvars {
    find: --spectrum-button-s-;
    replace: --spectrum-button-;
  }

  /* Fixup alignment of text on small */
  --spectrum-button-primary-text-padding-top: calc(var(--spectrum-button-primary-s-text-padding-top) - 3px);
}

Override for all sizes

In another rule with the base class name, reference each variable you want to override, and re-define it with -adjusted at the end. You cannot re-use the same variable name due to a limitation of the CSS custom properties specification!

.spectrum-Button {
  /* Adjustments for inset/outset padding in DNA */
  --spectrum-button-primary-padding-left-adjusted: calc(var(--spectrum-button-primary-padding-left) - var(--spectrum-button-primary-border-size));
  --spectrum-button-primary-padding-right-adjusted: calc(var(--spectrum-button-primary-padding-right) - var(--spectrum-button-primary-border-size));
  --spectrum-button-primary-textonly-padding-left-adjusted: calc(var(--spectrum-button-primary-textonly-padding-left) - var(--spectrum-button-primary-border-size));
  --spectrum-button-primary-textonly-padding-right-adjusted: calc(var(--spectrum-button-primary-textonly-padding-right) - var(--spectrum-button-primary-border-size));

  /* Adjust padding to make things look right */
  --spectrum-button-padding-y: calc(var(--spectrum-button-primary-text-padding-top) - 1px);
}

But there are like a thousand variables in there!

Right. Spectrum Tokens re-defines all possible variables, even color (which doesn't change between t-shirt sizes)! We really don't use all of those variables in our implementation, so we can start by filtering out color:

.spectrum-Button--sizeS {
  /* Define those sizeless custom properties with @remapvars */
  @remapvars {
    find: --spectrum-button-s-;
    filter: color; /* drop any variable that has color in it */
    replace: --spectrum-button-;
  }
}

There are still literally hundreds of unused variables in there that are now in the output. That's where postcss-dropunusedvars comes into play. Including this plugin analyzes all CSS custom property usage and drops definitions for variables that are never used. Neat.

However, you'll find your t-shirt size specific overrides are still defined twice! That's where postcss-dropdupedvars comes in, it removes all but the last definition of a given variable in a given rule, making the output as clean as possible.

Oh no, properties are out of order, how do I fix?

If property ordering is wrong, @remapvars supports regex search and replace with groups:

.spectrum-Button--sizeM {
  @remapvars {
    find: /--spectrum-button(.*)-m-/;
    filter: color;
    replace: --spectrum-button$1-;
  }
}

Ok, uh, can you just show me a complete example?

Of course! Check out the following components and imitate their patterns:

  • Field Label - simplest possible example, no regex
  • Link - includes a special case where the component is not t-shirt sized by default
  • Button - includes per t-shirt size overrides and overall overrides
  • Action Button - includes per t-shirt size overrides and overall overrides