diff --git a/.config/cem.yaml b/.config/cem.yaml
index febc90fcec..86f5f070a6 100644
--- a/.config/cem.yaml
+++ b/.config/cem.yaml
@@ -1,5 +1,6 @@
sourceControlRootUrl: https://github.com/patternfly/patternfly-elements/tree/main/
generate:
+ output: ./elements/custom-elements.json
files:
- ./elements/*/*.ts
- ./core/*/*.ts
diff --git a/elements/package.json b/elements/package.json
index fe8dda2080..11d9359e77 100644
--- a/elements/package.json
+++ b/elements/package.json
@@ -18,6 +18,7 @@
"./pf-v5-back-to-top/pf-v5-back-to-top.js": "./pf-v5-back-to-top/pf-v5-back-to-top.js",
"./pf-v5-background-image/pf-v5-background-image.js": "./pf-v5-background-image/pf-v5-background-image.js",
"./pf-v5-badge/pf-v5-badge.js": "./pf-v5-badge/pf-v5-badge.js",
+ "./pf-v6-badge/pf-v6-badge.js": "./pf-v6-badge/pf-v6-badge.js",
"./pf-v5-banner/pf-v5-banner.js": "./pf-v5-banner/pf-v5-banner.js",
"./pf-v5-button/pf-v5-button.js": "./pf-v5-button/pf-v5-button.js",
"./pf-v5-card/pf-v5-card.js": "./pf-v5-card/pf-v5-card.js",
diff --git a/elements/pf-v6-badge/demo/disabled.html b/elements/pf-v6-badge/demo/disabled.html
new file mode 100644
index 0000000000..3e57ccf9e2
--- /dev/null
+++ b/elements/pf-v6-badge/demo/disabled.html
@@ -0,0 +1,12 @@
+---
+name: Disabled
+description: Disabled badges indicate that the associated content or action is currently unavailable.
+---
+7
+24
+240
+999+
+
+
diff --git a/elements/pf-v6-badge/demo/index.html b/elements/pf-v6-badge/demo/index.html
new file mode 100644
index 0000000000..418d93581a
--- /dev/null
+++ b/elements/pf-v6-badge/demo/index.html
@@ -0,0 +1,9 @@
+---
+name: Basic
+description: A basic badge displays a numeric value.
+---
+7
+
+
diff --git a/elements/pf-v6-badge/demo/read.html b/elements/pf-v6-badge/demo/read.html
new file mode 100644
index 0000000000..b0239ac479
--- /dev/null
+++ b/elements/pf-v6-badge/demo/read.html
@@ -0,0 +1,12 @@
+---
+name: Read
+description: Read badges display a numeric value with read state styling, indicating the content has been viewed.
+---
+7
+24
+240
+999+
+
+
diff --git a/elements/pf-v6-badge/demo/threshold.html b/elements/pf-v6-badge/demo/threshold.html
new file mode 100644
index 0000000000..b27ea00fca
--- /dev/null
+++ b/elements/pf-v6-badge/demo/threshold.html
@@ -0,0 +1,10 @@
+---
+name: Threshold
+description: A threshold appends a "+" when the numeric value exceeds a set maximum.
+---
+400
+900
+
+
diff --git a/elements/pf-v6-badge/demo/unread.html b/elements/pf-v6-badge/demo/unread.html
new file mode 100644
index 0000000000..7e16b908e2
--- /dev/null
+++ b/elements/pf-v6-badge/demo/unread.html
@@ -0,0 +1,12 @@
+---
+name: Unread
+description: Unread badges use a bold, branded style to indicate content that has not yet been viewed.
+---
+7
+24
+240
+999+
+
+
diff --git a/elements/pf-v6-badge/pf-v6-badge.css b/elements/pf-v6-badge/pf-v6-badge.css
new file mode 100644
index 0000000000..e158aedfcd
--- /dev/null
+++ b/elements/pf-v6-badge/pf-v6-badge.css
@@ -0,0 +1,58 @@
+:host {
+ position: relative;
+ display: inline-block;
+ min-width: var(--pf-v6-c-badge--MinWidth,
+ var(--pf-t--global--spacer--xl, 2rem));
+ padding-inline-start: var(--pf-v6-c-badge--PaddingInlineStart,
+ var(--pf-t--global--spacer--sm, 0.5rem));
+ padding-inline-end: var(--pf-v6-c-badge--PaddingInlineEnd,
+ var(--pf-t--global--spacer--sm, 0.5rem));
+ font-size: var(--pf-v6-c-badge--FontSize,
+ var(--pf-t--global--font--size--body--sm, 0.75rem));
+ font-weight: var(--pf-v6-c-badge--FontWeight,
+ var(--pf-t--global--font--weight--body--bold, 700));
+ color: var(--pf-v6-c-badge--Color,
+ var(--pf-t--global--text--color--nonstatus--on-gray--default, #151515));
+ text-align: center;
+ white-space: nowrap;
+ background-color: var(--pf-v6-c-badge--BackgroundColor,
+ var(--pf-t--global--color--nonstatus--gray--default, #f0f0f0));
+ border-radius: var(--pf-v6-c-badge--BorderRadius,
+ var(--pf-t--global--border--radius--pill, 180em));
+}
+
+:host::after {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ content: "";
+ border: var(--pf-v6-c-badge--BorderWidth,
+ var(--pf-t--global--border--width--regular, 1px)) solid var(--pf-v6-c-badge--BorderColor, transparent);
+ border-radius: inherit;
+}
+
+:host([state="read"]) {
+ --pf-v6-c-badge--Color: var(--pf-v6-c-badge--m-read--Color,
+ var(--pf-t--global--text--color--nonstatus--on-gray--default, #151515));
+ --pf-v6-c-badge--BackgroundColor: var(--pf-v6-c-badge--m-read--BackgroundColor,
+ var(--pf-t--global--color--nonstatus--gray--default, #f0f0f0));
+ --pf-v6-c-badge--BorderColor: var(--pf-v6-c-badge--m-read--BorderColor,
+ var(--pf-t--global--border--color--high-contrast, #151515));
+}
+
+:host([state="unread"]) {
+ --pf-v6-c-badge--Color: var(--pf-v6-c-badge--m-unread--Color,
+ var(--pf-t--global--text--color--on-brand--default, #fff));
+ --pf-v6-c-badge--BackgroundColor: var(--pf-v6-c-badge--m-unread--BackgroundColor,
+ var(--pf-t--global--color--brand--default, #06c));
+}
+
+:host([disabled]) {
+ --pf-v6-c-badge--Color: var(--pf-v6-c-badge--m-disabled--Color,
+ var(--pf-t--global--text--color--on-disabled, #6a6e73));
+ --pf-v6-c-badge--BackgroundColor: var(--pf-v6-c-badge--m-disabled--BackgroundColor,
+ var(--pf-t--global--background--color--disabled--default, #d2d2d2));
+ --pf-v6-c-badge--BorderColor: var(--pf-v6-c-badge--m-disabled--BorderColor,
+ var(--pf-t--global--border--color--disabled, #d2d2d2));
+ pointer-events: none;
+}
diff --git a/elements/pf-v6-badge/pf-v6-badge.ts b/elements/pf-v6-badge/pf-v6-badge.ts
new file mode 100644
index 0000000000..e22a11ee9c
--- /dev/null
+++ b/elements/pf-v6-badge/pf-v6-badge.ts
@@ -0,0 +1,72 @@
+import { LitElement, html, type TemplateResult } from 'lit';
+import { customElement } from 'lit/decorators/custom-element.js';
+import { property } from 'lit/decorators/property.js';
+
+import styles from './pf-v6-badge.css';
+
+export type BadgeState = 'unread' | 'read';
+
+/**
+ * A **badge** is used to annotate other information like a label or an object name.
+ * @summary Displays a numeric value as an annotation
+ * @slot - Badge content, typically a number or short text
+ * @cssprop {} --pf-v6-c-badge--Color - Text color of the badge
+ * @cssprop {} --pf-v6-c-badge--BackgroundColor - Background color of the badge
+ * @cssprop {} --pf-v6-c-badge--BorderColor - Border color of the badge
+ * @cssprop {} --pf-v6-c-badge--BorderWidth - Border width of the badge
+ * @cssprop {} --pf-v6-c-badge--BorderRadius - Border radius of the badge
+ * @cssprop {} --pf-v6-c-badge--MinWidth - Minimum width of the badge
+ * @cssprop {} --pf-v6-c-badge--PaddingInlineStart - Inline start padding
+ * @cssprop {} --pf-v6-c-badge--PaddingInlineEnd - Inline end padding
+ * @cssprop {} --pf-v6-c-badge--FontSize - Font size of the badge text
+ * @cssprop {} --pf-v6-c-badge--FontWeight - Font weight of the badge text
+ * @cssprop {} --pf-v6-c-badge--m-read--Color - Text color in read state
+ * @cssprop {} --pf-v6-c-badge--m-read--BackgroundColor - Background color in read state
+ * @cssprop {} --pf-v6-c-badge--m-read--BorderColor - Border color in read state
+ * @cssprop {} --pf-v6-c-badge--m-unread--Color - Text color in unread state
+ * @cssprop {} --pf-v6-c-badge--m-unread--BackgroundColor - Background color in unread state
+ * @cssprop {} --pf-v6-c-badge--m-disabled--Color - Text color when disabled
+ * @cssprop {} --pf-v6-c-badge--m-disabled--BackgroundColor - Background color when disabled
+ * @cssprop {} --pf-v6-c-badge--m-disabled--BorderColor - Border color when disabled
+ */
+@customElement('pf-v6-badge')
+export class PfV6Badge extends LitElement {
+ static readonly styles: CSSStyleSheet[] = [styles];
+
+ /**
+ * Denotes the state-of-affairs this badge represents.
+ */
+ @property({ reflect: true }) state?: BadgeState;
+
+ /**
+ * Sets a numeric value for a badge.
+ *
+ * You can pair it with `threshold` attribute to add a `+` sign
+ * if the number exceeds the threshold value.
+ */
+ @property({ type: Number }) number?: number;
+
+ /**
+ * Sets a threshold for the numeric value and adds `+` sign if
+ * the numeric value exceeds the threshold value.
+ */
+ @property({ type: Number }) threshold?: number;
+
+ /** Disables the badge */
+ @property({ type: Boolean, reflect: true }) disabled = false;
+
+ override render(): TemplateResult {
+ const { threshold, number } = this;
+ const displayText =
+ (threshold && number && (threshold <= number)) ? `${threshold.toString()}+`
+ : (number != null) ? number.toString()
+ : '';
+ return html`${!displayText ? html`` : displayText}`;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'pf-v6-badge': PfV6Badge;
+ }
+}
diff --git a/elements/pf-v6-badge/test/pf-v6-badge.spec.ts b/elements/pf-v6-badge/test/pf-v6-badge.spec.ts
new file mode 100644
index 0000000000..ad2dee0e5b
--- /dev/null
+++ b/elements/pf-v6-badge/test/pf-v6-badge.spec.ts
@@ -0,0 +1,181 @@
+import { expect, html } from '@open-wc/testing';
+import { createFixture } from '@patternfly/pfe-tools/test/create-fixture.js';
+import { a11ySnapshot } from '@patternfly/pfe-tools/test/a11y-snapshot.js';
+import { getColor, hexToRgb } from '@patternfly/pfe-tools/test/hex-to-rgb.js';
+import { PfV6Badge } from '@patternfly/elements/pf-v6-badge/pf-v6-badge.js';
+
+describe('', function() {
+ it('imperatively instantiates', function() {
+ expect(document.createElement('pf-v6-badge')).to.be.an.instanceof(PfV6Badge);
+ });
+
+ it('should upgrade', async function() {
+ const el = await createFixture(html`10`);
+ expect(el)
+ .to.be.an.instanceOf(customElements.get('pf-v6-badge'))
+ .and
+ .to.be.an.instanceOf(PfV6Badge);
+ });
+
+ describe('with number attribute', function() {
+ let element: PfV6Badge;
+
+ beforeEach(async function() {
+ element = await createFixture(html`
+ 100
+ `);
+ await element.updateComplete;
+ });
+
+ it('should have the number property set', function() {
+ expect(element.number).to.equal(100);
+ });
+
+ it('should be visible in the accessibility tree', async function() {
+ const snapshot = await a11ySnapshot();
+ expect(snapshot.children?.length).to.be.greaterThan(0);
+ const badgeNode = snapshot.children?.find(
+ (child: { name?: string }) => child.name?.includes('100')
+ );
+ expect(badgeNode).to.exist;
+ });
+ });
+
+ describe('with number exceeding threshold', function() {
+ let element: PfV6Badge;
+
+ beforeEach(async function() {
+ element = await createFixture(html`
+ 900
+ `);
+ await element.updateComplete;
+ });
+
+ it('should display threshold with "+" in the accessibility tree', async function() {
+ const snapshot = await a11ySnapshot();
+ const badgeNode = snapshot.children?.find(
+ (child: { name?: string }) => child.name?.includes('100+')
+ );
+ expect(badgeNode).to.exist;
+ });
+ });
+
+ describe('with number below threshold', function() {
+ let element: PfV6Badge;
+
+ beforeEach(async function() {
+ element = await createFixture(html`
+ 50
+ `);
+ await element.updateComplete;
+ });
+
+ it('should display the number without "+" in the accessibility tree', async function() {
+ const snapshot = await a11ySnapshot();
+ const badgeNode = snapshot.children?.find(
+ (child: { name?: string }) =>
+ child.name?.includes('50') && !child.name?.includes('+')
+ );
+ expect(badgeNode).to.exist;
+ });
+ });
+
+ describe('without state attribute', function() {
+ let element: PfV6Badge;
+
+ beforeEach(async function() {
+ element = await createFixture(html`
+ 10
+ `);
+ await element.updateComplete;
+ });
+
+ it('should display default background color', function() {
+ const [r, g, b] = getColor(element, 'background-color');
+ expect([r, g, b]).to.deep.equal(hexToRgb('#f0f0f0'));
+ });
+ });
+
+ describe('with state="read"', function() {
+ let element: PfV6Badge;
+
+ beforeEach(async function() {
+ element = await createFixture(html`
+ 10
+ `);
+ await element.updateComplete;
+ });
+
+ it('should display read background color', function() {
+ const [r, g, b] = getColor(element, 'background-color');
+ expect([r, g, b]).to.deep.equal(hexToRgb('#f0f0f0'));
+ });
+ });
+
+ describe('with state="unread"', function() {
+ let element: PfV6Badge;
+
+ beforeEach(async function() {
+ element = await createFixture(html`
+ 10
+ `);
+ await element.updateComplete;
+ });
+
+ it('should display unread background color', function() {
+ const [r, g, b] = getColor(element, 'background-color');
+ expect([r, g, b]).to.deep.equal(hexToRgb('#0066cc'));
+ });
+
+ it('should display unread text color', function() {
+ const [r, g, b] = getColor(element, 'color');
+ expect([r, g, b]).to.deep.equal(hexToRgb('#ffffff'));
+ });
+ });
+
+ describe('with disabled attribute', function() {
+ let element: PfV6Badge;
+
+ beforeEach(async function() {
+ element = await createFixture(html`
+ 10
+ `);
+ await element.updateComplete;
+ });
+
+ it('should display disabled background color', function() {
+ const [r, g, b] = getColor(element, 'background-color');
+ expect([r, g, b]).to.deep.equal(hexToRgb('#d2d2d2'));
+ });
+
+ it('should have pointer-events: none', function() {
+ const styles = getComputedStyle(element);
+ expect(styles.pointerEvents).to.equal('none');
+ });
+ });
+
+ describe('accessibility', function() {
+ it('should contain text in the accessibility tree', async function() {
+ await createFixture(html`
+ 10
+ `);
+ const snapshot = await a11ySnapshot();
+ expect(snapshot.children?.length).to.be.greaterThan(0);
+ });
+ });
+
+ describe('slot content', function() {
+ let element: PfV6Badge;
+
+ beforeEach(async function() {
+ element = await createFixture(html`
+ Custom Text
+ `);
+ await element.updateComplete;
+ });
+
+ it('should display slotted text content when no number is set', function() {
+ expect(element.textContent?.trim()).to.equal('Custom Text');
+ });
+ });
+});