When you need to build something custom that isn't innately supported by Atlassian Design System components, utilize composition to empower your needs.
Composition is a common pattern in software development for code reusability and customization, saving time and effort. In the context of a design system, this is similarly a pattern for consistency and visual customization.
We'll dive into how to use composition effectively with the Atlassian Design System.
When to compose
We recommend that you take the following steps to decide when to compose:
Use ADS components wherever possible. These pre-built solutions will be the easiest to create and maintain.
When an ADS component is not enough, use our available customization options. For example, we offer
<Button appearance="primary" spacing="compact">
to customize a button, or in other places<Box xcss={{ padding: 'space.100' }}>
for bounded styles. Morexcss
props are also coming soon.Compose with primitives and ADS components to add spacing and styles where you need, using consistent APIs.
For purely custom components, use our tokens and other foundations to guide you. Also compose alongside our primitives and ADS components to have a UI that is consistent and easy to maintain.
Composing components and JSX
Developers and designers work with composition patterns every day. As a design system, we want to promote patterns like what you see below to give implementation and minor design control over components.
<Box padding="space.200">
<p>You have unread messages</p>
<Button appearance="primary">
Read messages <Badge appearance="primaryInverted">{5}</Badge>
</Button>
</Box>
As a design system, we're intentionally headed in this direction of promoting composition over monolithic components with complex interfaces. More inflexible APIs like our Dynamic Table may be changed and/or broken up into smaller components in the future to give you flexibility and control.
Composing styles and CSS-in-JS
As common practice, composing styles guides us to separate styling concerns into individual components, rather than styling in one large block. This way, changes are manageable as there's a clear (type-safe) connect to the component itself, rather than a loose connection through global or nested styles.
Recommended patterns
We recommend using patterns like the following. In the example below, you have all the context to understand this component locally, you can understand the styles, and you can determine what happens when your props.selected
changes. These patterns apply to both ADS primitives and native HTML elements (using Compiled or similar to style).
const wrapperStyles = xcss({
background: 'color.background.accent.blue.bolder',
});
const textStyles = css({
color: token('color.text.inverse'),
});
const selectedTextStyles = css({
textDecoration: 'underline',
});
export const Component = (props) => (
<Box padding="space.100" xcss={wrapperStyles}>
<span css={[textStyles, props.selected && selectedTextStyles]}>
Hello world
</span>
</Box>
);
Undesired patterns
We don't recommend these sorts of patterns as it becomes impossible to maintain at scale. In the example below, you have to interpret the DOM to understand the node tree and styles. If the <span>
element is changed to a <p>
, the component breaks. When implemented across multiple files, this becomes an anti-pattern is very inefficient, error-prone, and causes regressions to our customers.
// shared/styles.ts
export default styled.div<any>`
padding: 8px;
background: #0C66E4;
> span {
color: #fff;
${props => props.selected && 'text-decoration: underline !important'}
}
`;
// component.ts
import Wrapper from '../shared/styles.ts';
export const Component = (props) => (
<Wrapper selected={props.selected}>
<span>Hello world</span>
</Wrapper>
);
A practical example
Refer to our examples
Our primitive documentation and examples show composition in action
Risks with composition
Wherever possible, use pre-built components to have a more consistent, accessible, and easier to maintain experience.
Colocate your design and styling decisions into one file to keep the context locally so it makes sense in isolation. Split files based on context, not on type of code. This will help future development and tooling more clearly understand your code, so one file doesn't unknowingly impact other files. This is a concept taken from the UI Styling Standard that will be enforced via ESLint.
Do note, the parent/child relationship of composition can result in unexpected changes over time; for example, if the ADS Modal component for example changes its padding, your composed inner layout may look a bit out-of-place. When you're composing against design decisions you don't own, we suggest visual regression testing and image snapshots to keep this contract safer.