For Developers
API & Pattern Guidelines
Note: This repo hasn't seen a full audit, so you may find examples that contradict these guidelines. Some of the below rules are inspired by painpoints we've encountered in this project.
Canvas
- Ensure you're always using the Canvas primitives and enums wherever possible for things like:
- Space (e.g.
canvas.space.s
) - Depth (e.g.
canvas.depth[2]
) - Type (e.g.
...canvas.type.h1
). Always start from a type hierarchy level and override if needed.
- Space (e.g.
- Use the provided types (e.g.
CanvasSpaceValues
,CanvasSpaceNumbers
, etc.) to restrict prop values - Check out the
@workday/canvas-kit-react/tokens
README for the latest and greatest Canvas helpers.
Naming
Props
- Prop names should never include the component name (e.g.
type
, notbuttonType
) - Use the same props for the same concepts across components
- Avoid names that reference color, position, and size. For example:
blueIcon
can be bad because it may not be blue to everyone and changing colors or making colors variable is a breaking change.leftIcon
can be bad because we can change the position with RTL or add something to the left of that, then it wouldn't make sense anymore.mediumIcon
can be bad if we add another size in between... then which one is medium? Is it mediumLarge now?
T-shirt Sizes
- Always use the shortest enumeration (
xs, s, m, l, xl
, etc.) - Do not use longer versions (e.g.
sm
)
Theme Types
- Default - normal state/color for use on light background
- Inverse - inverted colors for use on a dark background
- Note: If you encounter somewhere you need another theme type, please let us know so we can document it
Event Handlers
- Always use standard
on{Descriptor}{Event}
naming (onClick
,onChange
,onBreakpointChange
, etc.) - Only use a descriptor if:
- You need more context
- There is already a handler for that type of event (e.g.
onChange
,onValidColorChange
)
Enums
Use disjoint string unions instead of enums. Enums have issues with overloading or extending. They are also more difficult to use and create a different experience from JavaScript. These union types also have a better autocomplete experience.
interface ButtonProps {size: 'small' | 'medium' | 'large';}// use<PrimaryButton size="medium" />;
JavaScript objects can be used as enums without the downsides:
const ButtonType = {Primary: 'primary',Secondary: 'secondary'} as const // as const locks object values as literalinterface ButtonProps {type: typeof ButtonType[keyof typeof ButtonType] // returns 'primary' | 'secondary'}Button.Type = ButtonType// use<Button type={Button.Type.Primary} /><Button type="primary" />
If objects are desired, the keys follow these rules:
- Singular
- PascalCase
- Include component name unless it's a generic enum shared across components. Since we export our enums, this prevents naming clashes
- Exclude component name in static variables (
Button.Type
vs.Button.ButtonType
):
Patterns
Event Handlers
- Use standard browser events wherever possible
- All event handlers should receive an event unless there's a good reason otherwise. This is for
consumer predictability. In other words, always opt for
onChange: e => void
overonChange: () => void
oronChange: value => void
, etc.
Grow Interface
- If your component needs to grow to fill to it's container, extend
GrowthInterface
(e.g.export interface MyComponentProps extends GrowthBehavior
) - Then use the
grow
boolean prop in your styles to achieve the desired effect (e.g.width: grow ? '100%' : undefined
)
Input Provider
- All Canvas Kit components should support an
InputProvider
component to provide the cleanest experience for mouse users. Read the docs here. - Do not use
InputProvider
within your components. It is meant to be used only once in your application. It does not require wrapping any children - Make sure you provide fully accessible styling by default, and only override for mouse usage.
[`[data-whatinput='mouse'] &:focus,[data-whatinput='touch'] &:focus,[data-whatinput='pointer'] &:focus`]: {outline: 'none',border: 'none',},
Prop Spread Behavior
Extend the interface of the primary element/component in your component (e.g.
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement>
)Intentionally destructure your props so that every prop is assigned. This allows you to use spread the way it was intended.
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {type: ButtonType,size: ButtonSize,icon: CanvasIcon}// ...somewhere in your button render()const { type, size, icon, ...elemProps } = this.props<ButtonContainer type={type} size={size} icon={icon} {...elemProps} />Only spread props on one element/component
Controlled Components
We opt for controlled components wherever possible.
We aim to manage the least amount of state within our components as possible.
For input type components:
- Always stick with the default
value
andonChange
if you can - Deviate where it makes sense and/or is required (e.g.
checked
andonChange
for checkboxes).
- Always stick with the default
Accessibility
- Use aria labels where required
- Ensure full keyboard navigation
- Check whether tabbing is enough or whether additional keyboard navigation is required (e.g. arrow keys)
- When in doubt, ask an expert!
Children
- We often add or augment props to React children within our components. Use
React.Children.map
along withReact.cloneElement()
- Use
React.isValidElement()
if you want to make sure it's a React component and not a regular DOM node. - If you're adding any event handlers to the children, make sure you also support existing ones
Logic Flow
- If vs. Switch: use switch statements when code branching is determined by the value of a single variable or expression.
- Nested Ternaries: maximum two levels and only if it's very obvious. If you have two or more levels, try rewriting it as if/else statements and compare the complexity & scanability.
- Opt for
pure functions
wherever possible. They make unit testing easier and always behave as expected. Because React can
be a bit of a magic black box, sometimes
this.x
values are not what you expect.
foo(number, bar) => {return number * bar}foo(this.number, this.bar);// is a much better option thanfoo() => {return this.number * this.bar}foo();
Server Side Rendering
- In order to support SSR, we cannot reference global objects (
window
,document
, etc.) before a component is hydrated/mounted. - Generally, it is only safe to use these freely within
componentDidMount
,useEffect
anduseLayoutEffect
. - This means that any reference to
window
ordocument
should be avoided wherever possible within the global scope, constructors, and render methods. - If you need to reference these variables in these avoided places, you must check whether it's
undefined first (e.g.
typeof window !== 'undefined'
) - Be particularly careful when initializing default props or state with something stored on the
window
/document
objects. These initializations will have to be skipped for SSR contexts (assignundefined
ornull
) and updated upon mounting.
Code Style
Default Props
- Use
defaultProps
whenever you find yourself checking for the existence of something before executing branching logic. It significantly reduces conditionals, facilitating easier testing and less bugs. - We prefer to colocate our default props and destructure them which allows consumers to rename our components on import.
- Note: If you assign a default value to a prop, make sure to make the prop as optional in the interface.
const someInterface {/*** If true, sets the Checkbox checked to true* @default false*/checked?: boolean;/*** If true, set the Checkbox to the disabled state.* @default false*/disabled?: boolean;/*** The value of the Checkbox.*/value?: string;}//...const {checked = false, disabled = false, value} = this.props;
Element Choice
- Use the correct native element wherever possible. This enables us to get as much behavior for free from the browser.
- For example, if something peforms an action on a click, it should generally use a
button
to get keypress handling for free.
Styled Components
- Always initialize styled components outside of your render function. Failing to do this will result in a big performance hit.
- When specifying the props a styled component can accept, it is up to you do define how restrictive
you should be. You can accept any prop that the component accepts (e.g.
styled('div')<ComponentProps>
) or only accept a subset (e.g.styled('div')<Pick<ComponentProps, 'someProp' | 'anotherProp'>>
) - We generally prefer the use of
styled
components over using thecss
function. However,css
can be handy for some basic styling.
Exports
- Avoid default exports
- Export everything else as a named export (
export * from ...
). Consider the naming of the things you're exporting (interfaces, enums, etc.) so you don't encounter any clashes.
// inside MyComponent/index.tsexport * from './lib/MyComponent';export * from './lib/AnotherComponent';
Documentation
Readmes
- Follow our README template
- Outline static properties (e.g.
Button.Type
), required props, and optional props - Usage example should be as standalone as possible. As long as it's not too complex, this snippet should be a working implementation so consumers can copy/paste
Storybook Structure
- Always opt for the most referenceable code in your stories. Storybook helps us test, but many consumers use it as an example of how to implement components.
- Avoid helper functions to reduce duplication that make it harder to parse.
- Avoid sharing wrappers, components, etc. from other story files.
- Essentially, try to keep each example as standalone and referencable as possible.
Prop Descriptions
We use JSDoc standards for our prop type definitions.
The base pattern for prop descriptions is: The <property> of the <component>.
For example:
/*** The value of the Checkbox.*/value?: string;
Be as specific as possible. For example, suppose there is a label
prop for Checkbox
which
specifies the text of the label. Rather than describe label
as The label of the Checkbox
, the
following is preferable:
/*** The text of the Checkbox label.*/label?: string;
Feel free to provide additional detail in the description:
/*** The value of the Slider. Goes to 11.*/value: number;
Be sure to specify a proper @default
for enum props. Listing the named values which are accepted
by the enum prop is encouraged:
/*** The side from which the SidePanel opens. Accepts `Left` or `Right`.* @default SidePanelOpenDirection.Left*/openDirection?: SidePanelOpenDirection;
Use a modified pattern for function props: The function called when <something happens>.
For
example:
/*** The function called when the Checkbox state changes.*/onChange?: (e: React.ChangeEvent) => void;
The pattern for booleans is also different: If true, <do something>.
For standard 2-state
booleans, set @default false
in the description. For example:
/*** If true, set the Checkbox to the disabled state.* @default false*/disabled?: boolean;
Provide additional detail for 2-state booleans where the false
outcome cannot be inferred:
/*** If true, center the Header navigation. If false, right-align the Header navigation.* @default false*/centeredNav?: boolean;
For 3-state booleans, you will need to describe all 3 cases:
If true <do something>. If false <do something else>. If undefined <do yet another thing>.
We also recommend the following pattern for errors:
/*** The type of error associated with the Checkbox (if applicable).*/error?: ErrorType;
Occasionally, you may encounter props which don't play nicely with the suggested guidelines. Rather
than following the patterns to the letter, adjust them to provide a better description if necessary.
For example, rather than ambiguously describing id
as The id of the Checkbox
, provide a more
explicit description:
/*** The HTML `id` of the underlying checkbox input element.*/id?: string;
Can't Find What You Need?
Check out our FAQ section which may help you find the information you're looking for.
FAQ Section