Workday Canvas

Merging Styles

handleCsProp

But what about when using components that use @emotion/react or @emotion/styled? Those libraries use a different approach. Instead of multiple class names, they use a single, merged class name.

handleCsProp was created to handle integration with existing components that use the css prop from @emotion/react or the styled components from @emotion/styled. If a class name from one of those libraries is detected, style merging will follow the same rules as those libraries. Instead of multiple class names, a single class name with all matching properties is created. The handleCsProp also takes care of merging style props, className props, and can handle the cs prop:

const myStencil = createStencil({ // ... }); const MyComponent = elemProps => { return <div {...handleProps(elemProps, myStencil({}))} />; }; // All props will be merged for you <MyComponent style={{color: 'red'}} className="my-classname" cs={{position: 'relative'}} />;

handleCsProp will make sure the style prop is passed to the div and that the my-classname CSS class name appears on the div’s class list. Also the cs prop will add the appropriate styles to the element via a CSS class name. If your component needs to handle being passed a className, style, or cs prop, use handleCsProp.

handleCsProp API

This function handles the cs prop for you, as well as local styles you want to define. It will force style merging with Emotion's runtime APIs, including styled components and the css prop.

Runtime style merging works by forcing Emotion's styling merging if use of runtime APIs have been detected. If only createStyles were used to style a component, the faster non-runtime styling will be used.

You can use handleCsProp if you wish to use on your own components and want your components to be compatible with Emotion's runtime styling APIs.

import {createStyles, handleCsProp, CSProps} from '@workday/canvas-kit-styling';

interface MyComponentProps extends CSProps {
  // other props
}

const myStyles = createStyles({
  background: 'green',
  height: 40,
  width: 40
})

const MyComponent = ({children, ...elemProps}: MyComponentProps) => {
  return (
    <div
      {...handleCsProp(elemProps, [myStyles])}
    >
      {children}
    </div>
  )
}

const StyledMyComponent(MyComponent)({
  background: 'red'
})

const myOverridingStyles = createStyles({
  background: 'blue'
})

// now everything works. Without `handleCsProp`, the last component would be a red box
export default () => (
  <>
    <MyComponent>Green box</MyComponent>
    <StyledMyComponent>Red box</StyledMyComponent>
    <StyledMyComponent cs={myOverridingStyles}>Blue box</StyledMyComponent>
  </>
)
<T extends & {
  className:  string undefined;
  style:  < string number> undefined;
}>(
  elemProps: T,
  localCs: 
) => <T, 'cs'>

mergeStyles
Deprecated

In v9, we used @emotion/styled or @emotion/react for all styling which is a runtime styling solution. Starting in v10, we’re migrating our styling to a more static solution using createStyles and the cs prop.

For a transition period, we’re opting for backwards compatibility. If style props are present, styled components are used, or the css prop is used in a component, Emotion’s style merging will be invoked to make sure the following style precedence:

createStyles > CSS Prop > Styled Component > Style props

This will mean that any css prop or use of styled within the component tree per element will cause style class merging. For example:

import styled from '@emotion/styled'; import {createStyles} from '@workday/canvas-kit-styling'; import {mergeStyles} from '@workday/canvas-kit-react/layout'; const styles1 = createStyles({ padding: 4, }); const styles2 = createStyles({ padding: 12, }); const Component1 = props => { return <div {...mergeStyles(props, [styles1])} />; }; const Component2 = props => { return <Component1 cs={styles2} />; }; const Component3 = styled(Component1)({ padding: 8, }); const Component4 = props => { return <Component3 cs={styles2} />; }; export default () => ( <> <Component1 /> <Component2 /> <Component3 /> <Component4 /> </> );

The styled component API is forcing mergeStyles to go into Emotion merge mode, which removes the style1 class name and creates a new class based on all the merged style properties. So .component3 is a new class created by Emotion at render time that merges .style1 and {padding: 8px}. Component4 renders Component3 with a cs prop, but Component3 is already in merge mode and so Component4 will also merge all styles into a new class name of .component4 that has the styles from .style1, .component3, and {padding: 12px}:

<head> <style> .styles1 { padding: 4px; } .styles2 { padding: 8px; } .component3 { padding: 4px; padding: 8px; } .component4 { padding: 4px; padding: 8px; padding: 12px; } </style> </head> <div class="styles1"></div> <div class="styles1 styles2"></div> <div class="component3"></div> <div class="component4"></div>

The css prop and styled component APIs will rewrite the className React prop by iterating over all class names and seeing if any exist within the cache. If a class name does exist in the cache, the CSS properties are copied to a new style property map until all the class names are evaluated and removed from the className prop. Emotion will then combine all the CSS properties and inject a new StyleSheet with a new class name and add that class name to the element.

The following example shows this style merging.

Buttons

Legend:

createStyles
CSS Prop
Styled Component
Style Props

Style Precedence: createStyles > CSS Props > Styled Component > Style Props

import * as React from 'react';
import styled from '@emotion/styled';
import {jsx} from '@emotion/react';

import {Flex} from '@workday/canvas-kit-react/layout';
import {PrimaryButton} from '@workday/canvas-kit-react/button';
import {base} from '@workday/canvas-tokens-web';
import {createStyles, cssVar} from '@workday/canvas-kit-styling';

const backgroundColors = {
  cssProp: cssVar(base.orange500),
  styledComponent: cssVar(base.green500),
  styleProps: cssVar(base.magenta500),
  createStyles: cssVar(base.purple500),
};

const StyledPrimaryButton = styled(PrimaryButton)({
  backgroundColor: backgroundColors.styledComponent,
});

const styles = createStyles({
  backgroundColor: backgroundColors.createStyles,
});

const CSSProp = () => (
  <div
    style={{
      color: 'white',
      padding: '0 4px',
      height: 40,
      width: 100,
      backgroundColor: backgroundColors.cssProp,
    }}
  >
    CSS Prop
  </div>
);
const StyledComponent = () => (
  <div
    style={{
      color: 'white',
      padding: '0 4px',
      height: 40,
      width: 100,
      backgroundColor: backgroundColors.styledComponent,
    }}
  >
    Styled Component
  </div>
);
const CreateStyles = () => (
  <div
    style={{
      color: 'white',
      padding: '0 4px',
      height: 40,
      width: 100,
      backgroundColor: backgroundColors.createStyles,
    }}
  >
    createStyles
  </div>
);
const StyleProps = () => (
  <div
    style={{
      color: 'white',
      padding: '0 4px',
      height: 40,
      width: 100,
      backgroundColor: backgroundColors.styleProps,
    }}
  >
    Style Props
  </div>
);

// We use this object and cast to `{}` to keep TypeScript happy. Emotion extends the JSX interface
// to include the `css` prop, but the `jsx` function type doesn't accept the `css` prop. Casting to
// an empty object keeps TypeScript happy and the `css` prop is valid at runtime.
const cssProp = {css: {backgroundColor: backgroundColors.cssProp}} as {};

export default () => {
  return (
    <Flex flexDirection="column" minHeight="100vh" gap="s">
      <Flex flexDirection="column" gap="s">
        <h2>Buttons</h2>
        <Flex flexDirection="row" gap="s">
          <PrimaryButton cs={styles}>createStyles</PrimaryButton>
          {jsx(PrimaryButton, {...cssProp}, 'CSS Prop')}
          <StyledPrimaryButton>Styled Component</StyledPrimaryButton>
          <PrimaryButton backgroundColor={backgroundColors.styleProps}>Style Props</PrimaryButton>
        </Flex>
        <div>
          {jsx(
            PrimaryButton,
            {
              ...cssProp,
              cs: styles,
            },
            'createStyles + CSS Prop'
          )}
        </div>
        <div>
          <StyledPrimaryButton cs={styles}>createStyles + Styled Component</StyledPrimaryButton>
        </div>
        <div>
          <PrimaryButton cs={styles} backgroundColor={backgroundColors.styleProps}>
            createStyles + Style Props
          </PrimaryButton>
        </div>
        <div>
          <StyledPrimaryButton backgroundColor={backgroundColors.styleProps} cs={styles}>
            createStyles + Styled Component + Style Props
          </StyledPrimaryButton>
        </div>
        <div>
          {jsx(
            StyledPrimaryButton,
            {
              ...cssProp,
              backgroundColor: backgroundColors.styleProps,
              cs: styles,
            },
            'createStyles + CSS Prop + Styled Component + Style Props'
          )}
        </div>
        <div>{jsx(StyledPrimaryButton, {...cssProp}, 'CSS Prop + Styled Component')}</div>
        <div>
          {jsx(
            PrimaryButton,
            {
              ...cssProp,
              backgroundColor: backgroundColors.styleProps,
            },
            'CSS Prop + Style Props'
          )}
        </div>
        <div>
          <StyledPrimaryButton backgroundColor={backgroundColors.styleProps}>
            Styled Component + Style Props
          </StyledPrimaryButton>
        </div>
      </Flex>
      <div>
        <p>Legend:</p>
        <CreateStyles />
        <CSSProp />
        <StyledComponent />
        <StyleProps />
      </div>
      <p>
        Style Precedence: <strong>createStyles</strong> &gt; <strong>CSS Props</strong> &gt;{' '}
        <strong>Styled Component</strong> &gt; <strong>Style Props</strong>
      </p>
    </Flex>
  );
};

CSS style property merging works by CSS specificity. If two matching selectors have the same specificity, the last defined property wins. Stencils take advantage of this by making all styles have the same specificity of 0-1-0 and inserting base styles, then modifiers (in order), then compound modifiers (in order). This means if a property was defined in base, a modifier, and a compound modifier, the compound modifier would win because it is the last defined. This should be the expected order.

Caution

While we support mergeStyles we’d advise against using this in your components so that users can get the performance benefit of static styling using utilities like createStyles and createStencil in tandem with the cs prop.

mergeStyles API

This function has the same signature as and also calls handleCsProps, but adds support for style props. It can be used as a drop-in replacement for handleCsProps.

<T extends {}>(
  allProps: T,
  localCs: 
) => <T, 'cs' keyof >

Can't Find What You Need?

Check out our FAQ section which may help you find the information you're looking for. For further information, contact the #ask-canvas-design or #ask-canvas-kitchannels on Slack.