-
Notifications
You must be signed in to change notification settings - Fork 195
How to: T shirt sizing in Spectrum CSS
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, orlarge
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.
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.
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 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.
There are two ways to override a variable:
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);
}
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);
}
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.
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-;
}
}
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