Almost all style conventions we use in Atlas are taken care by ESLint and Prettier, this document is meant to fill in the gap between the unspoken conventions and the ones enforced by tooling.
We do all the styling using the emotion
library, mostly using the styled
syntax. (Check here for more details on cVar
) Quick example:
import styled from '@emotion/styled'
import { cVar } from '@/styles'
export const StyledContainer = styled.div`
background-color: ${cVar('colorBackground')};
`
When defining spacing (padding/margin), we use sizes
helper imported from @/styles
. You provide a multiplier of base spacing (4px) and get back string with pixels value. For example sizes(2)
will produce "8px"
.
If a given piece of CSS is repeated multiple times inside a single file, it may make sense to extract that into a separate css
block to be reused by different components:
import { css } from '@emotion/react'
import styled from '@emotion/styled'
import { cVar, sizes } from '@/styles'
type Props = {
pressed: boolean
}
const blueIfPressed = ({ pressed }: Props) => css`
background-color: ${pressed ? cVar('colorBackgroundPrimary') : 'initial'};
`
const Container = styled.div<Props>`
display: flex;
${blueIfPressed};
`
const Button = styled.button<Props>`
padding: ${sizes(4)};
${blueIfPressed};
`
In a lot of cases, styles need to be based on some condition, for example different colors based on component variant, etc. In those cases we use props provided by the emotion
library. One important aspect to keep an eye on is to avoid keeping conditional styles inside raw string - this will make our tooling not properly understand the style, disabling benefits of linters, IDE features, etc. Example:
import { css } from '@emotion/react'
import styled from '@emotion/styled'
type Props = {
enabled: boolean
}
// don't do this:
const BadExample = styled.div<Props>`
// in this example opacity is passed in a raw string and will not be properly interpreted by tooling
${({ enabled }) => (!enabled ? 'opacity: 0.5;' : '')};
`
// do this:
const disabledCss = css`
opacity: 0.5;
`
const GoodExample = styled.div<Props>`
// in this example conditional style is defined in a separate css block and will be properly parsed by tooling
${({ enabled }) => !enabled && disabledCss};
`
Every reusable component should be placed inside src/components
. If any of the component categories (directories starting with underscore, e.g. _buttons
) is a good fit, the component should be placed there. Every component should have its own directory named the same as the component. The directory should contain the following files:
Component/
index.ts
- re-exporting anything neededComponent.tsx
- component code- (optional)
Component.styles.ts
- any styles needed for the component - (optional)
Component.stories.tsx
- Storybook stories exploring different use cases/variants of the component
In the main component file, we try to preserve a following structure:
- Hook calls
- Derived state
- Event handlers
- Conditional returns
Here is an example of a nicely written component:
// Component.tsx
import { FC, useEffect, useState } from 'react'
import { Button } from '@/components/_buttons/Button'
import { someSideEffect } from '@/utils'
type ComponentProps = {
hidden?: boolean
otherProp?: number
}
// all components should use named exports
export const Component: FC<ComponentProps> = ({ hidden, otherProp, ...rest }) => {
// hooks first
const [pressed, setPressed] = useState(false)
const [count, setCount] = useState(0)
useEffect(() => {
someSideEffect(count)
}, [count])
// derived state
const countedAndPressed = count > 0 && pressed
// event handlers
const handleClick = () => {
setCount((count) => count + 1)
setPressed(true)
}
// conditional return
if (hidden) {
return null
}
return (
<div>
<Button pressed={pressed} onClick={handleClick}>
Count: {count}
</Button>
{countedAndPressed && <span>Great job!</span>}
</div>
)
}
We try to leverage type-safety whenever possible, to reduce potential of human error. We use type
instead of interface
as it's usually easier to work with and compose. Another good practice is to avoid general types like unknown
or object
or types allowing any property name, because those can easily introduce errors that won't be caught by the compiler. Example:
// bad example
type BadFnInput = {
[key: string]: string
}
const fn = (input: BadFnInput) => {
console.log(input.hello)
}
// no TS error, result will be unexpected
fn({ h3ll0: 'hi' })
In terms of naming, PascalCase
should be used for component names and Typescript types. Constants should be named using UPPER_CASE
. Everything else should use camelCase
.
Also, to stay consistent, all event handlers used in components should be named with handle
prefix followed by the name of the event handled. So for example:
const handleClick = () => console.log('Clicked!')
return <Button onClick={handleClick} />