Behive Tech Registry

Badge

v0.4.0

A small status label with semantic color variants.

pnpm add @behivetech/atoms.badge

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";
4
5export type BadgeVariant =
6 "default" | "primary" | "success" | "warning" | "error" | "info";
7export type BadgeSize = "sm" | "md";
8
9export 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}
19
20export const Badge = ({
21 variant = "default",
22 size = "md",
23 className,
24 children,
25 ...rest
26}: BadgeProps) => {
27 const [rootClass] = getClassName({
28 className,
29 rootClass: "BHT__Badge",
30 modifiers: { [variant]: true, [size]: true },
31 styles,
32 });
33
34 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 a
3// `*-container` fill with its `on-*-container` text.
4//
5// Success and warning have no MD3 role, so they stay on the `--color-*` names.
6
7.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 grow
15 // inside rows and headings already laid out around them.
16 line-height: 1;
17 white-space: nowrap;
18
19 // --- Size modifiers ------------------------------------------------------
20
21 &--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 }
28
29 &--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 }
36
37 // --- Variant modifiers ---------------------------------------------------
38
39 &--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 }
48
49 &--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 }
55
56 &--success {
57 &.BHT__Badge {
58 background: var(--color-success-surface);
59 color: var(--color-success);
60 }
61 }
62
63 &--warning {
64 &.BHT__Badge {
65 background: var(--color-warning-surface);
66 color: var(--color-warning);
67 }
68 }
69
70 &--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 }
79
80 // Info already aliases to tertiary in base-styles; reading the container
81 // 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';
2
3export 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';
3
4describe('Badge', () => {
5 it('renders children', () => {
6 render(<Badge>Published</Badge>);
7 expect(screen.getByText('Published')).toBeTruthy();
8 });
9
10 it('renders as a span element', () => {
11 render(<Badge>Status</Badge>);
12 expect(screen.getByText('Status').tagName).toBe('SPAN');
13 });
14
15 it('applies additional className', () => {
16 render(<Badge className="custom">Label</Badge>);
17 expect(screen.getByText('Label').className).toContain('custom');
18 });
19});