diff --git a/docs/Coding-Standards/Styles.mdx b/docs/Coding-Standards/Styles.mdx index 4c4cb09fe62..bd2e132499f 100644 --- a/docs/Coding-Standards/Styles.mdx +++ b/docs/Coding-Standards/Styles.mdx @@ -4,154 +4,276 @@ import { Meta } from '@storybook/addon-docs/blocks'; # Coding standards: Styles -## Use Emotion best practices +## Use SCSS Modules, not Emotion, for new styling work -We use a library called [Emotion](https://emotion.sh/docs/introduction) for writing css styles with JavaScript. It builds on many other CSS-in-JS libraries such as [Styled Components](https://styled-components.com/). It has many benefits such as predictable composition to avoid specificity issues with CSS, abstractions for rendering critical CSS and the ability to colocate styles with components. +Simorgh is progressively migrating its styling foundation from [Emotion](https://emotion.sh/docs/introduction) (CSS-in-JS, `ThemeProvider`) to **SCSS Modules + CSS custom properties** (`ThemeProviderSCSSModules`). Both approaches co-exist in the codebase during the migration, but: -It is recommended to look over [Emotion's best practices](https://emotion.sh/docs/best-practices) for recommendations on how to best use this library. +- **All new component styling must use SCSS Modules.** Do not add new Emotion `css` prop or `styled` usage. +- **When you modify an existing Emotion-styled component, migrate it to SCSS Modules as part of that change.** -## Use the `css` prop from Emotion with object styles rather than the `styled` API. +### How to style a component -When Simorgh and Psammead components were first created, they used Styled Components rather than Emotion for attaching styles to components. When we migrated to Emotion we didn't have to remove use of Styled Component's `styled` API because Emotion provides support for this API. Now that we are building components with new standards there is the opportunity to use [Emotion's primary method to style components](https://emotion.sh/docs/css-prop) - the `css` prop. +Co-locate an `index.module.scss` file next to the component, import it as a `styles` object, and apply classes via `className`: -Here are some of the benefits of using the `css` prop: +✅ -- The Emotion docs state that using the CSS prop is the primary way to style components. This puts us in a good position if Emotion ever decides to drop support for the `styled` API -- With the styled API you're more likely to pollute the DOM with incorrect attributes — a common problem when passing props to styled components to achieve dynamic styles. -- The CSS object is typechecked and provides autocompletion for the CSS property with a description of the property and all possible values e.g. you type `display` and autocomplete gives you a list to choose from `block, flex, inline-flex, grid` etc. TypeScript will also highlight an incorrect value for a property and fail a type check. -- Naming components using the `styled` API (e.g. `StyledWrapper`, `StyledDateTime`, `StyledSpan`) can be burdensome especially when applying small custom styles, such as altering margins or padding. This results in many components which lack the obvious semantic importance you get when using the actual HTML elements. Using the `css` prop avoids having to create and name React components for every element that needs styles. -- Style reuse is easier because you can pass in an array of style objects to the css prop +```scss +// index.module.scss +@use '@scss/themeTokens' as theme; -**NB** -To use Emotion's css prop in TypeScript components we will need to specify the JSX factory at the top of every file. This is because Simorgh currently uses React's old JSX transform. More information can be found here https://emotion.sh/docs/typescript#with-the-old-jsx-transform. +.promoBox { + position: relative; + background-color: theme.$palette-white; + padding: #{theme.$spacings-double}; -Simorgh cannot currently use the new JSX transform because it is not supported in Opera Mini. + // Dark UI override + :global([data-is-dark-ui='true']) & { + background-color: theme.$palette-grey-7; + } +} -**Usage** +.link { + @include theme.fontSizes-gel-font-size(pica); + @include theme.fontVariants-gel-font-variant('serif-bold'); + color: theme.$palette-grey-8; -`css` prop with object styles + &:visited { + color: theme.$palette-grey-6; + } +} +``` -✅ +```tsx +// index.tsx +import styles from './index.module.scss'; -```jsx -import { css } from '@emotion/react'; +const Promo = ({ href, title }: PromoProps) => ( +
+ + {title} + +
+); +``` +❌ + +```jsx +// Do not add new Emotion styling const styles = { wrapper: css({ backgroundColor: 'white', - border: '1px solid #eee', - borderRadius: '0.5rem', padding: '1rem', }), - - title: css({ - fontSize: '1.25rem', - }), }; const Promo = ({ title, children }) => ( -
-
{title}
- {children} -
+
{children}
); ``` -Using an array for style reuse or applying one off styles to override default styles +## Use theme tokens instead of hard-coded values + +Design tokens (palette, spacing, font sizes/variants, media queries) are exposed as SCSS variables and mixins via `@scss/themeTokens`, which forwards from the shared files in `src/app/components/ThemeProviderSCSSModules/`. Always import and use these rather than hard-coding colours, spacings, font values or breakpoints. ✅ -```jsx -const Promo = ({ title, children }) => ( -
-
{title}
- {children} -
-); +```scss +@use '@scss/themeTokens' as theme; + +.label { + @include theme.fontSizes-gel-font-size(brevier); + @include theme.fontVariants-gel-font-variant('sans-regular'); + color: theme.$palette-shadow; + padding: 0 #{theme.$spacings-double}; + + @media #{theme.$mediaQueries-group-4-min-width} { + padding: 0; + } +} ``` -Getting access to the theme in styles +❌ + +```scss +.label { + font-size: 14px; + color: #4c4c4c; + padding: 0 16px; + + @media (min-width: 63em) { + padding: 0; + } +} +``` + +Service-specific brand/theme values (e.g. brand colours, font families, font scripts) are set as **CSS custom properties on `:root`** by each service's theme module (see `ThemeProviderSCSSModules/themes//`). Access these through the SCSS mixins provided by `themeTokens`, such as `theme.fontVariants-gel-font-variant('sans-regular')` and `theme.fontSizes-gel-font-size(pica)`, which resolve to the correct custom properties (with fallbacks) for the active service. + +## Handling dark UI + +Use the `:global([data-is-dark-ui='true']) &` selector inside a component's `.module.scss` to override styles for dark UI, rather than branching in JavaScript/TypeScript: ✅ -```jsx -import { css } from '@emotion/react'; +```scss +.title { + color: theme.$palette-grey-10; -const styles = { - wrapper: css({ - backgroundColor: 'white', - border: '1px solid #eee', - borderRadius: '0.5rem', - padding: '1rem', - }), - title: theme => - css({ - color: theme.colours.primary, - fontSize: '1.25rem', - }), -}; + :global([data-is-dark-ui='true']) & { + color: theme.$palette-ghost; + } +} +``` -const Promo = ({ title, children }) => ( -
-
{title}
- {children} -
-); +## Handling Opera Mini fallbacks + +[CSS Grid Layout](https://developer.mozilla.org/en-US/docs/Web/CSS/grid) is a popular layout mechanism suitable for many use cases, unfortunately, one of our supported browsers Opera Mini does [not support the API](https://caniuse.com/css-grid). Based on this we have the following guidelines. + +### CSS Grid can only be used for page layout + +Where CSS Grid is used Opera Mini just falls back to the default inline layout, so on a desktop this layout in CSS grid: + +![](./images/grid-modern-browser.png) + +Will become: + +![](./images/grid-opera-mini.png) + +This fallback behaviour in most cases will provide an acceptable experience for a mobile page layout. Given Opera Mini is a mobile-only browser, we can assume that a single-column page layout will be acceptable, and thus we can use CSS grid to lay out pages for other browsers with confidence the fallback on Opera Mini will be acceptable. We still need to test the layout on Opera Mini before going live to ensure we don't have any unacceptable layout issues. + +See for further context: https://jira.dev.bbc.co.uk/browse/WSTEAMA-109 + +To be clear, CSS Grid should not be used outside of page layout use cases (e.g. to lay out a smaller component such as a Promo) as the component will just lay out its constituent parts inline and render incorrectly. + +### Don't implement CSS Grid + another layout + +One approach to support the use of CSS grid is to "[progressively enhance](https://www.smashingmagazine.com/2017/07/enhancing-css-layout-floats-flexbox-grid/#grid-enhancements)" and use `@supports(display: grid)`. In this approach we could make one implementation using CSS grid and wrap it in a `supports` feature query, then implement the layout again in a layout supported by Opera Mini such as Flexbox. This approach can be advocated for if a feature's support is coming soon to a browser, so the implementation is only temporary — we see no prospect of Opera Mini supporting CSS Grid anytime soon, however. + +With this in mind we discourage creating parallel layout implementations; it is better to just implement the layout once using an approach compatible with Opera Mini and all other browsers. + +### Use the `is-opera-mini` global class for targeted overrides + +Where an Opera Mini-specific override is unavoidable (e.g. it doesn't support `position: sticky` or certain flex behaviours), scope it with the `:global(.is-opera-mini)` selector inside the component's `.module.scss`, rather than adding conditional rendering or a UA-based prop in the component: + +✅ + +```scss +.promoBox { + position: relative; + width: 100%; + + // Opera Mini can't reliably lay these boxes out with flex/grid sizing, + // so give it an explicit width based on the same spacing used elsewhere in this layout + :global(.is-opera-mini) & { + width: calc(100% - #{theme.$spacings-full}); + + @media #{theme.$mediaQueries-group-2-min-width} { + width: calc(50% - #{theme.$spacings-double}); + } + } +} ``` +### Don't use Psammead-Grid + +Historically we have used the [`psammead-grid`](https://github.com/bbc/simorgh/tree/latest/src/app/legacy/psammead-grid) component to implement a grid layout in Simorgh. This component supports a grid layout that works on all our supported browsers including Opera Mini. We have decided to move away from this for a few reasons: + +- Its API has proved confusing for engineers to follow, as opposed to using standard layout implementations such as flex and grid. +- It produces quite a lot of CSS, increasing our page weight. +- It was overused for layout in places where other layouts are more appropriate. + +Instead of using Psammead-Grid, native layout implementations should be used. + ## LTR/RTL design ### When using margins, paddings, borders, use logical CSS properties -By using logical properties, you don’t have to manually write selectors including `[dir="rtl"]` or write CSS-in-JS like: +By using logical properties, you don't have to manually write selectors including `[dir="rtl"]` or write conditional style logic like: ❌ -```js -dir === 'ltr' - ? `padding-left: ${GEL_SPACING};` - : `padding-right: ${GEL_SPACING};`; +```scss +.wrapper { + padding-left: #{theme.$spacings-full}; + + :global([dir='rtl']) & { + padding-left: 0; + padding-right: #{theme.$spacings-full}; + } +} ``` -Instead, you can use logical CSS properties for bi-drectional horizontal rules, for example: +Instead, you can use logical CSS properties for bi-directional horizontal rules, for example: ✅ -```js -padding-inline-start: ${GEL_SPACING}; +```scss +.wrapper { + padding-inline-start: #{theme.$spacings-full}; +} ``` To better understand this, using `padding-inline-start` we are saying, not that we want some padding on the left of content, but that we want some padding at the start of the content, regardless of reading direction. -## Use properties like `grid-gap` for spacings between elements +## Use properties like `gap` for spacing between elements -For correct bi-directional spacing between elements, use properties like `grid-gap` rather than margins or paddings. Properties like `grid-gap` are better suited to bi-directional layouts. +For correct bi-directional spacing between elements, use properties like `gap` (flex/grid) rather than margins or paddings. Properties like `gap` are better suited to bi-directional layouts. + +```scss +.list { + display: flex; + flex-direction: column; + gap: #{theme.$spacings-full}; +} +``` ## Do not pass dir to components as a prop for applying LTR/RTL styles -Do not pass `dir` as a prop to components for using in CSS-in-JS styles e.g. +Do not pass `dir` as a prop to components in order to pick between different styles, e.g. + +❌ -```js -dir === 'ltr' - ? `padding-right: ${GEL_SPACING};` - : `padding-left: ${GEL_SPACING};`; +```tsx +
``` -This adds unnecessary props and logic to components and styles. +This adds unnecessary props and logic to components and duplicates styles for each direction. Use logical CSS properties in the `.module.scss` file instead, so a single class works correctly in both directions: -## Do not use the :dir() psuedo class +✅ -Currently, browser support for this class is extremely limited. +```tsx +
+``` + +```scss +.wrapper { + padding-inline-start: #{theme.$spacings-full}; +} +``` + +## Do not use the :dir() pseudo class + +Although [browser support for `:dir()`](https://caniuse.com/css-dir-pseudo) has improved significantly in modern browsers, it is not supported by Opera Mini, which Simorgh must support. Use logical CSS properties instead, as described above. https://developer.mozilla.org/en-US/docs/Web/CSS/:dir -## Do not use `dir` prop from ServiceContext +## `dir` from `ServiceContext` is for content decisions, not styling -We will add `dir` to the theme object. For consistency, get `dir` prop from the theme object with the `useTheme` hook from Emotion. Avoid using this prop for styles. It should only be used for serving different content based on the reading direction. +Read `dir` from `ServiceContext` (e.g. `const { dir } = use(ServiceContext);`) only to make content/rendering decisions that depend on reading direction — for example choosing which way an icon should point: + +✅ + +```tsx +const { dir } = use(ServiceContext); + +; +``` + +Do not use it to choose between different classes or styles in components. Reading-direction-dependent styling belongs in the `.module.scss` file, using logical properties as described above. ## Only use `dir` as an attribute on HTML elements with different language content -The `dir` attribute should normally only be present on the `html` element unless we have different language content with a different direction on a page, which we do sometimes e.g. 3rd party podcasts. +The `dir` attribute should normally only be present on the `html` element unless we have multiple different languages within a page, e.g. 3rd party podcasts. ## Resources @@ -172,22 +294,22 @@ A mobile-first approach to styling means that styles are applied first to mobile ❌ -```css +```scss @media (max-width: 37.4375rem) { - .promo-wrapper { + .promoWrapper { display: inline-block; } } @media (min-width: 37.5rem) and (max-width: 62.9375rem) { - .promo-wrapper { + .promoWrapper { display: flex; justify-content: center; } } @media (min-width: 37.5rem) { - .promo-wrapper { + .promoWrapper { display: flex; justify-content: center; } @@ -196,20 +318,20 @@ A mobile-first approach to styling means that styles are applied first to mobile ✅ -```css -.promo-wrapper { +```scss +.promoWrapper { display: inline-block; } @media (min-width: 37.5rem) { - .promo-wrapper { + .promoWrapper { display: flex; justify-content: space-between; } } @media (min-width: 63rem) { - .promo-wrapper { + .promoWrapper { justify-content: center; } } @@ -219,95 +341,69 @@ A mobile-first approach to styling means that styles are applied first to mobile - https://zellwk.com/blog/how-to-write-mobile-first-css/ -## Responsive styles: Use media queries provided in the `theme` object +## Responsive styles: Use the media query variables provided by `themeTokens` ### Why? -Using the media queries provided in the `theme` object means our breakpoints will be consistent and styles across different viewport widths will be in sync. +Using the media query variables forwarded from `ThemeProviderSCSSModules/mediaQueries.scss` means our breakpoints will be consistent and styles across different viewport widths will be in sync. ### How? ❌ -```js -const styles = { - wrapper: theme => - css({ - padding: theme.spacings.HALF, - '@media (min-width: 40rem)': { - padding: theme.spacings.FULL, - }, - }), -}; -``` - -✅ +```scss +.wrapper { + padding: #{theme.$spacings-half}; -```js -const styles = { - wrapper: theme => - css({ - padding: theme.spacings.HALF, - [theme.mq.GROUP_3_MIN_WIDTH]: { - padding: theme.spacings.FULL, - }, - }), -}; + @media (min-width: 40rem) { + padding: #{theme.$spacings-full}; + } +} ``` -## CSS-in-JS: Be aware that passing props to styled components will generate a new class for different arguments - -### Why? - -It helps to be mindful of how Emotion will generate styles when you pass a styled component a prop use as CSS properties. - -Each time you render a component with a different style prop, Emotion generates a new CSS class. If for example, the styled component has a long base64 string for use as a background image, then it would generate 3 huge classes each with the same background: +✅ -❌ +```scss +@use '@scss/themeTokens' as theme; -```js -const Logo = styled.div` - background: some-huge-base64-string; - height: ${({ height }) => height}px; -` +.wrapper { + padding: #{theme.$spacings-half}; - - - + @media #{theme.$mediaQueries-group-3-min-width} { + padding: #{theme.$spacings-full}; + } +} ``` -We should avoid cases like this and generally try to reduce our use of props for styling a component if it will be invoked multiple times on a page with different props. - -[CSS Grid Layout](https://developer.mozilla.org/en-US/docs/Web/CSS/grid) is a popular layout mechanism suitable for many use cases, unfortuntately, one of our supported browsers Opera Mini does [not support the API](https://caniuse.com/css-grid). Based on this we have the following guidelines - -## CSS Grid can only be used for page layout - -Where CSS Grid is used Opera Mini just fall back to the default inline layout so on a desktop this layout in css grid: +## Avoid generating dynamic class names for per-instance values -![](./images/grid-modern-browser.png) - -Will become: - -![](./images/grid-opera-mini.png) - -This fallback behaviour in most cases will provide an acceptable experience for a mobile page layout. Given Opera Mini is a mobile only browser, we can assume that a single column page layout will be acceptable and thus we can use CSS grid to layout pages for other browsers with confidence the fallback on Opera Mini will be acceptable. We still need to test the layout on Opera Mini before going live to ensure we don’t have any unacceptable layout issues. - -See for further context: https://jira.dev.bbc.co.uk/browse/WSTEAMA-109 - -To be clear, CSS Grid should not be used outside of page layout use cases (e.g. to layout a smaller component such as a Promo) as the component will just lay out it’s consitituent parts inline and render incorrectly. +With SCSS Modules, classes are static and shared across every instance of a component, which avoids a common CSS-in-JS pitfall: generating a new class (and duplicate CSS) every time a component renders with a different prop value. -## Don’t implement CSS Grid + Another Layout +If a component genuinely needs a per-instance dynamic value (e.g. a computed height or an image position), set a single CSS custom property inline and reference it from the `.module.scss` file. Avoid switching between multiple class names based on the value, or inlining a full style object with several CSS properties, either of which bypasses the `.module.scss` file entirely: -One approach to support the use of CSS grid is to ‘[progressively enhance](https://www.smashingmagazine.com/2017/07/enhancing-css-layout-floats-flexbox-grid/#grid-enhancements)’ and use `@supports(display: grid)` . In this approach we could make one implementation using css grid and wrap it in `supports` feature query and then implement the layout again in a layout supported by Opera Mini such as Flexbox. This approach can be advocated if a feature’s support is coming soon to a browser so the implementation is only temporary - we see no prospect of Opera Mini supporting CSS Grid anytime soon however. +✅ -With this in mind we discourage creating paralell layout implementations it is better to just implement the layout once using an approach compatible with Opera Mini and all other browsers. +```tsx +
+``` -## Don’t use Psammead-Grid +```scss +.wrapper { + height: var(--promo-height); +} +``` -Historically we have used the `[psammead-grid](https://github.com/bbc/simorgh/tree/latest/src/app/legacy/psammead-grid)` [component](https://github.com/bbc/simorgh/tree/latest/src/app/legacy/psammead-grid) to implement a grid-layout in Simorgh. This component supports a grid layout that works on all our supported browsers including Opera Mini. We have decided to move away from this for a few reasons: +❌ -- It’s API has proved confusing for engineers to follow as opposed to using standard layout implementations such as flex and grid -- It produces quite a lot of CSS increasing our page weight -- It was overused for layout in places where other layouts are more appropriate +```tsx +// Switching between multiple classes based on the value +
100 ? styles.tallWrapper : styles.wrapper} /> +``` -Instead of using Psammead-Grid, native layout implementations should be used. +```tsx +// Inlining a full style object, bypassing the .module.scss file entirely +
+```