@workday/canvas-kit-docs
Version:
Documentation components of Canvas Kit components
401 lines (311 loc) • 14.6 kB
text/mdx
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:
- `/canvas-kit-styling` - Core styling utilities for runtime use
- `/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 /canvas-kit-styling
```
## Usage
```tsx
import React from 'react';
import {createRoot} from 'react-dom/client';
import {createStyles} from '/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 `/canvas-kit-styling` package uses `@emotion/css` to inject styles during JavaScript
evaluation time rather than `/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 '/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 `/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 `/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 `/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 `/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 '/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('/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 '/canvas-tokens-webs';
import {PrimaryButton} from '/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 `/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 '/canvas-tokens-webs';
import {PrimaryButton} from '/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 '/canvas-tokens-webs';
import {PrimaryButton} from '/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 '/canvas-kit-styling';
import {Card} from '/canvas-kit-react/card';
import {system} from '/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>