diff --git a/packages/badge/package.json b/packages/badge/package.json
index a6d988d039b..c1215790ab0 100644
--- a/packages/badge/package.json
+++ b/packages/badge/package.json
@@ -36,6 +36,7 @@
"dependencies": {
"@vaadin/a11y-base": "25.2.0-alpha12",
"@vaadin/component-base": "25.2.0-alpha12",
+ "@vaadin/tooltip": "25.2.0-alpha12",
"@vaadin/vaadin-themable-mixin": "25.2.0-alpha12",
"lit": "^3.0.0"
},
diff --git a/packages/badge/src/vaadin-badge.d.ts b/packages/badge/src/vaadin-badge.d.ts
index d3462f32e44..463933afbdd 100644
--- a/packages/badge/src/vaadin-badge.d.ts
+++ b/packages/badge/src/vaadin-badge.d.ts
@@ -19,6 +19,7 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
* ---------|-------------
* (none) | Default slot for the badge text content
* `icon` | Slot for an icon to place before the text
+ * `tooltip` | Slot for a tooltip
*
* ### Styling
*
@@ -37,6 +38,7 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
* `has-icon` | Set when the badge has content in the icon slot
* `has-content` | Set when the badge has content in the default slot
* `has-number` | Set when the badge has a number value
+ * `has-tooltip` | Set when the badge has a slotted tooltip
*
* The following custom CSS properties are available for styling:
*
@@ -57,6 +59,11 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
*/
declare class Badge extends ElementMixin(ThemableMixin(HTMLElement)) {
+ /**
+ * When enabled, hides the content visually and shows it in a tooltip.
+ */
+ autoTooltip: boolean;
+
/**
* The number to display in the badge.
*/
diff --git a/packages/badge/src/vaadin-badge.js b/packages/badge/src/vaadin-badge.js
index ee174f1c616..cd812d325e3 100644
--- a/packages/badge/src/vaadin-badge.js
+++ b/packages/badge/src/vaadin-badge.js
@@ -3,6 +3,7 @@
* Copyright (c) 2026 - 2026 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
+import '@vaadin/tooltip/src/vaadin-tooltip.js';
import { html, LitElement } from 'lit';
import { classMap } from 'lit/directives/class-map.js';
import { screenReaderOnly } from '@vaadin/a11y-base/src/styles/sr-only-styles.js';
@@ -11,6 +12,7 @@ import { isEmptyTextNode } from '@vaadin/component-base/src/dom-utils.js';
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
import { SlotObserver } from '@vaadin/component-base/src/slot-observer.js';
+import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js';
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
import { badgeStyles } from './styles/vaadin-badge-base-styles.js';
@@ -28,6 +30,7 @@ import { badgeStyles } from './styles/vaadin-badge-base-styles.js';
* ---------|-------------
* (none) | Default slot for the badge text content
* `icon` | Slot for an icon to place before the text
+ * `tooltip` | Slot for a tooltip
*
* ### Styling
*
@@ -46,6 +49,7 @@ import { badgeStyles } from './styles/vaadin-badge-base-styles.js';
* `has-icon` | Set when the badge has content in the icon slot
* `has-content` | Set when the badge has content in the default slot
* `has-number` | Set when the badge has a number value
+ * `has-tooltip` | Set when the badge has a slotted tooltip
*
* The following custom CSS properties are available for styling:
*
@@ -93,6 +97,16 @@ class Badge extends ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(L
number: {
type: Number,
},
+
+ /**
+ * When enabled, hides the content visually and shows it in a tooltip.
+ */
+ autoTooltip: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ sync: true,
+ },
};
}
@@ -108,9 +122,10 @@ class Badge extends ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(L
${this.number}
-
-
+
+
+
`;
}
@@ -123,6 +138,24 @@ class Badge extends ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(L
}
}
+ /** @protected */
+ updated(props) {
+ super.updated(props);
+
+ if (props.has('autoTooltip')) {
+ this.__updateAutoTooltip();
+ }
+ }
+
+ /** @protected */
+ ready() {
+ super.ready();
+
+ this._tooltipController = new TooltipController(this);
+ this.addController(this._tooltipController);
+ this.__updateAutoTooltip();
+ }
+
/** @protected */
firstUpdated() {
super.firstUpdated();
@@ -137,6 +170,45 @@ class Badge extends ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(L
this.toggleAttribute('has-icon', currentNodes.length > 0);
});
}
+
+ /** @private */
+ __getContentText() {
+ const slot = this.shadowRoot.querySelector('slot:not([name])');
+ return slot
+ ? slot
+ .assignedNodes({ flatten: true })
+ .map((node) => node.textContent)
+ .join('')
+ .trim()
+ : '';
+ }
+
+ /** @private */
+ __hasCustomTooltip() {
+ return Array.from(this.children).some((node) => node !== this.__autoTooltip && node.slot === 'tooltip');
+ }
+
+ /** @private */
+ __updateAutoTooltip() {
+ const text = this.__getContentText();
+
+ if (!this.autoTooltip || !text || this.__hasCustomTooltip()) {
+ this.__autoTooltip?.remove();
+ return;
+ }
+
+ if (!this.__autoTooltip) {
+ this.__autoTooltip = document.createElement('vaadin-tooltip');
+ this.__autoTooltip.setAttribute('slot', 'tooltip');
+ this.__autoTooltip.ariaTarget = null;
+ }
+
+ this.__autoTooltip.setAttribute('text', text);
+
+ if (!this.__autoTooltip.isConnected) {
+ this.appendChild(this.__autoTooltip);
+ }
+ }
}
defineCustomElement(Badge);
diff --git a/packages/badge/test/badge.test.ts b/packages/badge/test/badge.test.ts
index 21e3f4f84ec..20a59b464f4 100644
--- a/packages/badge/test/badge.test.ts
+++ b/packages/badge/test/badge.test.ts
@@ -116,4 +116,33 @@ describe('vaadin-badge', () => {
expect(badge.hasAttribute('has-icon')).to.be.false;
});
});
+
+ describe('auto-tooltip', () => {
+ beforeEach(async () => {
+ badge = fixtureSync('
New');
+ await nextRender();
+ });
+
+ it('should hide the content visually', () => {
+ const content = badge.shadowRoot!.querySelector('[part="content"]')!;
+
+ expect(content.classList.contains('sr-only')).to.be.true;
+ });
+
+ it('should add a tooltip with content text', () => {
+ const tooltip = badge.querySelector('vaadin-tooltip[slot="tooltip"]')!;
+
+ expect(tooltip).to.be.ok;
+ expect(tooltip.getAttribute('text')).to.equal('New');
+ });
+
+ it('should not set aria-describedby', async () => {
+ const tooltip = badge.querySelector('vaadin-tooltip[slot="tooltip"]')! as any;
+ await nextUpdate(tooltip);
+
+ expect(badge.hasAttribute('auto-tooltip')).to.be.true;
+ expect(tooltip.ariaTarget).to.equal(null);
+ expect(badge.hasAttribute('aria-describedby')).to.be.false;
+ });
+ });
});
diff --git a/packages/badge/test/dom/__snapshots__/badge.test.snap.js b/packages/badge/test/dom/__snapshots__/badge.test.snap.js
index 4c501ce9fdb..54c29388318 100644
--- a/packages/badge/test/dom/__snapshots__/badge.test.snap.js
+++ b/packages/badge/test/dom/__snapshots__/badge.test.snap.js
@@ -31,6 +31,8 @@ snapshots["vaadin-badge shadow default"] =
+
+
`;
/* end snapshot vaadin-badge shadow default */
@@ -46,6 +48,8 @@ snapshots["vaadin-badge shadow number"] =
+
+
`;
/* end snapshot vaadin-badge shadow number */
@@ -69,6 +73,8 @@ snapshots["vaadin-badge shadow dot"] =
+
+
`;
/* end snapshot vaadin-badge shadow dot */
@@ -89,6 +95,8 @@ snapshots["vaadin-badge shadow icon-only"] =
+
+
`;
/* end snapshot vaadin-badge shadow icon-only */
@@ -109,6 +117,8 @@ snapshots["vaadin-badge shadow number-only"] =
+
+
`;
/* end snapshot vaadin-badge shadow number-only */
diff --git a/packages/button/package.json b/packages/button/package.json
index 9cf7770b79e..487a9c7098d 100644
--- a/packages/button/package.json
+++ b/packages/button/package.json
@@ -37,6 +37,7 @@
"@open-wc/dedupe-mixin": "^1.3.0",
"@vaadin/a11y-base": "25.2.0-alpha12",
"@vaadin/component-base": "25.2.0-alpha12",
+ "@vaadin/tooltip": "25.2.0-alpha12",
"@vaadin/vaadin-themable-mixin": "25.2.0-alpha12",
"lit": "^3.0.0"
},
diff --git a/packages/button/src/vaadin-button.d.ts b/packages/button/src/vaadin-button.d.ts
index 530c9bbbd43..ad8136b1233 100644
--- a/packages/button/src/vaadin-button.d.ts
+++ b/packages/button/src/vaadin-button.d.ts
@@ -54,6 +54,11 @@ import { ButtonMixin } from './vaadin-button-mixin.js';
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
*/
declare class Button extends ButtonMixin(ElementMixin(ThemableMixin(HTMLElement))) {
+ /**
+ * When enabled, hides the label visually and shows it in a tooltip.
+ */
+ autoTooltip: boolean;
+
/**
* When disabled, the button is rendered as "dimmed".
*
diff --git a/packages/button/src/vaadin-button.js b/packages/button/src/vaadin-button.js
index d50336e7545..23544e6637c 100644
--- a/packages/button/src/vaadin-button.js
+++ b/packages/button/src/vaadin-button.js
@@ -3,7 +3,10 @@
* Copyright (c) 2017 - 2026 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
+import '@vaadin/tooltip/src/vaadin-tooltip.js';
import { html, LitElement } from 'lit';
+import { classMap } from 'lit/directives/class-map.js';
+import { screenReaderOnly } from '@vaadin/a11y-base/src/styles/sr-only-styles.js';
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
@@ -68,7 +71,7 @@ class Button extends ButtonMixin(ElementMixin(ThemableMixin(PolylitMixin(LumoInj
}
static get styles() {
- return buttonStyles;
+ return [buttonStyles, screenReaderOnly];
}
static get properties() {
@@ -95,6 +98,16 @@ class Button extends ButtonMixin(ElementMixin(ThemableMixin(PolylitMixin(LumoInj
reflectToAttribute: true,
sync: true,
},
+
+ /**
+ * When enabled, hides the label visually and shows it in a tooltip.
+ */
+ autoTooltip: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ sync: true,
+ },
};
}
@@ -105,30 +118,79 @@ class Button extends ButtonMixin(ElementMixin(ThemableMixin(PolylitMixin(LumoInj
-
-
+
+
-
+
`;
}
+ /** @protected */
+ updated(props) {
+ super.updated(props);
+
+ if (props.has('autoTooltip')) {
+ this.__updateAutoTooltip();
+ }
+ }
+
/** @protected */
ready() {
super.ready();
this._tooltipController = new TooltipController(this);
this.addController(this._tooltipController);
+ this.__updateAutoTooltip();
}
/** @override */
__shouldAllowFocusWhenDisabled() {
return window.Vaadin.featureFlags.accessibleDisabledButtons;
}
+
+ /** @private */
+ __getLabelText() {
+ const slot = this.shadowRoot.querySelector('slot:not([name])');
+ return slot
+ ? slot
+ .assignedNodes({ flatten: true })
+ .map((node) => node.textContent)
+ .join('')
+ .trim()
+ : '';
+ }
+
+ /** @private */
+ __hasCustomTooltip() {
+ return Array.from(this.children).some((node) => node !== this.__autoTooltip && node.slot === 'tooltip');
+ }
+
+ /** @private */
+ __updateAutoTooltip() {
+ const text = this.__getLabelText();
+
+ if (!this.autoTooltip || !text || this.__hasCustomTooltip()) {
+ this.__autoTooltip?.remove();
+ return;
+ }
+
+ if (!this.__autoTooltip) {
+ this.__autoTooltip = document.createElement('vaadin-tooltip');
+ this.__autoTooltip.setAttribute('slot', 'tooltip');
+ this.__autoTooltip.ariaTarget = null;
+ }
+
+ this.__autoTooltip.setAttribute('text', text);
+
+ if (!this.__autoTooltip.isConnected) {
+ this.appendChild(this.__autoTooltip);
+ }
+ }
}
defineCustomElement(Button);
diff --git a/packages/button/test/button.test.ts b/packages/button/test/button.test.ts
index 58ce2f0e747..3e90b3f5587 100644
--- a/packages/button/test/button.test.ts
+++ b/packages/button/test/button.test.ts
@@ -206,4 +206,33 @@ describe('vaadin-button', () => {
expect(document.activeElement).to.equal(lastGlobalFocusable);
});
});
+
+ describe('auto-tooltip', () => {
+ beforeEach(async () => {
+ button = fixtureSync('Press me');
+ await nextRender();
+ });
+
+ it('should hide the label visually', () => {
+ const label = button.shadowRoot!.querySelector('[part="label"]')!;
+
+ expect(label.classList.contains('sr-only')).to.be.true;
+ });
+
+ it('should add a tooltip with label text', () => {
+ const tooltip = button.querySelector('vaadin-tooltip[slot="tooltip"]')!;
+
+ expect(tooltip).to.be.ok;
+ expect(tooltip.getAttribute('text')).to.equal('Press me');
+ });
+
+ it('should not set aria-describedby', async () => {
+ const tooltip = button.querySelector('vaadin-tooltip[slot="tooltip"]')! as any;
+ await nextUpdate(tooltip);
+
+ expect(button.hasAttribute('auto-tooltip')).to.be.true;
+ expect(tooltip.ariaTarget).to.equal(null);
+ expect(button.hasAttribute('aria-describedby')).to.be.false;
+ });
+ });
});