Future of styling #2265
Replies: 8 comments
-
We've been converting our Button family of components to use a style function from a style functionimport {cs, cssVar, createVars, createModifiers, CSProps} from '@workday/canvas-kit-styling';
// component variables
const buttonVars = createVars('backgroundColor');
// main styles
export const buttonStyles = cs({
backgroundColor: cssVar(buttonVars.backgroundColor),
});
// modifiers - like if/switch statements inside Emotion style blocks
export const buttonModifiers = createModifiers({
size: {
small: cs({
padding: 8,
}),
medium: cs({
padding: 12,
}),
},
});
export interface ButtonProps extends CSProps, React.ButtonHTMLAttributes<HTMLButtonElement> {
size: 'small' | 'medium'; // or "keyof typeof buttonModifiers.size",
backgroundColor: string;
}
export const Button = ({cs, size, backgroundColor, ...elemProps}: ButtonProps) => {
return (
<Box
as="button"
{...elemProps}
cs={[cs, buttonStyles, buttonModifiers({size}), buttonVars({backgroundColor})]}
/>
);
};
|
Beta Was this translation helpful? Give feedback.
-
This API looks similar to Vanilla Extract. Why not use Vanilla Extract? This API is definitely inspired by Vanilla Extract, but Vanilla Extract has an issue: You must use a bundler plugin and so do your dependents. This means if Canvas Kit adopted Vanilla Extract, everyone using Canvas Kit would have to modify their webpack, rollup, vite, etc bundlers for styling to work. This seemed like too much of a lift. This solution makes tradeoffs:
Cons:
|
Beta Was this translation helpful? Give feedback.
-
Love this, just a few thoughts as I was reading through it: I like the ease of the getter and setter on myVars, but would a Map maybe work more intuitively?
vs
Not sure if that complicates your fancy typescript things, I know Map and Set leave something to be desired in TS land The cs function and the cs prop being different things threw me off a bit, maybe the function should be The getter for I found this a little confusing comparing to myVars where the function was a setter. Again perhaps Map notion, not sure 100%
Big fan of cs prop Curious what the use case is for the static parser, feels like a powerful pattern, but curious what the driver is. |
Beta Was this translation helpful? Give feedback.
-
The variables are not really designed as a getter/setter pattern. The getter accessor is for creating selectors that override CSS variables. You have 2 ways of overriding CSS variables:
The API is meant to serve these purposes. For a style: const myStyles = cs({
[myVars.color]: 'red'
}) In a component: <Box
cs={[
myVars({color: 'red'})
]} /> In the component, it isn't setting any internal state, the function is returning a object:
We've thrown around different ideas for the style function:
We also threw around prop names:
I have no opposition to a different function name for creating styles. I think
For example, here's how you could do it without using the modifier utility function: const myModifiers = {
size: {
small: cs({}),
medium: cs({}),
},
};
const MyComponent = ({size}: {size: 'small' | 'medium'}) => {
return (
<Box
cs={[
size === 'small'
? myModifiers.size.small
: size === 'medium'
? myModifiers.size.medium
: '',
]}
/>
);
}; Using the modifier function is more straightforward: const myModifiers = createModifiers({
size: {
small: cs({}),
medium: cs({}),
},
});
const MyComponent = ({size}: {size: 'small' | 'medium'}) => {
return (
<Box
cs={[
myModifiers({size})
]}
/>
);
}; This is why I say the It also returns the CSS class name chosen in case you want to use it for something else: import {buttonStyles, buttonModifiers} from './button';
const smallButtonStyles = cs(buttonStyles, buttonModifiers.size.small)
const SmallButton = (props) => {
return <button className={smallButtonStyles}>Small Button</button>
}
Without the static parser, the implementation of import {css} from '@emotion/css';
const myStyles = css({
backgroundColor: 'blue'
}) The static parser will rewrite the AST if you use the // before
const myStyles = cs({
backgroundColor: 'blue'
})
// after
const myStyles = cs({name: '{hash}', styles: 'background-color: blue;'}) This doesn't look like much now, but with a lot of styles and use of Here's a real-world transform: const baseButtonStyles = cs({ name: "1pbwsel", styles: "font-family:\"Roboto\", \"Helvetica Neue\", \"Helvetica\", Arial, sans-serif;font-size:0.875rem;line-height:1.25rem;letter-spacing:0.015rem;font-weight:bold;background-color:var(--css-button-vars-default-background);color:var(--css-button-vars-default-color);border:1px solid transparent;border-color:var(--css-button-vars-default-border);max-width:min-content;min-width:96px;cursor:pointer;display:inline-flex;gap:0.5rem;align-items:center;justify-content:center;box-sizing:border-box;box-shadow:none;outline:2px transparent;padding-inline:1.5rem;white-space:nowrap;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;border-radius:var(--css-button-vars-default-borderRadius);position:relative;vertical-align:middle;overflow:hidden;transition:box-shadow 120ms linear, border 120ms linear, background-color 120ms linear, color 120ms linear;&:disabled, &:disabled:active{cursor:default;box-shadow:none;opacity:0.4px;}&:focus-visible, &.focus{background-color:var(--css-button-vars-focus-background);border-color:var(--css-button-vars-focus-border);box-shadow:var(--css-button-vars-focus-boxShadowInner) 0px 0px 0px 2px, var(--css-button-vars-focus-boxShadowOuter) 0px 0px 0px 4px;color:var(--css-button-vars-focus-color);& span .wd-icon-fill{fill:var(--css-button-vars-focus-icon);}}& span .wd-icon-fill{transition-duration:40ms;fill:var(--css-button-vars-default-icon);}&:hover, &.hover{background-color:var(--css-button-vars-hover-background);border-color:var(--css-button-vars-hover-border);color:var(--css-button-vars-hover-color);& span .wd-icon-fill{fill:var(--css-button-vars-hover-icon);}}&:hover:active{transition-duration:40ms;}&:active, &.active{background-color:var(--css-button-vars-active-background);border-color:var(--css-button-vars-active-border);color:var(--css-button-vars-active-color);& span .wd-icon-fill{fill:var(--css-button-vars-active-icon);}}&:disabled, &:active:disabled, &:focus:disabled, &:hover:disabled{background-color:var(--css-button-vars-disabled-background);border-color:var(--css-button-vars-disabled-border);color:var(--css-button-vars-disabled-color);& span .wd-icon-fill{fill:var(--css-button-vars-disabled-icon);}}"}) The |
Beta Was this translation helpful? Give feedback.
-
This looks great - perf gains 🎉 . I also like the |
Beta Was this translation helpful? Give feedback.
-
Thanks for the clarification @NicholasBoll, the examples helped a lot. FWIIW i like |
Beta Was this translation helpful? Give feedback.
-
Why?tl;dr; Emotion's dynamic runtime has a real cost. With CSS Variables, we no longer have to make Workday's users pay that cost. There's many articles lately about companies moving away from CSS-in-JS or at least a heavy CSS-in-JS runtime. Emotion React/Styled is such heavy CSS-in-JS solution. And by "heavy" I mean the overhead of style serialization and hashing per render. We no longer have to support IE11 which means we now have access to CSS Custom Properties aka CSS Variables. This means we can support dynamic styles without the cost of Emotion's runtime system. We also want to maintain our CSS packages without the additional work of maintaining a separate package set. With static CSS extraction, we can extract CSS from our React packages without the extra cost of maintaining CSS separately. What's Next?tl;dr; We're going to use @emotion/css and some utility functions instead. This shouldn't change the way you style your application or Canvas Kit components. You can still use the css prop or the styled API. You can also use className, our styling solution, or any other styling solution. We looked into other solutions like Panda CSS, Linaria, Vanilla Extract, Compiled. None really fit the bill, so we created our own. That sounds crazy, but we really have a ~100 line wrapper around @emotion/css which has an API similar to Vanilla Extract + Recipes. In Canvas Kit v10, we're updating the Button family of components to use this system. Button is usually the most used component of any design system and is also the component with the most edge cases - it is the perfect component to test the capability of a styling solution. Our goal is to style our components in a way that has the least negative impact on end users and allows for easy and intuitive style overrides for our developer users. Furthermore, the way we've designed our style system, nothing is needed on your part to use Canvas Kit. Vanilla Extract would have required your bundling system to have a special build process for Canvas Kit. Using Canvas Kit still requires no additional setup on your end. We have the added bonus of running a build-time optimizer that erases as much of the runtime cost of @emotion/css as possible by pre-processing serialization and hashing. This reduces both bytes and JavaScript engine parsing time because CSS objects are converted to strings. This also requires nothing additional on your part. Do I have to use Canvas Kit's styling solution?tl;dr; No. You can continue styling Canvas Kit and your own components however you did before. How? We have to make sure all our components use styling that includes a (0,1,0) specificity (single class name). Since Emotion works by creating a single class name specificity CSS selector and injects into the document's style sheet last, your styles will win. Instead of Emotion merging all style rules into a single class name, there will be more than one class name and the browser's CSS engine will merge using specificity rules. In practice, this should be the same as before. In reality, there might be edge cases where CSS property merging doesn't match JavaScript property merging. We'll do our best to make property merging as intuitive and predictable as possible. Can I use the CKv10 styling solution myself?tl;dr; Maybe. It will be released as @workday/canvas-kit-styling in v10+. Yes if you don't mix and match @emotion/react or @emotion/styled for defining the same CSS properties. @emotion/css is a little different than @emotion/react or @emotion/styled in when a style is injected into a document's stylesheet list. @emotion/css will inject styles when called while the others will inject when a component is first rendered to the page. This means, if you import a component file, the styles required by that component are already injected into the page. If you use our styling solution, which uses @emotion/css, your styles will be injected before any @emotion/react or @emotion/styled styles are injected into the page. This means there may be an unpredictable style merge order. @emotion/react and @emotion/styled will always be injected last meaning any property defined using the React-specific solutions will always win. For example, if you use our styling solution to set padding, then use our padding style prop (which uses @emotion/styled), the padding style prop will always win. So Yes, you can use our styling solution, but any use of the @emotion/react or @emotion/styled will always override unless your selector has a higher specificity than a single class. What about style props?tl;dr; Style props are supported in v10, but are being deprecated and are planned for removal in the future. When depends on use in the wild and codemod complexity. It is possible style props are supported in the future using atomic styling. It depends on how much people like style props and the runtime cost of supporting them. If we use atomic styles of our tokens only, the runtime cost in terms of CPU processing time will be minimal, but there's still the issue of bundle size. Vanilla Extract's Sprinkles can define thousands of token rewrites because the entire system disappears at build time. Any system with a runtime would require an increased bundle size to support token remapping. We're already paying the cost in bytes by using CSS Variables both in CSS files and JS files. Adding token remapping would be a 3rd thing that would take extra bytes to send over the wire, so we'll have to weigh that. |
Beta Was this translation helpful? Give feedback.
-
The greatest cost to using Emotion is dynamic styling. When Emotion takes a rule block (collection of styles), it will serialize every render and create a hash. If this hash is not in cache, Emotion will inject the style into the StyleSheetList. Injecting a StyleSheet causes the browser to throw away all Style recalc and layout caches and start from scratch. While any change to a property/attribute that effects styling causes a Style Recalc, injecting a style sheet is a fresh style recalc. On a moderately complex web application like Workday, this could mean over 100ms. Normally a cached style recalc takes less than 1ms. Don't use dynamic style properties that can change during the lifecycle of your components. The cost is linear to the amount of elements and linear to the amount of CSS selectors. Combined, the cost is multiplicative to both selector count and element count. The less extreme performance issue of Emotion is simply running serialization every render. This is a linear cost of the style properties per component using Emotion styles. A lot of CSS properties in a component means more CPU time spent in JavaScript converting every CSS property to a CSS string. Emotion uses stylis and a number of So the benefit of our solution is removing serialization and hashing per render and instead pay that cost once upfront during a page's initialization process when style recalcs are being batched rather than many expensive style recalcs as the page is rendering. Essentially batching all expensive style recalcs into a single expensive style recalc that is only as big as the most expensive style recalc. The optimizer can be part of your production build-process to remove serialization and hashing from the runtime entirely. The future benefit will be to remove all style processing by moving Bottom line, using |
Beta Was this translation helpful? Give feedback.
-
Workday has been using Emotion CSS for a long time. It solved an important problem - many teams needing to style components without style collision. Workday is a single page application with many different teams loading components into the page depending on user navigation. A common problem was style collisions only when certain pages were loaded and unloaded, which made for difficult to track down bug reports.
From my testing, Emotion doesn't unload styles, but it does use unique class names for CSS selectors based on a hash of the style being applied. Emotion also avoids most specificity issues by merging styles into a single class name.
For example:
This doesn't always work, especially when pseudo selectors are used or more complex selectors.
Some teams use the
styled
API while others use thecss
prop. Some teams use object styles while others use string templates. Emotion is no doubt a powerful and popular solution. But it does come at a cost. There is overhead to converting style objects to strings and hashing those strings. The worst performance issue comes at how Emotion injects styles during runtime which can cause expensive style recalculations. The browser typically caches and optimizes selectors and tracks DOM changes to know what elements to recalculate styles for. When Emotion injects a new stylesheet into the browser, that cache is invalid and the browser has to start from scratch. This can be a difference of 1ms style recalculations to 80ms style recalculations on a moderately complex page (2,000 elements and 5,000 selector rules).80ms might not be much if the component is rendered the first time and reused many times. Emotion only injects a stylesheet if the style hash is unique and hasn't been cached yet. If a
PrimaryButton
component is used without any style modification, only the first render will inject a new style. The real performance problem comes when developers use the dynamic nature of Emotion for unique properties liketop
ortransform
properties based on the mouse position (drag and drop) where the difference between 1ms and 80ms style recalculations can result in 5ms frames/second drag animations which look janky.More is covered in an article by a top Emotion contributor: Why we're breaking up with CSS-in-JS.
So what's next?
Workday recently dropped support for IE11 which opens the door for newer CSS features, most notably CSS Variables (officially named "CSS Custom Properties"). With Emotion having a rocky future, we're looking to eventually transition off Emotion entirely.
There are a few features we like about Emotion. Here's a list of features we want in a CSS-in-JS solution:
We've evaluated some CSS-in-JS solutions with little or no runtime including Linaria, Compiled, Panda CSS and Vanilla Extract. Ultimately, none of those solutions solved all that we wanted. Ultimately, we want to incrementally move away from Emotion and provide support for other teams using Canvas Kit to do so as well.
This leads us to a custom solution using
@emotion/css
at runtime. During development,@emotion/css
is used to get styles into the document. In the future, we can use document.adoptedStyleSheets and a babel plugin to remove Emotion entirely.Beta Was this translation helpful? Give feedback.
All reactions