From 42f000bcb99427e077e415a1b60518c9114c3113 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Tue, 29 Sep 2026 15:19:29 +0530 Subject: [PATCH 1/3] feat: [badge] add outline and dot variants, fix default variants Move defaultVariants out of variants so CVA applies them, and drop the manual fallbacks. Make children optional. Add outline and dot boolean props. --- .../src/content/docs/components/badge/demo.ts | 32 ++++++++++ .../content/docs/components/badge/index.mdx | 16 ++++- .../content/docs/components/badge/props.ts | 12 ++++ .../components/badge/__tests__/badge.test.tsx | 29 ++++++++++ .../components/badge/badge.module.css | 58 +++++++++++++++++++ packages/raystack/components/badge/badge.tsx | 26 +++++---- 6 files changed, 162 insertions(+), 11 deletions(-) diff --git a/apps/www/src/content/docs/components/badge/demo.ts b/apps/www/src/content/docs/components/badge/demo.ts index 85e695c1b..e3f4886a7 100644 --- a/apps/www/src/content/docs/components/badge/demo.ts +++ b/apps/www/src/content/docs/components/badge/demo.ts @@ -27,6 +27,14 @@ export const playground = { options: ['micro', 'small', 'regular'], defaultValue: 'small' }, + outline: { + type: 'checkbox', + defaultValue: false + }, + dot: { + type: 'checkbox', + defaultValue: false + }, icon: { type: 'icon' }, @@ -55,6 +63,30 @@ export const variantDemo = { ` }; +export const outlineDemo = { + type: 'code', + code: ` + + Accent + Warning + Danger + Success + Neutral + ` +}; + +export const dotDemo = { + type: 'code', + code: ` + + + + + + + ` +}; + export const sizesDemo = { type: 'code', code: ` diff --git a/apps/www/src/content/docs/components/badge/index.mdx b/apps/www/src/content/docs/components/badge/index.mdx index 0033cad1f..1d8138449 100644 --- a/apps/www/src/content/docs/components/badge/index.mdx +++ b/apps/www/src/content/docs/components/badge/index.mdx @@ -8,6 +8,8 @@ import { playground, sizesDemo, variantDemo, + outlineDemo, + dotDemo, iconDemo, screenReaderTextDemo, } from "./demo.ts"; @@ -34,9 +36,21 @@ Choose between different variants to convey different meanings or importance lev +### Outline + +Set `outline` to draw a border in the variant color and remove the fill. + + + +### Dot + +Set `dot` to render a small colored circle, for example a notification or status marker. The badge does not render `children` or `icon` in this mode. Pass `screenReaderText` so screen readers can announce the dot. + + + ### Size -Three sizes. `regular` is the default; `small` and `micro` fit inside table rows and beside dense labels. +Three sizes. `small` is the default. `micro` fits inside table rows and beside dense labels. diff --git a/apps/www/src/content/docs/components/badge/props.ts b/apps/www/src/content/docs/components/badge/props.ts index cf77604b1..32f4272f1 100644 --- a/apps/www/src/content/docs/components/badge/props.ts +++ b/apps/www/src/content/docs/components/badge/props.ts @@ -17,6 +17,18 @@ export interface BadgeProps { */ size?: 'micro' | 'small' | 'regular'; + /** + * Draws a border and removes the fill + * @defaultValue false + */ + outline?: boolean; + + /** + * Renders a colored dot without `children` or `icon` + * @defaultValue false + */ + dot?: boolean; + /** Optional ReactNode to display an icon before the text */ icon?: React.ReactNode; diff --git a/packages/raystack/components/badge/__tests__/badge.test.tsx b/packages/raystack/components/badge/__tests__/badge.test.tsx index 1300c6dde..f787f2623 100644 --- a/packages/raystack/components/badge/__tests__/badge.test.tsx +++ b/packages/raystack/components/badge/__tests__/badge.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react'; +import { createRef } from 'react'; import { describe, expect, it } from 'vitest'; import { Badge } from '../badge'; import styles from '../badge.module.css'; @@ -28,6 +29,12 @@ describe('Badge', () => { expect(screen.getByText('Count:')).toBeInTheDocument(); expect(screen.getByText('5')).toBeInTheDocument(); }); + + it('passes ref to the root span', () => { + const ref = createRef(); + const { container } = render(); + expect(ref.current).toBe(container.firstChild); + }); }); describe('Variants', () => { @@ -51,6 +58,28 @@ describe('Badge', () => { const badge = container.querySelector('span'); expect(badge).toHaveClass(styles['badge-accent']); }); + + it('renders outline', () => { + const { container } = render(Outline); + const badge = container.querySelector('span'); + expect(badge).toHaveClass(styles['badge-outline']); + }); + + it('renders a dot without children or icon', () => { + const { container } = render( + } + screenReaderText='3 unread' + > + 3 + + ); + const badge = container.querySelector('span'); + expect(badge).toHaveClass(styles['badge-dot']); + expect(badge).toHaveTextContent(/^3 unread$/); + expect(screen.queryByTestId('icon')).not.toBeInTheDocument(); + }); }); describe('Sizes', () => { diff --git a/packages/raystack/components/badge/badge.module.css b/packages/raystack/components/badge/badge.module.css index 57cfbb358..3e0da085c 100644 --- a/packages/raystack/components/badge/badge.module.css +++ b/packages/raystack/components/badge/badge.module.css @@ -70,6 +70,64 @@ color: var(--rs-color-foreground-base-primary); } +/* Outline. An inset shadow draws the border without changing the fixed height. */ +.badge-outline { + background: transparent; + box-shadow: inset 0 0 0 0.5px var(--rs-color-border-base-secondary); +} + +.badge-outline.badge-accent { + box-shadow: inset 0 0 0 0.5px var(--rs-color-border-accent-primary); +} + +.badge-outline.badge-warning { + box-shadow: inset 0 0 0 0.5px var(--rs-color-border-attention-primary); +} + +.badge-outline.badge-danger { + box-shadow: inset 0 0 0 0.5px var(--rs-color-border-danger-primary); +} + +.badge-outline.badge-success { + box-shadow: inset 0 0 0 0.5px var(--rs-color-border-success-primary); +} + +/* Dot */ +.badge-dot { + width: var(--rs-space-3); + height: var(--rs-space-3); + padding: 0; + border-radius: var(--rs-radius-full); +} + +.badge-dot.badge-accent { + background: var(--rs-color-background-accent-emphasis); +} + +.badge-dot.badge-warning { + background: var(--rs-color-background-attention-emphasis); +} + +.badge-dot.badge-danger { + background: var(--rs-color-background-danger-emphasis); +} + +.badge-dot.badge-success { + background: var(--rs-color-background-success-emphasis); +} + +.badge-dot.badge-neutral { + background: var(--rs-color-background-neutral-emphasis); +} + +.badge-dot.badge-gradient { + background: linear-gradient( + to right, + oklch(0.5674 0.2831 312.58) 0%, + oklch(0.5988 0.2445 29.12) 100% + ); +} + /* Icon */ .icon { display: inline-flex; diff --git a/packages/raystack/components/badge/badge.tsx b/packages/raystack/components/badge/badge.tsx index 5d937c8f6..cc804ede6 100644 --- a/packages/raystack/components/badge/badge.tsx +++ b/packages/raystack/components/badge/badge.tsx @@ -20,25 +20,31 @@ const badge = cva(styles['badge'], { small: styles['badge-small'], regular: styles['badge-regular'] }, - defaultVariants: { - variant: 'accent', - size: 'small' + outline: { + true: styles['badge-outline'] + }, + dot: { + true: styles['badge-dot'] } + }, + defaultVariants: { + variant: 'accent', + size: 'small' } }); type BadgeProps = VariantProps & ComponentProps<'span'> & { icon?: ReactNode; - children: ReactNode; - className?: string; screenReaderText?: string; }; export const Badge = ({ - variant = 'accent', - size = 'small', + variant, + size, radius, + outline, + dot, icon, children, className, @@ -47,11 +53,11 @@ export const Badge = ({ }: BadgeProps) => { return ( - {icon && ( + {icon && !dot && ( {icon} @@ -64,7 +70,7 @@ export const Badge = ({ {screenReaderText} )} - {children} + {!dot && children} ); }; From b6b571736f05ac7d01f4e9bb40ec293551dfd306 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Tue, 29 Sep 2026 16:35:36 +0530 Subject: [PATCH 2/3] refactor: [badge] drop outline and dot variants Indicator already renders a status dot, and no design or consumer asks for an outline badge. Drop the ref test: ref already reached the span through the props spread. --- .../src/content/docs/components/badge/demo.ts | 32 ---------- .../content/docs/components/badge/index.mdx | 14 ----- .../content/docs/components/badge/props.ts | 12 ---- .../components/badge/__tests__/badge.test.tsx | 29 ---------- .../components/badge/badge.module.css | 58 ------------------- packages/raystack/components/badge/badge.tsx | 14 +---- 6 files changed, 3 insertions(+), 156 deletions(-) diff --git a/apps/www/src/content/docs/components/badge/demo.ts b/apps/www/src/content/docs/components/badge/demo.ts index e3f4886a7..85e695c1b 100644 --- a/apps/www/src/content/docs/components/badge/demo.ts +++ b/apps/www/src/content/docs/components/badge/demo.ts @@ -27,14 +27,6 @@ export const playground = { options: ['micro', 'small', 'regular'], defaultValue: 'small' }, - outline: { - type: 'checkbox', - defaultValue: false - }, - dot: { - type: 'checkbox', - defaultValue: false - }, icon: { type: 'icon' }, @@ -63,30 +55,6 @@ export const variantDemo = { ` }; -export const outlineDemo = { - type: 'code', - code: ` - - Accent - Warning - Danger - Success - Neutral - ` -}; - -export const dotDemo = { - type: 'code', - code: ` - - - - - - - ` -}; - export const sizesDemo = { type: 'code', code: ` diff --git a/apps/www/src/content/docs/components/badge/index.mdx b/apps/www/src/content/docs/components/badge/index.mdx index 1d8138449..c3e6396cc 100644 --- a/apps/www/src/content/docs/components/badge/index.mdx +++ b/apps/www/src/content/docs/components/badge/index.mdx @@ -8,8 +8,6 @@ import { playground, sizesDemo, variantDemo, - outlineDemo, - dotDemo, iconDemo, screenReaderTextDemo, } from "./demo.ts"; @@ -36,18 +34,6 @@ Choose between different variants to convey different meanings or importance lev -### Outline - -Set `outline` to draw a border in the variant color and remove the fill. - - - -### Dot - -Set `dot` to render a small colored circle, for example a notification or status marker. The badge does not render `children` or `icon` in this mode. Pass `screenReaderText` so screen readers can announce the dot. - - - ### Size Three sizes. `small` is the default. `micro` fits inside table rows and beside dense labels. diff --git a/apps/www/src/content/docs/components/badge/props.ts b/apps/www/src/content/docs/components/badge/props.ts index 32f4272f1..cf77604b1 100644 --- a/apps/www/src/content/docs/components/badge/props.ts +++ b/apps/www/src/content/docs/components/badge/props.ts @@ -17,18 +17,6 @@ export interface BadgeProps { */ size?: 'micro' | 'small' | 'regular'; - /** - * Draws a border and removes the fill - * @defaultValue false - */ - outline?: boolean; - - /** - * Renders a colored dot without `children` or `icon` - * @defaultValue false - */ - dot?: boolean; - /** Optional ReactNode to display an icon before the text */ icon?: React.ReactNode; diff --git a/packages/raystack/components/badge/__tests__/badge.test.tsx b/packages/raystack/components/badge/__tests__/badge.test.tsx index f787f2623..1300c6dde 100644 --- a/packages/raystack/components/badge/__tests__/badge.test.tsx +++ b/packages/raystack/components/badge/__tests__/badge.test.tsx @@ -1,5 +1,4 @@ import { render, screen } from '@testing-library/react'; -import { createRef } from 'react'; import { describe, expect, it } from 'vitest'; import { Badge } from '../badge'; import styles from '../badge.module.css'; @@ -29,12 +28,6 @@ describe('Badge', () => { expect(screen.getByText('Count:')).toBeInTheDocument(); expect(screen.getByText('5')).toBeInTheDocument(); }); - - it('passes ref to the root span', () => { - const ref = createRef(); - const { container } = render(); - expect(ref.current).toBe(container.firstChild); - }); }); describe('Variants', () => { @@ -58,28 +51,6 @@ describe('Badge', () => { const badge = container.querySelector('span'); expect(badge).toHaveClass(styles['badge-accent']); }); - - it('renders outline', () => { - const { container } = render(Outline); - const badge = container.querySelector('span'); - expect(badge).toHaveClass(styles['badge-outline']); - }); - - it('renders a dot without children or icon', () => { - const { container } = render( - } - screenReaderText='3 unread' - > - 3 - - ); - const badge = container.querySelector('span'); - expect(badge).toHaveClass(styles['badge-dot']); - expect(badge).toHaveTextContent(/^3 unread$/); - expect(screen.queryByTestId('icon')).not.toBeInTheDocument(); - }); }); describe('Sizes', () => { diff --git a/packages/raystack/components/badge/badge.module.css b/packages/raystack/components/badge/badge.module.css index 3e0da085c..57cfbb358 100644 --- a/packages/raystack/components/badge/badge.module.css +++ b/packages/raystack/components/badge/badge.module.css @@ -70,64 +70,6 @@ color: var(--rs-color-foreground-base-primary); } -/* Outline. An inset shadow draws the border without changing the fixed height. */ -.badge-outline { - background: transparent; - box-shadow: inset 0 0 0 0.5px var(--rs-color-border-base-secondary); -} - -.badge-outline.badge-accent { - box-shadow: inset 0 0 0 0.5px var(--rs-color-border-accent-primary); -} - -.badge-outline.badge-warning { - box-shadow: inset 0 0 0 0.5px var(--rs-color-border-attention-primary); -} - -.badge-outline.badge-danger { - box-shadow: inset 0 0 0 0.5px var(--rs-color-border-danger-primary); -} - -.badge-outline.badge-success { - box-shadow: inset 0 0 0 0.5px var(--rs-color-border-success-primary); -} - -/* Dot */ -.badge-dot { - width: var(--rs-space-3); - height: var(--rs-space-3); - padding: 0; - border-radius: var(--rs-radius-full); -} - -.badge-dot.badge-accent { - background: var(--rs-color-background-accent-emphasis); -} - -.badge-dot.badge-warning { - background: var(--rs-color-background-attention-emphasis); -} - -.badge-dot.badge-danger { - background: var(--rs-color-background-danger-emphasis); -} - -.badge-dot.badge-success { - background: var(--rs-color-background-success-emphasis); -} - -.badge-dot.badge-neutral { - background: var(--rs-color-background-neutral-emphasis); -} - -.badge-dot.badge-gradient { - background: linear-gradient( - to right, - oklch(0.5674 0.2831 312.58) 0%, - oklch(0.5988 0.2445 29.12) 100% - ); -} - /* Icon */ .icon { display: inline-flex; diff --git a/packages/raystack/components/badge/badge.tsx b/packages/raystack/components/badge/badge.tsx index cc804ede6..742bb21aa 100644 --- a/packages/raystack/components/badge/badge.tsx +++ b/packages/raystack/components/badge/badge.tsx @@ -19,12 +19,6 @@ const badge = cva(styles['badge'], { micro: styles['badge-micro'], small: styles['badge-small'], regular: styles['badge-regular'] - }, - outline: { - true: styles['badge-outline'] - }, - dot: { - true: styles['badge-dot'] } }, defaultVariants: { @@ -43,8 +37,6 @@ export const Badge = ({ variant, size, radius, - outline, - dot, icon, children, className, @@ -53,11 +45,11 @@ export const Badge = ({ }: BadgeProps) => { return ( - {icon && !dot && ( + {icon && ( {icon} @@ -70,7 +62,7 @@ export const Badge = ({ {screenReaderText} )} - {!dot && children} + {children} ); }; From 74b3012c2b00a707068c859ad2121e6a1e5767b6 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Thu, 1 Oct 2026 13:00:55 +0530 Subject: [PATCH 3/3] fix: [badge] describe when to use the regular size --- apps/www/src/content/docs/components/badge/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/www/src/content/docs/components/badge/index.mdx b/apps/www/src/content/docs/components/badge/index.mdx index c3e6396cc..b5570db7d 100644 --- a/apps/www/src/content/docs/components/badge/index.mdx +++ b/apps/www/src/content/docs/components/badge/index.mdx @@ -36,7 +36,7 @@ Choose between different variants to convey different meanings or importance lev ### Size -Three sizes. `small` is the default. `micro` fits inside table rows and beside dense labels. +Three sizes. `small` is the default. `regular` matches body text and suits badges that stand alone, for example beside a heading. `micro` fits inside table rows and beside dense labels.