Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
396470a
feat(badge): add recipe and tokens
thetaPC Mar 27, 2026
9a2e0d1
feat(badge): more defaults for md
thetaPC Mar 27, 2026
a93bc68
feat(badge): remove crisp
thetaPC Mar 27, 2026
cd2364c
Merge branch 'ionic-modular' of github.com:ionic-team/ionic-framework…
thetaPC Mar 30, 2026
9c691b4
feat(avatar, badge, tab-button): a lot of avatar updates
thetaPC Mar 31, 2026
b40e733
feat(avatar, badge): implement all combos
thetaPC Mar 31, 2026
bf6c885
feat(badge, avatar, button): add position utility
thetaPC Apr 3, 2026
459a508
refactor(button): cleanup for badge
thetaPC Apr 3, 2026
1493f90
feat(avatar, badge, button, tab-button): update position
thetaPC Apr 6, 2026
556b111
test(avatar, button, tab-button): update badge pages
thetaPC Apr 6, 2026
50fe67f
feat(badge): remove max-width for now
thetaPC Apr 6, 2026
89e9b04
test(avatar, button, tab-button): update test pages
thetaPC Apr 6, 2026
e029fbc
refactor(badge): update names for types
thetaPC Apr 6, 2026
3837435
feat(badge): remove old complex hue getter
thetaPC Apr 7, 2026
929929e
refactor(avatar): cleanup
thetaPC Apr 7, 2026
25f4e87
test(badge): update test pages
thetaPC Apr 7, 2026
539c476
docs(badge): remove comments
thetaPC Apr 7, 2026
9f7e6c1
refactor(badge): remove extra
thetaPC Apr 7, 2026
2a27548
docs(badge): remove todos
thetaPC Apr 7, 2026
6a8a4cc
docs(badge): remove more comments
thetaPC Apr 7, 2026
ba7f2ba
feat(badge): remove ionic styles
thetaPC Apr 7, 2026
bd801b8
feat(badge): remove theme styles
thetaPC Apr 7, 2026
57e3faf
feat(badge): remove theme
thetaPC Apr 7, 2026
47fa6d5
feat(badge): update classes
thetaPC Apr 7, 2026
cf65c7a
feat(tab-button): check for label
thetaPC Apr 7, 2026
d8ae5b5
refactor(many): cleanup
thetaPC Apr 7, 2026
6e794f2
feat(badge): add interfaces
thetaPC Apr 7, 2026
ce5d5e9
feat(badge): update interfaces and import
thetaPC Apr 7, 2026
15033a0
docs(badge): add CSS variables definitions
thetaPC Apr 7, 2026
6737cb6
feat(badge): add position utility file
thetaPC Apr 7, 2026
14e2460
feat(badge): add config type
thetaPC Apr 7, 2026
2d3ce7f
test(badge): update snapshots
thetaPC Apr 8, 2026
c468fa8
feat(themes): update badge sizes
thetaPC Apr 8, 2026
d5abd1e
test(avatar): add badge snapshots
thetaPC Apr 8, 2026
03497cb
test(button): add badge snapshots
thetaPC Apr 8, 2026
0aaca71
test(tab-button): add badge snapshots
thetaPC Apr 8, 2026
9365800
test(badge): remove vertical tests
thetaPC Apr 8, 2026
40463b7
chore(badge): run build
thetaPC Apr 8, 2026
8096abf
feat(badge): use min height for content font scaling
thetaPC Apr 8, 2026
340a4a1
chore(badge): run build
thetaPC Apr 8, 2026
0fe440f
feat(badge): add height and min-height
thetaPC Apr 8, 2026
795c0de
test(badge): update snapshots
thetaPC Apr 8, 2026
5f92eab
test(item): update badge snapshots
thetaPC Apr 8, 2026
f262f4c
feat(badge): update sizes for native
thetaPC Apr 8, 2026
593ab49
feat(badge): update sizes for md and ios
thetaPC Apr 8, 2026
fb6f80c
feat(badge): update border radius for soft
thetaPC Apr 8, 2026
6d660b7
feat(tab-button): update barge target
thetaPC Apr 8, 2026
bfb9a29
test(tab-button): update badge snapshots
thetaPC Apr 8, 2026
75175f6
feat(badge): update sizes
thetaPC Apr 8, 2026
a40e4d1
test(tab-bar): revert
thetaPC Apr 8, 2026
cbc4a19
test(tab-bar): force correct snapshot
thetaPC Apr 8, 2026
08c8eb1
test(tab-bar): correct snapshot
thetaPC Apr 8, 2026
a7240cb
test(item): update badge snapshots
thetaPC Apr 9, 2026
0ed4e14
test(many): update badge snapshots
thetaPC Apr 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,19 +469,19 @@ export namespace Components {
*/
"color"?: Color;
/**
* Set to `"bold"` for a badge with vibrant, bold colors or to `"subtle"` for a badge with muted, subtle colors. Only applies to the `ionic` theme.
* Set to `"bold"` for a badge with vibrant, bold colors or to `"subtle"` for a badge with muted, subtle colors. Defaults to `"bold"` if both the hue property and theme config are unset.
*/
"hue"?: 'bold' | 'subtle';
/**
* The mode determines the platform behaviors of the component.
*/
"mode"?: "ios" | "md";
/**
* Set to `"rectangular"` for non-rounded corners. Set to `"soft"` for slightly rounded corners. Set to `"round"` for fully rounded corners. Defaults to `"round"` for the `ionic` theme, undefined for all other themes.
* Set to `"crisp"` for a badge with even slightly rounded corners, `"soft"` for a badge with slightly rounded corners, `"round"` for a badge with fully rounded corners, or `"rectangular"` for a badge without rounded corners. Defaults to `"soft"` if both the shape property and theme config are unset.
*/
"shape"?: 'soft' | 'round | rectangular';
"shape"?: 'crisp' | 'soft' | 'round' | 'rectangular';
/**
* Set to `"small"` for a small badge. Set to `"medium"` for a medium badge. Set to `"large"` for a large badge, when it is empty (no text or icon). Defaults to `"small"` for the `ionic` theme, undefined for all other themes.
* Set to `"small"` for a smaller size. Set to `"medium"` for a medium size. Set to `"large"` for a larger size. Defaults to `"small"` if both the size property and theme config are unset.
*/
"size"?: 'small' | 'medium' | 'large';
/**
Expand Down Expand Up @@ -898,7 +898,7 @@ export namespace Components {
*/
"shape"?: IonChipShape;
/**
* Set to `"small"` for a chip with less height and padding. Defaults to `"large"` if both the size property and theme config are unset.
* Set to `"small"` for a chip with less height and padding, or `"large"` for a chip with more height and padding. Defaults to `"large"` if both the size property and theme config are unset.
*/
"size"?: IonChipSize;
}
Expand Down Expand Up @@ -6400,19 +6400,19 @@ declare namespace LocalJSX {
*/
"color"?: Color;
/**
* Set to `"bold"` for a badge with vibrant, bold colors or to `"subtle"` for a badge with muted, subtle colors. Only applies to the `ionic` theme.
* Set to `"bold"` for a badge with vibrant, bold colors or to `"subtle"` for a badge with muted, subtle colors. Defaults to `"bold"` if both the hue property and theme config are unset.
*/
"hue"?: 'bold' | 'subtle';
/**
* The mode determines the platform behaviors of the component.
*/
"mode"?: "ios" | "md";
/**
* Set to `"rectangular"` for non-rounded corners. Set to `"soft"` for slightly rounded corners. Set to `"round"` for fully rounded corners. Defaults to `"round"` for the `ionic` theme, undefined for all other themes.
* Set to `"crisp"` for a badge with even slightly rounded corners, `"soft"` for a badge with slightly rounded corners, `"round"` for a badge with fully rounded corners, or `"rectangular"` for a badge without rounded corners. Defaults to `"soft"` if both the shape property and theme config are unset.
*/
"shape"?: 'soft' | 'round | rectangular';
"shape"?: 'crisp' | 'soft' | 'round' | 'rectangular';
/**
* Set to `"small"` for a small badge. Set to `"medium"` for a medium badge. Set to `"large"` for a large badge, when it is empty (no text or icon). Defaults to `"small"` for the `ionic` theme, undefined for all other themes.
* Set to `"small"` for a smaller size. Set to `"medium"` for a medium size. Set to `"large"` for a larger size. Defaults to `"small"` if both the size property and theme config are unset.
*/
"size"?: 'small' | 'medium' | 'large';
/**
Expand Down Expand Up @@ -6864,7 +6864,7 @@ declare namespace LocalJSX {
*/
"shape"?: IonChipShape;
/**
* Set to `"small"` for a chip with less height and padding. Defaults to `"large"` if both the size property and theme config are unset.
* Set to `"small"` for a chip with less height and padding, or `"large"` for a chip with more height and padding. Defaults to `"large"` if both the size property and theme config are unset.
*/
"size"?: IonChipSize;
}
Expand Down
6 changes: 3 additions & 3 deletions core/src/components/avatar/avatar.common.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@import "../../themes/mixins.scss";
@use "../../themes/mixins" as mixins;

// Avatar
// --------------------------------------------------
Expand All @@ -7,7 +7,7 @@
/**
* @prop --border-radius: Border radius of the avatar and inner image
*/
@include border-radius(var(--border-radius));
@include mixins.border-radius(var(--border-radius));

display: block;

Expand All @@ -16,7 +16,7 @@

::slotted(ion-img),
::slotted(img) {
@include border-radius(var(--border-radius));
@include mixins.border-radius(var(--border-radius));

width: 100%;
height: 100%;
Expand Down
73 changes: 0 additions & 73 deletions core/src/components/avatar/avatar.ionic.scss
Original file line number Diff line number Diff line change
Expand Up @@ -169,79 +169,6 @@
height: globals.$ion-scale-800;
}

// Avatar Badge Empty (hint)
// --------------------------------------------------

:host ::slotted(ion-badge.badge-vertical-top:empty) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These styles were removed in favor of the positionBadge utility that generates the position. The same goes for any styles like these for the other files.

@include globals.transform(translate(globals.$ion-scale-050, calc(globals.$ion-scale-050 * -1)));
}

:host(.avatar-xxsmall) ::slotted(ion-badge.badge-vertical-top:empty) {
@include globals.transform(translate(globals.$ion-scale-100, calc(globals.$ion-scale-100 * -1)));
}

:host ::slotted(ion-badge.badge-vertical-bottom:empty) {
@include globals.transform(translate(0, globals.$ion-scale-100));
}

:host(.avatar-xxsmall) ::slotted(ion-badge.badge-vertical-bottom:empty) {
@include globals.transform(translate(globals.$ion-scale-100, globals.$ion-scale-100));
}

// Avatar Badge Bottom (hint)
// --------------------------------------------------

:host ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) {
@include globals.transform(translate(50%, 50%));
}

:host(.avatar-xxsmall) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) {
@include globals.position(null, globals.$ion-scale-100, globals.$ion-scale-100, null);
@include globals.transform(translate(100%, 100%));
}

:host(.avatar-xsmall) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) {
@include globals.position(null, calc(globals.$ion-scale-050 * -1), calc(globals.$ion-scale-050 * -1), null);
}

:host(.avatar-small) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)),
:host(.avatar-medium) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)),
:host(.avatar-large) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) {
@include globals.position(null, globals.$ion-scale-050, globals.$ion-scale-050, null);
}

:host(.avatar-xlarge) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) {
@include globals.position(null, globals.$ion-scale-150, globals.$ion-scale-150, null);
}

// Avatar Badge Top (hint)
// --------------------------------------------------

:host ::slotted(ion-badge.badge-vertical-top:not(:empty)) {
@include globals.transform(translate(50%, -50%));
}

:host(.avatar-xxsmall) ::slotted(ion-badge.badge-vertical-top:not(:empty)) {
@include globals.position(globals.$ion-scale-050, 0, null, null);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we worried about no longer using the variables here or is this not needed?

}

:host(.avatar-xsmall) ::slotted(ion-badge.badge-vertical-top:not(:empty)) {
@include globals.position(globals.$ion-scale-100, calc(globals.$ion-scale-050 * -1), null, null);
}

:host(.avatar-small) ::slotted(ion-badge.badge-vertical-top:not(:empty)),
:host(.avatar-medium) ::slotted(ion-badge.badge-vertical-top:not(:empty)) {
@include globals.position(globals.$ion-scale-150, 0, null, null);
}

:host(.avatar-large) ::slotted(ion-badge.badge-vertical-top:not(:empty)) {
@include globals.position(globals.$ion-scale-150, globals.$ion-scale-050, null, null);
}

:host(.avatar-xlarge) ::slotted(ion-badge.badge-vertical-top:not(:empty)) {
@include globals.position(globals.$ion-scale-150, globals.$ion-scale-150, null, null);
}

// Avatar Disabled
// --------------------------------------------------
:host(.avatar-disabled)::after {
Expand Down
19 changes: 0 additions & 19 deletions core/src/components/avatar/avatar.md.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,3 @@
width: $avatar-md-width;
height: $avatar-md-height;
}

// Avatar Empty Badge (hint)
// --------------------------------------------------

::slotted(ion-badge.badge-vertical-top:empty) {
@include globals.transform(translate(-50%, 50%));
}

::slotted(ion-badge.badge-vertical-bottom:empty) {
@include globals.transform(translateX(-100%));
}

:host ::slotted(ion-badge.badge-vertical-top:not(:empty)) {
@include globals.transform(translate(0, 100%));
}

:host ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) {
@include globals.transform(translate(0, -100%));
}
62 changes: 55 additions & 7 deletions core/src/components/avatar/avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { ComponentInterface } from '@stencil/core';
import { Component, Element, Host, Prop, h } from '@stencil/core';
import type { BadgeObserver } from '@utils/helpers';
import { createBadgeObserver } from '@utils/helpers';

import { getIonTheme } from '../../global/ionic-global';

Expand All @@ -18,6 +20,7 @@ import { getIonTheme } from '../../global/ionic-global';
})
export class Avatar implements ComponentInterface {
@Element() el!: HTMLElement;
private badgeObserver?: BadgeObserver;

/**
* Set to `"xxsmall"` for the smallest size.
Expand Down Expand Up @@ -45,6 +48,14 @@ export class Avatar implements ComponentInterface {
*/
@Prop() disabled = false;

componentDidLoad(): void {
this.setupBadgeObserver();
}

disconnectedCallback() {
this.destroyBadgeObserver();
}

get hasImage() {
return !!this.el.querySelector('ion-img') || !!this.el.querySelector('img');
}
Expand All @@ -53,14 +64,51 @@ export class Avatar implements ComponentInterface {
return !!this.el.querySelector('ion-icon');
}

private getSize(): string | undefined {
const theme = getIonTheme(this);
const { size } = this;
private get hasBadge() {
return !!this.el.querySelector('ion-badge');
}

// TODO(ROU-10752): Remove theme check when sizes are defined for all themes.
if (theme !== 'ionic') {
return undefined;
private onSlotChanged = () => {
/**
* Badges can be added or removed dynamically to mimic use
* cases like notifications. Based on the presence of a
* badge, we need to set up or destroy the badge observer.
*
* If the badge observer is already set up and there is a badge, then we don't need to do anything.
*/
if (this.hasBadge && this.badgeObserver) {
return;
}

if (this.hasBadge) {
this.setupBadgeObserver();
} else {
this.destroyBadgeObserver();
}
};

private setupBadgeObserver() {
this.destroyBadgeObserver();

// Only set up the badge observer if there is a badge and it's anchored
const badge = this.el.querySelector('ion-badge[vertical]') as HTMLElement | null;

if (!badge) {
return;
}

this.badgeObserver = createBadgeObserver({
target: this.el,
badge,
});
}

private destroyBadgeObserver() {
this.badgeObserver?.disconnect();
}

private getSize(): string | undefined {
const { size } = this;

if (size === undefined) {
return 'medium';
Expand Down Expand Up @@ -102,7 +150,7 @@ export class Avatar implements ComponentInterface {
[`avatar-disabled`]: disabled,
}}
>
<slot></slot>
<slot onSlotchange={this.onSlotChanged}></slot>
</Host>
);
}
Expand Down
99 changes: 99 additions & 0 deletions core/src/components/avatar/test/hint/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Avatar - Hint</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>

<style>
.row {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin: 16px;
}

h2 {
font-size: 12px;
font-weight: normal;
color: #6f7378;
margin-top: 10px;
margin-left: 5px;
}
</style>
</head>

<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Avatar - Hint</ion-title>
</ion-toolbar>
</ion-header>

<ion-content id="content"></ion-content>
</ion-app>

<script>
const avatarSizes = ['xxsmall', 'xsmall', 'small', 'medium', 'large', 'xlarge'];
const badgeSizes = ['small', 'medium', 'large'];
const positions = ['top', 'bottom', ''];
const imgSrc = '/src/components/avatar/test/avatar.svg';

// Badge content variants: empty (dot), text, icon
const badgeContents = [
{ label: 'empty', html: '' },
{ label: 'text', html: '1' },
{ label: 'longText', html: '999+' },
{ label: 'icon', html: '<ion-icon icon="star"></ion-icon>' },
];

const content = document.getElementById('content');

avatarSizes.forEach((avatarSize) => {
positions.forEach((position) => {
const heading = document.createElement('h2');
heading.textContent = `avatar size: ${avatarSize} / badge position: ${position || 'none'}`;
content.appendChild(heading);

const row = document.createElement('div');
row.className = 'row';

badgeSizes.forEach((badgeSize) => {
badgeContents.forEach(({ html }) => {
const avatar = document.createElement('ion-avatar');
avatar.setAttribute('size', avatarSize);

const img = document.createElement('img');
img.setAttribute('src', imgSrc);
avatar.appendChild(img);

const badge = document.createElement('ion-badge');
badge.setAttribute('hue', 'bold');
badge.setAttribute('color', 'danger');
badge.setAttribute('shape', 'round');
badge.setAttribute('size', badgeSize);
if (position) {
badge.setAttribute('vertical', position);
}
badge.innerHTML = html;

avatar.appendChild(badge);
row.appendChild(avatar);
});
});

content.appendChild(row);
});
});
</script>
</body>
</html>
Loading
Loading