Skip to content

Commit 2c5e31e

Browse files
authored
Replace Primer React Flash with a house Banner component (#63722)
1 parent 1b7b25d commit 2c5e31e

11 files changed

Lines changed: 122 additions & 23 deletions

File tree

‎src/color-schemes/README.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,27 @@ The resolved mode comes from the **effective theme**, not the raw `color_mode`,
5353

5454
This is a workaround for a gap in Brand and belongs upstream; until it lands, every consumer has to hand Brand a concrete mode.
5555

56+
### Color contrast requirements
57+
58+
Colors must meet WCAG 2.1 AA in every supported mode. Primer documents the requirements in [ADR-010: Color contrast requirements](https://github.com/primer/primitives/blob/main/contributor-docs/adrs/adr-010-color-contrast.md). The ones that come up most often are:
59+
60+
| Pair | Minimum ratio |
61+
|---|---|
62+
| Text vs. any background it can sit on | 4.5:1 |
63+
| Large text, icons, and other non-text UI vs. background | 3:1 |
64+
| Link vs. surrounding text | 3:1, unless the link is underlined. Links also need 4.5:1 against the background. |
65+
| Focus indicator vs. adjacent colors | 3:1 |
66+
| Border of a control that contains text, or a decorative border | None |
67+
68+
Check both light and dark mode. Brand's base color scales (`--base-color-scale-*`) don't invert in dark mode, so a step that passes in light mode can fail in dark mode.
69+
70+
Also check the no-JavaScript fallback, where `<html>` keeps `data-color-mode="auto"`. Brand stays on its light palette there, so a component that darkens its own background still gets light-mode link blue. Scope fallback selectors to `html`, because the `@primer/react` `ThemeProvider` wrapper also starts as `auto`.
71+
72+
On colored surfaces, such as the `Banner` component in `src/frame/components/ui/Banner/`, we enforce these rules:
73+
74+
* Always underline links. Don't rely on link color to separate links from surrounding text.
75+
* If the link color can't be guaranteed to reach 4.5:1 against the surface, set links to the surface's text color.
76+
5677
## Setup & Usage
5778

5879
To access the current theme in a component:
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Colors follow the Docs 2026 Figma warning banner: Brand yellow-9 text with a yellow-7 accent.
2+
// Default and danger reuse the same scale steps in blue and red.
3+
// Brand's base scales do not invert in dark mode, so dark mode mirrors the steps explicitly,
4+
// the same way Brand's own component tokens pair a light step with a dark step.
5+
@mixin banner-colors($hue, $mode) {
6+
@if $mode == light {
7+
--banner-bg: var(--base-color-scale-#{$hue}-0);
8+
--banner-border: var(--base-color-scale-#{$hue}-7);
9+
--banner-fg: var(--base-color-scale-#{$hue}-9);
10+
} @else {
11+
--banner-bg: var(--base-color-scale-#{$hue}-9);
12+
--banner-border: var(--base-color-scale-#{$hue}-2);
13+
--banner-fg: var(--base-color-scale-#{$hue}-0);
14+
}
15+
}
16+
17+
@mixin banner-variant($hue) {
18+
@include banner-colors($hue, light);
19+
20+
:global([data-color-mode="dark"]) & {
21+
@include banner-colors($hue, dark);
22+
}
23+
24+
// No-JS fallback; see the note in src/frame/stylesheets/index.scss. Scoped to html because
25+
// the @primer/react ThemeProvider wrapper also starts as auto, even when html is light.
26+
// Brand keeps its light link blue here, so links use the banner text color for contrast.
27+
@media (prefers-color-scheme: dark) {
28+
:global(html[data-color-mode="auto"][data-dark-theme*="dark"]) & {
29+
@include banner-colors($hue, dark);
30+
31+
a {
32+
color: var(--banner-fg);
33+
}
34+
}
35+
}
36+
}
37+
38+
.banner {
39+
padding: var(--base-size-16, 1rem);
40+
color: var(--banner-fg);
41+
background-color: var(--banner-bg);
42+
border: var(--brand-borderWidth-thin, 1px) solid var(--banner-border);
43+
border-radius: var(--brand-borderRadius-medium, 0.5rem);
44+
45+
p:last-child {
46+
margin-bottom: 0;
47+
}
48+
49+
// Link color alone is not distinct enough from banner text, especially in dark mode.
50+
a {
51+
text-decoration: underline;
52+
}
53+
}
54+
55+
.default {
56+
@include banner-variant(blue);
57+
}
58+
59+
.warning {
60+
@include banner-variant(yellow);
61+
}
62+
63+
.danger {
64+
@include banner-variant(red);
65+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { ReactNode } from 'react'
2+
import cx from 'clsx'
3+
4+
import styles from './Banner.module.scss'
5+
6+
export type BannerPropsT = {
7+
variant?: 'default' | 'warning' | 'danger'
8+
className?: string
9+
children: ReactNode
10+
}
11+
12+
// No live region role: every banner renders with the page and never updates in place.
13+
export function Banner({ variant = 'default', className, children }: BannerPropsT) {
14+
return <div className={cx(styles.banner, styles[variant], className)}>{children}</div>
15+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { Banner } from './Banner'
2+
export type { BannerPropsT } from './Banner'

‎src/rest/components/RestBanner.tsx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import React from 'react'
2-
import { Flash } from '@primer/react'
32
import { useRouter } from 'next/router'
43

54
import { DEFAULT_VERSION, useVersion } from '@/versions/components/useVersion'
65
import { Link } from '@/frame/components/Link'
6+
import { Banner } from '@/frame/components/ui/Banner'
77
import { useMainContext } from '@/frame/components/context/MainContext'
88
import { useTranslation } from '@/languages/components/useTranslation'
99
import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
@@ -76,7 +76,7 @@ export const RestBanner = () => {
7676
data-testid="rest-api-versioning-temporary-banner"
7777
className="container-xl mt-3 mx-auto p-responsive"
7878
>
79-
<Flash>
79+
<Banner>
8080
<RenderedHTML as="span" html={bannerText} />{' '}
8181
<RenderedHTML
8282
as="span"
@@ -85,7 +85,7 @@ export const RestBanner = () => {
8585
versionWithApiVersion === DEFAULT_VERSION ? '' : `/${versionWithApiVersion}`,
8686
)}
8787
/>
88-
</Flash>
88+
</Banner>
8989
</div>
9090
)
9191
}
@@ -130,15 +130,15 @@ export const RestBanner = () => {
130130

131131
return (
132132
<div data-testid="rest-api-repos-banner" className="container-xl mt-3 mx-auto p-responsive">
133-
<Flash variant="warning">
133+
<Banner variant="warning">
134134
<p>
135135
<b className="text-bold">
136136
<span>
137137
{t('rest.banner.redirect_notice')} {noticeString}
138138
</span>
139139
</b>{' '}
140140
</p>
141-
</Flash>
141+
</Banner>
142142
</div>
143143
)
144144
}

‎src/search/components/results/NoQuery.module.scss‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55
line-height: 1.2;
66
}
77

8-
.flash {
8+
.banner {
99
margin: 2rem;
1010
}
Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import { Flash } from '@primer/react'
21
import { Heading } from '@primer/react-brand'
32

43
import { useMainContext } from '@/frame/components/context/MainContext'
4+
import { Banner } from '@/frame/components/ui/Banner'
55
import { useTranslation } from '@/languages/components/useTranslation'
66

77
import styles from './NoQuery.module.scss'
88

9-
// NoQuery keeps the callout on Primer React because Brand lacks Flash, Banner, or Alert.
10-
// The Docs 2026 callout system is the planned replacement.
119
export function NoQuery() {
1210
const { t } = useTranslation('old_search')
1311
const mainContext = useMainContext()
@@ -20,9 +18,9 @@ export function NoQuery() {
2018
{page.title}
2119
</Heading>
2220

23-
<Flash variant="danger" className={styles.flash}>
21+
<Banner variant="danger" className={styles.banner}>
2422
{t('description')}
25-
</Flash>
23+
</Banner>
2624
</>
2725
)
2826
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
.flash {
1+
.banner {
22
margin: 3rem;
33
}

‎src/search/components/results/ValidationErrors.tsx‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { Flash } from '@primer/react'
2-
1+
import { Banner } from '@/frame/components/ui/Banner'
32
import { useTranslation } from '@/languages/components/useTranslation'
43
import type { SearchValidationErrorEntry } from '../../types'
54

@@ -16,11 +15,11 @@ export function ValidationErrors({ errors }: Props) {
1615
<div>
1716
{errors.map((error) => {
1817
return (
19-
<Flash key={error.error} variant="warning" className={styles.flash}>
18+
<Banner key={error.error} variant="warning" className={styles.banner}>
2019
{t('search_validation_error')}
2120
<br />
2221
<code>{error.error}</code>
23-
</Flash>
22+
</Banner>
2423
)
2524
})}
2625
</div>

‎src/versions/components/DeprecationBanner.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { EnterpriseDeprecation } from '@/frame/components/context/MainContext'
22
import { useMainContext } from '@/frame/components/context/MainContext'
33
import { useVersion } from '@/versions/components/useVersion'
4-
import { Flash } from '@primer/react'
54
import cx from 'clsx'
65

76
import styles from './DeprecationBanner.module.scss'
7+
import { Banner } from '@/frame/components/ui/Banner'
88
import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
99

1010
export const DeprecationBanner = () => {
@@ -27,7 +27,7 @@ export const DeprecationBanner = () => {
2727
data-testid="deprecation-banner"
2828
className={cx('container-xl mt-3 mx-auto p-responsive', styles.DeprecationBanner)}
2929
>
30-
<Flash variant="warning">
30+
<Banner variant="warning">
3131
<p>
3232
<b className="text-bold">
3333
<RenderedHTML as="span" html={message} />{' '}
@@ -38,7 +38,7 @@ export const DeprecationBanner = () => {
3838
</b>{' '}
3939
<RenderedHTML as="span" html={enterpriseDeprecation.deprecation_details} />
4040
</p>
41-
</Flash>
41+
</Banner>
4242
</div>
4343
)
4444
}

0 commit comments

Comments
 (0)