From fd74fb61a2eb5e77bc14691d5b15a47ca52622c2 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Thu, 21 May 2026 17:11:48 +0100 Subject: [PATCH 1/6] Make centre rule configurable in grid API --- .../src/components/GalleryCaption.tsx | 24 --------- .../src/components/GalleryImage.tsx | 7 ++- dotcom-rendering/src/grid.ts | 53 +++++++++++++------ 3 files changed, 43 insertions(+), 41 deletions(-) diff --git a/dotcom-rendering/src/components/GalleryCaption.tsx b/dotcom-rendering/src/components/GalleryCaption.tsx index 26a71ba6205..9b5cf754a00 100644 --- a/dotcom-rendering/src/components/GalleryCaption.tsx +++ b/dotcom-rendering/src/components/GalleryCaption.tsx @@ -31,34 +31,10 @@ const styles = css` ${between.desktop.and.leftCol} { ${grid.column.right} - - position: relative; /* allows the ::before to be positioned relative to this */ - - &::before { - content: ''; - position: absolute; - left: -10px; /* 10px to the left of this element */ - top: 0; - bottom: 0; - width: 1px; - background-color: ${palette('--article-border')}; - } } ${from.leftCol} { ${grid.column.left} - - position: relative; /* allows the ::before to be positioned relative to this */ - - &::after { - content: ''; - position: absolute; - right: -11px; - top: -12px; - bottom: 0; - width: 1px; - background-color: ${palette('--article-border')}; - } } `; diff --git a/dotcom-rendering/src/components/GalleryImage.tsx b/dotcom-rendering/src/components/GalleryImage.tsx index 5ce317dcd10..5a37ed732c7 100644 --- a/dotcom-rendering/src/components/GalleryImage.tsx +++ b/dotcom-rendering/src/components/GalleryImage.tsx @@ -22,8 +22,9 @@ type Props = { }; const styles = css` + overflow: hidden; ${grid.paddedContainer} - ${grid.verticalRules()} + ${grid.verticalRules({ plusChild: 1 })} grid-auto-flow: row dense; background-color: ${palette('--article-inner-background')}; @@ -34,7 +35,9 @@ const styles = css` ${from.desktop} { &:first-of-type { - padding-top: ${space[3]}px; + & > * { + padding-top: ${space[3]}px; + } } } `; diff --git a/dotcom-rendering/src/grid.ts b/dotcom-rendering/src/grid.ts index f60ae0ffc5d..4aec02a60c2 100644 --- a/dotcom-rendering/src/grid.ts +++ b/dotcom-rendering/src/grid.ts @@ -88,36 +88,58 @@ const paddedContainer = ` // ----- Vertical Rules ----- // type VerticalRuleOptions = { - centre?: boolean; color?: string; + plusChild?: number; }; /** * Render Guardian grid vertical rules. * * Left and right rules are always present. - * A centre rule can optionally be enabled. + * A centre rule can optionally be enabled, anchored to the nth + * child of the grid container as specified by the `plusChild` option * * Usage: * css([grid.container, grid.verticalRules()]) - * css([grid.container, grid.verticalRules({ centre: true })]) + * css([grid.container, grid.verticalRules({ plusChild: 3 })]) */ -const optionalCentreRule = `/* CENTRE RULE */ - & > *:first-child::before { - grid-column: centre-column-start; - transform: translateX(-${columnGap}); - ${fromBreakpoint.leftCol} { - transform: translateX(calc(-${columnGap} / 2)); - } + +// The centre rule is self-contained on the nth child element rather than on +// the grid container, so that `top: 0` aligns to that element's top edge. +// `bottom` uses a large negative value to extend the rule down to the +// container's bottom; ensure `overflow: hidden` is set on the container +const optionalCentreRule = ( + nth: number, + color?: string, +): string => `/* CENTRE RULE */ + & > *:nth-child(${nth}) { + position: relative; + + &::before { + position: absolute; + top: 0; + bottom: -9999px; + width: 1px; + background-color: ${color ?? palette('--article-border')}; + content: ''; + grid-column: centre-column-start; + transform: translateX(-${columnGap}); + + ${fromBreakpoint.leftCol} { + transform: translateX(calc(-${columnGap} / 2)); + } + } }`; -const verticalRules = (options: VerticalRuleOptions = {}): string => ` +const verticalRules = (options: VerticalRuleOptions = {}): string => { + const { plusChild, color } = options; + + return ` ${fromBreakpoint.tablet} { position: relative; &::before, - &::after - ${options.centre ? ', & > *:first-child::before' : ''} { + &::after { position: absolute; top: 0; bottom: 0; @@ -146,8 +168,9 @@ const verticalRules = (options: VerticalRuleOptions = {}): string => ` } } - ${options.centre ? optionalCentreRule : ''} -`; + ${plusChild !== undefined ? optionalCentreRule(plusChild, color) : ''} + }`; +}; // ----- API ----- // From 7ba2773b37247e5aa442ebbfe921320b25c4a12c Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Fri, 29 May 2026 17:14:09 +0100 Subject: [PATCH 2/6] Move gallery centre rule depending on breakpoint --- dotcom-rendering/src/components/GalleryImage.tsx | 6 +++++- dotcom-rendering/src/grid.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/dotcom-rendering/src/components/GalleryImage.tsx b/dotcom-rendering/src/components/GalleryImage.tsx index 5a37ed732c7..8907b414ac2 100644 --- a/dotcom-rendering/src/components/GalleryImage.tsx +++ b/dotcom-rendering/src/components/GalleryImage.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/react'; import { isUndefined } from '@guardian/libs'; -import { from, space, until } from '@guardian/source/foundations'; +import { between, from, space, until } from '@guardian/source/foundations'; import { grid } from '../grid'; import { type ArticleFormat } from '../lib/articleFormat'; import { getImage } from '../lib/image'; @@ -40,6 +40,10 @@ const styles = css` } } } + + ${between.desktop.and.leftCol} { + ${grid.verticalRules({ plusChild: 2 })} + } `; const galleryBodyImageStyles = css` diff --git a/dotcom-rendering/src/grid.ts b/dotcom-rendering/src/grid.ts index 4aec02a60c2..caaaf8f8c70 100644 --- a/dotcom-rendering/src/grid.ts +++ b/dotcom-rendering/src/grid.ts @@ -125,7 +125,7 @@ const optionalCentreRule = ( grid-column: centre-column-start; transform: translateX(-${columnGap}); - ${fromBreakpoint.leftCol} { + ${fromBreakpoint.desktop} { transform: translateX(calc(-${columnGap} / 2)); } } From c76b7aa661fcff9285f4a5a410b55b825ce477e7 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Wed, 3 Jun 2026 11:00:34 +0100 Subject: [PATCH 3/6] Fix centre rule, clearer naming --- .../src/components/GalleryImage.tsx | 14 ++++----- dotcom-rendering/src/grid.ts | 30 +++++++++++-------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/dotcom-rendering/src/components/GalleryImage.tsx b/dotcom-rendering/src/components/GalleryImage.tsx index 8907b414ac2..2a6339e6d07 100644 --- a/dotcom-rendering/src/components/GalleryImage.tsx +++ b/dotcom-rendering/src/components/GalleryImage.tsx @@ -22,9 +22,8 @@ type Props = { }; const styles = css` - overflow: hidden; ${grid.paddedContainer} - ${grid.verticalRules({ plusChild: 1 })} + ${grid.verticalRules()} grid-auto-flow: row dense; background-color: ${palette('--article-inner-background')}; @@ -34,15 +33,16 @@ const styles = css` } ${from.desktop} { - &:first-of-type { - & > * { - padding-top: ${space[3]}px; - } + &:first-of-type > * { + padding-top: ${space[3]}px; } } ${between.desktop.and.leftCol} { - ${grid.verticalRules({ plusChild: 2 })} + ${grid.verticalRules({ centreRuleOnChildElement: 2 })} + } + ${from.leftCol} { + ${grid.verticalRules({ centreRuleOnChildElement: 1 })} } `; diff --git a/dotcom-rendering/src/grid.ts b/dotcom-rendering/src/grid.ts index caaaf8f8c70..ed9fa86eca6 100644 --- a/dotcom-rendering/src/grid.ts +++ b/dotcom-rendering/src/grid.ts @@ -89,7 +89,7 @@ const paddedContainer = ` type VerticalRuleOptions = { color?: string; - plusChild?: number; + centreRuleOnChildElement?: number; }; /** @@ -97,22 +97,24 @@ type VerticalRuleOptions = { * * Left and right rules are always present. * A centre rule can optionally be enabled, anchored to the nth - * child of the grid container as specified by the `plusChild` option + * child of the grid container as specified by the `centreRuleOnChildElement` option + * + * The centre rule is self-contained on the nth child element rather than on + * the grid container, so that `top: 0` aligns to that element's top edge. + * `bottom` uses a large negative value to extend the rule down to the + * container's bottom so `overflow: hidden` is set on the container. * * Usage: * css([grid.container, grid.verticalRules()]) - * css([grid.container, grid.verticalRules({ plusChild: 3 })]) + * css([grid.container, grid.verticalRules({ centreRuleOnChildElement: 3 })]) */ - -// The centre rule is self-contained on the nth child element rather than on -// the grid container, so that `top: 0` aligns to that element's top edge. -// `bottom` uses a large negative value to extend the rule down to the -// container's bottom; ensure `overflow: hidden` is set on the container const optionalCentreRule = ( - nth: number, + n: number, color?: string, ): string => `/* CENTRE RULE */ - & > *:nth-child(${nth}) { + overflow: hidden; + + & > *:nth-child(${n}) { position: relative; &::before { @@ -132,7 +134,7 @@ const optionalCentreRule = ( }`; const verticalRules = (options: VerticalRuleOptions = {}): string => { - const { plusChild, color } = options; + const { centreRuleOnChildElement, color } = options; return ` ${fromBreakpoint.tablet} { @@ -168,7 +170,11 @@ const verticalRules = (options: VerticalRuleOptions = {}): string => { } } - ${plusChild !== undefined ? optionalCentreRule(plusChild, color) : ''} + ${ + centreRuleOnChildElement !== undefined + ? optionalCentreRule(centreRuleOnChildElement, color) + : '' + } }`; }; From debdf6e666551d24e7a9de7845cef22d12c9d156 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Wed, 3 Jun 2026 15:06:54 +0100 Subject: [PATCH 4/6] Refactor grid to separate outer and inner rules --- .../components/DirectoryPageNav.island.tsx | 7 +- .../src/components/GalleryImage.tsx | 4 +- dotcom-rendering/src/grid.ts | 80 +++++++++++-------- 3 files changed, 49 insertions(+), 42 deletions(-) diff --git a/dotcom-rendering/src/components/DirectoryPageNav.island.tsx b/dotcom-rendering/src/components/DirectoryPageNav.island.tsx index c661f97f8be..98e6e70314a 100644 --- a/dotcom-rendering/src/components/DirectoryPageNav.island.tsx +++ b/dotcom-rendering/src/components/DirectoryPageNav.island.tsx @@ -304,12 +304,7 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { }); const navWeb = css({ - '&': css( - grid.paddedContainer, - grid.verticalRules({ - color: borderColor, - }), - ), + '&': css(grid.paddedContainer, grid.verticalRules(borderColor)), }); const largeLinkStyles = css({ diff --git a/dotcom-rendering/src/components/GalleryImage.tsx b/dotcom-rendering/src/components/GalleryImage.tsx index 2a6339e6d07..4567bb24933 100644 --- a/dotcom-rendering/src/components/GalleryImage.tsx +++ b/dotcom-rendering/src/components/GalleryImage.tsx @@ -39,10 +39,10 @@ const styles = css` } ${between.desktop.and.leftCol} { - ${grid.verticalRules({ centreRuleOnChildElement: 2 })} + ${grid.centreRule(2)} } ${from.leftCol} { - ${grid.verticalRules({ centreRuleOnChildElement: 1 })} + ${grid.centreRule(1)} } `; diff --git a/dotcom-rendering/src/grid.ts b/dotcom-rendering/src/grid.ts index ed9fa86eca6..6414cd0e723 100644 --- a/dotcom-rendering/src/grid.ts +++ b/dotcom-rendering/src/grid.ts @@ -87,33 +87,28 @@ const paddedContainer = ` // ----- Vertical Rules ----- // -type VerticalRuleOptions = { - color?: string; - centreRuleOnChildElement?: number; -}; - /** - * Render Guardian grid vertical rules. + * CSS for a centre vertical rule anchored to the top of the nth child of the + * grid container. * - * Left and right rules are always present. - * A centre rule can optionally be enabled, anchored to the nth - * child of the grid container as specified by the `centreRuleOnChildElement` option + * The rule is self-contained on the nth child element rather than on the grid + * container, so that `top: 0` aligns to that element's top edge. `bottom` + * uses a large negative value to extend the rule down to the container's + * bottom, so ensure `overflow: hidden` is set on the container. * - * The centre rule is self-contained on the nth child element rather than on - * the grid container, so that `top: 0` aligns to that element's top edge. - * `bottom` uses a large negative value to extend the rule down to the - * container's bottom so `overflow: hidden` is set on the container. + * @example + * css` + * overflow: hidden; + * ${grid.container} + * ${grid.verticalRules()} + * ${grid.centreRule(1)} * - * Usage: - * css([grid.container, grid.verticalRules()]) - * css([grid.container, grid.verticalRules({ centreRuleOnChildElement: 3 })]) + * ${from.leftCol} { + * ${grid.centreRule(3)} + * } + * ` */ -const optionalCentreRule = ( - n: number, - color?: string, -): string => `/* CENTRE RULE */ - overflow: hidden; - +const centreRule = (n: number, color?: string): string => `/* CENTRE RULE */ & > *:nth-child(${n}) { position: relative; @@ -133,10 +128,21 @@ const optionalCentreRule = ( } }`; -const verticalRules = (options: VerticalRuleOptions = {}): string => { - const { centreRuleOnChildElement, color } = options; +/** + * CSS for the left and right Guardian grid vertical rules. + * + * Use `grid.centreRule` separately to add a centre rule anchored to a + * specific child element. + * + * @example + * css` + * ${grid.container} + * ${grid.verticalRules()} + * ` + */ +const verticalRules = (color?: string): string => ` + overflow: hidden; - return ` ${fromBreakpoint.tablet} { position: relative; @@ -146,7 +152,7 @@ const verticalRules = (options: VerticalRuleOptions = {}): string => { top: 0; bottom: 0; width: 1px; - background-color: ${options.color ?? palette('--article-border')}; + background-color: ${color ?? palette('--article-border')}; content: ''; } @@ -169,14 +175,7 @@ const verticalRules = (options: VerticalRuleOptions = {}): string => { grid-column: centre-column-end; } } - - ${ - centreRuleOnChildElement !== undefined - ? optionalCentreRule(centreRuleOnChildElement, color) - : '' - } }`; -}; // ----- API ----- // @@ -277,10 +276,23 @@ const grid = { * breakpoint. */ mobileColumnGap, - + /** + * CSS for the left and right vertical rules. Use `grid.centreRule` + * separately to add a centre rule anchored to a specific child element. + */ verticalRules, + /** + * CSS for a centre vertical rule anchored to the top of the nth child of + * the grid container. Can be called multiple times within different + * breakpoint contexts to vary which child the rule anchors to. + */ + centreRule, } as const; +// ----- Types ----- // +type ColumnPreset = keyof typeof grid.column; + // ----- Exports ----- // +export type { Line, ColumnPreset }; export { grid }; From ad559c4a845a9e97838d4d74f6b7be9bdfac498b Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Wed, 3 Jun 2026 15:17:23 +0100 Subject: [PATCH 5/6] Rename verticalRules to outerRules --- .../src/components/DirectoryPageNav.island.tsx | 2 +- dotcom-rendering/src/components/GalleryImage.tsx | 2 +- dotcom-rendering/src/grid.ts | 8 ++------ dotcom-rendering/src/layouts/GalleryLayout.tsx | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/dotcom-rendering/src/components/DirectoryPageNav.island.tsx b/dotcom-rendering/src/components/DirectoryPageNav.island.tsx index 98e6e70314a..28e2379a3b9 100644 --- a/dotcom-rendering/src/components/DirectoryPageNav.island.tsx +++ b/dotcom-rendering/src/components/DirectoryPageNav.island.tsx @@ -304,7 +304,7 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { }); const navWeb = css({ - '&': css(grid.paddedContainer, grid.verticalRules(borderColor)), + '&': css(grid.paddedContainer, grid.outerRules(borderColor)), }); const largeLinkStyles = css({ diff --git a/dotcom-rendering/src/components/GalleryImage.tsx b/dotcom-rendering/src/components/GalleryImage.tsx index 4567bb24933..5c849fc37d5 100644 --- a/dotcom-rendering/src/components/GalleryImage.tsx +++ b/dotcom-rendering/src/components/GalleryImage.tsx @@ -23,7 +23,7 @@ type Props = { const styles = css` ${grid.paddedContainer} - ${grid.verticalRules()} + ${grid.outerRules()} grid-auto-flow: row dense; background-color: ${palette('--article-inner-background')}; diff --git a/dotcom-rendering/src/grid.ts b/dotcom-rendering/src/grid.ts index 6414cd0e723..60540ff435c 100644 --- a/dotcom-rendering/src/grid.ts +++ b/dotcom-rendering/src/grid.ts @@ -140,7 +140,7 @@ const centreRule = (n: number, color?: string): string => `/* CENTRE RULE */ * ${grid.verticalRules()} * ` */ -const verticalRules = (color?: string): string => ` +const outerRules = (color?: string): string => ` overflow: hidden; ${fromBreakpoint.tablet} { @@ -280,7 +280,7 @@ const grid = { * CSS for the left and right vertical rules. Use `grid.centreRule` * separately to add a centre rule anchored to a specific child element. */ - verticalRules, + outerRules, /** * CSS for a centre vertical rule anchored to the top of the nth child of * the grid container. Can be called multiple times within different @@ -289,10 +289,6 @@ const grid = { centreRule, } as const; -// ----- Types ----- // -type ColumnPreset = keyof typeof grid.column; - // ----- Exports ----- // -export type { Line, ColumnPreset }; export { grid }; diff --git a/dotcom-rendering/src/layouts/GalleryLayout.tsx b/dotcom-rendering/src/layouts/GalleryLayout.tsx index b922146c1c9..c00dccf3a5e 100644 --- a/dotcom-rendering/src/layouts/GalleryLayout.tsx +++ b/dotcom-rendering/src/layouts/GalleryLayout.tsx @@ -579,7 +579,7 @@ const BodyAdSlot = (props: { const WebAdSlot = (props: { adIndex: number }) => (
Date: Wed, 10 Jun 2026 14:33:20 +0100 Subject: [PATCH 6/6] Put overflow: hidden on the right container --- dotcom-rendering/src/grid.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dotcom-rendering/src/grid.ts b/dotcom-rendering/src/grid.ts index 60540ff435c..50d462bc358 100644 --- a/dotcom-rendering/src/grid.ts +++ b/dotcom-rendering/src/grid.ts @@ -94,11 +94,10 @@ const paddedContainer = ` * The rule is self-contained on the nth child element rather than on the grid * container, so that `top: 0` aligns to that element's top edge. `bottom` * uses a large negative value to extend the rule down to the container's - * bottom, so ensure `overflow: hidden` is set on the container. + * bottom, so `overflow: hidden` is applied to the container. * * @example * css` - * overflow: hidden; * ${grid.container} * ${grid.verticalRules()} * ${grid.centreRule(1)} @@ -109,6 +108,8 @@ const paddedContainer = ` * ` */ const centreRule = (n: number, color?: string): string => `/* CENTRE RULE */ + overflow: hidden; + & > *:nth-child(${n}) { position: relative; @@ -141,8 +142,6 @@ const centreRule = (n: number, color?: string): string => `/* CENTRE RULE */ * ` */ const outerRules = (color?: string): string => ` - overflow: hidden; - ${fromBreakpoint.tablet} { position: relative;