Installation
Start by making sure you have an npm token from the @ovotech npm org.
yarn add @ovotech/element
or
yarn add @ovotech/element-native
Or if you prefer:
npm install --save @ovotech/element
or
npm install --save @ovotech/element-native
Element has been built with an opinionated stack of react and styled-component. This means you'll need to make sure you’ve installed the following peer dependencies:
- react
- styled-components (react-native)
- react-native (react-native)
- react-native-svg (react-native)
- react-native-reanimated (react-native)
- react-native-gesture-handler (react-native)
- react-native-safe-area-context (react-native)
- @react-native-masked-view/masked-view (react-native)
CSS reset
When using them on the web, the design system components rely on global styles to provide a CSS reset.
We export one of these that uses sensible defaults, and most crucially, responsive font sizing. It can be imported as below:
// Webpack will need a css-loader to import this.
import '@ovotech/element/reset.css';
Cascade Layers
Starting with version 4.6.1 of the Element web package, cascade layers now wrap the reset (`reset` layer) and all component CSS module styles (`element` layer). This helps with loading our CSS in a predictable order, so it is possible to configure it at a lower level than any required overrides in a consuming project, e.g.
/* from lowest to highest priority, with overrides as an example of YOUR styles. */
@layer reset, element, overrides;
When working with NextJS, it isn’t always simple to predict the loading order of CSS files.
For this reason, it’s important to place the above @layer declaration at the top of a global file in src/app/global.css as this will load before other CSS files and set the cascading order correctly.
When working with NextJS <16, it has also been found that setting Turbopack as the compiler (with the --turbo flag) is required for this loading order (Oct. ‘25). Version 16 sets Turbo as the default compiler.
Using the fonts
Download the font files here: 2023.04.03-OVOCircular.zip (opens in new window)
It's best to use the fonts locally to your project. Use @font-face declarations to load these and set up the family in line with the typography tokens in Element.
For example:
@font-face {
font-family: 'OVOCircular';
src: url('./path/to/OVOCircularWeb-Book.woff2') format('woff2');
font-weight: 450;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'OVOCircular';
src: url('./path/to/OVOCircularWeb-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'OVOCircular';
src: url('./path/to/OVOCircularWeb-Black.woff2') format('woff2');
font-weight: 900;
font-style: normal;
font-display: swap;
}
Setting the theme
Element native uses styled-components, which need a theme - you can import from @ovotech/element-native. Use styled-component's <ThemeProvider> at the root of the app.
Element (web) uses CSS Modules under the hood, which need a theme.css from @ovotech/element . These are a set of CSS variables set on the :root element, with a capability in the Core package to generate sets for modes and brands (please reach out if this is of interest!).
import '@ovotech/element/theme.css'
ReactDOM.createRoot(document.getElementById('root')).render(<MyApp />)
import { ThemeProvider } from 'styled-components/native';
import { themeNative } from '@ovotech/element-native';
<ThemeProvider theme={themeNative}>
<MyApp />
</ThemeProvider>;
Dark mode
Web
Wrap your app once in ElementThemeProvider. It holds the active theme and writes data-element-mode onto the document, which the design system's dark CSS keys on:
import { ElementThemeProvider } from '@ovotech/element';
<ElementThemeProvider>
<App />
</ElementThemeProvider>;
Read or change the theme anywhere with useTheme():
import { useTheme } from '@ovotech/element';
const { themeName, toggleTheme } = useTheme();
toggleTheme(themeName === 'dark' ? 'light' : 'dark');
On web the choice persists in localStorage. The attribute is written in an effect, so the server and first client paint are light before settling to the stored theme - a dark-on-load user may briefly see one light frame.
Native
There's a matching ElementThemeProvider in @ovotech/element-native that wraps styled-components' ThemeProvider. There's no localStorage, so run it controlled themeName + onThemeChange) or uncontrolled defaultThem, in-memory). If you customise your theme with createTheme, pass it as light / dark so the provider switches between your themes rather than the design-system defaults:
<ElementThemeProvider light={myLightTheme} dark={myDarkTheme}>
<App />
</ElementThemeProvider>;
Responsive Breakpoints (web)
From 5.14.0, Element ships its breakpoints as named custom media queries, so your CSS can say what it means instead of repeating pixel values:
.sidebar {
display: none;
}
@media (--medium-and-up) {
.sidebar {
display: block;
}
}
The full set, matching the breakpoint design tokens:
|
Query |
Condition |
|---|---|
|
--xsmall-and-up |
min-width: 360px |
|
--small-and-up |
min-width: 577px |
|
--medium-and-up |
min-width: 961px |
|
--large-and-up |
min-width: 1201px |
|
--xsmall-and-down |
max-width: 359px |
|
--small-and-down |
max-width: 576px |
|
--medium-and-down |
max-width: 960px |
|
--large-and-down |
max-width: 1200px |
Because they come from the package, your breakpoints stay in step with Element’s tokens across upgrades - no more hand-copied pixel values drifting from the source.
Setup
Browsers don't support custom media queries natively yet, so they're transformed at build time by PostCSS. Install the two plugins:
pnpm install --dev postcss-custom-media @csstools/postcss-global-data
Then in postcss.config.js, feed Element’s definitions in as global data (Vite, Next.js and anything else PostCSS-based picks this file up automatically):
import { createRequire } from "node:module";
import postcssGlobalData from "@csstools/postcss-global-data";
import postcssCustomMedia from "postcss-custom-media";
export default {
plugins: [
postcssGlobalData({
// Resolve through the package export map so the path survives pnpm's symlinked layout.
files: [
createRequire(import.meta.url).resolve(
"@ovotech/element/custom-media.css",
),
],
}),
postcssCustomMedia(),
],
};
Your app can add its own project-specific queries by listing another file in files so that you can extend ours if needed.
Using the components
Element exports all components as named exports as documented throughout, for example:
import { Button } from '@ovotech/element';
const Example = () => (
<Button onClick={() => alert('Yippee!')}>
I ❤ Design Systems
</Button>
);
import { PrimaryCTAButton } from '@ovotech/element-native';
const Example = () => (
<PrimaryCTAButton onPress={() => alert('Yippee!')}>
I ❤ Design Systems
</PrimaryCTAButton>
)
Using the theme object
In an ideal scenario user interfaces (UIs) would be built entirely using Element components. However, considering the components and layout utilities provided by the system are far from a complete set, there will be times when you will need to build your UI using custom styling. In these situations, we'd recommend leveraging the theme object provided by Element in order to maintain consistency with our components, and by extension enabling themability in future.
The theme comprises of core, semantic, and component tokens, with core containing unique values that are generally aliased in the two other sub-objects. For this reason, unless modifying the underlying theme and visuals, it is recommended to always use semantic and component tokens wherever possible when working with the Element theme.
import '@ovotech/element/theme.css'
export default function Theme() {
return (
<div>
<p>Some text</p>
</div>
);
}
import { createTheme } from '@ovotech/element-native';
const theme = createTheme({});
export default function Theme() {
return (
<View>
{theme}
</View>
);
}
Linting your CSS (web)
Also from 5.14.0, Element ships a shareable Stylelint config that catches the two most common ways CSS drifts off the design system:
- Unknown custom properties: a var(—colour-primary) typo, or a token that was removed in an upgrade, is an error. Values are checked against the tokens Element ships (theme.css).
- Hardcoded values where a token exists: a few hexrgb() colour, or a pixel value in spacing, radius or typography properties, gets a warning pointing you back to the token set.
Setup
Install the peer dependencies (Stylelint 16 ot 17):
pnpm install --dev stylelint stylelint-value-no-unknown-custom-properties
And extend the config in stylelint.config.js:
export default {
extends: ["@ovotech/element/stylelint-config"],
}
Why hardcoded values are warnings, not errors
Existing codebases may well have some hardcoded values, and a minor Element upgrade shouldn't turn your CI red. Adopt the config, burn the warnings down at your own pace, then raise the rule to error severity in your own config once you're clean. Where no token genuinely fits, disable the rule on that line with a comment explaining why.
Adding your own tokens
If your app defines its own custom properties, the unknown-properties rule needs to know about them. Stylelint's extends replaces rule options rather than merging them, so re-specify the rule with Element's sources (exported for exactly this) plus your own files:
import { elementTokenSources } from "@ovotech/element/stylelint-config";
export default {
extends: ["@ovotech/element/stylelint-config"],
rules: {
"csstools/value-no-unknown-custom-properties": [
true,
{
importFrom: [...elementTokenSources, "src/styles/tokens.css"],
},
],
},
};
Two more named exports are available for composition:
- elementTokenBypassRules - the hardcoded-value rules on their own, if you want them without the rest.
- elementInternalTokenSources - Element's component-internal variables (the per-component CSS in dist/assets/). These are not public API and can change in any release; opt in only to unblock existing code that already references them, and treat that as a migration marker, not a licence to add more.