Repository: Codecademy/gamut
Pull request #3055: feat(Badge): add new custom variant to allow styling of background
Author: login: LinKCoding
Created: 2025-03-24T16:00:01Z | Merged: 2025-04-02T17:49:50Z

### Overview

<!--- CHANGELOG-DESCRIPTION -->

<!--- END-CHANGELOG-DESCRIPTION -->

### PR Checklist

- [ ] Related to designs:
- [x] Related to JIRA ticket: [GM-979]
- [x] I have run this code to verify it works
- [x] This PR includes unit tests for the code change
- [x] This PR includes testing instructions tests for the code change
- [x] The alpha package of this PR is passing end-to-end tests in all relevant Codecademy repositories

#### Testing Instructions

1. Go to the [Badge story](https://67e32d5fff428750515ed4ca--gamut-preview.netlify.app/?path=/docs/atoms-badge--docs)
2. Check that the Badges still look like they're supposed to 
3. Go to the "Custom" section to see Badges that have different colors aside from the listed variants (Primary, Secondary, Tertiary, Tertiary-fill, and Accent). 
4. (Optional) locally open up `packages/gamut/src/Badge/index.tsx` and uncomment the test code at the bottom. Check that the proper TS errors are throwing
5. ...
6. Profit!


#### PR Links and Envs

| Repository   | PR Link                                                  | PR Env                                                   |
| :----------- | :------------------------------------------------------- | :------------------------------------------------------- |
| Monolith     | [Monolith PR](https://github.com/codecademy-engineering/Codecademy/pull/39302)  | [Monolith Env](https://pr-39302-monolith.dev-eks.codecademy.com/) |
| Portal       | [Portal Link](https://github.com/codecademy-engineering/mono/pull/9657)  | [Portal Env](https://tengu.codecademy.com/catalog?PR_ENV=portal-app-pr-9657)   |

<!--
Merging your changes

1. Follow the [PR Title Guide](https://github.com/Codecademy/gamut#pr-title-guide), the title (which becomes the commit message) determines the version bump for the packages you changed.

7. Wrap the text describing your change in more detail in the "CHANGELOG-DESCRIPTION" comment tags above, this is what will show up in the changelog!

8. DO NOT MERGE MANUALLY! When you are ready to merge and publish your changes, add the "Ship It" label to your Pull Request. This will trigger the merge process as long as all checks have completed, if the checks haven't completed the branch will be merged when they all pass.

**IMPORTANT:** If your PR contains breaking changes, please remember to follow the instructions for breaking changes!
-->


[GM-979]: https://skillsoftdev.atlassian.net/browse/GM-979?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Diff:
diff --git a/packages/gamut/src/Badge/index.tsx b/packages/gamut/src/Badge/index.tsx
index a55e7ef4501..e247c9b822e 100644
--- a/packages/gamut/src/Badge/index.tsx
+++ b/packages/gamut/src/Badge/index.tsx
@@ -1,4 +1,9 @@
-import { styledOptions, system, variant } from '@codecademy/gamut-styles';
+import {
+  Colors,
+  styledOptions,
+  system,
+  variant,
+} from '@codecademy/gamut-styles';
 import { StyleProps, variance } from '@codecademy/variance';
 import styled from '@emotion/styled';
 
@@ -33,16 +38,19 @@ const colorVariants = variant({
       textColor: 'background',
     },
     tertiary: {
+      bg: 'transparent',
       border: 1,
       borderColor: 'border-secondary',
-      color: 'text-secondary',
-      bg: 'transparent',
+      textColor: 'text-secondary',
     },
     tertiaryFill: {
+      bg: 'background',
       border: 1,
       borderColor: 'border-secondary',
-      color: 'text-secondary',
-      bg: 'background',
+      textColor: 'text-secondary',
+    },
+    custom: {
+      textColor: 'text',
     },
   },
 });
@@ -66,13 +74,39 @@ const sizeVariants = variant({
 const badgeProps = variance.compose(
   system.space,
   system.layout,
-  system.typography
+  system.typography,
+  system.background,
+  system.color
 );
-export interface BadgeBaseProps
-  extends StyleProps<typeof badgeProps>,
-    StyleProps<typeof colorVariants>,
-    StyleProps<typeof sizeVariants>,
-    WithChildrenProp {}
+
+type StandardBadgeProps = {
+  background?: never;
+  bg?: never;
+  variant?: 'primary' | 'secondary' | 'tertiary' | 'tertiaryFill' | 'accent';
+};
+
+type CustomBadgeBackgroundProps = {
+  background: string;
+  bg?: never;
+  variant: 'custom';
+};
+
+type CustomBadgeBgProps = {
+  background?: never;
+  bg: Colors;
+  variant: 'custom';
+};
+
+type BadgeBgPropsType =
+  | StandardBadgeProps
+  | CustomBadgeBackgroundProps
+  | CustomBadgeBgProps;
+
+export type BadgeBaseProps = StyleProps<typeof badgeProps> &
+  StyleProps<typeof colorVariants> &
+  StyleProps<typeof sizeVariants> &
+  BadgeBgPropsType &
+  WithChildrenProp;
 
 const BadgeBase = styled('div', styledOptions)<BadgeBaseProps>(
   badgeProps,
@@ -80,9 +114,7 @@ const BadgeBase = styled('div', styledOptions)<BadgeBaseProps>(
   sizeVariants
 );
 
-export interface BadgeProps
-  extends Partial<IconComponentType>,
-    BadgeBaseProps {}
+export type BadgeProps = Partial<IconComponentType> & BadgeBaseProps;
 
 export const Badge: React.FC<BadgeProps> = ({ icon, children, ...rest }) => {
   const size = rest.size === 'sm' ? 'sm' : 'base';
@@ -98,3 +130,54 @@ export const Badge: React.FC<BadgeProps> = ({ icon, children, ...rest }) => {
   });
   return <BadgeBase {...rest}>{content}</BadgeBase>;
 };
+
+// FOR TESTING PROPS
+/*
+export const TestBadgeCustomBackground = () => (
+  <Badge
+    variant="custom"
+    background="linear-gradient(91deg, #FFE712 0.08%, #FF9641 100%)"
+  >
+    ✅ this works, no TS errors
+  </Badge>
+)
+
+export const TestBadgeCustomBg = () => (
+  <Badge
+    variant="custom"
+    bg="white"
+  >
+    ✅ this works, no TS errors
+  </Badge>
+)
+
+export const TestBadgeIncorrectCustomBg = () => (
+  <Badge
+    variant="custom"
+    bg="white"
+    background="linear-gradient(91deg, #FFE712 0.08%, #FF9641 100%)"
+  >
+    ✅ this works, gets TS errors when both bg and background are used
+  </Badge>
+)
+
+
+export const TestBadgeIncorrectVariantBg = () => (
+  <Badge
+    variant="primary"
+    bg="white"
+  >
+    ✅ this works, gets TS errors when both a variant and bg are used
+  </Badge>
+)
+
+
+export const TestBadgeIncorrectVariantBackground = () => (
+  <Badge
+    variant="primary"
+    background="linear-gradient(91deg, #FFE712 0.08%, #FF9641 100%)"
+  >
+    ✅ this works, gets TS errors when both a variant and background are used
+  </Badge>
+)
+*/
diff --git a/packages/styleguide/src/lib/Atoms/Badge/Badge.mdx b/packages/styleguide/src/lib/Atoms/Badge/Badge.mdx
index af8babc5d31..d00b74b0088 100644
--- a/packages/styleguide/src/lib/Atoms/Badge/Badge.mdx
+++ b/packages/styleguide/src/lib/Atoms/Badge/Badge.mdx
@@ -35,6 +35,7 @@ Use `Badge`s to display read-only information like statuses, attributes, and oth
 ### Best practices:
 
 - Use badges sparingly, as too many can dilute their effectiveness.
+- Custom colors should be used sparingly, consistently across similar contexts, and with sufficient contrast for accessibility.
 
 ### When NOT to use:
 
@@ -79,18 +80,24 @@ Use the tertiary variant to indicate background or supplementary information wit
 
 ### Tertiary-fill
 
-Use the tertiary-fill variant when a tertiary badge needs to sit above other content. This variant includes a bg-color that can overlap color modes and unpredictable content.
+Use the tertiary-fill variant when a tertiary `Badge` needs to sit above other content. This variant includes a bg-color that can overlap color modes and unpredictable content.
 
 <Canvas of={BadgeStories.TertiaryFill} />
 
 ### Accent
 
-Use the accent variant to draw attention to important details or statuses with a noticeable but less dominant emphasis than the primary badge.
+Use the accent variant to draw attention to important details or statuses with a noticeable but less dominant emphasis than the primary `Badge`.
 
-<Callout text="The accent variant does not respond to Color Mode." />
+<Callout text="The accent variant does not respond to ColorMode." />
 
 <Canvas of={BadgeStories.Accent} />
 
+### Custom
+
+Use the custom variant to create a `Badge` with a unique color scheme that fits your design needs. This variant allows for more flexibility in styling, but keep in mind best practices for color usage and accessibility.
+
+<Canvas of={BadgeStories.Custom} />
+
 ## Sizes
 
 We have two `size` options, which should be used according to their context.
diff --git a/packages/styleguide/src/lib/Atoms/Badge/Badge.stories.tsx b/packages/styleguide/src/lib/Atoms/Badge/Badge.stories.tsx
index ad72f798990..56afce2dc0d 100644
--- a/packages/styleguide/src/lib/Atoms/Badge/Badge.stories.tsx
+++ b/packages/styleguide/src/lib/Atoms/Badge/Badge.stories.tsx
@@ -1,4 +1,4 @@
-import { Badge } from '@codecademy/gamut';
+import { Badge, FlexBox } from '@codecademy/gamut';
 import { MiniStarIcon, MiniWarningTriangleIcon } from '@codecademy/gamut-icons';
 import type { Meta, StoryObj } from '@storybook/react';
 
@@ -40,6 +40,21 @@ export const Accent: Story = {
   },
 };
 
+export const Custom: Story = {
+  render: (args) => (
+    <FlexBox gap={8}>
+      <Badge
+        variant="custom"
+        background="linear-gradient(91deg, #FFE712 0.08%, #FF9641 100%)"
+        {...args}
+      />
+      <Badge variant="custom" bg="green" {...args} />
+      <Badge variant="custom" bg="orange-500" {...args} />
+      <Badge variant="custom" bg="lightBlue" {...args} icon={MiniStarIcon} />
+    </FlexBox>
+  ),
+};
+
 export const DefaultSize: Story = {
   args: {
     children: 'default size',


Reviews:
user: login: dreamwasp
body: looks great, just think the discriminated union will help with correct usage/dev experience.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T13:53:41Z
user: login: dreamwasp
body: perfection
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T20:12:03Z
user: login: aresnik11
body: One small question but looks great!! Love the discriminated union!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T17:25:24Z
user: login: LinKCoding
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T17:38:51Z
user: login: LinKCoding
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T17:39:03Z

Review comments:
diff_hunk: @@ -21,28 +21,38 @@ const colorVariants = variant({
   },
   variants: {
     primary: {
+      // Setting background: 'none' to prevent conflicting background and backgroundColor styles
+      // this way, only the custom variant should be able to set background
+      background: 'none',
       bg: `text`,
       textColor: 'background',
path: packages/gamut/src/Badge/index.tsx
user: login: dreamwasp
body: this looks awesome! could you add a [discriminated type](https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#discriminating-unions) to only allow `bg` to be passed to the custom variant? we can pair!
created_at: 2025-03-28T13:53:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,23 +74,47 @@ const sizeVariants = variant({
 const badgeProps = variance.compose(
   system.space,
   system.layout,
-  system.typography
+  system.typography,
+  system.background,
+  system.color
 );
-export interface BadgeBaseProps
-  extends StyleProps<typeof badgeProps>,
-    StyleProps<typeof colorVariants>,
-    StyleProps<typeof sizeVariants>,
-    WithChildrenProp {}
+
+type StandardBadgeProps = {
+  background?: never;
+  bg?: never;
+  variant?: 'primary' | 'secondary' | 'tertiary' | 'tertiaryFill' | 'accent';
+};
+
+type CustomBadgeBackgroundProps = {
+  background: string;
path: packages/gamut/src/Badge/index.tsx
user: login: aresnik11
body: is this so that you can pass any color to `background`? and then below `bg` has to be one of our colors?
created_at: 2025-04-02T17:09:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,23 +74,47 @@ const sizeVariants = variant({
 const badgeProps = variance.compose(
   system.space,
   system.layout,
-  system.typography
+  system.typography,
+  system.background,
+  system.color
 );
-export interface BadgeBaseProps
-  extends StyleProps<typeof badgeProps>,
-    StyleProps<typeof colorVariants>,
-    StyleProps<typeof sizeVariants>,
-    WithChildrenProp {}
+
+type StandardBadgeProps = {
+  background?: never;
+  bg?: never;
+  variant?: 'primary' | 'secondary' | 'tertiary' | 'tertiaryFill' | 'accent';
+};
+
+type CustomBadgeBackgroundProps = {
+  background: string;
path: packages/gamut/src/Badge/index.tsx
user: login: aresnik11
body: oh i see its for a gradient
created_at: 2025-04-02T17:11:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,6 +41,21 @@ export const Accent: Story = {
   },
 };
 
+export const Custom: Story = {
+  render: (args) => (
+    <FlexBox gap={8}>
+      <Badge
+        variant="custom"
+        background="linear-gradient(91deg, #FFE712 0.08%, #FF9641 100%)"
+        {...args}
+      />
+      <Badge variant="custom" bg="green" {...args} />
+      <Badge variant="custom" bg={`${theme.colors['orange-500']}`} {...args} />
path: packages/styleguide/src/lib/Atoms/Badge/Badge.stories.tsx
user: login: aresnik11
body: can this not just be `bg="orange-500"`?
created_at: 2025-04-02T17:12:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,23 +74,47 @@ const sizeVariants = variant({
 const badgeProps = variance.compose(
   system.space,
   system.layout,
-  system.typography
+  system.typography,
+  system.background,
+  system.color
 );
-export interface BadgeBaseProps
-  extends StyleProps<typeof badgeProps>,
-    StyleProps<typeof colorVariants>,
-    StyleProps<typeof sizeVariants>,
-    WithChildrenProp {}
+
+type StandardBadgeProps = {
+  background?: never;
+  bg?: never;
+  variant?: 'primary' | 'secondary' | 'tertiary' | 'tertiaryFill' | 'accent';
+};
+
+type CustomBadgeBackgroundProps = {
+  background: string;
path: packages/gamut/src/Badge/index.tsx
user: login: LinKCoding
body: Yeap, exactly right :) 
And I couldn't think of when someone might want to use both `background` and `bg` so I kept them in separate types
created_at: 2025-04-02T17:38:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,6 +41,21 @@ export const Accent: Story = {
   },
 };
 
+export const Custom: Story = {
+  render: (args) => (
+    <FlexBox gap={8}>
+      <Badge
+        variant="custom"
+        background="linear-gradient(91deg, #FFE712 0.08%, #FF9641 100%)"
+        {...args}
+      />
+      <Badge variant="custom" bg="green" {...args} />
+      <Badge variant="custom" bg={`${theme.colors['orange-500']}`} {...args} />
path: packages/styleguide/src/lib/Atoms/Badge/Badge.stories.tsx
user: login: LinKCoding
body: totally right, updating! 
created_at: 2025-04-02T17:39:03Z
author_association: CONTRIBUTOR

PR comments:
user: login: nx-cloud[bot]
created_at: 2025-03-24T16:00:55Z
body: 
View your [CI Pipeline Execution ↗](https://cloud.nx.app/cipes/67ed78a4d23eff75937f3155?utm_source=pull-request&utm_medium=comment) for commit 328141f8761fa4ba5fb091408bf97738bd38429c.

---

☁️ [Nx Cloud](https://cloud.nx.app?utm_source=pull-request&utm_medium=comment) last updated this comment at `2025-04-02 17:49:25` UTC

<!-- NX_CLOUD_APP_COMMENT_END -->
author_association: NONE
user: login: codecademydev
created_at: 2025-04-02T17:45:01Z
body: <h3>📬Published Alpha Packages:</h3><pre>@codecademy/gamut@60.0.2-alpha.328141.0<br/>@codecademy/gamut-kit@0.6.491-alpha.328141.0<br/>@codecademy/styleguide@71.0.2-alpha.328141.0<br/></pre><!--LERNA_VERSION_COMMENT-->
author_association: COLLABORATOR
user: login: codecademydev
created_at: 2025-04-02T17:46:21Z
body: <p>🚀 Styleguide deploy preview ready!</p><p><a href=https://67ed77e7cd7b44d764d3cdb5--gamut-preview.netlify.app>https://67ed77e7cd7b44d764d3cdb5--gamut-preview.netlify.app</a></p><p><a href=https://app.netlify.com/sites/gamut-preview/deploys/67ed77e7cd7b44d764d3cdb5>Deploy Logs</a></p><!--ALPHA_DEPLOY_COMMENT-->
author_association: COLLABORATOR