UI Styling Standard
The eslint plugin to enforce and educate on Atlassian's UI Styling StandardPatterns
All styles should be written using Compiled, (opens new window) or Primitives and conform to the following patterns.
Static styles
For styles that are not affected by props or state, use the css() or xcss() API.
Compiled
When using Compiled, write static styles with css() and apply them with the css prop.
import { css } from '@compiled/react';
import { token } from '@atlaskit/tokens';
const cardStyles = css({
backgroundColor: token('elevation.surface.raised'),
boxShadow: token('elevation.shadow.raised'),
padding: token('space.100'),
});
const Card = () => <div css={cardStyles} />;Primitives
When using Primitives, write static styles with xcss() and apply them with the xcss prop.
import { Box, xcss } from '@atlaskit/primitives';
const cardStyles = xcss({
backgroundColor: 'elevation.surface.raised',
boxShadow: 'elevation.shadow.raised',
padding: 'space.100',
});
const Card = () => <Box xcss={cardStyles} />;Toggled styles
For styles that are toggled on and off, write them as static styles and then conditionally apply them.
Compiled
When using Compiled, conditionally apply styles with the css prop.
import { css } from '@compiled/react';
import { token } from '@atlaskit/tokens';
const baseStyles = css({
backgroundColor: token('color.background.neutral'),
border: `1px solid ${token('color.border')}`,
});
const selectedStyles = css({
backgroundColor: token('color.background.selected'),
borderColor: token('color.border.selected'),
});
const Selectable = ({ isSelected = false }: { isSelected?: boolean }) => (
// The `selectedStyles` are conditionally applied
<div css={[baseStyles, isSelected && selectedStyles]} />
);Primitives
When using Primitives, conditionally apply styles with the xcss prop.
import { Box, xcss } from '@atlaskit/primitives';
const baseStyles = xcss({
backgroundColor: 'color.background.neutral',
borderWidth: 'border.width',
borderStyle: 'solid',
borderColor: 'color.border',
});
const selectedStyles = xcss({
backgroundColor: 'color.background.selected',
borderColor: 'color.border.selected',
});
const Selectable = ({ isSelected = false }: { isSelected?: boolean }) => (
// The `selectedStyles` are conditionally applied
<Box xcss={[baseStyles, isSelected && selectedStyles]} />
);Variant styles
For styles that compose a set of known variants, use cssMap or an object of xcss() styles and
then dynamically access them.
Compiled
When using Compiled, write variants with cssMap and apply them with the css prop.
import { css, cssMap } from '@compiled/react';
import { token } from '@atlaskit/tokens';
type Status = 'success' | 'warning' | 'danger';
const baseStyles = css({
font: token('font.body.large'),
padding: token('space.100'),
});
const statusStyles = cssMap({
success: {
color: token('color.text.success'),
backgroundColor: token('color.background.success'),
},
warning: {
color: token('color.text.warning'),
backgroundColor: token('color.background.warning'),
},
danger: {
color: token('color.text.danger'),
backgroundColor: token('color.background.danger'),
},
});
const StatusMessage = ({ message, status }: { message: string; status: Status }) => (
// The `statusStyles` are dynamically accessed
<div css={[baseStyles, statusStyles[status]]} />
);Primitives
When using Primitives, write variants using an object with xcss() values and apply them with the
xcss prop.
import { Box, xcss } from '@atlaskit/primitives';
type Status = 'success' | 'warning' | 'danger';
const baseStyles = xcss({
font: 'font.body.large',
padding: 'space.100',
});
const statusStyles = {
success: xcss({
color: 'color.text.success',
backgroundColor: 'color.background.success',
}),
warning: xcss({
color: 'color.text.warning',
backgroundColor: 'color.background.warning',
}),
danger: xcss({
color: 'color.text.danger',
backgroundColor: 'color.background.danger',
}),
};
const StatusMessage = ({ message, status }: { message: string; status: Status }) => (
// The `statusStyles` are dynamically accessed
<Box xcss={[baseStyles, statusStyles[status]]} />
);Dynamic styles
Use with caution
For styles that cannot be expressed statically, use the style prop.
This might be needed when:
- Styles are derived from user input
- Integrating with a third party library that provides a
styleobject
Compiled
import { css } from '@compiled/react';
import { token } from '@atlaskit/tokens';
const baseStyles = css({
height: '100%',
minWidth: '200px',
backgroundColor: token('color.background.neutral'),
});
const Resizable = ({ width }: { width: string }) => <div css={baseStyles} style={{ width }} />;Primitives
import { Box, xcss } from '@atlaskit/primitives';
const baseStyles = xcss({
height: '100%',
minWidth: '200px',
backgroundColor: 'color.background.neutral',
});
const Resizable = ({ width }: { width: string }) => <Box css={baseStyles} style={{ width }} />;More examples
Feature flagging styles
Dynamic styles are forbidden by no-dynamic-styles so cannot be used to implement feature flagged styles.
There are two supported approaches for feature flagging styles:
- Toggled styles to dynamically apply feature flagged styles.
- Multiple components which are dynamically switched between.
Toggled styles
Prefer toggled styles for feature flagging as it requires less code duplication.
Adding styles
Use a logical expression, (opens new window) to add new styles behind a feature flag.
import { css } from '@compiled/react';
import { token } from '@atlaskit/tokens';
import { ff } from '@atlassian/feature-flags';
const linkStyles = css({ color: text('color.link') });
const underlineStyles = css({ textDecoration: 'underline' });
export const Link = ({ children, href }: { children: string; href: string }) => (
<a href={href} css={[linkStyles, ff('my-feature-flag') && underlineStyles]}>
{children}
</a>
);Replacing styles
Use a conditional (ternary) expression, (opens new window) to replace styles behind a feature flag.
import { css } from '@compiled/react';
import { token } from '@atlaskit/tokens';
import { ff } from '@atlassian/feature-flags';
const originalStyles = css({ color: text('color.text.accent.red') });
const featureFlaggedStyles = css({ color: text('color.text.danger') });
export const ErrorMessage = ({ children }: { children: string }) => (
<span css={ff('my-feature-flag') ? featureFlaggedStyles : originalStyles}>{children}</span>
);Multiple components
Use multiple components for feature flagging if toggled styles are not suitable.
import { css } from '@compiled/react';
import { token } from '@atlaskit/tokens';
import { ff } from '@atlassian/feature-flags';
const linkStyles = css({ color: text('color.link') });
const underlineStyles = css({ textDecoration: 'underline' });
const LinkWithoutUnderline = ({ children, href }: { children: string; href: string }) => (
<a href={href} css={linkStyles}>
{children}
</a>
);
const LinkWithUnderline = ({
children,
href,
hasUnderline = true,
}: {
children: string;
href: string;
hasUnderline?: boolean;
}) => (
<a href={href} css={[linkStyles, ff('my-feature-flag') && hasUnderline && underlineStyles]}>
{children}
</a>
);
export const Link = ff('my-feature-flag') ? LinkWithUnderline : LinkWithoutUnderline;Migrating from Emotion or styled-components
Using @emotion/* or styled-components for styling is forbidden by
use-compiled.
For internal Atlassians we have a guide for migrating to Compiled, (opens new window).
Otherwise refer to the official documentation for migrating to compiled, (opens new window).
Migrating from styled
The styled API is forbidden by
no-styled.
Use css() or xcss() to declare styles instead.
Before
import { styled } from '@compiled/react';
type Status = 'success' | 'warning' | 'danger';
type Props = {
width: string;
status: Status;
isSelected: boolean;
};
const textColors: Record<Status, string> = {
success: token('color.text.success'),
warning: token('color.text.warning'),
danger: token('color.text.danger'),
};
const Component = styled.div<Props>(
{
padding: token('space.100'),
width: (props) => props.width,
},
({ status }: { status: Status }) => ({
color: textColors[status],
}),
({ isSelected }) => (isSelected ? { border: `1px solid ${token('color.border.selected')}` } : {}),
);After
We apply a combination of the patterns above:
- The padding is a static style so uses
css()that is always applied. - The selected border is a toggled style so uses
css()that is conditionally applied. - The status color is a variant style so is written using
cssMap()that is dynamically accessed. - The width is a completely dynamic style so uses the
styleprop.
import type { ReactNode } from 'react';
import { css, cssMap } from '@compiled/react';
type Status = 'success' | 'warning' | 'danger';
type Props = {
width: string;
status: Status;
isSelected: boolean;
// Props that were implicitly added now need to be made explicit
children: ReactNode;
};
const baseStyles = css({
padding: token('space.100'),
});
const selectedStyles = css({
border: `1px solid ${token('color.border.selected')}`,
});
const statusStyles = cssMap({
success: {
color: token('color.text.success'),
},
warning: {
color: token('color.text.warning'),
},
danger: {
color: token('color.text.danger'),
},
});
const Component = ({ width, status, isSelected, children }: Props) => (
<div css={[baseStyles, isSelected && selectedStyles, statusStyles[status]]} style={{ width }}>
{children}
</div>
);Migrating nested selectors
Nested selectors are forbidden by no-nested-selectors.
Apply styles directly to elements instead.
Third party libraries
If a third party library does not let you control rendering then investigate possible workarounds. For example, if you're using a React abstraction then consider using the lower level JS-only library instead.
If you've exhausted all avenues you can suppress the lint rule and move on, but this is considered tech debt.
Atlassian platform styles
Never use nested selectors to override Atlassian platform styles (including Design System components). Overriding platform styles is unsafe and will break. This blocks shipping changes to all customers, and is a pain point for everyone.
Before
The example below uses nested selectors to style child elements that it does not render.
import { css } from '@compiled/react';
import { token } from '@atlaskit/tokens';
const tableStyles = css({
tbody: {
border: 'none',
},
tr: {
border: 'none',
borderBlockEnd: `1px solid ${token('color.border')}`,
'&:hover': {
backgroundColor: token('color.background.neutral.subtle.hovered'),
},
},
td: {
verticalAlign: 'middle',
},
});
export const Table = ({ children }: { children: ReactNode }) => (
<table css={tableStyles}>{children}</table>
);After
Instead we should apply styles directly to elements, even if it requires API changes.
import { css } from '@compiled/react';
import { token } from '@atlaskit/tokens';
const tableBodyStyles = css({
border: 'none',
});
export const TableBody = ({ children }: { children: ReactNode }) => (
<tbody css={tableBodyStyles}>{children}</tbody>
);
const tableRowStyles = css({
border: 'none',
borderBlockEnd: `1px solid ${token('color.border')}`,
'&:hover': {
backgroundColor: token('color.background.neutral.subtle.hovered'),
},
});
export const TableRow = ({ children }: { children: ReactNode }) => (
<tr css={tableRowStyles}>{children}</tr>
);
const tableCellStyles = css({
verticalAlign: 'middle',
});
export const TableCell = ({ children }: { children: ReactNode }) => (
<td css={tableCellStyles}>{children}</td>
);Migrating spread props
Explicitly define and apply all props used by a component.
Before
import { css } from '@compiled/react';
type IconProps = {
color: string;
};
const Icon = ({ color, ...props }: IconProps) => {
return <svg fill={color} {...props} />;
};After
import { css } from '@compiled/react';
type IconProps = {
color: string;
'aria-label'?: string;
};
const Icon = ({ color, 'aria-label': ariaLabel }: IconProps) => {
return <svg fill={color} aria-label={ariaLabel} />;
};Share reusable components instead of exporting styles.
Before
import { css } from '@compiled/react';
export const ellipsisStyles = css({
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
});After
import { css } from '@compiled/react';
const ellipsisStyles = css({
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
});
export const ClampedText = ({ children }: { children: string }) => (
<span css={ellipsisStyles}>{children}</span>
);