color.stories.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. import styled from '@emotion/styled';
  2. import DoAccentColors from 'sentry-images/stories/color/do-accent-colors.svg';
  3. import DoContrast from 'sentry-images/stories/color/do-contrast.svg';
  4. import DoDifferentiation from 'sentry-images/stories/color/do-differentiation.svg';
  5. import DontAccentColors from 'sentry-images/stories/color/dont-accent-colors.svg';
  6. import DontContrast from 'sentry-images/stories/color/dont-contrast.svg';
  7. import DontDifferentiation from 'sentry-images/stories/color/dont-differentiation.svg';
  8. import ExternalLink from 'sentry/components/links/externalLink';
  9. import Panel from 'sentry/components/panels/panel';
  10. import PanelItem from 'sentry/components/panels/panelItem';
  11. import ThemeToggle from 'sentry/components/stories/themeToggle';
  12. import {IconCheckmark, IconClose} from 'sentry/icons';
  13. import {space} from 'sentry/styles/space';
  14. import type {ColorOrAlias} from 'sentry/utils/theme';
  15. import theme from 'sentry/utils/theme';
  16. interface Palette {
  17. color: ColorOrAlias;
  18. text: ColorOrAlias;
  19. }
  20. const GRAY_PALETTES: Palette[][] = [
  21. [{color: 'gray500', text: 'lightModeWhite'}],
  22. [{color: 'gray400', text: 'lightModeWhite'}],
  23. [{color: 'gray300', text: 'lightModeWhite'}],
  24. [{color: 'gray200', text: 'lightModeBlack'}],
  25. [{color: 'gray100', text: 'lightModeBlack'}],
  26. ];
  27. const LEVELS_PALETTES: Palette[][] = [
  28. [
  29. {color: 'purple400', text: 'lightModeWhite'},
  30. {color: 'purple300', text: 'lightModeWhite'},
  31. {color: 'purple200', text: 'lightModeBlack'},
  32. {color: 'purple100', text: 'lightModeBlack'},
  33. ],
  34. [
  35. {color: 'blue400', text: 'lightModeWhite'},
  36. {color: 'blue300', text: 'lightModeWhite'},
  37. {color: 'blue200', text: 'lightModeBlack'},
  38. {color: 'blue100', text: 'lightModeBlack'},
  39. ],
  40. [
  41. {color: 'green400', text: 'lightModeWhite'},
  42. {color: 'green300', text: 'lightModeBlack'},
  43. {color: 'green200', text: 'lightModeBlack'},
  44. {color: 'green100', text: 'lightModeBlack'},
  45. ],
  46. [
  47. {color: 'yellow400', text: 'lightModeBlack'},
  48. {color: 'yellow300', text: 'lightModeBlack'},
  49. {color: 'yellow200', text: 'lightModeBlack'},
  50. {color: 'yellow100', text: 'lightModeBlack'},
  51. ],
  52. [
  53. {color: 'red400', text: 'lightModeWhite'},
  54. {color: 'red300', text: 'lightModeWhite'},
  55. {color: 'red200', text: 'lightModeBlack'},
  56. {color: 'red100', text: 'lightModeBlack'},
  57. ],
  58. [
  59. {color: 'pink400', text: 'lightModeWhite'},
  60. {color: 'pink300', text: 'lightModeWhite'},
  61. {color: 'pink200', text: 'lightModeBlack'},
  62. {color: 'pink100', text: 'lightModeBlack'},
  63. ],
  64. ];
  65. const FixedWidth = styled('div')`
  66. max-width: 800px;
  67. `;
  68. export default function ColorStories() {
  69. return (
  70. <FixedWidth>
  71. <h3>Colors</h3>
  72. <p>
  73. Sentry has a flexible, tiered color system that adapts to both light and dark
  74. mode. Our color palette consists of neutral grays and 6 accent colors.
  75. </p>
  76. <hr />
  77. <h4>Grays</h4>
  78. <p>
  79. There are 5 shades of gray, ranging from Gray 500 (darkest) to Gray 100
  80. (lightest).
  81. </p>
  82. <p>
  83. <strong>Gray 300 and above</strong> are accessible foreground colors that conform
  84. to{' '}
  85. <ExternalLink href="https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html">
  86. WCAG standards
  87. </ExternalLink>
  88. . Use them as text and icon colors.
  89. </p>
  90. <p>Here are the recommended use cases:</p>
  91. <ul>
  92. <li>
  93. <strong>Gray 500:</strong> headings, button labels, tags/badges, and alerts.
  94. </li>
  95. <li>
  96. <strong>Gray 400:</strong> body text, input values & labels.
  97. </li>
  98. <li>
  99. <strong>Gray 300:</strong> input placeholders, inactive/disabled inputs and
  100. buttons, chart labels, supplemental and non-essential text
  101. </li>
  102. <li>
  103. <strong>Gray 200:</strong> borders around large elements (cards, panels,
  104. dialogs, tables).
  105. </li>
  106. <li>
  107. <strong>Gray 100:</strong> dividers and borders around small elements (buttons,
  108. form inputs).
  109. </li>
  110. </ul>
  111. <ThemeToggle>
  112. <ColorPalette name="grays" palette={GRAY_PALETTES} />
  113. </ThemeToggle>
  114. <hr />
  115. <h4>Accent Colors</h4>
  116. <p>
  117. Accent colors help shift the user's focus to certain interactive and high-priority
  118. elements, like links, buttons, and warning banners.
  119. </p>
  120. <h5>Hues</h5>
  121. <p>There are 6 hues to choose from. Each has specific connotations:</p>
  122. <ul>
  123. <li>
  124. <strong>Purple:</strong> brand, current/active/focus state, or new information.
  125. </li>
  126. <li>
  127. <strong>Blue:</strong> hyperlink.
  128. </li>
  129. <li>
  130. <strong>Green:</strong> success, resolution, approval, availability, or
  131. creation.
  132. </li>
  133. <li>
  134. <strong>Yellow:</strong> warning, missing, or impeded progress.
  135. </li>
  136. <li>
  137. <strong>Red:</strong> fatal error, deletion, or removal.
  138. </li>
  139. <li>
  140. <strong>Pink:</strong> new feature or promotion.
  141. </li>
  142. </ul>
  143. <h5>Levels</h5>
  144. <p>
  145. Each hue comes in 4 levels: 400 (dark), 300 (full opacity), 200 (medium opacity),
  146. and 100 (low opacity).
  147. </p>
  148. <ul>
  149. <li>
  150. <strong>The 400 level</strong> is a darkened version of 300. It is useful for
  151. hover/active states in already accentuated elements. For example, a button could
  152. have a background of Purple 300 in normal state and Purple 400 on hover.
  153. </li>
  154. <li>
  155. <strong>The 300 level</strong> has full opacity and serves well as text and icon
  156. colors (with the exception of Yellow 300, which does not meet{' '}
  157. <ExternalLink href="https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html">
  158. WCAG's contrast standards
  159. </ExternalLink>
  160. ).
  161. </li>
  162. <li>
  163. <strong>The 200 level</strong> has medium opacity, useful for borders and
  164. dividers.
  165. </li>
  166. <li>
  167. <strong>The 100 level</strong> has very low opacity, useful as background fills.
  168. </li>
  169. </ul>
  170. <ThemeToggle>
  171. <ColorPalette name="levels" palette={LEVELS_PALETTES} />
  172. </ThemeToggle>
  173. <hr />
  174. <h4>Accessibility</h4>
  175. <p>
  176. When it comes to using color, there are two main accessibility concerns:
  177. readability and separation.
  178. </p>
  179. <h5>Readability</h5>
  180. <p>
  181. <ExternalLink href="https://www.w3.org/TR/WCAG21/">WCAG</ExternalLink> requires
  182. that normal text elements have a contrast ratio of at least 4.5:1 against the
  183. background. For large text (at least 16px in size AND in medium/bold weight), the
  184. required ratio is lower, at 3:1. This is to ensure a comfortable reading
  185. experience in different lighting conditions.{' '}
  186. <ExternalLink href="https://webaim.org/resources/contrastchecker/">
  187. Use this tool
  188. </ExternalLink>{' '}
  189. to confirm text contrast ratios.
  190. </p>
  191. <p>
  192. In Sentry's color palette, only Gray 300 and above satisfy the contrast
  193. requirement for normal text. This applies to both light and dark mode.
  194. </p>
  195. <p>
  196. Accent colors in the 300 series, except for Yellow 300, satisfy the contrast
  197. requirement for large text.
  198. </p>
  199. <SideBySideList>
  200. <ExampleCard
  201. imgSrc={DoContrast}
  202. text="Use Gray 300 and above for normal text"
  203. isPositive
  204. />
  205. <ExampleCard
  206. imgSrc={DontContrast}
  207. text="Use Gray 100 or 200 for normal text, as they don't have the required the contrast levels"
  208. />
  209. <ExampleCard
  210. imgSrc={DoAccentColors}
  211. text="Use accent colors in the 300 series (except for Yellow 300) for large text, if needed"
  212. isPositive
  213. />
  214. <ExampleCard
  215. imgSrc={DontAccentColors}
  216. text="Use accent colors in the 100 or 200 series for any text"
  217. />
  218. </SideBySideList>
  219. <h5>Separation</h5>
  220. <p>
  221. Color can be an effective way to visually separate elements in the user interface.
  222. However, not all users see color in the same way. Some are color-blind and cannot
  223. reliably differentiate one color from another. Some have color filters on their
  224. screens, like Night Shift in MacOS. Others are in bright environments with high
  225. levels of glare, reducing their ability to see color clearly.
  226. </p>
  227. <p>
  228. As such, color is an unreliable way to separate elements. Whenever possible,
  229. provide additional visual cues like icons, text labels, line type (solid, dashed,
  230. dotted),… to further reinforce the separation.
  231. </p>
  232. <SideBySideList>
  233. <ExampleCard
  234. imgSrc={DoDifferentiation}
  235. text="Provide additional visual encoding (e.g. line type) besides color to differentiate elements"
  236. isPositive
  237. />
  238. <ExampleCard
  239. imgSrc={DontDifferentiation}
  240. text="Use color as the only way to differentiate elements"
  241. />
  242. </SideBySideList>
  243. </FixedWidth>
  244. );
  245. }
  246. const SideBySideList = styled('ul')`
  247. /* Reset */
  248. list-style-type: none;
  249. margin: 0;
  250. padding: 0;
  251. & > li {
  252. margin: 0;
  253. }
  254. & > li > div {
  255. margin-bottom: 0;
  256. }
  257. /* Side-by-side display */
  258. display: grid;
  259. grid-template-columns: 1fr 1fr;
  260. gap: ${space(2)};
  261. `;
  262. const PalettePanel = styled(Panel)`
  263. margin-bottom: 0;
  264. `;
  265. const PalettePanelItem = styled(PanelItem)<{
  266. color: ColorOrAlias;
  267. text: ColorOrAlias;
  268. }>`
  269. flex-direction: column;
  270. gap: ${space(0.5)};
  271. &:first-child {
  272. border-radius: ${p => p.theme.borderRadiusTop};
  273. }
  274. &:last-child {
  275. border-radius: ${p => p.theme.borderRadiusBottom};
  276. }
  277. &:first-child:last-child {
  278. border-radius: ${p => p.theme.borderRadius};
  279. }
  280. background: ${p => p.theme[p.color]};
  281. color: ${p => p.theme[p.text]};
  282. `;
  283. function ColorPalette({name, palette}: {name: string; palette: Palette[][]}) {
  284. return (
  285. <SideBySideList>
  286. {palette.map((section, i) => {
  287. return (
  288. <li key={`${name}-${i}`}>
  289. <PalettePanel typeof="ul">
  290. {section.map(color => {
  291. return (
  292. <PalettePanelItem
  293. key={`${name}-${color.color}`}
  294. color={color.color}
  295. text={color.text}
  296. >
  297. <strong>{color.color}</strong>
  298. {theme[color.color]}
  299. </PalettePanelItem>
  300. );
  301. })}
  302. </PalettePanel>
  303. </li>
  304. );
  305. })}
  306. </SideBySideList>
  307. );
  308. }
  309. const ExampleImg = styled('img')`
  310. border: 1px solid ${p => p.theme.border};
  311. border-radius: ${p => p.theme.borderRadius};
  312. max-width: 400px;
  313. `;
  314. const PositiveLabel = styled(({className}: {className?: string}) => (
  315. <div className={className}>
  316. <IconCheckmark />
  317. DO
  318. </div>
  319. ))`
  320. color: ${p => p.theme.green400};
  321. align-items: center;
  322. display: flex;
  323. font-weight: bold;
  324. gap: ${space(0.5)};
  325. `;
  326. const NegativeLabel = styled(({className}: {className?: string}) => (
  327. <div className={className}>
  328. <IconClose color="red400" />
  329. DON'T
  330. </div>
  331. ))`
  332. color: ${p => p.theme.red400};
  333. align-items: center;
  334. display: flex;
  335. font-weight: bold;
  336. gap: ${space(0.5)};
  337. `;
  338. const ExampleCardGrid = styled('figcaption')`
  339. display: grid;
  340. grid-template-columns: 1fr 2fr;
  341. align-items: flex-start;
  342. color: ${p => p.theme.subText};
  343. padding: ${space(1)} ${space(1)} 0;
  344. `;
  345. interface ExampleCardProps {
  346. imgSrc: string;
  347. text: string;
  348. isPositive?: boolean;
  349. }
  350. function ExampleCard({imgSrc, text, isPositive}: ExampleCardProps) {
  351. return (
  352. <figure>
  353. <ExampleImg src={imgSrc} />
  354. <ExampleCardGrid>
  355. {isPositive ? <PositiveLabel /> : <NegativeLabel />}
  356. <span>{text}</span>
  357. </ExampleCardGrid>
  358. </figure>
  359. );
  360. }