Badge
v0.4.0A small status label with semantic color variants.
registry/behivetech/atoms/badge · depends on @behivetech/get-class-name
badge.tsx
1import { type HTMLAttributes } from "react";2import { getClassName } from "@behivetech/get-class-name";3import styles from "./badge.module.scss";45export type BadgeVariant =6 "default" | "primary" | "success" | "warning" | "error" | "info";7export type BadgeSize = "sm" | "md";89export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {10 /** Controls the color scheme; defaults to default */11 variant?: BadgeVariant;12 /** Controls the size; defaults to md */13 size?: BadgeSize;14 /** Additional class names to merge with the component root element */15 className?: string;16 /** Content to render inside the badge */17 children?: React.ReactNode;18}1920export const Badge = ({21 variant = "default",22 size = "md",23 className,24 children,25 ...rest26}: BadgeProps) => {27 const [rootClass] = getClassName({28 className,29 rootClass: "BHT__Badge",30 modifiers: { [variant]: true, [size]: true },31 styles,32 });3334 return (35 <span {...rest} className={rootClass}>36 {children}37 </span>38 );39};
badge.module.scss
1// Material Design 3 tonal label. MD3's own Badge is a notification dot/count,2// so this follows the tonal-container pattern instead: each variant pairs a3// `*-container` fill with its `on-*-container` text.4//5// Success and warning have no MD3 role, so they stay on the `--color-*` names.67.BHT__Badge {8 display: inline-flex;9 align-items: center;10 justify-content: center;11 border-radius: var(--md-sys-shape-corner-full, 9999px);12 font-family: var(--md-sys-typescale-label-large-font, var(--font-body));13 font-weight: var(--md-sys-typescale-label-large-weight, 500);14 // Held at 1 rather than the role's line-height so badge heights don't grow15 // inside rows and headings already laid out around them.16 line-height: 1;17 white-space: nowrap;1819 // --- Size modifiers ------------------------------------------------------2021 &--sm {22 &.BHT__Badge {23 font-size: var(--md-sys-typescale-label-medium-size, 0.75rem);24 letter-spacing: var(--md-sys-typescale-label-medium-tracking, 0.0417em);25 padding: 0.125rem var(--spacing-2, 0.5rem);26 }27 }2829 &--md {30 &.BHT__Badge {31 font-size: var(--md-sys-typescale-label-large-size, 0.875rem);32 letter-spacing: var(--md-sys-typescale-label-large-tracking, 0.0071em);33 padding: var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem);34 }35 }3637 // --- Variant modifiers ---------------------------------------------------3839 &--default {40 &.BHT__Badge {41 background: var(42 --md-sys-color-surface-container-highest,43 var(--color-border)44 );45 color: var(--md-sys-color-on-surface-variant, var(--color-text-body));46 }47 }4849 &--primary {50 &.BHT__Badge {51 background: var(--md-sys-color-primary-container, var(--color-primary));52 color: var(--md-sys-color-on-primary-container, var(--color-on-primary));53 }54 }5556 &--success {57 &.BHT__Badge {58 background: var(--color-success-surface);59 color: var(--color-success);60 }61 }6263 &--warning {64 &.BHT__Badge {65 background: var(--color-warning-surface);66 color: var(--color-warning);67 }68 }6970 &--error {71 &.BHT__Badge {72 background: var(73 --md-sys-color-error-container,74 var(--color-error-surface)75 );76 color: var(--md-sys-color-on-error-container, var(--color-error));77 }78 }7980 // Info already aliases to tertiary in base-styles; reading the container81 // roles directly keeps text contrast right in both schemes.82 &--info {83 &.BHT__Badge {84 background: var(85 --md-sys-color-tertiary-container,86 var(--color-info-surface)87 );88 color: var(--md-sys-color-on-tertiary-container, var(--color-info));89 }90 }91}
index.ts
1export { Badge } from "./badge.js";2export type { BadgeProps, BadgeVariant, BadgeSize } from "./badge.js";
badge.composition.tsx
1import { Badge } from './badge.js';23export const DefaultBadge = () => <Badge variant="default">Default</Badge>;4export const PrimaryBadge = () => <Badge variant="primary">Primary</Badge>;5export const SuccessBadge = () => <Badge variant="success">Success</Badge>;6export const WarningBadge = () => <Badge variant="warning">Warning</Badge>;7export const ErrorBadge = () => <Badge variant="error">Error</Badge>;8export const InfoBadge = () => <Badge variant="info">Info</Badge>;9export const SmallBadge = () => <Badge variant="primary" size="sm">New</Badge>;
badge.spec.tsx
1import { render, screen } from '@testing-library/react';2import { Badge } from './badge.js';34describe('Badge', () => {5 it('renders children', () => {6 render(<Badge>Published</Badge>);7 expect(screen.getByText('Published')).toBeTruthy();8 });910 it('renders as a span element', () => {11 render(<Badge>Status</Badge>);12 expect(screen.getByText('Status').tagName).toBe('SPAN');13 });1415 it('applies additional className', () => {16 render(<Badge className="custom">Label</Badge>);17 expect(screen.getByText('Label').className).toContain('custom');18 });19});