{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "badge", "title": "Badge", "description": "A badge component with variants, colours, sizes, and interactive button/link variants.", "dependencies": [ "@base-ui/react", "class-variance-authority", "clsx", "tailwind-merge" ], "registryDependencies": [ "https://ui.digital.nsw.gov.au/registry/r/theme.json", "https://ui.digital.nsw.gov.au/registry/r/button.json", "https://ui.digital.nsw.gov.au/registry/r/link.json" ], "files": [ { "path": "src/components/badge.tsx", "content": "'use client'\n\nimport { Button as ButtonPrimitive } from '@base-ui/react/button'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport clsx from 'clsx'\nimport React from 'react'\n\nimport { cn } from '@/lib/utils'\nimport { TouchTarget } from '@/components/button'\nimport { Link } from '@/components/link'\n\nconst focusOutline = {\n 'primary/grey': [\n // Base\n '[--focus-outline:var(--color-primary-800)]',\n // Base dark mode\n 'dark:[--focus-outline:var(--color-grey-600)]',\n // Soft, Surface and outline\n 'data-[variant=soft]:[--focus-outline:var(--color-primary-800)]/20',\n // Soft, Surface and outline dark mode\n 'dark:data-[variant=soft]:[--focus-outline:var(--color-grey-200)]/30',\n ],\n light: [\n // Base\n '[--focus-outline:var(--color-grey-300)]',\n // Base dark mode\n 'dark:[--focus-outline:var(--color-grey-100)]',\n // Soft dark mode\n 'dark:data-[variant=soft]:[--focus-outline:var(--color-grey-200)]/30',\n // Surface\n 'data-[variant=surface]:[--focus-outline:var(--color-grey-600)]',\n // Surface dark mode\n 'dark:data-[variant=surface]:[--focus-outline:var(--color-white)]/70',\n ],\n 'primary/white': [\n // Base\n '[--focus-outline:var(--color-primary-800)]',\n // Base dark mode\n 'dark:[--focus-outline:var(--color-white)]',\n // Soft\n 'data-[variant=soft]:[--focus-outline:var(--color-primary-800)]/20',\n // Soft dark mode\n 'dark:data-[variant=soft]:[--focus-outline:var(--color-grey-200)]/30',\n ],\n grey: [\n // Base\n '[--focus-outline:var(--color-grey-600)]',\n // Base dark mode\n 'dark:[--focus-outline:var(--color-grey-500)]',\n // Soft\n 'data-[variant=soft]:[--focus-outline:var(--color-grey-600)]/20',\n // Soft dark mode\n 'dark:data-[variant=soft]:[--focus-outline:var(--color-grey-200)]/20',\n // Outline dark mode\n 'dark:data-[variant=outline]:[--focus-outline:var(--color-grey-100)]/30',\n // Surface dark mode\n 'dark:data-[variant=surface]:[--focus-outline:var(--color-grey-200)]/20',\n ],\n white: [\n // Base\n '[--focus-outline:var(--color-white)]',\n // Soft\n 'data-[variant=soft]:[--focus-outline:var(--color-white)]/20',\n ],\n primary: [\n // Base\n '[--focus-outline:var(--color-primary-800)]',\n // Soft\n 'data-[variant=soft]:[--focus-outline:var(--color-primary-800)]/20',\n // Soft dark mode\n 'dark:data-[variant=soft]:[--focus-outline:var(--color-primary-800)]/60',\n ],\n secondary: [\n // Base\n '[--focus-outline:var(--color-primary-200)]',\n // Soft\n 'data-[variant=soft]:[--focus-outline:var(--color-primary-200)]/20',\n // Soft dark mode\n 'dark:data-[variant=soft]:[--focus-outline:var(--color-primary-200)]/60',\n ],\n tertiary: [\n // Base\n '[--focus-outline:var(--color-primary-600)]',\n // Soft\n 'data-[variant=soft]:[--focus-outline:var(--color-primary-600)]/20',\n // Soft dark mode\n 'dark:data-[variant=soft]:[--focus-outline:var(--color-primary-600)]/60',\n ],\n accent: [\n // Base\n '[--focus-outline:var(--color-accent-600)]',\n // Soft\n 'data-[variant=soft]:[--focus-outline:var(--color-accent-600)]/20',\n // Soft dark mode\n 'dark:data-[variant=soft]:[--focus-outline:var(--color-accent-600)]/60',\n ],\n}\n\nconst styles = {\n base: [\n // Base\n 'relative isolate inline-flex items-center rounded-sm font-normal border forced-colors:outline motion-safe:transition-all h-fit shrink-0',\n // Icon — one flat size, for the same reason the type scale below is flat.\n '*:data-[slot=icon]:size-4 *:data-[slot=icon]:shrink-0 *:data-[slot=icon]:self-center',\n ],\n solid: [\n // Text color\n 'text-(--badge-text)',\n // Optical border, implemented as the button background to avoid corner artifacts\n 'border-transparent bg-(--badge-border)',\n // Dark mode: border is rendered on `after` so background is set to button background\n 'dark:bg-(--badge-bg)',\n // Button background, implemented as foreground layer to stack on top of pseudo-border layer\n 'before:absolute before:inset-0 before:-z-10 before:rounded-[calc(var(--radius-sm)-1px)] before:bg-(--badge-bg)',\n // Drop shadow, applied to the inset `before` layer so it blends with the border\n 'before:shadow-sm',\n // Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo\n 'dark:before:hidden',\n // Dark mode: Subtle white outline is applied using a border\n 'dark:border-white/5',\n // Shim/overlay, inset to match button foreground and used for hover state + highlight shadow\n 'after:absolute after:inset-0 after:-z-10 after:rounded-[calc(var(--radius-sm)-1px)]',\n // White overlay on hover\n 'group-active:after:bg-(--badge-hover-overlay) group-hover:after:bg-(--badge-hover-overlay)',\n // Dark mode: `after` layer expands to cover entire button\n 'dark:after:-inset-px dark:after:rounded-sm',\n // Disabled\n 'data-disabled:before:shadow-none data-disabled:after:shadow-none',\n ],\n soft: [\n // Text color\n 'text-(--badge-bg)',\n // Optical border, implemented as the button background to avoid corner artifacts\n 'border-transparent bg-(--badge-bg)/5',\n // Dark mode: border is rendered on `after` so background is set to button background\n 'dark:bg-(--badge-bg)/20',\n // Button background, implemented as foreground layer to stack on top of pseudo-border layer\n 'before:absolute before:inset-0 before:-z-10 before:rounded-[calc(var(--radius-sm)-1px)] before:bg-(--badge-bg)/5',\n // Drop shadow, applied to the inset `before` layer so it blends with the border\n 'before:shadow-sm',\n // Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo\n 'dark:before:bg-white/5',\n // Dark mode: Subtle white outline is applied using a border\n 'dark:border-white/5',\n // Shim/overlay, inset to match button foreground and used for hover state + highlight shadow\n 'after:absolute after:inset-0 after:-z-10 after:rounded-[calc(var(--radius-sm)-1px)]',\n // White overlay on hover\n 'group-active:after:bg-(--badge-hover-overlay) group-hover:after:bg-(--badge-hover-overlay)',\n // Dark mode: `after` layer expands to cover entire button\n 'dark:after:-inset-px dark:after:rounded-sm',\n // Disabled\n 'data-disabled:before:shadow-none data-disabled:after:shadow-none',\n ],\n surface: [\n // Text color\n 'text-(--badge-bg)',\n // Optical border, implemented as the button background to avoid corner artifacts\n 'border-(--badge-bg)/50 bg-(--badge-bg)/5',\n // Dark mode: border is rendered on `after` so background is set to button background\n 'dark:bg-(--badge-bg)/30',\n // Button background, implemented as foreground layer to stack on top of pseudo-border layer\n 'before:absolute before:inset-0 before:-z-10 before:rounded-[calc(var(--radius-sm)-1px)] before:bg-(--badge-bg)/5',\n // Drop shadow, applied to the inset `before` layer so it blends with the border\n 'before:shadow-sm',\n // Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo\n 'dark:before:hidden',\n // Dark mode: Subtle white outline is applied using a border\n 'dark:border-(--badge-bg)/50',\n // Shim/overlay, inset to match button foreground and used for hover state + highlight shadow\n 'after:absolute after:inset-0 after:-z-10 after:rounded-[calc(var(--radius-sm)-1px)]',\n // Border color on hover\n 'group-active:border-(--badge-bg) group-hover:border-(--badge-bg)',\n // White overlay on hover\n 'group-active:after:bg-(--badge-hover-overlay) group-hover:after:bg-(--badge-hover-overlay)',\n // Dark mode: `after` layer expands to cover entire button\n 'dark:after:-inset-px dark:after:rounded-sm',\n // Disabled\n 'data-disabled:before:shadow-none data-disabled:after:shadow-none',\n ],\n outline: [\n // Text color\n 'border-(--badge-bg) text-(--badge-bg)',\n // Optical border, implemented as the button background to avoid corner artifacts\n 'bg-transparent',\n // Button background, implemented as foreground layer to stack on top of pseudo-border layer\n 'before:absolute before:inset-0 before:-z-10 before:rounded-[calc(var(--radius-sm)-1px)] before:bg-transparent',\n // Drop shadow, applied to the inset `before` layer so it blends with the border\n 'before:shadow-sm',\n // Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo\n 'dark:before:hidden',\n // Dark mode: Subtle white outline is applied using a border\n 'dark:border-(--badge-bg)',\n // Shim/overlay, inset to match button foreground and used for hover state + highlight shadow\n 'after:absolute after:inset-0 after:-z-10 after:rounded-[calc(var(--radius-sm)-1px)]',\n // White overlay on hover\n 'group-active:after:bg-(--badge-hover-overlay) group-hover:after:bg-(--badge-hover-overlay)',\n // Dark mode: `after` layer expands to cover entire button\n 'dark:after:-inset-px dark:after:rounded-sm',\n // Disabled\n 'data-disabled:before:shadow-none data-disabled:after:shadow-none',\n ],\n colors: {\n 'primary/grey': [\n // Base\n '[--badge-bg:var(--color-primary-800)] [--badge-border:var(--color-primary-800)]/90 [--badge-text:var(--color-white)]',\n // States\n '[--badge-hover-overlay:var(--color-primary-800)]/10 data-[variant=solid]:[--badge-hover-overlay:var(--color-white)]/10',\n // Dark mode\n 'dark:[--badge-bg:var(--color-grey-600)] dark:[--badge-border:var(--color-grey-600)]/90 dark:[--badge-text:var(--color-white)] dark:text-white',\n // Dark mode states\n 'dark:[--badge-hover-overlay:var(--color-grey-200)]/20',\n ],\n light: [\n // Base\n '[--badge-bg:var(--color-grey-200)] [--badge-border:var(--color-grey-200)]/90 [--badge-text:var(--color-grey-800)]',\n 'text-grey-800',\n // Soft\n 'data-[variant=soft]:[--badge-bg:var(--color-grey-500)]',\n // Surface\n 'data-[variant=surface]:[--badge-bg:var(--color-grey-500)]',\n // States\n '[--badge-hover-overlay:var(--color-grey-200)]/10 data-[variant=outline]:[--badge-hover-overlay:var(--color-grey-200)]/20 data-[variant=solid]:[--badge-hover-overlay:var(--color-grey-400)]/50',\n // Dark mode\n 'dark:text-grey-200 dark:data-[variant=solid]:text-grey-800',\n // Dark mode states\n '',\n ],\n 'primary/white': [\n // Base\n '[--badge-bg:var(--color-primary-800)] [--badge-border:var(--color-primary-800)]/90 [--badge-text:var(--color-white)]',\n // States\n '[--badge-hover-overlay:var(--color-primary-800)]/10 data-[variant=solid]:[--badge-hover-overlay:var(--color-white)]/10',\n // Dark mode\n 'dark:[--badge-bg:var(--color-white)] dark:[--badge-border:var(--color-white)]/90 dark:[--badge-text:var(--color-grey-800)]',\n // Dark mode states\n '',\n ],\n grey: [\n // Base\n '[--badge-bg:var(--color-grey-600)] [--badge-border:var(--color-grey-600)]/90 [--badge-text:var(--color-white)]',\n // States\n '[--badge-hover-overlay:var(--color-primary-800)]/10 data-[variant=solid]:[--badge-hover-overlay:var(--color-white)]/10',\n // Dark mode\n 'dark:text-white',\n // Dark mode states\n '',\n ],\n white: [\n // Base\n '[--badge-bg:var(--color-white)] [--badge-border:var(--color-white)]/90 text-white data-[variant=solid]:text-grey-800',\n // States\n '[--badge-hover-overlay:var(--color-primary-800)]/10 data-[variant=solid]:[--badge-hover-overlay:var(--color-white)]/10',\n // Dark mode\n 'dark:text-grey-200 dark:data-[variant=solid]:text-grey-800',\n // Dark mode states\n '',\n ],\n primary: [\n // Base\n '[--badge-bg:var(--color-primary-800)] [--badge-border:var(--color-primary-800)]/90 [--badge-text:var(--color-white)]',\n // States\n '[--badge-hover-overlay:var(--color-primary-800)]/10 data-[variant=solid]:[--badge-hover-overlay:var(--color-white)]/10',\n // Dark mode\n 'dark:text-white dark:bg-(--badge-bg)/40 dark:data-[variant=solid]:bg-(--badge-bg)',\n // Dark mode states\n 'dark:[--badge-hover-overlay:var(--color-primary-800)]/50 dark:data-[variant=solid]:[--badge-hover-overlay:var(--color-white)]/5',\n // Dark border\n 'dark:data-[variant=surface]:border-(--badge-border)/60',\n 'dark:data-[variant=outline]:border-(--badge-border)',\n // Dark border states\n 'dark:data-[variant=surface]:active:border-(--badge-border)',\n 'dark:data-[variant=surface]:hover:border-(--badge-border)',\n ],\n secondary: [\n // Base\n '[--badge-bg:var(--color-primary-200)] [--badge-border:var(--color-primary-200)]/90 [--badge-text:var(--color-primary-800)]',\n // States\n '[--badge-hover-overlay:var(--color-primary-200)]/30 data-[variant=solid]:[--badge-hover-overlay:var(--color-primary-250)]',\n // Dark mode\n 'dark:[--badge-bg:var(--color-primary-400)] dark:[--badge-border:var(--color-primary-400)]/90 dark:[--badge-text:white]',\n // Dark mode states\n 'dark:[--badge-hover-overlay:var(--color-primary-400)]/30 dark:data-[variant=solid]:[--badge-hover-overlay:var(--color-white)]/10',\n ],\n tertiary: [\n // Base\n '[--badge-bg:var(--color-primary-600)] [--badge-border:var(--color-primary-600)]/90 [--badge-text:var(--color-white)]',\n // States\n '[--badge-hover-overlay:var(--color-primary-600)]/10 data-[variant=solid]:[--badge-hover-overlay:var(--color-white)]/10',\n // Dark mode\n 'dark:[--badge-bg:var(--color-primary-500)] dark:[--badge-border:var(--color-primary-500)]/90 dark:[--badge-text:white]',\n // Dark mode states\n 'dark:[--badge-hover-overlay:var(--color-primary-500)]/30 dark:data-[variant=solid]:[--badge-hover-overlay:var(--color-white)]/10',\n ],\n accent: [\n // Base\n '[--badge-bg:var(--color-accent-600)] [--badge-border:var(--color-accent-600)]/90 [--badge-text:var(--color-white)]',\n // States\n '[--badge-hover-overlay:var(--color-accent-600)]/10 data-[variant=solid]:[--badge-hover-overlay:var(--color-white)]/10',\n // Dark mode\n 'dark:[--badge-bg:var(--color-accent-500)] dark:[--badge-border:var(--color-accent-500)]/90 dark:[--badge-text:white]',\n // Dark mode states\n 'dark:[--badge-hover-overlay:var(--color-accent-500)]/30 dark:data-[variant=solid]:[--badge-hover-overlay:var(--color-white)]/10',\n ],\n },\n // ───────────────────────────────────────────────────────────────────────────\n // Type scale (flat — do not reintroduce a `sm:` shrink)\n // ───────────────────────────────────────────────────────────────────────────\n //\n // Each size holds one font-size at every viewport. Sizes are chosen from the\n // hierarchy field.tsx documents: 16px is the body-copy ideal, 14px reads as\n // supporting information, and 12px is below the NSW Government digital\n // service standards minimum body-text size.\n //\n // These variants used to shrink at `sm:` (640px) — `text-sm/5 sm:text-xs/5`\n // and `text-base/5 sm:text-sm/5` — inherited from the Catalyst-derived\n // scaffolding this component started life as (4889aba). That put `default`\n // and `sm` badges at 12px from 640px up, i.e. under the floor precisely where\n // there is the most room, and capped `lg` at 14px so no badge in the system\n // could meet a 16px minimum on desktop. Buttons — which badges sit beside in\n // the header — have always been a flat `text-base/7`, so the shrink was also\n // out of step with its closest sibling.\n //\n // Below 640px nothing changed: every value here is the size that breakpoint\n // already rendered.\n size: {\n default:\n 'py-[calc(--spacing(1))] px-[calc(--spacing(2))] gap-[calc(--spacing(1)*1.5)] text-sm/5',\n sm: 'py-[calc(--spacing(1)*0.5)] px-[calc(--spacing(1)*1.5)] gap-[calc(--spacing(1)*1.5)] text-sm/5',\n lg: 'py-[calc(--spacing(1))] px-[calc(--spacing(2)*1.25)] gap-[calc(--spacing(2))] text-base/5',\n },\n}\n\nconst badgeVariants = cva(styles.base, {\n variants: {\n variant: {\n solid: styles.solid,\n soft: styles.soft,\n surface: styles.surface,\n outline: styles.outline,\n },\n color: {\n 'primary/grey': styles.colors['primary/grey'],\n light: styles.colors.light,\n 'primary/white': styles.colors['primary/white'],\n white: styles.colors.white,\n grey: styles.colors.grey,\n primary: styles.colors.primary,\n secondary: styles.colors.secondary,\n tertiary: styles.colors.tertiary,\n accent: styles.colors.accent,\n },\n size: {\n default: styles.size.default,\n sm: styles.size.sm,\n lg: styles.size.lg,\n },\n },\n defaultVariants: {\n variant: 'soft',\n color: 'primary',\n size: 'default',\n },\n})\n\ntype BadgeProps = VariantProps &\n React.ComponentPropsWithoutRef<'span'> & { ref?: React.Ref }\n\nfunction Badge({ variant, color, size, className, ref, ...props }: BadgeProps) {\n return (\n \n )\n}\n\n/** Visual/content props shared by `BadgeButton` and `BadgeLink`. */\ntype BadgeOwnProps = VariantProps & {\n className?: string\n children: React.ReactNode\n}\n\ntype BadgeButtonProps = BadgeOwnProps & Omit\n\ntype BadgeLinkProps = BadgeOwnProps &\n Omit, 'className' | 'variant'> & {\n ref?: React.Ref\n }\n\nfunction badgeWrapperClasses({ color, className }: Pick) {\n return clsx(className, focusOutline[(color ?? undefined) as keyof typeof focusOutline], [\n // Base\n 'group relative inline-flex rounded-sm',\n // Focus\n 'focus:outline-1 focus:outline-offset-1 focus:outline-(--focus-outline)',\n // Disabled\n 'disabled:pointer-events-none disabled:opacity-50',\n ])\n}\n\n/**\n * Interactive badge on the Base UI button primitive. For badge-styled\n * navigation, use `BadgeLink` — the `href` polymorphism that previously\n * lived on this component was removed in v2.\n */\nfunction BadgeButton({\n variant,\n color,\n size,\n className,\n children,\n ref,\n ...props\n}: BadgeButtonProps) {\n return (\n \n \n \n {children}\n \n \n \n )\n}\n\n/** Badge-styled anchor; renders through `Link` (so `LinkProvider` applies). */\nfunction BadgeLink({ variant, color, size, className, children, ref, ...props }: BadgeLinkProps) {\n return (\n \n \n \n {children}\n \n \n \n )\n}\n\nexport { Badge, BadgeButton, BadgeLink, badgeVariants }\nexport type { BadgeButtonProps, BadgeLinkProps, BadgeProps }\n", "type": "registry:ui", "target": "components/badge.tsx" }, { "path": "src/lib/utils.ts", "content": "import { clsx, type ClassValue } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n", "type": "registry:lib", "target": "lib/utils.ts" } ], "type": "registry:ui", "meta": { "nswdsVersion": "5.1.0" } }