---
name: attio-frontend-rules
description: Styling conventions and component guidelines for the Attio frontend codebase. Covers styled-components patterns, transient props, data attributes, spacing, color tokens, and design system usage. Use when modifying frontend UI code in the Attio monorepo.
---

# Attio Frontend Rules

Guidelines and conventions for working on the Attio frontend codebase. Use whenever modifying the frontend.

---

## Styling

### General

- We use `styled-components` to style our components within our TS code.
- We use the `$` prefix to indicate props that are consumed in Styled Components. This prevents them from being passed to the DOM as attributes.

### Explore available components

Before writing custom components or reaching for custom CSS, check if there are existing components that fit what you're trying to do. Try asking AI agents to find existing components or have a look at available ones in storybook.

Run `yarn workspace @attio/design start-storybook` to open the storybook in a new tab in your browser.

### Re-styling existing components

Most reusable components offer props which affect their styling.

For example, `Layout.Stack` exposes various props to adjust padding (`p`, `px`, `py`, …), margins (`m`, `mx`, …), width, height, flex layout properties, etc.

Components like `Button` or `Typography` expose a `variant` prop to select between multiple colour variants commonly used in the codebase.

**Always prefer using these props over creating custom styling.** When you need to change the styling of a reusable component just for one particular part of the UI, use styled-components:

```jsx
import {styled} from "styled-components"

const Container = styled(Layout.Stack)`
    border: 1px solid ${({theme}) => theme.tokens.stroke.primary};
    border-radius: ${({theme}) => theme.borderRadii["12"]};
    background-color: ${({theme}) => theme.tokens.surface.secondary};
    overflow: hidden;
`
```

When implementing reusable components, add a `className` prop and assign it to the topmost component in the DOM subtree. This enables re-styling via `styled`.

```jsx
export function Stack({..., className}: {..., className: string | undefined}) {
    return (
        <div className={className}>
            ...
        </div>
    )
}
```

If the same re-styling is applied multiple times, it should become its own reusable component (or component variant).

### Layout.Stack defaults

`Layout.Stack` defaults `align` to `"center"` (i.e. `align-items: center`). **Always explicitly set `align="flex-start"`** when you need left/top alignment — don't assume it will be the default.

```tsx
// Good — explicit alignment
<Layout.Stack direction="column" align="flex-start">
    <Typography.Body.Standard.Component>Title</Typography.Body.Standard.Component>
    <Typography.Caption.Standard.Component>Description</Typography.Caption.Standard.Component>
</Layout.Stack>

// Bad — text will be centered, not left-aligned
<Layout.Stack direction="column">
    <Typography.Body.Standard.Component>Title</Typography.Body.Standard.Component>
    <Typography.Caption.Standard.Component>Description</Typography.Caption.Standard.Component>
</Layout.Stack>
```

Other useful `Layout.Stack` props: `direction`, `justify`, `gap`, `flex`, `shrink`, `minWidth`, `width`, `height`, and all spacing props (`p`, `px`, `py`, `pt`, `pb`, `pl`, `pr`, `m`, `mx`, `my`, etc.). **Always prefer these props over writing custom styled divs with `display: flex`.**

### Avoid layout assumptions

Components should not generally include external layout styles such as `width`, `z-index`, `margin` or `flex`. These properties should instead be set by the parent component using a `styled(MyComponent)` override.

### Transient props

Use [`$transient` props](https://styled-components.com/docs/api#transient-props) for anything consumed only inside a component's style. This prevents these props being passed as attributes to the underlying DOM node and generating noisy runtime errors.

```tsx
// Good
const GoodContainer = styled.div<{$isReady: boolean}>`
   background: ${p => p.$isReady ? "green" : "red"};
`

// Bad
const BadContainer = styled.div<{isReady: boolean}>`
   background: ${p => p.isReady ? "green" : "red"};
`
```

### Data attributes vs transient props

While transient props offer flexibility, consider using data attributes for variant styling. This improves CSS readability with many variants/selectors and offers marginal performance improvements.

**Rule of thumb:**
- **Transient props** — CSS that needs to be interpolated at runtime.
- **Data attributes** — styling variants that can be statically defined.

```jsx
// Good: transient props for runtime interpolation
const Container = styled.div<{$size: Size}>`
    font-size: ${({ $size }) => getFontSize($size)}px;
`

// Good: data attributes for variant definitions
const Container = styled.div`
    &[data-variant="subtle"] {
        background-color: light-blue;
    }

    &[data-variant="outline"] {
        background-color: transparent;
    }
`

// Data attributes can be typed
enum Variants {
    SUBTLE = "subtle",
    OUTLINE = "outline"
}

const Container = styled.div`
    &[data-variant=${Variants.SUBTLE}] {
        background-color: light-blue;
    }

    &[data-variant=${Variants.OUTLINE}] {
        background-color: transparent;
    }
`
```

### High cardinality props

Any prop which changes between many values at runtime should **not** be passed to a styled component. This avoids the overhead of styled-components generating a new class for each value. Use a traditional inline `style` prop instead.

```tsx
interface Props {
   isReady: boolean
   widthPx: number // Varies with a continuous distribution
}

// Good
const GoodContainer = styled.div<{$isReady: boolean}>`
   background: ${p => p.$isReady ? "green" : "red"};
`

export function GoodComponent({isReady, widthPx}: Props) {
   return <GoodContainer $isReady={isReady} style={{width: `${widthPx}px`}} />
}

// Bad
const BadComponent = styled.div<{$isReady: boolean, $widthPx: number}>`
   background: ${p => p.$isReady ? "green" : "red"};
   width: ${p => p.$widthPx}px;
`
```

### Spacing (padding, margins, gaps)

Many reusable components expose props for padding (`p`, `px`, `py`, `pr`, `pl`, `pt`, `pb`), margin (`m`, `mx`, `my`, …) and gap (`gap`). Prefer setting spacing through these props. They are limited to a specific subset of values (e.g. `"4px"`, `"8px"`) matching the design system — don't use other values for spacing 99% of the time.

When creating new components or overriding styles, use the `Spacing` constant from `@attio/picasso`:

```tsx
import {styled} from "styled-components"
import {Form, Spacing} from "@attio/picasso"

const StyledFormHelpText = styled(Form.HelpText)`
    ${Spacing.px("16px")}
    ${Spacing.pt("16px")}
    ${Spacing.pb("0px")}
`
```

Use predefined values for border radii from theme:

```jsx
export const Link = styled.a`
    border-radius: ${({theme}) => theme.borderRadii[6]};
`
```

### Color tokens

We have fixed tokens for colours. **Never define raw hex values or use string names for colours.**

Many components expose a `variant` prop to select predefined colour variants:

```tsx
<Typography.Caption.Standard.Component variant="secondary">
    Some text...
</Typography.Caption.Standard.Component>
```

When creating new components or adjusting styling, always use tokens from the theme. This is critical because we support light and dark mode — tokens ensure the correct colour is selected for both.

In styled-components:

```tsx
export const CheckoutPreviewContainer = styled(Layout.Stack)`
    ${Spacing.p("20px")}
    border-left: 1px solid ${({theme}) => theme.tokens.stroke.primary};
    border-bottom-right-radius: ${({theme}) => theme.borderRadii["16"]};
    background: ${({theme}) => theme.tokens.surface.secondary};
`
```

In React components, access colours through the `useTheme` hook. Modify colours using utilities like `opacify`:

```jsx
import Color from "color"
import {opacify} from "polished"

const theme = useTheme()
const backdropActiveColor = theme.alphas.bgOverlay
const backdropColor = opacify(1)(backdropActiveColor)
const activeOpacity = Color(backdropActiveColor).alpha()
```

### Turn on scrollbars

Develop with scrollbars set to "Always" in macOS System Settings. This ensures you spot unexpected overflow issues that users with this setting (or Windows users) will see.
