@guardian/stand
v0.0.7
Published
_Find what you need on the (news)stand!_
Downloads
278
Keywords
Readme
Stand - a tools component library
Find what you need on the (news)stand!
Stand is component library for Guardian editorial tools. It is co-located within flexible-content as Composer is expected to be the main consumer of the UI components within Stand. But any editorial tool should be able to make use of the components as an npm package - @guardian/stand - and developers should feel comfortable contributing.
Installation
$ pnpm add @guardian/stand # or yarn or npmYou'll also need to have TypeScript, React, and Emotion installed in your project.
The compatible versions are listed in the peerDependencies section of package.json.
Some components have additional dependencies that you will need to install too. See the Components section for more details for which components have which peer dependencies.
Foundations
The Editorial Design System foundations are available via Stand. These are split into two categories: Semantic and Base / Primitives.
In most cases consumers should use the Semantic tokens, which are more meaningful abstractions of the Base / Primitives tokens, i.e applied to specific use cases.
The base / primitives tokens are available for low-level use cases, or very specific cases, but these should be avoided where possible in favour of the semantic tokens.
Stand provides the foundations via CSS variables, as well as JS/TS exports for use in code, which could also be used in CSS-in-JS solutions.
Base / Primitive tokens should not be overridden if they are used directly, as this could have unintended consequences. Instead override the Semantic tokens which are designed to be overridden.
Fonts
Most applications should only need to load the Open Sans and Guardian Headline fonts, as these are the primary fonts used across the Guardian's Editorial Tool design system.
You only need to load Guardian Text Egyptian if you're planning to use it in your project, in most cases you only need this when working on Guardian editorial content on an editorial tool, i.e. when using article-body-* semantic tokens.
Open Sans
We're currently looking at how to best self host this font under a Guardian specific domain, before we release @guardian/stand design system for wider usage.
For now, the Open Sans variable font can be loaded via Google Fonts:
- Visit Google Fonts - Open Sans
- Click "Get Font" -> "Get embed code"
- Click "Change styles" dropdown
- Use "Full axis" for all options (Italic, Weight, Width)
- Copy the relevant
<link>tag or@importcode snippet into your project- You don't need to include the CSS class, as the design system will handle applying the correct font-family via CSS variables or JS/TS tokens.
Guardian Fonts
Make sure to visit guardian/fonts repo for the latest information on how to self-host these fonts.
In general, we always want to use the full-not-hinted versions of the fonts where possible.
Guardian Headline
We only use the bold weight (700) of Guardian Headline in the design system.
@font-face {
font-family: 'GH Guardian Headline';
src: url('https://assets.guim.co.uk/static/frontend/fonts/guardian-headline/full-not-hinted/GHGuardianHeadline-Bold.woff2')
format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}Guardian Text Egyptian
We want the regular/normal weight (400), the bold weight (700), and the italic version of each weight for Guardian Text Egyptian in the design system.
@font-face {
font-family: 'GuardianTextEgyptian';
src: url('https://assets.guim.co.uk/static/frontend/fonts/guardian-textegyptian/full-not-hinted/GuardianTextEgyptian-Regular.woff2')
format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'GuardianTextEgyptian';
src: url('https://assets.guim.co.uk/static/frontend/fonts/guardian-textegyptian/full-not-hinted/GuardianTextEgyptian-RegularItalic.woff2')
format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'GuardianTextEgyptian';
src: url('https://assets.guim.co.uk/static/frontend/fonts/guardian-textegyptian/full-not-hinted/GuardianTextEgyptian-Bold.woff2')
format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'GuardianTextEgyptian';
src: url('https://assets.guim.co.uk/static/frontend/fonts/guardian-textegyptian/full-not-hinted/GuardianTextEgyptian-BoldItalic.woff2')
format('woff2');
font-weight: 700;
font-style: italic;
font-display: swap;
}Semantic
Colors
import { css } from '@emotion/react';
import { semanticColors } from '@guardian/stand'; // JS/TS usage
import '@guardian/stand/semantic/colors.css'; // CSS usage
const stringStyle = css`
color: ${semanticColors.text.primary}; /* JS/TS usage */
background-color: var(
--semantic-colors-bg-default-on-light
); /* CSS usage */
`;
const objectStyle = {
color: semanticColors.text.primary /* JS/TS usage */,
backgroundColor:
'var(--semantic-colors-bg-default-on-light)' /* CSS usage */,
};For a list of available semantic color styles see the Storybook Semantic Colors section.
For a full list of CSS Semantic Color tokens see semantic/colors.css.
Typography
import { css } from '@emotion/react';
import {
semanticColors,
semanticTypography,
convertTypographyToEmotionObjectStyle, // helper function to convert from typography token object to emotion CSS object style
convertTypographyToEmotionStringStyle, // helper function to convert from typography token object to emotion CSS string style
} from '@guardian/stand'; // JS/TS usage
import '@guardian/stand/semantic/typography.css'; // CSS usage
/* JS/TS usage */
const stringStyleJS = css`
${convertTypographyToEmotionStringStyle(
semanticTypography['body-compact-md'],
)}
`;
const objectStyleJS = {
// other styles e.g.
color: semanticColors.text.primary,
// typography styles
...convertTypographyToEmotionObjectStyle(
semanticTypography['body-compact-sm'],
),
};
/* CSS usage */
const stringStyleCSS = css`
/* CSS usage */
font: var(--semantic-typography-body-compact-sm-font);
letter-spacing: var(--semantic-typography-body-compact-sm-letter-spacing);
font-variation-settings: 'wdth'
var(--semantic-typography-body-compact-sm-font-width);
`;
const objectStyleCSS = {
fontFamily: 'var(--semantic-typography-body-compact-sm-font-family)',
fontWeight: 'var(--semantic-typography-body-compact-sm-font-weight)',
fontSize: 'var(--semantic-typography-body-compact-sm-font-size)',
lineHeight: 'var(--semantic-typography-body-compact-sm-line-height)',
letterSpacing: 'var(--semantic-typography-body-compact-sm-letter-spacing)',
fontVariationSettings: `'wdth' var(--semantic-typography-body-compact-sm-font-variation-settings)`,
};For a list of available typography styles see the Storybook Semantic Typography section.
For a full list of CSS Semantic Typography tokens see semantic/typography.css.
Base / Primitives
Colors
import { css } from '@emotion/react';
import { baseColors } from '@guardian/stand'; // JS/TS usage
import '@guardian/stand/base/colors.css'; // CSS usage
const stringStyle = css`
color: ${baseColors.neutral['900']}; /* JS/TS usage */
background-color: var(--base-colors-blue-500); /* CSS usage */
`;
const objectStyle = {
color: baseColors.neutral['900'] /* JS/TS usage */,
backgroundColor: 'var(--base-colors-blue-500)' /* CSS usage */,
};For a list of the available base/primitives color styles see the Storybook Base Colors section.
For a full list of CSS Base/Primitives Color tokens see base/colors.css.
Typography
import { css } from '@emotion/react';
import { baseTypography } from '@guardian/stand'; // JS/TS usage
import '@guardian/stand/base/typography.css'; // CSS usage
/* JS/TS usage */
const stringStyleJS = css`
font-family: ${baseTypography.family['Open Sans']};
font-size: ${baseTypography.size['14-px']};
font-weight: ${baseTypography.weight['Open Sans'].normal};
font-variation-settings: 'wdth' ${baseTypography.width['Open Sans']};
style: ${baseTypography.style.normal};
line-height: ${baseTypography.lineHeight.normal};
letter-spacing: ${baseTypography.letterSpacing['default-px']};
`;
const objectStyleJS = {
fontFamily: baseTypography.family['Open Sans'],
fontSize: baseTypography.size['14-px'],
fontWeight: baseTypography.weight['Open Sans'].normal,
fontVariationSettings: `'wdth' ${baseTypography.width['Open Sans']}`,
fontStyle: baseTypography.style.normal,
lineHeight: baseTypography.lineHeight.normal,
letterSpacing: baseTypography.letterSpacing['default-px'],
};
/* CSS usage */
const stringStyleCSS = css`
font-family: var(--base-typography-family-open-sans);
font-size: var(--base-typography-size-14-px);
font-weight: var(--base-typography-weight-open-sans-normal);
font-variation-settings: 'wdth' var(--base-typography-width-open-sans);
font-style: var(--base-typography-style-normal);
line-height: var(--base-typography-line-height-normal);
letter-spacing: var(--base-typography-letter-spacing-default-px);
`;
const objectStyleCSS = {
fontFamily: 'var(--base-typography-family-open-sans)',
fontSize: 'var(--base-typography-size-14-px)',
fontWeight: 'var(--base-typography-weight-open-sans-normal)',
fontVariationSettings: `'wdth' var(--base-typography-width-open-sans)`,
fontStyle: 'var(--base-typography-style-normal)',
lineHeight: 'var(--base-typography-line-height-normal)',
letterSpacing: 'var(--base-typography-letter-spacing-default-px)',
};For a list of the available base/primitives typography tokens see the Storybook Base Typography section.
For a full list of CSS Base/Primitives Typography tokens see base/typography.css.
Components
Byline
A flexible byline editor component built in ProseMirror and React with usability and accessibility in mind.
Peer dependencies:
You'll need to install the following peer dependencies in your project to use the Byline component:
@guardian/prosemirror-invisiblesprosemirror-dropcursorprosemirror-historyprosemirror-keymapprosemirror-modelprosemirror-stateprosemirror-view
See the peerDependencies section of package.json for compatible versions to install.
Usage
import type { BylineModel } from '@guardian/stand';
import { Byline } from '@guardian/stand';
const Component = () => {
const bylineModel: BylineModel = {
// ...set up your byline model here
};
...
return (
<>
...
<Byline initialValue={bylineModel} />
...
</>
);
};By itself the Byline component is just the editor UI. You will need to set up the ProseMirror editor state, schema, and plugins to get a fully functioning byline editor. See the props and example below for a more complete implementation.
The BylineModel type defines the structure of the byline data which is agnostic from any other data structure. You must convert to/from this model when integrating with your application's data structures.
Props
See BylineProps for the full list of props, usage example can be seen in Storybook.
Example
The ContentByline component in flexible-frontend has a detailed example of how to use the Byline component from Stand. See ContentByline.tsx.
TagPicker
TagAutocomplete
Status: Testing
Part of the overall TagPicker component, the TagAutocomplete provides an accessible autocomplete input for selecting tags from a list of options, based on the React Aria ComboBox component.
Peer dependencies:
react-aria-components
See the peerDependencies section of the package.json for compatible versions to install.
Props
See TagAutocompleteProps for the full list of props, usage example can be seen in Storybook.
TagTable
Status: Testing
Part of the overall TagPicker component, the TagTable provides an accessible table for displaying tags, with options to add, remove, and reorder tags via drag and drop, based on the React Aria Table component.
Peer dependencies:
react-aria-components
See the peerDependencies section of the package.json for compatible versions to install.
Props
See TagTableProps for the full list of props, usage example can be seen in Storybook.
Usage
Example with TagAutocomplete and TagTable combined:
import { TagAutocomplete, TagTable } from '@guardian/stand';
const Component = () => {
const [selectedTags, setSelectedTags] = useState<
TagManagerObjectData[] // TagManagerObjectData is an internal type representing a Tag
>([]);
const [options, setOptions] = useState<TagManagerObjectData[]>([]);
const [value, setValue] = useState('');
const onChange = (inputText: string) => {
setValue(inputText);
if (inputText === '') {
setOptions([]);
return;
}
if (inputText === '*') {
setOptions(exampleTags); // exampleTags is an array of Tags
return;
}
// Simple filtering against exampleTags
const filteredItems = exampleTags.filter((t) =>
t.internalName.toLowerCase().includes(inputText.toLowerCase()),
);
return setOptions(filteredItems);
};
return (
<>
<div
css={css`
display: flex;
`}
>
<TagAutocomplete
onChange={onChange}
options={options}
label="Tags"
addTag={(tag) =>
setSelectedTags((tags) => {
return [...tags, tag];
})
}
loading={false}
placeholder={''}
disabled={false}
value={value}
/>
<select>
option>All tags</option>
</select>
</div>
<TagTable rows={selectedTags} filterRows={() => true} />
</>
);
};Example
This is currently still in testing phase, so a production implementation is not yet available.
Contributing
See the Contributing to Stand documentation for guidelines on contributing to this project. Project setup and common tasks are listed below.
Setup
- Run
./setup.shin the project root (flexible-content) directory to set up pnpm, install dependencies, and build the project.
Tasks
- Run
pnpm installto install dependencies. - Run
pnpm buildto build, this makes any changes available to flexible-frontend - Run
pnpm storybookto run Storybook - Run
pnpm build:storybookto build the Storybook static site - Run
pnpm build-styledto build the Style Dictionary styles - Run
pnpm testto run tests - Run
pnpm test:e2eto run end-to-end tests using Playwright - Run
pnpm test:react-matrixto run matrix tests (see Compatibility section below) - Run
pnpm tscto run check TypeScript types - Run
pnpm lintto run the linter- Run
pnpm lint:fixto fix any auto-fixable issues
- Run
- Run
pnpm format:checkto check code formatting- Run
pnpm format:fixto fix code formatting issues
- Run
Style Dictionary
The project uses Style Dictionary to manage design tokens.
Tokens are defined in the src/styleD/tokens folder.
The output styles are generated into the src/styleD/build folder.
We use rollup to copy the built styles into the dist/styleD/build folder during the build process, and these are published with the package.
Most tokens are generated from Figma variables using a script to make these available in the src/styleD/tokens folder. See the Generate Design Tokens from Figma Variables documentation for more details.
Use pnpm build-styled to generate the styles after making changes to the tokens and make sure to test and commit the changes to the built styles.
See the Style Dictionary documentation for more details on how we structure and generate the styles.
Compatibility
See the package.json peerDependencies section for compatible versions of React and other dependencies that Stand works with.
Version sets for matrix testing live in ./scripts/deps-matrix-versions.json:
The test script ./scripts/test-deps-matrix.sh reads this JSON file first, then applies any environment overrides you supply. Precedence is:
- Explicit env var (e.g.
REACT_VERSIONS="18.0.0 19.0.0") - Value from
deps-matrix-versions.json
All three variables (REACT_VERSIONS, EMOTION_VERSIONS, TS_VERSIONS) must be defined after loading; otherwise the script exits with an error.
Matrix generation in CI uses the same JSON file in the workflow: ../.github/workflows/stand-component-library-deps-matrix.yml to ensure consistency.
Updating Supported Versions
- Edit
./scripts/deps-matrix-versions.jsonwith new versions - Run the matrix test locally:
./scripts/test-deps-matrix.sh - (Optional) Narrow the matrix with overrides:
REACT_VERSIONS="18.0.0" EMOTION_VERSIONS="11.14.0" TS_VERSIONS="5.1" ./scripts/test-deps-matrix.sh - Review results (table output and any failures). Fix issues or adjust code
- Update
peerDependenciesinpackage.jsonto reflect the new minimum / tested range - Open a PR, the CI pipeline will comment with the compatibility matrix
Tips
- Keep versions in ascending order for readability
- Remove deprecated versions only after confirming no downstream tool depends on them
- Add a new version first, then run the matrix, then adjust
peerDependenciesonce green - Changes to
peerDependenciesare always a breaking change to the library, as per our recommendations
