UNPKG

@workday/canvas-kit-docs

Version:

Documentation components of Canvas Kit components

401 lines (311 loc) • 14.6 kB
import {InformationHighlight} from '@workday/canvas-kit-preview-react/information-highlight'; import {Hyperlink} from '@workday/canvas-kit-react/button'; import {system} from '@workday/canvas-tokens-web'; # Canvas Kit Styling ## Introduction Canvas Kit styling is a custom CSS-in-JS solution that provides both a runtime for development and a static parsing process for build time. This system offers several key benefits: - TypeScript autocomplete for enhanced developer experience - Low runtime overhead for better performance - Static CSS compilation for optimized builds - Dynamic styling with CSS Variables for flexible design The motivation behind this custom styling solution stems from the need to move beyond IE11 support and implement performance improvements using static styling methods. For more details, refer to the [Why Canvas Kit Styling](https://workday.github.io/canvas-kit/?path=/docs/styling-why-canvas-styling--docs) section. ## Overview The Canvas Kit styling system consists of two main packages: - `@workday/canvas-kit-styling` - Core styling utilities for runtime use - `@workday/canvas-kit-styling-transform` - Build-time optimization tools These packages work together to provide a CSS-in-JS experience during development while enabling optimized static CSS in production. ## Installation ```sh yarn add @workday/canvas-kit-styling ``` ## Usage ```tsx import React from 'react'; import {createRoot} from 'react-dom/client'; import {createStyles} from '@workday/canvas-kit-styling'; const myStyles = createStyles({ backgroundColor: 'red', }); // returns the CSS class name created for this style myStyles; // something like "css-{hash}" const domNode = document.getElementById('root'); const root = createRoot(domNode); root.render(<div className={myStyles}>Hello!</div>); ``` ## Style Merging The `@workday/canvas-kit-styling` package uses `@emotion/css` to inject styles during JavaScript evaluation time rather than `@emotion/react` or `@emotion/styled` injecting when a component is rendered. This means the Emotion cache needs to be known before any style is created. In order to properly merge styles with components using either dynamic styling package, the Emotion cache must be changed on any React application. Without this, styles will not be merged correctly when static and dynamic styles are used on the same element. If you're using Canvas Kit React, you should use the `<CanvasProvider>` which includes Emotion's `<CacheProvider>` with the proper cache already set. If you're not using Canvas Kit React, you should use the `<CacheProvider>`: ```tsx // ONLY use if not using the <CanvasProvider> import {getCache} from '@workday/canvas-kit-styling'; // in your application bootstrap const root = React.createRoot(document.getElementById('root')); root.render( <CacheProvider value={getCache()}> <App /> </CacheProvider> ); ``` ## Known issues ### Hot Reloading Style merging works by using CSS specificity rather than JavaScript runtime. This can cause problems during hot reloading. If you specify all styles in the same file, hot reloading shouldn't result in any style merging problems. But if you use `extends` in `createStencil` that references another file, you may run into style merge issues. For example: ```ts // base.tsx file export const baseStencil = createStencil({ base: { color: 'red', }, }); // extended.tsx file import {baseStencil} from './base'; export const extendedStencil = createStencil({ extends: baseStencil, base: { color: 'blue', }, }); ``` This will render correctly until you change `color` in `base.tsx` and get a hot reload: ```tsx // base.tsx file export const baseStencil = createStencil({ base: { color: 'purple', }, }); ``` The hot reload will evaluate this update and inject a new style. ## Development Canvas Kit Styling comes with a runtime that doesn't need anything special for development. The runtime uses `@emotion/css` to include your styles on the page. If you plan to use static compilation, we recommend enabling in production as well so you can fix static compilation errors as you develop rather than get errors only in production builds. ## Static compilation The `@workday/canvas-kit-styling-transform` package can to pre-build styles. This process takes style objects and turns them into CSS strings. This process moves serialization and hashing to build time rather than browser runtime when `@emotion/css` is processing styles. This will speed up production builds at runtime. Static compilation has stricter requirements than when doing runtime styling. The static compiler uses the TypeScript type system to statically analyze style values and thus requires value types to be known by TypeScript. See [Restrictions](#restrictions). Static compilation may be required for server side rendering (SSR), especially when using React Server Components. ### Hash generation Emotion generates hashes based on the serialized style object. This means a style should always give the same hash. Static styling hashes differently. Every `createStyles` or `createStencil` call will generate a unique hash even if the style object is the same. This is required for proper style merging because static styling doesn't give single class names, but rather merges styles using CSS specificity. For runtime development, the hash is always unique. For static compilation, the hash is based on the start and end character count in the source file of the style block. This is required for SSR so that the server and client agree on the same value during hydration. This means that while debugging, the hash depends on any code before it. If you add a `console.log` for example, the character index of a style block could shift which will generate a new hash. ### Restrictions The static compiler uses the TypeScript type checker. The easiest way to think of these restrictions is if TypeScript knows the exact value, the static compiler will also know. A simple example: ```ts // won't work - `value` is a type of `string` because `let` allows a value to be mutated let value = 'absolute'; // `string` const myStyles = createStyles({ position: value, // error - `string` isn't specific enough. }); // will work - `value` is a type of `'absolute'` because `const` restricts to a string literal const value = 'absolute'; // `'absolute'` const myStyles = createStyles({ position: value, // works. If you mouse over `value` in your editor, you'll see the type is `'absolute'` }); ``` More complex examples may be objects: ```ts // won't work. TypeScript will not understand that the position will only be `'absolute'` and makes it a `string` instead const reusableStyles = { position: 'absolute', }; // `{ position: string }` const myStyles = createStyles({ ...reusableStyles, // error - `position` is a `string` and not specific enough }); // will work. Adding `as const` tells TypeScript the object is readonly and therefore no values can change const reusableStyles = { position: 'absolute', } as const; // `{ readonly position: 'absolute' }` const myStyles = createStyles({ ...reusableStyles, // works. If you mouse over, the position is a string literal `'absolute'` }); ``` Functions are a little more tricky and may require generics. ```ts // generic makes the type be statically knowable function getPosition<V extends 'relative' | 'absolute'>(value: V): {position: V} { return {position: value}; } // mouse over `position` in your editor an the type will be `{ position: 'absolute' }` const position = getPosition('absolute'); // { position: 'absolute' } const myStyles = createStyles({ ...getPosition('absolute'), // works - `{ position: 'absolute' }` }); ``` ### Webpack The `@webpack/canvas-kit-styling-transform` package comes with a webpack loader that can be added to development and/or production. ```js // import the transform - CJS and ESM are supported import {StylingWebpackPlugin} from '@workday/canvas-kit-styling-transform'; // somewhere only once. For static webpack config files, this can be near the top. // If inside Storybook, Gatsby, Next.js, etc configs, put inside the function that is called that // returns a webpack config const tsPlugin = const tsPlugin = new StylingWebpackPlugin({ tsconfigPath: path.resolve(__dirname, '../tsconfig.json'), // allows your TS config to be used // A different tsconfig could be used if you want to use TS to transpile to something like ES2019 and // also have Babel process the file. }); // However you need to define rules. // This is different for using webpack directly or in Storybook/Gatsby/Next.js/etc { rules: [ //... { test: /.\.tsx?$/, use: [ { loader: require.resolve('@workday/canvas-kit-styling-transform/webpack-loader'), options: tsPlugin.getLoaderOptions(), }, ], enforce: 'pre' }, ]; // We need to pass the plugin to Webpack's plugin list. Failure to do this will result in a // production build hanging plugins: [tsPlugin] } ``` ## Core Styling Approaches for Static Styling For proper static styling there's two methods that you can use to apply styles. 1. Using `createStyles` for simple object base styles. 2. Using `createStencil` for dynamic styles and reusable components. Both approaches are intended to be used in tandem with the `cs` prop when applying styles to our components. ### `cs` Prop The `cs` prop takes in a single, or an array of values that are created by the `cs` function, a string representing a CSS class name, or the return of the `createVars` function. It merges everything together and applies `className` and `style` attributes to a React element. Most of our components extend the `cs` prop so that you can statically apply styles to them. > **Important**: While the `cs` prop accepts a style object, **this will not** be considered > statically styling an element and you will lose the performance benefits. We plan on providing a > babel plugin to extract these styles statically in a future version. ```tsx import {system} from '@workday/canvas-tokens-webs'; import {PrimaryButton} from '@workday/canvas-kit-react/button'; const styles = createStyles({color: system.color.static.red.default}); function MyComponent() { return <PrimaryButton cs={styles}>Text</PrimaryButton>; } ``` ### `createStyles` The primary utility function is the `createStyles` function. It makes a call to the `css` function from `@emotion/css`. Emotion still does most of the heavy lifting by handling the serialization, hashing, caching, and style injection. ```tsx // Bad example (inside render function) import {system} from '@workday/canvas-tokens-webs'; import {PrimaryButton} from '@workday/canvas-kit-react/button'; function MyComponent() { const styles = createStyles({color: system.color.static.red.default}); // Don't do this return <PrimaryButton cs={styles}>Text</PrimaryButton>; } // Good example (outside render function) import {system} from '@workday/canvas-tokens-webs'; import {PrimaryButton} from '@workday/canvas-kit-react/button'; const styles = createStyles({color: system.color.static.red.default}); function MyComponent() { return <PrimaryButton cs={styles}>Text</PrimaryButton>; } ``` Most of our components support using the `cs` prop to apply the static styles. It merges everything together and applies `className` and `style` attributes to a React element. <InformationHighlight className="sb-unstyled" cs={{marginBlock: system.space.x4}}> <InformationHighlight.Icon /> <InformationHighlight.Heading>Information</InformationHighlight.Heading> <InformationHighlight.Body> For a more in depth overview, please view our <Hyperlink href="https://workday.github.io/canvas-kit/?path=/docs/styling-getting-started-create-styles--docs"> Create Styles </Hyperlink> docs. </InformationHighlight.Body> </InformationHighlight> ### `createStencil` `createStencil` is a function for creating reusable, complex component styling systems. It manages `base` styles, `parts`, `modifiers`, `variables`, and `compound` modifiers. Most of our components also export their own Stencil that might expose CSS variables in order to modify the component. In the example below, we leverage `parts`, `vars`, `base` and `modifiers` to create a reusable `Card` component. The Stencil allows us to dynamic style the component based on the props. ```tsx import {createStencil}from '@workday/canvas-kit-styling'; import {Card} from '@workday/canvas-kit-react/card'; import {system} from '@workday/canvas-tokens-webs'; const themedCardStencil = createStencil({ vars: { // Create CSS variables for the color of the header headerColor: '' }, parts: { // Allows for styling a sub element of the component that may not be exposed through the API header: 'themed-card-header' }, base: ({headerPart, headerColor}) => ({ padding: system.space.x4, boxShadow: system.depth[2], backgroundColor: system.color.bg.default, color: system.color.text.default, // Targets the header part via [data-part="themed-card-header"]"] [headerPart]: { color: headerColor } }), modifiers: { isDarkTheme: { // If the prop `isDarkTheme` is true, style the component and it's parts true: ({headerPart}) => ({ backgroundColor: system.color.bg.contrast.default, color: system.color.text.inverse [headerPart]: { color: system.color.text.inverse } }) } } }) const ThemedCard = ({isDarkTheme, headerColor, elemProps}) => { return ( /* Use the `cs` prop to apply the stencil and pass it the dynamic properties it needs to style accordingly */ <Card cs={themedCardStencil({isDarkTheme, headerColor})} {...elemProps}> /* Apply the data part selector to the header */ <Card.Heading {...themedCardStencil.parts.header}>Canvas Supreme</Card.Heading> <Card.Body> Our house special supreme pizza includes pepperoni, sausage, bell peppers, mushrooms, onions, and oregano. </Card.Body> </Card> ); }; ``` <InformationHighlight className="sb-unstyled" cs={{marginBlock: system.space.x4}}> <InformationHighlight.Icon /> <InformationHighlight.Heading>Information</InformationHighlight.Heading> <InformationHighlight.Body> For a more in depth overview, please view our <Hyperlink href="https://workday.github.io/canvas-kit/?path=/docs/styling-getting-started-create-stencil--docs">Create Stencil</Hyperlink> docs. </InformationHighlight.Body> </InformationHighlight>