Using Styled Components
To apply styles to HTML, we use a CSS-in-JS library called Emotion. This allows us to define styles in the same file where the component is defined and used. Styles are scoped to avoid global selectors and having to manually specify classnames as strings. You can also leverage the theme
object to reference common values and utilities.
The best styles are the ones you don’t write - whenever possible use existing components.
To use Style Components pull in styled
and apply your CSS. For consistent spacing you should also use space()
:
import styled from '@emotion/styled';
import {space} from 'sentry/styles/space';
const Numeric = styled('span')`
font-variant-numeric: tabular-nums;
`;
const SideBySide = styled('div')`
display: flex;
gap: ${space(2)};
flex-wrap: wrap;
align-items: flex-start;
`;
export default ExampleComponent() {
return (
<SideBySide>
<p>a number: <Numeric>1</Numeric></p>
<p>and another: <Numeric>2</Numeric></p>
</SideBySide>
)
}
Take constants (z-indexes, paddings, colors) from props.theme
import styled from "@emotion/styled";
import { space } from "sentry/styles/space";
const SomeComponent = styled("div")`
border-radius: 1.45em;
font-weight: bold;
z-index: ${(p) => p.theme.zIndex.modal};
padding: ${space(1)} ${space(2)};
border: 1px solid ${(p) => p.theme.borderLight};
color: ${(p) => p.theme.purple};
box-shadow: ${(p) => p.theme.dropShadowHeavy};
`;
Most of the time dynamic properties are not needed!
You can pass in custom props to your custom component and use that to set styles. This does create overhead at runtime and should be avoided. What follows is an example of how to pass custom props, and how to avoid doing so.
import styled from "@emotion/styled";
// ❌ Try to avoid this
const Label = styled("label")<{ isDisabled: boolean; isError: boolean }>`
color: ${(p) =>
p.isDisabled
? p.theme.gray200
: p.isError
? p.theme.error400
: inherit};
`;
return (
<Label disabled={false} isError={true}>
There is an error
</Label>
);
Choose one of the strategies below instead.
Data selectors can be an easy way to set styles based on simple/pre-defined state. When defining these attributes, don't forget that browsers have built-in selectors like :disabled
, :focus
, :first-child
and so on.
import styled from "@emotion/styled";
// ✅ Use a data attribute to pass in your state, and leverage CSS selectors:
const Label = styled("label")`
color: inherit;
&[data-is-error="true"] {
color: ${(p) => p.theme.error400};
}
&:disabled {
color: ${(p) => p.theme.gray200};
}
`;
return (
<Label disabled={false} data-is-error={true}>
There is an error
</Label>
);
The style
and css
attributes can be used, but the values of style
are not linted. Avoid setting too many values at a time for readability.
import styled from "@emotion/styled";
import { css } from "@emotion/react";
// ✅ Don't be afraid of inline styles for one-off values
const Grid = styled("div")`
display: grid;
grid-template-areas:
"logo search"
"sidebar main";
gap: var(--grid-gap, 0px);
@media (max-width: ${(p) => p.theme.breakpoints.small}) {
grid-template:
"logo" auto
"search" auto
"main" auto / 1fr;
/* Hide sidebar, which is in an auto-column */
grid-auto-columns: 0px;
}
`;
return (
<Grid
css={css`
--grid-gap: ${space(1)};
`}
>
<HomeLink style={{ gridArea: "logo" }} />
<SearchForm style={{ gridArea: "search" }} />
<Navigation style={{ gridArea: "sidebar" }} />
<DataTable style={{ gridArea: "main" }} />
</Grid>
);
For values that change frequently it's beneficial to use a component ref and set the values imperatively. This can be done without triggering react which can result in much more efficient updates.
Note that you can set CSS values directly, or if the component is reading a CSS variable, call elem.style.setProperty('--foo', 'value')
instead.
import styled from "@emotion/styled";
import { useRef } from "react";
const Area = styled("div")`
width: 500px;
height: 500px;
position: relative;
`;
const MovingCircle = styled("div")`
position: absolute;
width: 50px;
height: 50px;
background: black;
border-radius: 50%;
`;
const ref = useRef<HTMLDivElement>(null);
return (
<Area
onMouseMove={(event) => {
if (!ref.current) {
return;
}
// ✅ Imperatively set values that change frequently to avoid react re-renders
ref.current.style.top = event.clientY + "px";
ref.current.style.left = event.clientX + "px";
}}
>
<MovingCircle ref={ref} />
</Area>
);
This happens when you use a styled component as a selector, we need to tell stylelint that what we are interpolating is a selector by using comments to assist the linter. e.g.
const ButtonBar = styled("div")`
${/* sc-selector */Button) {
border-radius: 0;
}
`;
See https://styled-components.com/docs/tooling#interpolation-tagging for other tags and more information.
Our documentation is open source and available on GitHub. Your contributions are welcome, whether fixing a typo (drat!) or suggesting an update ("yeah, this would be better").