From df496587c42bb5e6d784b5d888e70df41e686e26 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 23 Oct 2026 20:06:01 -0700 Subject: [PATCH 1/8] feat(button): add the subtle hue --- core/api.txt | 121 ++++++++ core/src/components.d.ts | 9 + core/src/components/button/_button.docs.scss | 120 ++++++++ core/src/components/button/button.scss | 255 ++++++++-------- core/src/components/button/button.tsx | 16 +- .../components/button/test/hue/button.e2e.ts | 17 ++ .../src/components/button/test/hue/index.html | 97 ++++++ core/src/themes/ionic/default.tokens.ts | 238 +++++++++++++++ core/src/themes/ios/default.tokens.ts | 221 ++++++++++++++ core/src/themes/md/default.tokens.ts | 281 ++++++++++++++++++ .../angular/src/lazy/directives/proxies.ts | 4 +- .../src/standalone/directives/ion-button.ts | 4 +- packages/vue/src/proxies.ts | 1 + 13 files changed, 1252 insertions(+), 132 deletions(-) create mode 100644 core/src/components/button/test/hue/button.e2e.ts create mode 100644 core/src/components/button/test/hue/index.html diff --git a/core/api.txt b/core/api.txt index 65146f6ab93..10697645eaf 100644 --- a/core/api.txt +++ b/core/api.txt @@ -439,6 +439,7 @@ ion-button,prop,expand,"block" | "full" | undefined,undefined,false,true ion-button,prop,fill,"clear" | "default" | "outline" | "solid" | undefined,undefined,false,true ion-button,prop,form,HTMLFormElement | string | undefined,undefined,false,false ion-button,prop,href,string | undefined,undefined,false,false +ion-button,prop,hue,"bold" | "subtle" | undefined,undefined,false,false ion-button,prop,mode,"ios" | "md",undefined,false,false ion-button,prop,rel,string | undefined,undefined,false,false ion-button,prop,routerAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false @@ -571,6 +572,126 @@ ion-button,css-prop,--ion-button-hue-bold-solid-semantic-hover-color ion-button,css-prop,--ion-button-hue-bold-solid-semantic-ripple-effect-color ion-button,css-prop,--ion-button-hue-bold-solid-semantic-ripple-effect-opacity ion-button,css-prop,--ion-button-hue-bold-solid-strong-font-weight +ion-button,css-prop,--ion-button-hue-subtle-clear-activated-background +ion-button,css-prop,--ion-button-hue-subtle-clear-activated-box-shadow +ion-button,css-prop,--ion-button-hue-subtle-clear-activated-color +ion-button,css-prop,--ion-button-hue-subtle-clear-activated-opacity +ion-button,css-prop,--ion-button-hue-subtle-clear-default-background +ion-button,css-prop,--ion-button-hue-subtle-clear-default-border-color +ion-button,css-prop,--ion-button-hue-subtle-clear-default-border-style +ion-button,css-prop,--ion-button-hue-subtle-clear-default-border-width +ion-button,css-prop,--ion-button-hue-subtle-clear-default-box-shadow +ion-button,css-prop,--ion-button-hue-subtle-clear-default-color +ion-button,css-prop,--ion-button-hue-subtle-clear-default-font-weight +ion-button,css-prop,--ion-button-hue-subtle-clear-disabled-background +ion-button,css-prop,--ion-button-hue-subtle-clear-disabled-border-color +ion-button,css-prop,--ion-button-hue-subtle-clear-disabled-box-shadow +ion-button,css-prop,--ion-button-hue-subtle-clear-disabled-color +ion-button,css-prop,--ion-button-hue-subtle-clear-disabled-opacity +ion-button,css-prop,--ion-button-hue-subtle-clear-focus-background +ion-button,css-prop,--ion-button-hue-subtle-clear-focus-box-shadow +ion-button,css-prop,--ion-button-hue-subtle-clear-focus-color +ion-button,css-prop,--ion-button-hue-subtle-clear-hover-background +ion-button,css-prop,--ion-button-hue-subtle-clear-hover-box-shadow +ion-button,css-prop,--ion-button-hue-subtle-clear-hover-color +ion-button,css-prop,--ion-button-hue-subtle-clear-hover-opacity +ion-button,css-prop,--ion-button-hue-subtle-clear-ripple-effect-color +ion-button,css-prop,--ion-button-hue-subtle-clear-ripple-effect-opacity +ion-button,css-prop,--ion-button-hue-subtle-clear-semantic-activated-background +ion-button,css-prop,--ion-button-hue-subtle-clear-semantic-activated-color +ion-button,css-prop,--ion-button-hue-subtle-clear-semantic-default-background +ion-button,css-prop,--ion-button-hue-subtle-clear-semantic-default-border-color +ion-button,css-prop,--ion-button-hue-subtle-clear-semantic-default-color +ion-button,css-prop,--ion-button-hue-subtle-clear-semantic-disabled-background +ion-button,css-prop,--ion-button-hue-subtle-clear-semantic-disabled-border-color +ion-button,css-prop,--ion-button-hue-subtle-clear-semantic-disabled-color +ion-button,css-prop,--ion-button-hue-subtle-clear-semantic-focus-background +ion-button,css-prop,--ion-button-hue-subtle-clear-semantic-focus-color +ion-button,css-prop,--ion-button-hue-subtle-clear-semantic-hover-background +ion-button,css-prop,--ion-button-hue-subtle-clear-semantic-hover-color +ion-button,css-prop,--ion-button-hue-subtle-clear-semantic-ripple-effect-color +ion-button,css-prop,--ion-button-hue-subtle-clear-semantic-ripple-effect-opacity +ion-button,css-prop,--ion-button-hue-subtle-clear-strong-font-weight +ion-button,css-prop,--ion-button-hue-subtle-outline-activated-background +ion-button,css-prop,--ion-button-hue-subtle-outline-activated-box-shadow +ion-button,css-prop,--ion-button-hue-subtle-outline-activated-color +ion-button,css-prop,--ion-button-hue-subtle-outline-activated-opacity +ion-button,css-prop,--ion-button-hue-subtle-outline-default-background +ion-button,css-prop,--ion-button-hue-subtle-outline-default-border-color +ion-button,css-prop,--ion-button-hue-subtle-outline-default-border-style +ion-button,css-prop,--ion-button-hue-subtle-outline-default-border-width +ion-button,css-prop,--ion-button-hue-subtle-outline-default-box-shadow +ion-button,css-prop,--ion-button-hue-subtle-outline-default-color +ion-button,css-prop,--ion-button-hue-subtle-outline-default-font-weight +ion-button,css-prop,--ion-button-hue-subtle-outline-disabled-background +ion-button,css-prop,--ion-button-hue-subtle-outline-disabled-border-color +ion-button,css-prop,--ion-button-hue-subtle-outline-disabled-box-shadow +ion-button,css-prop,--ion-button-hue-subtle-outline-disabled-color +ion-button,css-prop,--ion-button-hue-subtle-outline-disabled-opacity +ion-button,css-prop,--ion-button-hue-subtle-outline-focus-background +ion-button,css-prop,--ion-button-hue-subtle-outline-focus-box-shadow +ion-button,css-prop,--ion-button-hue-subtle-outline-focus-color +ion-button,css-prop,--ion-button-hue-subtle-outline-hover-background +ion-button,css-prop,--ion-button-hue-subtle-outline-hover-box-shadow +ion-button,css-prop,--ion-button-hue-subtle-outline-hover-color +ion-button,css-prop,--ion-button-hue-subtle-outline-hover-opacity +ion-button,css-prop,--ion-button-hue-subtle-outline-ripple-effect-color +ion-button,css-prop,--ion-button-hue-subtle-outline-ripple-effect-opacity +ion-button,css-prop,--ion-button-hue-subtle-outline-semantic-activated-background +ion-button,css-prop,--ion-button-hue-subtle-outline-semantic-activated-color +ion-button,css-prop,--ion-button-hue-subtle-outline-semantic-default-background +ion-button,css-prop,--ion-button-hue-subtle-outline-semantic-default-border-color +ion-button,css-prop,--ion-button-hue-subtle-outline-semantic-default-color +ion-button,css-prop,--ion-button-hue-subtle-outline-semantic-disabled-background +ion-button,css-prop,--ion-button-hue-subtle-outline-semantic-disabled-border-color +ion-button,css-prop,--ion-button-hue-subtle-outline-semantic-disabled-color +ion-button,css-prop,--ion-button-hue-subtle-outline-semantic-focus-background +ion-button,css-prop,--ion-button-hue-subtle-outline-semantic-focus-color +ion-button,css-prop,--ion-button-hue-subtle-outline-semantic-hover-background +ion-button,css-prop,--ion-button-hue-subtle-outline-semantic-hover-color +ion-button,css-prop,--ion-button-hue-subtle-outline-semantic-ripple-effect-color +ion-button,css-prop,--ion-button-hue-subtle-outline-semantic-ripple-effect-opacity +ion-button,css-prop,--ion-button-hue-subtle-outline-strong-font-weight +ion-button,css-prop,--ion-button-hue-subtle-solid-activated-background +ion-button,css-prop,--ion-button-hue-subtle-solid-activated-box-shadow +ion-button,css-prop,--ion-button-hue-subtle-solid-activated-color +ion-button,css-prop,--ion-button-hue-subtle-solid-activated-opacity +ion-button,css-prop,--ion-button-hue-subtle-solid-default-background +ion-button,css-prop,--ion-button-hue-subtle-solid-default-border-color +ion-button,css-prop,--ion-button-hue-subtle-solid-default-border-style +ion-button,css-prop,--ion-button-hue-subtle-solid-default-border-width +ion-button,css-prop,--ion-button-hue-subtle-solid-default-box-shadow +ion-button,css-prop,--ion-button-hue-subtle-solid-default-color +ion-button,css-prop,--ion-button-hue-subtle-solid-default-font-weight +ion-button,css-prop,--ion-button-hue-subtle-solid-disabled-background +ion-button,css-prop,--ion-button-hue-subtle-solid-disabled-border-color +ion-button,css-prop,--ion-button-hue-subtle-solid-disabled-box-shadow +ion-button,css-prop,--ion-button-hue-subtle-solid-disabled-color +ion-button,css-prop,--ion-button-hue-subtle-solid-disabled-opacity +ion-button,css-prop,--ion-button-hue-subtle-solid-focus-background +ion-button,css-prop,--ion-button-hue-subtle-solid-focus-box-shadow +ion-button,css-prop,--ion-button-hue-subtle-solid-focus-color +ion-button,css-prop,--ion-button-hue-subtle-solid-hover-background +ion-button,css-prop,--ion-button-hue-subtle-solid-hover-box-shadow +ion-button,css-prop,--ion-button-hue-subtle-solid-hover-color +ion-button,css-prop,--ion-button-hue-subtle-solid-hover-opacity +ion-button,css-prop,--ion-button-hue-subtle-solid-ripple-effect-color +ion-button,css-prop,--ion-button-hue-subtle-solid-ripple-effect-opacity +ion-button,css-prop,--ion-button-hue-subtle-solid-semantic-activated-background +ion-button,css-prop,--ion-button-hue-subtle-solid-semantic-activated-color +ion-button,css-prop,--ion-button-hue-subtle-solid-semantic-default-background +ion-button,css-prop,--ion-button-hue-subtle-solid-semantic-default-border-color +ion-button,css-prop,--ion-button-hue-subtle-solid-semantic-default-color +ion-button,css-prop,--ion-button-hue-subtle-solid-semantic-disabled-background +ion-button,css-prop,--ion-button-hue-subtle-solid-semantic-disabled-border-color +ion-button,css-prop,--ion-button-hue-subtle-solid-semantic-disabled-color +ion-button,css-prop,--ion-button-hue-subtle-solid-semantic-focus-background +ion-button,css-prop,--ion-button-hue-subtle-solid-semantic-focus-color +ion-button,css-prop,--ion-button-hue-subtle-solid-semantic-hover-background +ion-button,css-prop,--ion-button-hue-subtle-solid-semantic-hover-color +ion-button,css-prop,--ion-button-hue-subtle-solid-semantic-ripple-effect-color +ion-button,css-prop,--ion-button-hue-subtle-solid-semantic-ripple-effect-opacity +ion-button,css-prop,--ion-button-hue-subtle-solid-strong-font-weight ion-button,css-prop,--ion-button-letter-spacing ion-button,css-prop,--ion-button-margin-bottom ion-button,css-prop,--ion-button-margin-end diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 77dc1472557..5ba767a3504 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -625,6 +625,10 @@ export namespace Components { * Contains a URL or a URL fragment that the hyperlink points to. If this property is set, an anchor tag will be rendered. */ "href": string | undefined; + /** + * Set to `"bold"` for a button with vibrant, bold colors or to `"subtle"` for a button with muted, subtle colors. Defaults to `"bold"` if both the hue property and theme config are unset. + */ + "hue"?: Hue; /** * The mode determines the platform behaviors of the component. */ @@ -6478,6 +6482,10 @@ declare namespace LocalJSX { * Contains a URL or a URL fragment that the hyperlink points to. If this property is set, an anchor tag will be rendered. */ "href"?: string | undefined; + /** + * Set to `"bold"` for a button with vibrant, bold colors or to `"subtle"` for a button with muted, subtle colors. Defaults to `"bold"` if both the hue property and theme config are unset. + */ + "hue"?: Hue; /** * The mode determines the platform behaviors of the component. */ @@ -10477,6 +10485,7 @@ declare namespace LocalJSX { "disabled": boolean; "expand": IonButtonExpand; "fill": IonButtonFill | 'default'; + "hue": Hue; "routerDirection": RouterDirection; "download": string | undefined; "href": string | undefined; diff --git a/core/src/components/button/_button.docs.scss b/core/src/components/button/_button.docs.scss index 73773466aee..b333c4136e6 100644 --- a/core/src/components/button/_button.docs.scss +++ b/core/src/components/button/_button.docs.scss @@ -13,6 +13,12 @@ * @prop --ion-button-hue-bold-outline-strong-font-weight: Font weight of the `bold` hue and `outline` fill combination on a `strong` button * @prop --ion-button-hue-bold-clear-default-font-weight: Font weight of the `bold` hue and `clear` fill combination in the default state * @prop --ion-button-hue-bold-clear-strong-font-weight: Font weight of the `bold` hue and `clear` fill combination on a `strong` button + * @prop --ion-button-hue-subtle-solid-default-font-weight: Font weight of the `subtle` hue and `solid` fill combination in the default state + * @prop --ion-button-hue-subtle-solid-strong-font-weight: Font weight of the `subtle` hue and `solid` fill combination on a `strong` button + * @prop --ion-button-hue-subtle-outline-default-font-weight: Font weight of the `subtle` hue and `outline` fill combination in the default state + * @prop --ion-button-hue-subtle-outline-strong-font-weight: Font weight of the `subtle` hue and `outline` fill combination on a `strong` button + * @prop --ion-button-hue-subtle-clear-default-font-weight: Font weight of the `subtle` hue and `clear` fill combination in the default state + * @prop --ion-button-hue-subtle-clear-strong-font-weight: Font weight of the `subtle` hue and `clear` fill combination on a `strong` button * @prop --ion-button-size-small-min-width: Minimum width of the `small` button size * @prop --ion-button-size-small-min-height: Minimum height of the `small` button size, and the width and height of an icon-only button of that size * @prop --ion-button-size-small-font-size: Font size of the `small` button size @@ -267,4 +273,118 @@ * @prop --ion-button-hue-bold-clear-semantic-disabled-background: Background of the `bold` hue and `clear` fill combination in the disabled state when a semantic color is applied * @prop --ion-button-hue-bold-clear-semantic-disabled-color: Text color of the `bold` hue and `clear` fill combination in the disabled state when a semantic color is applied * @prop --ion-button-hue-bold-clear-semantic-disabled-border-color: Border color of the `bold` hue and `clear` fill combination in the disabled state when a semantic color is applied + * @prop --ion-button-hue-subtle-outline-default-border-color: Border color of the `subtle` hue and `outline` fill combination in the default state + * @prop --ion-button-hue-subtle-outline-default-color: Text color of the `subtle` hue and `outline` fill combination in the default state + * @prop --ion-button-hue-subtle-clear-default-color: Text color of the `subtle` hue and `clear` fill combination in the default state + * @prop --ion-button-hue-subtle-solid-default-background: Background of the `subtle` hue and `solid` fill combination in the default state + * @prop --ion-button-hue-subtle-solid-default-color: Text color of the `subtle` hue and `solid` fill combination in the default state + * @prop --ion-button-hue-subtle-solid-default-border-width: Border width of the `subtle` hue and `solid` fill combination in the default state + * @prop --ion-button-hue-subtle-solid-default-border-style: Border style of the `subtle` hue and `solid` fill combination in the default state + * @prop --ion-button-hue-subtle-solid-default-border-color: Border color of the `subtle` hue and `solid` fill combination in the default state + * @prop --ion-button-hue-subtle-solid-default-box-shadow: Box shadow of the `subtle` hue and `solid` fill combination in the default state + * @prop --ion-button-hue-subtle-solid-ripple-effect-color: Ripple color of the `subtle` hue and `solid` fill combination + * @prop --ion-button-hue-subtle-solid-ripple-effect-opacity: Ripple opacity of the `subtle` hue and `solid` fill combination + * @prop --ion-button-hue-subtle-solid-semantic-default-border-color: Border color of the `subtle` hue and `solid` fill combination in the default state when a semantic color is applied + * @prop --ion-button-hue-subtle-solid-semantic-default-background: Background of the `subtle` hue and `solid` fill combination in the default state when a semantic color is applied + * @prop --ion-button-hue-subtle-solid-semantic-default-color: Text color of the `subtle` hue and `solid` fill combination in the default state when a semantic color is applied + * @prop --ion-button-hue-subtle-solid-semantic-ripple-effect-color: Ripple color of the `subtle` hue and `solid` fill combination when a semantic color is applied + * @prop --ion-button-hue-subtle-solid-semantic-ripple-effect-opacity: Ripple opacity of the `subtle` hue and `solid` fill combination when a semantic color is applied + * @prop --ion-button-hue-subtle-solid-focus-color: Text color of the `subtle` hue and `solid` fill combination in the focus state + * @prop --ion-button-hue-subtle-solid-focus-background: Background of the `subtle` hue and `solid` fill combination in the focus state + * @prop --ion-button-hue-subtle-solid-focus-box-shadow: Box shadow of the `subtle` hue and `solid` fill combination in the focus state + * @prop --ion-button-hue-subtle-solid-semantic-focus-background: Background of the `subtle` hue and `solid` fill combination in the focus state when a semantic color is applied + * @prop --ion-button-hue-subtle-solid-semantic-focus-color: Text color of the `subtle` hue and `solid` fill combination in the focus state when a semantic color is applied + * @prop --ion-button-hue-subtle-solid-hover-color: Text color of the `subtle` hue and `solid` fill combination in the hover state + * @prop --ion-button-hue-subtle-solid-hover-opacity: Opacity of the whole button of the `subtle` hue and `solid` fill combination in the hover state + * @prop --ion-button-hue-subtle-solid-hover-background: Background of the `subtle` hue and `solid` fill combination in the hover state + * @prop --ion-button-hue-subtle-solid-hover-box-shadow: Box shadow of the `subtle` hue and `solid` fill combination in the hover state + * @prop --ion-button-hue-subtle-solid-semantic-hover-background: Background of the `subtle` hue and `solid` fill combination in the hover state when a semantic color is applied + * @prop --ion-button-hue-subtle-solid-semantic-hover-color: Text color of the `subtle` hue and `solid` fill combination in the hover state when a semantic color is applied + * @prop --ion-button-hue-subtle-solid-activated-color: Text color of the `subtle` hue and `solid` fill combination in the activated state + * @prop --ion-button-hue-subtle-solid-activated-opacity: Opacity of the whole button of the `subtle` hue and `solid` fill combination in the activated state + * @prop --ion-button-hue-subtle-solid-activated-background: Background of the `subtle` hue and `solid` fill combination in the activated state + * @prop --ion-button-hue-subtle-solid-activated-box-shadow: Box shadow of the `subtle` hue and `solid` fill combination in the activated state + * @prop --ion-button-hue-subtle-solid-semantic-activated-background: Background of the `subtle` hue and `solid` fill combination in the activated state when a semantic color is applied + * @prop --ion-button-hue-subtle-solid-semantic-activated-color: Text color of the `subtle` hue and `solid` fill combination in the activated state when a semantic color is applied + * @prop --ion-button-hue-subtle-solid-disabled-background: Background of the `subtle` hue and `solid` fill combination in the disabled state + * @prop --ion-button-hue-subtle-solid-disabled-opacity: Opacity of the whole button of the `subtle` hue and `solid` fill combination in the disabled state + * @prop --ion-button-hue-subtle-solid-disabled-color: Text color of the `subtle` hue and `solid` fill combination in the disabled state + * @prop --ion-button-hue-subtle-solid-disabled-border-color: Border color of the `subtle` hue and `solid` fill combination in the disabled state + * @prop --ion-button-hue-subtle-solid-disabled-box-shadow: Box shadow of the `subtle` hue and `solid` fill combination in the disabled state + * @prop --ion-button-hue-subtle-solid-semantic-disabled-background: Background of the `subtle` hue and `solid` fill combination in the disabled state when a semantic color is applied + * @prop --ion-button-hue-subtle-solid-semantic-disabled-color: Text color of the `subtle` hue and `solid` fill combination in the disabled state when a semantic color is applied + * @prop --ion-button-hue-subtle-solid-semantic-disabled-border-color: Border color of the `subtle` hue and `solid` fill combination in the disabled state when a semantic color is applied + * @prop --ion-button-hue-subtle-outline-default-border-width: Border width of the `subtle` hue and `outline` fill combination in the default state + * @prop --ion-button-hue-subtle-outline-default-border-style: Border style of the `subtle` hue and `outline` fill combination in the default state + * @prop --ion-button-hue-subtle-outline-default-background: Background of the `subtle` hue and `outline` fill combination in the default state + * @prop --ion-button-hue-subtle-outline-default-box-shadow: Box shadow of the `subtle` hue and `outline` fill combination in the default state + * @prop --ion-button-hue-subtle-outline-ripple-effect-color: Ripple color of the `subtle` hue and `outline` fill combination + * @prop --ion-button-hue-subtle-outline-ripple-effect-opacity: Ripple opacity of the `subtle` hue and `outline` fill combination + * @prop --ion-button-hue-subtle-outline-semantic-default-border-color: Border color of the `subtle` hue and `outline` fill combination in the default state when a semantic color is applied + * @prop --ion-button-hue-subtle-outline-semantic-default-background: Background of the `subtle` hue and `outline` fill combination in the default state when a semantic color is applied + * @prop --ion-button-hue-subtle-outline-semantic-default-color: Text color of the `subtle` hue and `outline` fill combination in the default state when a semantic color is applied + * @prop --ion-button-hue-subtle-outline-semantic-ripple-effect-color: Ripple color of the `subtle` hue and `outline` fill combination when a semantic color is applied + * @prop --ion-button-hue-subtle-outline-semantic-ripple-effect-opacity: Ripple opacity of the `subtle` hue and `outline` fill combination when a semantic color is applied + * @prop --ion-button-hue-subtle-outline-focus-color: Text color of the `subtle` hue and `outline` fill combination in the focus state + * @prop --ion-button-hue-subtle-outline-focus-background: Background of the `subtle` hue and `outline` fill combination in the focus state + * @prop --ion-button-hue-subtle-outline-focus-box-shadow: Box shadow of the `subtle` hue and `outline` fill combination in the focus state + * @prop --ion-button-hue-subtle-outline-semantic-focus-background: Background of the `subtle` hue and `outline` fill combination in the focus state when a semantic color is applied + * @prop --ion-button-hue-subtle-outline-semantic-focus-color: Text color of the `subtle` hue and `outline` fill combination in the focus state when a semantic color is applied + * @prop --ion-button-hue-subtle-outline-hover-color: Text color of the `subtle` hue and `outline` fill combination in the hover state + * @prop --ion-button-hue-subtle-outline-hover-opacity: Opacity of the whole button of the `subtle` hue and `outline` fill combination in the hover state + * @prop --ion-button-hue-subtle-outline-hover-background: Background of the `subtle` hue and `outline` fill combination in the hover state + * @prop --ion-button-hue-subtle-outline-hover-box-shadow: Box shadow of the `subtle` hue and `outline` fill combination in the hover state + * @prop --ion-button-hue-subtle-outline-semantic-hover-background: Background of the `subtle` hue and `outline` fill combination in the hover state when a semantic color is applied + * @prop --ion-button-hue-subtle-outline-semantic-hover-color: Text color of the `subtle` hue and `outline` fill combination in the hover state when a semantic color is applied + * @prop --ion-button-hue-subtle-outline-activated-color: Text color of the `subtle` hue and `outline` fill combination in the activated state + * @prop --ion-button-hue-subtle-outline-activated-opacity: Opacity of the whole button of the `subtle` hue and `outline` fill combination in the activated state + * @prop --ion-button-hue-subtle-outline-activated-background: Background of the `subtle` hue and `outline` fill combination in the activated state + * @prop --ion-button-hue-subtle-outline-activated-box-shadow: Box shadow of the `subtle` hue and `outline` fill combination in the activated state + * @prop --ion-button-hue-subtle-outline-semantic-activated-background: Background of the `subtle` hue and `outline` fill combination in the activated state when a semantic color is applied + * @prop --ion-button-hue-subtle-outline-semantic-activated-color: Text color of the `subtle` hue and `outline` fill combination in the activated state when a semantic color is applied + * @prop --ion-button-hue-subtle-outline-disabled-background: Background of the `subtle` hue and `outline` fill combination in the disabled state + * @prop --ion-button-hue-subtle-outline-disabled-opacity: Opacity of the whole button of the `subtle` hue and `outline` fill combination in the disabled state + * @prop --ion-button-hue-subtle-outline-disabled-color: Text color of the `subtle` hue and `outline` fill combination in the disabled state + * @prop --ion-button-hue-subtle-outline-disabled-border-color: Border color of the `subtle` hue and `outline` fill combination in the disabled state + * @prop --ion-button-hue-subtle-outline-disabled-box-shadow: Box shadow of the `subtle` hue and `outline` fill combination in the disabled state + * @prop --ion-button-hue-subtle-outline-semantic-disabled-background: Background of the `subtle` hue and `outline` fill combination in the disabled state when a semantic color is applied + * @prop --ion-button-hue-subtle-outline-semantic-disabled-color: Text color of the `subtle` hue and `outline` fill combination in the disabled state when a semantic color is applied + * @prop --ion-button-hue-subtle-outline-semantic-disabled-border-color: Border color of the `subtle` hue and `outline` fill combination in the disabled state when a semantic color is applied + * @prop --ion-button-hue-subtle-clear-default-border-width: Border width of the `subtle` hue and `clear` fill combination in the default state + * @prop --ion-button-hue-subtle-clear-default-border-style: Border style of the `subtle` hue and `clear` fill combination in the default state + * @prop --ion-button-hue-subtle-clear-default-border-color: Border color of the `subtle` hue and `clear` fill combination in the default state + * @prop --ion-button-hue-subtle-clear-default-background: Background of the `subtle` hue and `clear` fill combination in the default state + * @prop --ion-button-hue-subtle-clear-default-box-shadow: Box shadow of the `subtle` hue and `clear` fill combination in the default state + * @prop --ion-button-hue-subtle-clear-ripple-effect-color: Ripple color of the `subtle` hue and `clear` fill combination + * @prop --ion-button-hue-subtle-clear-ripple-effect-opacity: Ripple opacity of the `subtle` hue and `clear` fill combination + * @prop --ion-button-hue-subtle-clear-semantic-default-border-color: Border color of the `subtle` hue and `clear` fill combination in the default state when a semantic color is applied + * @prop --ion-button-hue-subtle-clear-semantic-default-background: Background of the `subtle` hue and `clear` fill combination in the default state when a semantic color is applied + * @prop --ion-button-hue-subtle-clear-semantic-default-color: Text color of the `subtle` hue and `clear` fill combination in the default state when a semantic color is applied + * @prop --ion-button-hue-subtle-clear-semantic-ripple-effect-color: Ripple color of the `subtle` hue and `clear` fill combination when a semantic color is applied + * @prop --ion-button-hue-subtle-clear-semantic-ripple-effect-opacity: Ripple opacity of the `subtle` hue and `clear` fill combination when a semantic color is applied + * @prop --ion-button-hue-subtle-clear-focus-color: Text color of the `subtle` hue and `clear` fill combination in the focus state + * @prop --ion-button-hue-subtle-clear-focus-background: Background of the `subtle` hue and `clear` fill combination in the focus state + * @prop --ion-button-hue-subtle-clear-focus-box-shadow: Box shadow of the `subtle` hue and `clear` fill combination in the focus state + * @prop --ion-button-hue-subtle-clear-semantic-focus-background: Background of the `subtle` hue and `clear` fill combination in the focus state when a semantic color is applied + * @prop --ion-button-hue-subtle-clear-semantic-focus-color: Text color of the `subtle` hue and `clear` fill combination in the focus state when a semantic color is applied + * @prop --ion-button-hue-subtle-clear-hover-color: Text color of the `subtle` hue and `clear` fill combination in the hover state + * @prop --ion-button-hue-subtle-clear-hover-opacity: Opacity of the whole button of the `subtle` hue and `clear` fill combination in the hover state + * @prop --ion-button-hue-subtle-clear-hover-background: Background of the `subtle` hue and `clear` fill combination in the hover state + * @prop --ion-button-hue-subtle-clear-hover-box-shadow: Box shadow of the `subtle` hue and `clear` fill combination in the hover state + * @prop --ion-button-hue-subtle-clear-semantic-hover-background: Background of the `subtle` hue and `clear` fill combination in the hover state when a semantic color is applied + * @prop --ion-button-hue-subtle-clear-semantic-hover-color: Text color of the `subtle` hue and `clear` fill combination in the hover state when a semantic color is applied + * @prop --ion-button-hue-subtle-clear-activated-color: Text color of the `subtle` hue and `clear` fill combination in the activated state + * @prop --ion-button-hue-subtle-clear-activated-opacity: Opacity of the whole button of the `subtle` hue and `clear` fill combination in the activated state + * @prop --ion-button-hue-subtle-clear-activated-background: Background of the `subtle` hue and `clear` fill combination in the activated state + * @prop --ion-button-hue-subtle-clear-activated-box-shadow: Box shadow of the `subtle` hue and `clear` fill combination in the activated state + * @prop --ion-button-hue-subtle-clear-semantic-activated-background: Background of the `subtle` hue and `clear` fill combination in the activated state when a semantic color is applied + * @prop --ion-button-hue-subtle-clear-semantic-activated-color: Text color of the `subtle` hue and `clear` fill combination in the activated state when a semantic color is applied + * @prop --ion-button-hue-subtle-clear-disabled-background: Background of the `subtle` hue and `clear` fill combination in the disabled state + * @prop --ion-button-hue-subtle-clear-disabled-opacity: Opacity of the whole button of the `subtle` hue and `clear` fill combination in the disabled state + * @prop --ion-button-hue-subtle-clear-disabled-color: Text color of the `subtle` hue and `clear` fill combination in the disabled state + * @prop --ion-button-hue-subtle-clear-disabled-border-color: Border color of the `subtle` hue and `clear` fill combination in the disabled state + * @prop --ion-button-hue-subtle-clear-disabled-box-shadow: Box shadow of the `subtle` hue and `clear` fill combination in the disabled state + * @prop --ion-button-hue-subtle-clear-semantic-disabled-background: Background of the `subtle` hue and `clear` fill combination in the disabled state when a semantic color is applied + * @prop --ion-button-hue-subtle-clear-semantic-disabled-color: Text color of the `subtle` hue and `clear` fill combination in the disabled state when a semantic color is applied + * @prop --ion-button-hue-subtle-clear-semantic-disabled-border-color: Border color of the `subtle` hue and `clear` fill combination in the disabled state when a semantic color is applied */ diff --git a/core/src/components/button/button.scss b/core/src/components/button/button.scss index 259ef176cea..dd411f3aedb 100644 --- a/core/src/components/button/button.scss +++ b/core/src/components/button/button.scss @@ -4,6 +4,7 @@ // Button // -------------------------------------------------- +$hues: bold, subtle; $fills: solid, outline, clear; $sizes: small, medium, large; $shapes: crisp, soft, round, rectangular; @@ -137,15 +138,17 @@ $spinner-sizes: xsmall, small, medium, large, xlarge; // Button: Fill Typography // -------------------------------------------------- -@each $fill in $fills { - :host(.button-hue-bold.button-fill-#{$fill}) { - font-weight: var(--ion-button-hue-bold-#{$fill}-default-font-weight); +@each $hue in $hues { + @each $fill in $fills { + :host(.button-hue-#{$hue}.button-fill-#{$fill}) { + font-weight: var(--ion-button-hue-#{$hue}-#{$fill}-default-font-weight); + } } -} -@each $fill in $fills { - :host(.button-hue-bold.button-fill-#{$fill}.button-strong) { - font-weight: var(--ion-button-hue-bold-#{$fill}-strong-font-weight); + @each $fill in $fills { + :host(.button-hue-#{$hue}.button-fill-#{$fill}.button-strong) { + font-weight: var(--ion-button-hue-#{$hue}-#{$fill}-strong-font-weight); + } } } @@ -292,168 +295,170 @@ $spinner-sizes: xsmall, small, medium, large, xlarge; // Button: Hues and Fills // -------------------------------------------------- -/** - * Each fill has a resting look and one for each state. Without a color, - * the button reads the plain tokens. With a color, it reads the `semantic` - * tokens, which use the color's base, contrast and so on. - */ -@each $fill in $fills { - $token: --ion-button-hue-bold-#{$fill}; +@each $hue in $hues { + /** + * Each fill has a resting look and one for each state. Without a color, + * the button reads the plain tokens. With a color, it reads the `semantic` + * tokens, which use the color's base, contrast and so on. + */ + @each $fill in $fills { + $token: --ion-button-hue-#{$hue}-#{$fill}; - :host(.button-hue-bold.button-fill-#{$fill}) { - color: var(#{$token}-default-color); - } + :host(.button-hue-#{$hue}.button-fill-#{$fill}) { + color: var(#{$token}-default-color); + } - :host(.button-hue-bold.button-fill-#{$fill}) .button-native { - border-width: var(#{$token}-default-border-width); - border-style: var(#{$token}-default-border-style); - border-color: var(#{$token}-default-border-color); + :host(.button-hue-#{$hue}.button-fill-#{$fill}) .button-native { + border-width: var(#{$token}-default-border-width); + border-style: var(#{$token}-default-border-style); + border-color: var(#{$token}-default-border-color); - background: var(#{$token}-default-background); + background: var(#{$token}-default-background); - box-shadow: var(#{$token}-default-box-shadow); - } + box-shadow: var(#{$token}-default-box-shadow); + } - // The ripple sits in this shadow root, out of reach of the global token CSS - :host(.button-hue-bold.button-fill-#{$fill}) ion-ripple-effect { - // A variable, because the ripple animates its opacity in a keyframe - --ion-ripple-effect-opacity: var(#{$token}-ripple-effect-opacity); + // The ripple sits in this shadow root, out of reach of the global token CSS + :host(.button-hue-#{$hue}.button-fill-#{$fill}) ion-ripple-effect { + // A variable, because the ripple animates its opacity in a keyframe + --ion-ripple-effect-opacity: var(#{$token}-ripple-effect-opacity); - color: var(#{$token}-ripple-effect-color); - } + color: var(#{$token}-ripple-effect-color); + } - :host(.button-hue-bold.button-fill-#{$fill}.ion-color) .button-native { - border-color: var(#{$token}-semantic-default-border-color); + :host(.button-hue-#{$hue}.button-fill-#{$fill}.ion-color) .button-native { + border-color: var(#{$token}-semantic-default-border-color); - background: var(#{$token}-semantic-default-background); - color: var(#{$token}-semantic-default-color); - } + background: var(#{$token}-semantic-default-background); + color: var(#{$token}-semantic-default-color); + } - :host(.button-hue-bold.button-fill-#{$fill}.ion-color) ion-ripple-effect { - --ion-ripple-effect-opacity: var(#{$token}-semantic-ripple-effect-opacity); + :host(.button-hue-#{$hue}.button-fill-#{$fill}.ion-color) ion-ripple-effect { + --ion-ripple-effect-opacity: var(#{$token}-semantic-ripple-effect-opacity); - color: var(#{$token}-semantic-ripple-effect-color); + color: var(#{$token}-semantic-ripple-effect-color); + } } -} -/** - * The states are written in this order: focus -> hover -> activated -> disabled. - * Their rules share a specificity, so a later state overrides an earlier - * one: disabled overrides all, and activated overrides hover and focus. - */ + /** + * The states are written in this order: focus -> hover -> activated -> disabled. + * Their rules share a specificity, so a later state overrides an earlier + * one: disabled overrides all, and activated overrides hover and focus. + */ -// Focused -@each $fill in $fills { - $token: --ion-button-hue-bold-#{$fill}; + // Focused + @each $fill in $fills { + $token: --ion-button-hue-#{$hue}-#{$fill}; - :host(.button-hue-bold.button-fill-#{$fill}.ion-focused) { - color: var(#{$token}-focus-color); - } + :host(.button-hue-#{$hue}.button-fill-#{$fill}.ion-focused) { + color: var(#{$token}-focus-color); + } - :host(.button-hue-bold.button-fill-#{$fill}.ion-focused) .button-native { - background: var(#{$token}-focus-background); + :host(.button-hue-#{$hue}.button-fill-#{$fill}.ion-focused) .button-native { + background: var(#{$token}-focus-background); - box-shadow: var(#{$token}-focus-box-shadow); - } + box-shadow: var(#{$token}-focus-box-shadow); + } - :host(.button-hue-bold.button-fill-#{$fill}.ion-color.ion-focused) .button-native { - background: var(#{$token}-semantic-focus-background); - color: var(#{$token}-semantic-focus-color); + :host(.button-hue-#{$hue}.button-fill-#{$fill}.ion-color.ion-focused) .button-native { + background: var(#{$token}-semantic-focus-background); + color: var(#{$token}-semantic-focus-color); + } } -} -// Hover -@media (any-hover: hover) { - @each $fill in $fills { - $token: --ion-button-hue-bold-#{$fill}; + // Hover + @media (any-hover: hover) { + @each $fill in $fills { + $token: --ion-button-hue-#{$hue}-#{$fill}; - :host(.button-hue-bold.button-fill-#{$fill}:hover) { - color: var(#{$token}-hover-color); - } + :host(.button-hue-#{$hue}.button-fill-#{$fill}:hover) { + color: var(#{$token}-hover-color); + } - // Disabled keeps its own opacity - :host(.button-hue-bold.button-fill-#{$fill}:not(.button-disabled):hover) { - opacity: var(#{$token}-hover-opacity); - } + // Disabled keeps its own opacity + :host(.button-hue-#{$hue}.button-fill-#{$fill}:not(.button-disabled):hover) { + opacity: var(#{$token}-hover-opacity); + } - :host(.button-hue-bold.button-fill-#{$fill}:hover) .button-native { - background: var(#{$token}-hover-background); + :host(.button-hue-#{$hue}.button-fill-#{$fill}:hover) .button-native { + background: var(#{$token}-hover-background); - box-shadow: var(#{$token}-hover-box-shadow); - } + box-shadow: var(#{$token}-hover-box-shadow); + } - :host(.button-hue-bold.button-fill-#{$fill}.ion-color:hover) .button-native { - background: var(#{$token}-semantic-hover-background); - color: var(#{$token}-semantic-hover-color); + :host(.button-hue-#{$hue}.button-fill-#{$fill}.ion-color:hover) .button-native { + background: var(#{$token}-semantic-hover-background); + color: var(#{$token}-semantic-hover-color); + } } } -} -// Activated -@each $fill in $fills { - $token: --ion-button-hue-bold-#{$fill}; + // Activated + @each $fill in $fills { + $token: --ion-button-hue-#{$hue}-#{$fill}; - :host(.button-hue-bold.button-fill-#{$fill}.ion-activated) { - color: var(#{$token}-activated-color); - } + :host(.button-hue-#{$hue}.button-fill-#{$fill}.ion-activated) { + color: var(#{$token}-activated-color); + } - // Disabled keeps its own opacity - :host(.button-hue-bold.button-fill-#{$fill}.ion-activated:not(.button-disabled)) { - opacity: var(#{$token}-activated-opacity); - } + // Disabled keeps its own opacity + :host(.button-hue-#{$hue}.button-fill-#{$fill}.ion-activated:not(.button-disabled)) { + opacity: var(#{$token}-activated-opacity); + } - :host(.button-hue-bold.button-fill-#{$fill}.ion-activated) .button-native { - background: var(#{$token}-activated-background); + :host(.button-hue-#{$hue}.button-fill-#{$fill}.ion-activated) .button-native { + background: var(#{$token}-activated-background); - box-shadow: var(#{$token}-activated-box-shadow); - } + box-shadow: var(#{$token}-activated-box-shadow); + } - :host(.button-hue-bold.button-fill-#{$fill}.ion-color.ion-activated) .button-native { - background: var(#{$token}-semantic-activated-background); - color: var(#{$token}-semantic-activated-color); - } + :host(.button-hue-#{$hue}.button-fill-#{$fill}.ion-color.ion-activated) .button-native { + background: var(#{$token}-semantic-activated-background); + color: var(#{$token}-semantic-activated-color); + } - /** - * Only `md` mode renders a ripple. When it does, the ripple shows the press, - * so the button keeps its default background instead of the activated one. - */ - :host(.button-hue-bold.button-fill-#{$fill}.ion-activated) .button-native:has(ion-ripple-effect) { - background: var(#{$token}-default-background); - } + /** + * Only `md` mode renders a ripple. When it does, the ripple shows the press, + * so the button keeps its default background instead of the activated one. + */ + :host(.button-hue-#{$hue}.button-fill-#{$fill}.ion-activated) .button-native:has(ion-ripple-effect) { + background: var(#{$token}-default-background); + } - :host(.button-hue-bold.button-fill-#{$fill}.ion-color.ion-activated) .button-native:has(ion-ripple-effect) { - background: var(#{$token}-semantic-default-background); + :host(.button-hue-#{$hue}.button-fill-#{$fill}.ion-color.ion-activated) .button-native:has(ion-ripple-effect) { + background: var(#{$token}-semantic-default-background); + } } -} -// Disabled -@each $fill in $fills { - $token: --ion-button-hue-bold-#{$fill}; + // Disabled + @each $fill in $fills { + $token: --ion-button-hue-#{$hue}-#{$fill}; - :host(.button-hue-bold.button-fill-#{$fill}.button-disabled) { - // Keeps the default color when the theme doesn't set one - color: var(#{$token}-disabled-color, var(#{$token}-default-color)); + :host(.button-hue-#{$hue}.button-fill-#{$fill}.button-disabled) { + // Keeps the default color when the theme doesn't set one + color: var(#{$token}-disabled-color, var(#{$token}-default-color)); - opacity: var(#{$token}-disabled-opacity); - } + opacity: var(#{$token}-disabled-opacity); + } - :host(.button-hue-bold.button-fill-#{$fill}.button-disabled) .button-native { - // Keeps the default border color when the theme doesn't set one - border-color: var(#{$token}-disabled-border-color, var(#{$token}-default-border-color)); + :host(.button-hue-#{$hue}.button-fill-#{$fill}.button-disabled) .button-native { + // Keeps the default border color when the theme doesn't set one + border-color: var(#{$token}-disabled-border-color, var(#{$token}-default-border-color)); - background: var(#{$token}-disabled-background); + background: var(#{$token}-disabled-background); - box-shadow: var(#{$token}-disabled-box-shadow); - } + box-shadow: var(#{$token}-disabled-box-shadow); + } - :host(.button-hue-bold.button-fill-#{$fill}.ion-color.button-disabled) .button-native { - // Keeps the default border color when the theme doesn't set one - border-color: var(#{$token}-semantic-disabled-border-color, var(#{$token}-semantic-default-border-color)); + :host(.button-hue-#{$hue}.button-fill-#{$fill}.ion-color.button-disabled) .button-native { + // Keeps the default border color when the theme doesn't set one + border-color: var(#{$token}-semantic-disabled-border-color, var(#{$token}-semantic-default-border-color)); - background: var(#{$token}-semantic-disabled-background); + background: var(#{$token}-semantic-disabled-background); - // Keeps the default color when the theme doesn't set one - color: var(#{$token}-semantic-disabled-color, var(#{$token}-semantic-default-color)); + // Keeps the default color when the theme doesn't set one + color: var(#{$token}-semantic-disabled-color, var(#{$token}-semantic-default-color)); + } } } diff --git a/core/src/components/button/button.tsx b/core/src/components/button/button.tsx index 32475d01f4d..f6def341022 100644 --- a/core/src/components/button/button.tsx +++ b/core/src/components/button/button.tsx @@ -95,6 +95,14 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf */ @Prop({ reflect: true, mutable: true }) fill?: IonButtonFill | 'default'; + /** + * Set to `"bold"` for a button with vibrant, bold colors or to `"subtle"` for + * a button with muted, subtle colors. + * + * Defaults to `"bold"` if both the hue property and theme config are unset. + */ + @Prop() hue?: Hue; + /** * When using a router, it specifies the transition direction when navigating to * another page using `href`. @@ -294,10 +302,13 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf } /** - * Gets the button hue from the theme config. Defaults to `bold` if it is unset. + * Gets the button hue. Uses the `hue` property if set, otherwise + * checks the theme config. Defaults to `bold` if neither is set. */ get hueValue(): Hue { - return config.getObjectValue('IonButton.hue', 'bold') as Hue; + const hueConfig = config.getObjectValue('IonButton.hue', 'bold') as Hue; + + return this.hue ?? hueConfig; } /** @@ -494,7 +505,6 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf [`${buttonType}-size-${sizeValue}`]: true, [`${buttonType}-shape-${shapeValue}`]: true, [`${buttonType}-fill-${fillValue}`]: true, - // TODO(FW-6838): read the hue prop first in the hue PR [`${buttonType}-hue-${hueValue}`]: true, [`${buttonType}-strong`]: strong, 'in-datetime': this.inDatetime, diff --git a/core/src/components/button/test/hue/button.e2e.ts b/core/src/components/button/test/hue/button.e2e.ts new file mode 100644 index 00000000000..808738a7226 --- /dev/null +++ b/core/src/components/button/test/hue/button.e2e.ts @@ -0,0 +1,17 @@ +import { expect } from '@playwright/test'; +import { configs, test } from '@utils/test/playwright'; + +/** + * This behavior does not vary across directions + */ +configs({ directions: ['ltr'], modes: ['ios', 'md', 'ionic-md'] }).forEach(({ title, screenshot, config }) => { + test.describe(title('button: hue'), () => { + test('should render each hue in each fill and state', async ({ page }) => { + await page.goto(`/src/components/button/test/hue`, config); + + await page.setIonViewport(); + + await expect(page).toHaveScreenshot(screenshot(`button-hue`)); + }); + }); +}); diff --git a/core/src/components/button/test/hue/index.html b/core/src/components/button/test/hue/index.html new file mode 100644 index 00000000000..4f9cfd9383f --- /dev/null +++ b/core/src/components/button/test/hue/index.html @@ -0,0 +1,97 @@ + + + + + Button - Hue + + + + + + + + + + + + + + + Button - Hue + + + + + + + + + diff --git a/core/src/themes/ionic/default.tokens.ts b/core/src/themes/ionic/default.tokens.ts index eda8207d811..897ca6b4df0 100644 --- a/core/src/themes/ionic/default.tokens.ts +++ b/core/src/themes/ionic/default.tokens.ts @@ -558,6 +558,244 @@ export const defaultTheme: DefaultTheme = { }, }, }, + + subtle: { + solid: { + strong: { + font: { + weight: 'var(--ion-font-weight-medium)', + }, + }, + + default: { + background: ionColor('primary', 'base', { subtle: true }), + color: ionColor('primary', 'contrast', { subtle: true }), + + font: { + weight: 'var(--ion-font-weight-medium)', + }, + }, + + focus: { + background: ionColor('primary', 'base', { subtle: true }), + color: ionColor('primary', 'contrast', { subtle: true }), + }, + + hover: { + background: ionColor('primary', 'shade', { subtle: true }), + color: ionColor('primary', 'contrast', { subtle: true }), + }, + + activated: { + background: ionColor('primary', 'shade', { subtle: true }), + color: ionColor('primary', 'contrast', { subtle: true }), + }, + + // A white wash over the button + disabled: { + background: ionColor('primary', 'base', { subtle: true }), + + opacity: '0.4', + }, + + rippleEffect: { + color: ionColor('primary', 'shade', { subtle: true }), + opacity: '1', + }, + + // Any of the semantic colors like primary, secondary, etc. + semantic: { + default: { + background: currentColor('base', { subtle: true }), + color: currentColor('contrast', { subtle: true }), + }, + + focus: { + background: currentColor('base', { subtle: true }), + color: currentColor('contrast', { subtle: true }), + }, + + hover: { + background: currentColor('shade', { subtle: true }), + color: currentColor('contrast', { subtle: true }), + }, + + activated: { + background: currentColor('shade', { subtle: true }), + color: currentColor('contrast', { subtle: true }), + }, + + disabled: { + background: currentColor('base', { subtle: true }), + }, + + rippleEffect: { + color: currentColor('shade', { subtle: true }), + opacity: '1', + }, + }, + }, + + outline: { + strong: { + font: { + weight: 'var(--ion-font-weight-medium)', + }, + }, + + default: { + background: baseColors.backgroundColor, + color: ionColor('primary', 'foreground', { subtle: true }), + + border: { + color: ionColor('primary', 'shade', { subtle: true }), + style: 'solid', + width: 'var(--ion-border-width-xxxs)', + }, + + font: { + weight: 'var(--ion-font-weight-medium)', + }, + }, + + focus: { + background: baseColors.backgroundColor, + color: ionColor('primary', 'foreground', { subtle: true }), + }, + + hover: { + background: ionColor('primary', 'shade', { subtle: true }), + color: ionColor('primary', 'contrast', { subtle: true }), + }, + + activated: { + background: ionColor('primary', 'shade', { subtle: true }), + color: ionColor('primary', 'contrast', { subtle: true }), + }, + + // A white wash over the button + disabled: { + background: baseColors.backgroundColor, + + opacity: '0.4', + }, + + rippleEffect: { + color: ionColor('primary', 'shade', { subtle: true }), + opacity: '1', + }, + + semantic: { + default: { + background: baseColors.backgroundColor, + color: currentColor('foreground', { subtle: true }), + + border: { + color: currentColor('shade', { subtle: true }), + }, + }, + + focus: { + background: baseColors.backgroundColor, + color: currentColor('foreground', { subtle: true }), + }, + + hover: { + background: currentColor('shade', { subtle: true }), + color: currentColor('contrast', { subtle: true }), + }, + + activated: { + background: currentColor('shade', { subtle: true }), + color: currentColor('contrast', { subtle: true }), + }, + + disabled: { + background: baseColors.backgroundColor, + }, + + rippleEffect: { + color: currentColor('shade', { subtle: true }), + opacity: '1', + }, + }, + }, + + clear: { + strong: { + font: { + weight: 'var(--ion-font-weight-medium)', + }, + }, + + default: { + background: 'transparent', + color: ionColor('primary', 'foreground', { subtle: true }), + + font: { + weight: 'var(--ion-font-weight-medium)', + }, + }, + + focus: { + background: 'transparent', + color: ionColor('primary', 'foreground', { subtle: true }), + }, + + hover: { + background: ionColor('primary', 'shade', { subtle: true }), + color: ionColor('primary', 'contrast', { subtle: true }), + }, + + activated: { + background: ionColor('primary', 'shade', { subtle: true }), + color: ionColor('primary', 'contrast', { subtle: true }), + }, + + // A white wash over the button + disabled: { + background: 'transparent', + + opacity: '0.4', + }, + + rippleEffect: { + color: ionColor('primary', 'shade', { subtle: true }), + opacity: '1', + }, + + semantic: { + default: { + background: 'transparent', + color: currentColor('foreground', { subtle: true }), + }, + + focus: { + background: 'transparent', + color: currentColor('foreground', { subtle: true }), + }, + + hover: { + background: currentColor('shade', { subtle: true }), + color: currentColor('contrast', { subtle: true }), + }, + + activated: { + background: currentColor('shade', { subtle: true }), + color: currentColor('contrast', { subtle: true }), + }, + + disabled: { + background: 'transparent', + }, + + rippleEffect: { + color: currentColor('shade', { subtle: true }), + opacity: '1', + }, + }, + }, + }, }, // Sizes diff --git a/core/src/themes/ios/default.tokens.ts b/core/src/themes/ios/default.tokens.ts index 47c97e538b8..fc6e4b0b976 100644 --- a/core/src/themes/ios/default.tokens.ts +++ b/core/src/themes/ios/default.tokens.ts @@ -543,6 +543,227 @@ export const defaultTheme: DefaultTheme = { }, }, }, + + subtle: { + solid: { + strong: { + font: { + weight: 'var(--ion-font-weight-semi-bold)', + }, + }, + + default: { + background: ionColor('primary', 'base', { subtle: true }), + color: ionColor('primary', 'contrast', { subtle: true }), + + font: { + weight: 'var(--ion-font-weight-normal)', + }, + }, + + focus: { + background: ionColor('primary', 'shade', { subtle: true }), + color: ionColor('primary', 'contrast', { subtle: true }), + }, + + hover: { + background: ionColor('primary', 'tint', { subtle: true }), + color: ionColor('primary', 'contrast', { subtle: true }), + }, + + activated: { + background: ionColor('primary', 'shade', { subtle: true }), + color: ionColor('primary', 'contrast', { subtle: true }), + }, + + disabled: { + background: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.08), + color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.25), + }, + + // Any of the semantic colors like primary, secondary, etc. + semantic: { + default: { + background: currentColor('base', { subtle: true }), + color: currentColor('contrast', { subtle: true }), + }, + + focus: { + background: currentColor('shade', { subtle: true }), + color: currentColor('contrast', { subtle: true }), + }, + + hover: { + background: currentColor('tint', { subtle: true }), + color: currentColor('contrast', { subtle: true }), + }, + + activated: { + background: currentColor('shade', { subtle: true }), + color: currentColor('contrast', { subtle: true }), + }, + + disabled: { + background: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.08), + color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.25), + }, + }, + }, + + outline: { + strong: { + font: { + weight: 'var(--ion-font-weight-semi-bold)', + }, + }, + + default: { + background: 'transparent', + color: ionColor('primary', 'foreground', { subtle: true }), + + border: { + color: ionColor('primary', 'shade', { subtle: true }), + style: 'solid', + width: 'var(--ion-border-width-25)', + }, + + font: { + weight: 'var(--ion-font-weight-normal)', + }, + }, + + focus: { + background: ionColor('primary', 'base', { alpha: 0.1 }), + color: ionColor('primary', 'foreground', { subtle: true }), + }, + + // Fades the whole button instead of changing its color + hover: { + background: 'transparent', + color: ionColor('primary', 'foreground', { subtle: true }), + opacity: '0.6', + }, + + activated: { + background: ionColor('primary', 'base', { subtle: true }), + color: ionColor('primary', 'contrast', { subtle: true }), + }, + + disabled: { + background: 'transparent', + + border: { + color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.25), + }, + + color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.25), + }, + + semantic: { + default: { + background: 'transparent', + color: currentColor('foreground', { subtle: true }), + + border: { + color: currentColor('shade', { subtle: true }), + }, + }, + + focus: { + background: currentColor('base', { alpha: 0.1 }), + color: currentColor('foreground', { subtle: true }), + }, + + hover: { + background: 'transparent', + color: currentColor('foreground', { subtle: true }), + }, + + activated: { + background: currentColor('base', { subtle: true }), + color: currentColor('contrast', { subtle: true }), + }, + + disabled: { + background: 'transparent', + + border: { + color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.25), + }, + + color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.25), + }, + }, + }, + + clear: { + strong: { + font: { + weight: 'var(--ion-font-weight-semi-bold)', + }, + }, + + default: { + background: 'transparent', + color: ionColor('primary', 'foreground', { subtle: true }), + + // The clear fill uses a regular weight font + font: { + weight: 'normal', + }, + }, + + focus: { + background: ionColor('primary', 'base', { alpha: 0.1 }), + color: ionColor('primary', 'foreground', { subtle: true }), + }, + + // Fades the whole button instead of changing its color + hover: { + background: 'transparent', + color: ionColor('primary', 'foreground', { subtle: true }), + opacity: '0.6', + }, + + activated: { + background: 'transparent', + color: ionColor('primary', 'foreground', { subtle: true }), + opacity: '0.4', + }, + + disabled: { + background: 'transparent', + color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.25), + }, + + semantic: { + default: { + background: 'transparent', + color: currentColor('foreground', { subtle: true }), + }, + + focus: { + background: currentColor('base', { alpha: 0.1 }), + color: currentColor('foreground', { subtle: true }), + }, + + hover: { + background: 'transparent', + color: currentColor('foreground', { subtle: true }), + }, + + activated: { + background: 'transparent', + color: currentColor('foreground', { subtle: true }), + }, + + disabled: { + background: 'transparent', + color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.25), + }, + }, + }, + }, }, // Sizes diff --git a/core/src/themes/md/default.tokens.ts b/core/src/themes/md/default.tokens.ts index b7579e92539..e4a0a226b8e 100644 --- a/core/src/themes/md/default.tokens.ts +++ b/core/src/themes/md/default.tokens.ts @@ -604,6 +604,287 @@ export const defaultTheme: DefaultTheme = { }, }, }, + + subtle: { + solid: { + strong: { + font: { + weight: 'var(--ion-font-weight-bold)', + }, + }, + + default: { + background: ionColor('primary', 'base', { subtle: true }), + color: ionColor('primary', 'contrast', { subtle: true }), + box: { + shadow: + '0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12)', + }, + + font: { + weight: 'var(--ion-font-weight-medium)', + }, + }, + + focus: { + background: overlay( + ionColor('primary', 'base', { subtle: true }), + ionColor('primary', 'contrast', { alpha: 0.24, subtle: true }) + ), + color: ionColor('primary', 'contrast', { subtle: true }), + + box: { + shadow: + '0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12)', + }, + }, + + hover: { + background: overlay( + ionColor('primary', 'base', { subtle: true }), + ionColor('primary', 'contrast', { alpha: 0.08, subtle: true }) + ), + + color: ionColor('primary', 'contrast', { subtle: true }), + + box: { + shadow: + '0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12)', + }, + }, + + // The ripple is the pressed feedback + activated: { + background: ionColor('primary', 'base', { subtle: true }), + color: ionColor('primary', 'contrast', { subtle: true }), + + box: { + shadow: + '0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12)', + }, + }, + + disabled: { + background: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.12), + + box: { + shadow: 'none', + }, + + color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.38), + }, + + rippleEffect: { + opacity: '0.24', + }, + + // Any of the semantic colors like primary, secondary, etc. + semantic: { + default: { + background: currentColor('base', { subtle: true }), + color: currentColor('contrast', { subtle: true }), + }, + + focus: { + background: overlay( + currentColor('base', { subtle: true }), + currentColor('contrast', { alpha: 0.24, subtle: true }) + ), + color: currentColor('contrast', { subtle: true }), + }, + + hover: { + background: overlay( + currentColor('base', { subtle: true }), + currentColor('contrast', { alpha: 0.08, subtle: true }) + ), + color: currentColor('contrast', { subtle: true }), + }, + + activated: { + background: currentColor('base', { subtle: true }), + color: currentColor('contrast', { subtle: true }), + }, + + disabled: { + background: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.12), + color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.38), + }, + + rippleEffect: { + opacity: '0.24', + }, + }, + }, + + outline: { + strong: { + font: { + weight: 'var(--ion-font-weight-bold)', + }, + }, + + default: { + background: 'transparent', + color: ionColor('primary', 'foreground', { subtle: true }), + + border: { + color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.12), + style: 'solid', + width: 'var(--ion-border-width-xxxs)', + }, + + font: { + weight: 'var(--ion-font-weight-medium)', + }, + }, + + focus: { + background: ionColor('primary', 'base', { alpha: 0.12 }), + color: ionColor('primary', 'foreground', { subtle: true }), + }, + + hover: { + background: ionColor('primary', 'base', { alpha: 0.04 }), + color: ionColor('primary', 'foreground', { subtle: true }), + }, + + // The ripple is the pressed feedback + activated: { + background: 'transparent', + color: ionColor('primary', 'foreground', { subtle: true }), + }, + + disabled: { + background: 'transparent', + + border: { + color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.12), + }, + + color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.38), + }, + + rippleEffect: { + opacity: '0.12', + }, + + semantic: { + default: { + background: 'transparent', + color: currentColor('foreground', { subtle: true }), + + border: { + color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.12), + }, + }, + + focus: { + background: currentColor('base', { alpha: 0.12 }), + color: currentColor('foreground', { subtle: true }), + }, + + hover: { + background: currentColor('base', { alpha: 0.04 }), + color: currentColor('foreground', { subtle: true }), + }, + + activated: { + background: 'transparent', + color: currentColor('foreground', { subtle: true }), + }, + + disabled: { + background: 'transparent', + + border: { + color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.12), + }, + + color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.38), + }, + + rippleEffect: { + opacity: '0.12', + }, + }, + }, + + clear: { + strong: { + font: { + weight: 'var(--ion-font-weight-bold)', + }, + }, + + default: { + background: 'transparent', + color: ionColor('primary', 'foreground', { subtle: true }), + + font: { + weight: 'var(--ion-font-weight-medium)', + }, + }, + + focus: { + background: ionColor('primary', 'base', { alpha: 0.12 }), + color: ionColor('primary', 'foreground', { subtle: true }), + }, + + hover: { + background: ionColor('primary', 'base', { alpha: 0.04 }), + color: ionColor('primary', 'foreground', { subtle: true }), + }, + + // The ripple is the pressed feedback + activated: { + background: 'transparent', + color: ionColor('primary', 'foreground', { subtle: true }), + }, + + disabled: { + background: 'transparent', + + color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.38), + }, + + rippleEffect: { + opacity: '0.12', + }, + + semantic: { + default: { + background: 'transparent', + color: currentColor('foreground', { subtle: true }), + }, + + focus: { + background: currentColor('base', { alpha: 0.12 }), + color: currentColor('foreground', { subtle: true }), + }, + + hover: { + background: currentColor('base', { alpha: 0.04 }), + color: currentColor('foreground', { subtle: true }), + }, + + activated: { + background: 'transparent', + color: currentColor('foreground', { subtle: true }), + }, + + disabled: { + background: 'transparent', + + color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.38), + }, + + rippleEffect: { + opacity: '0.12', + }, + }, + }, + }, }, // Sizes diff --git a/packages/angular/src/lazy/directives/proxies.ts b/packages/angular/src/lazy/directives/proxies.ts index 8f691ca8593..8baba379dcd 100644 --- a/packages/angular/src/lazy/directives/proxies.ts +++ b/packages/angular/src/lazy/directives/proxies.ts @@ -385,14 +385,14 @@ export declare interface IonBreadcrumbs extends Components.IonBreadcrumbs { @ProxyCmp({ - inputs: ['buttonType', 'color', 'disabled', 'download', 'expand', 'fill', 'form', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', 'strong', 'target', 'type'] + inputs: ['buttonType', 'color', 'disabled', 'download', 'expand', 'fill', 'form', 'href', 'hue', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', 'strong', 'target', 'type'] }) @Component({ selector: 'ion-button', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['buttonType', 'color', { name: 'disabled', transform: nullableBooleanAttribute }, 'download', 'expand', 'fill', 'form', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', { name: 'strong', transform: nullableBooleanAttribute }, 'target', 'type'], + inputs: ['buttonType', 'color', { name: 'disabled', transform: nullableBooleanAttribute }, 'download', 'expand', 'fill', 'form', 'href', 'hue', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', { name: 'strong', transform: nullableBooleanAttribute }, 'target', 'type'], outputs: ['ionFocus', 'ionBlur'], standalone: false }) diff --git a/packages/angular/src/standalone/directives/ion-button.ts b/packages/angular/src/standalone/directives/ion-button.ts index 3e2d0a4c863..02547f53074 100644 --- a/packages/angular/src/standalone/directives/ion-button.ts +++ b/packages/angular/src/standalone/directives/ion-button.ts @@ -11,14 +11,14 @@ import { defineCustomElement as defineIonButton } from '@ionic/core/components/i @ProxyCmp({ defineCustomElementFn: defineIonButton, - inputs: ['buttonType', 'color', 'disabled', 'download', 'expand', 'fill', 'form', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', 'strong', 'target', 'type'] + inputs: ['buttonType', 'color', 'disabled', 'download', 'expand', 'fill', 'form', 'href', 'hue', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', 'strong', 'target', 'type'] }) @Component({ selector: 'ion-button', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['buttonType', 'color', { name: 'disabled', transform: nullableBooleanAttribute }, 'download', 'expand', 'fill', 'form', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', { name: 'strong', transform: nullableBooleanAttribute }, 'target', 'type'], + inputs: ['buttonType', 'color', { name: 'disabled', transform: nullableBooleanAttribute }, 'download', 'expand', 'fill', 'form', 'href', 'hue', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', { name: 'strong', transform: nullableBooleanAttribute }, 'target', 'type'], outputs: ['ionFocus', 'ionBlur'], }) export class IonButton { diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index 8b7ed5a3297..750d2fa11c1 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -182,6 +182,7 @@ export const IonButton: StencilVueComponent = /*@__PURE__*/ defin 'disabled', 'expand', 'fill', + 'hue', 'routerDirection', 'routerAnimation', 'download', From 16be6258c998bd6f6b2945faf716d9d677f1a963 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 26 Oct 2026 14:54:23 -0700 Subject: [PATCH 2/8] test(button): check subtle hue contrast with axe --- .../components/button/test/a11y/button.e2e.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/core/src/components/button/test/a11y/button.e2e.ts b/core/src/components/button/test/a11y/button.e2e.ts index dae8cfd59e2..664eacb3b68 100644 --- a/core/src/components/button/test/a11y/button.e2e.ts +++ b/core/src/components/button/test/a11y/button.e2e.ts @@ -282,3 +282,32 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { }); }); }); + +/** + * TODO(FW-6417): the subtle palette fails contrast for success, warning and + * light, and in the high contrast palettes. Add them back once it is fixed. + */ +const SUBTLE_COLORS = ['primary', 'secondary', 'tertiary', 'danger', 'medium', 'dark']; + +configs({ directions: ['ltr'], palettes: ['light', 'dark'] }).forEach(({ title, config }) => { + test.describe(title('button: subtle hue a11y'), () => { + test('should not have accessibility violations', async ({ page }) => { + await page.setContent( + ['solid', 'outline', 'clear'] + .map((fill) => + [undefined, ...SUBTLE_COLORS] + .map( + (color) => + `Button` + ) + .join('') + ) + .join(''), + config + ); + + const results = await new AxeBuilder({ page }).analyze(); + expect(results.violations).toEqual([]); + }); + }); +}); From 9d6413d26c405bd6efa584c9ca46c2642e7482dc Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 26 Oct 2026 19:35:20 -0700 Subject: [PATCH 3/8] test(button): rename the subtle a11y color list --- core/src/components/button/test/a11y/button.e2e.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/components/button/test/a11y/button.e2e.ts b/core/src/components/button/test/a11y/button.e2e.ts index 664eacb3b68..6d8343b6764 100644 --- a/core/src/components/button/test/a11y/button.e2e.ts +++ b/core/src/components/button/test/a11y/button.e2e.ts @@ -287,7 +287,7 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { * TODO(FW-6417): the subtle palette fails contrast for success, warning and * light, and in the high contrast palettes. Add them back once it is fixed. */ -const SUBTLE_COLORS = ['primary', 'secondary', 'tertiary', 'danger', 'medium', 'dark']; +const COLORS = ['primary', 'secondary', 'tertiary', 'danger', 'medium', 'dark']; configs({ directions: ['ltr'], palettes: ['light', 'dark'] }).forEach(({ title, config }) => { test.describe(title('button: subtle hue a11y'), () => { @@ -295,7 +295,7 @@ configs({ directions: ['ltr'], palettes: ['light', 'dark'] }).forEach(({ title, await page.setContent( ['solid', 'outline', 'clear'] .map((fill) => - [undefined, ...SUBTLE_COLORS] + [undefined, ...COLORS] .map( (color) => `Button` From e6edaea8ee9ce01b997b57e83db05f5dc985d3a3 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 27 Oct 2026 16:39:32 -0700 Subject: [PATCH 4/8] test(button): loop over the fills array in the subtle hue a11y test --- .../components/button/test/a11y/button.e2e.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/core/src/components/button/test/a11y/button.e2e.ts b/core/src/components/button/test/a11y/button.e2e.ts index 6d8343b6764..52aa10b33c9 100644 --- a/core/src/components/button/test/a11y/button.e2e.ts +++ b/core/src/components/button/test/a11y/button.e2e.ts @@ -2,6 +2,8 @@ import AxeBuilder from '@axe-core/playwright'; import { expect } from '@playwright/test'; import { configs, test } from '@utils/test/playwright'; +import { ION_BUTTON_FILLS } from '../../button.interface'; + configs({ directions: ['ltr'], palettes: ['light', 'dark'] }).forEach(({ title, config }) => { test.describe(title('button: a11y for ion-color()'), () => { test('should not have accessibility violations', async ({ page }) => { @@ -293,16 +295,14 @@ configs({ directions: ['ltr'], palettes: ['light', 'dark'] }).forEach(({ title, test.describe(title('button: subtle hue a11y'), () => { test('should not have accessibility violations', async ({ page }) => { await page.setContent( - ['solid', 'outline', 'clear'] - .map((fill) => - [undefined, ...COLORS] - .map( - (color) => - `Button` - ) - .join('') - ) - .join(''), + ION_BUTTON_FILLS.map((fill) => + [undefined, ...COLORS] + .map( + (color) => + `Button` + ) + .join('') + ).join(''), config ); From 6e010c3f70584841d1c959f74e502f61c3c48267 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 27 Oct 2026 19:43:51 -0700 Subject: [PATCH 5/8] fix(button): use foreground for outline text in md and ios --- BREAKING.md | 11 ++++++----- core/src/themes/ios/default.tokens.ts | 16 ++++++++-------- core/src/themes/md/default.tokens.ts | 16 ++++++++-------- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/BREAKING.md b/BREAKING.md index 6a58c284261..fbe7343bd1a 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -61,11 +61,12 @@ The following breaking changes apply to `ion-button`: 7. The `ios` button follows iOS 17 for its font, icons and disabled state. [7](#version-10x-button-ios-17) 8. A badge without a `vertical` position now has a gap between it and the label, and no longer makes the button taller than its `min.height`. To remove the gap, set `IonButton.size..badge.margin.start` for global styles or `--ion-button-size--badge-margin-start` for component-specific styles to `0`. 9. A button with an icon in the `icon-only` slot now shows only that icon and any badge with a `vertical` position. Text, elements in the `start` and `end` slots, and other content are hidden. Hidden text still names the button for assistive technology. To show other content, remove the `icon-only` slot and use the `start` or `end` slot for the icon. -10. CSS variables have been replaced. [10](#version-10x-button-replaced-css-variables) -11. `--padding-*` CSS variables are no longer part of the documented public API (but remain functional). [11](#version-10x-button-padding-variables) -12. The fill, size, shape and expand classes have been renamed. [12](#version-10x-button-renamed-classes) -13. The `default` size has been deprecated. [13](#version-10x-button-deprecated-default-size) -14. Theme classes (`ion-button.md`, `ion-button.ios`) are no longer supported. [14](#version-10x-button-theme-classes) +10. The text of `outline` buttons in `md` and `ios` now uses the color's darker `foreground` variant instead of `base`, matching `clear` buttons, since it sits on the page rather than on a fill of the color. In `ios`, `clear` buttons with a `color` also keep `foreground` on hover and focus. To revert, set `IonButton.hue.bold.outline..color` (`.semantic..color` with a `color`) for global styles or `--ion-button-hue-bold-outline--color` (`--ion-button-hue-bold-outline-semantic--color`) for component-specific styles to the color's `base`. +11. CSS variables have been replaced. [11](#version-10x-button-replaced-css-variables) +12. `--padding-*` CSS variables are no longer part of the documented public API (but remain functional). [12](#version-10x-button-padding-variables) +13. The fill, size, shape and expand classes have been renamed. [13](#version-10x-button-renamed-classes) +14. The `default` size has been deprecated. [14](#version-10x-button-deprecated-default-size) +15. Theme classes (`ion-button.md`, `ion-button.ios`) are no longer supported. [15](#version-10x-button-theme-classes)
Material Design 2 in md
diff --git a/core/src/themes/ios/default.tokens.ts b/core/src/themes/ios/default.tokens.ts index fc6e4b0b976..41335213276 100644 --- a/core/src/themes/ios/default.tokens.ts +++ b/core/src/themes/ios/default.tokens.ts @@ -398,7 +398,7 @@ export const defaultTheme: DefaultTheme = { default: { background: 'transparent', - color: ionColor('primary', 'base'), + color: ionColor('primary', 'foreground'), border: { color: ionColor('primary', 'base'), @@ -413,13 +413,13 @@ export const defaultTheme: DefaultTheme = { focus: { background: ionColor('primary', 'base', { alpha: 0.1 }), - color: ionColor('primary', 'base'), + color: ionColor('primary', 'foreground'), }, // Fades the whole button instead of changing its color hover: { background: 'transparent', - color: ionColor('primary', 'base'), + color: ionColor('primary', 'foreground'), opacity: '0.6', }, @@ -441,7 +441,7 @@ export const defaultTheme: DefaultTheme = { semantic: { default: { background: 'transparent', - color: currentColor('base'), + color: currentColor('foreground'), border: { color: currentColor('base'), @@ -450,12 +450,12 @@ export const defaultTheme: DefaultTheme = { focus: { background: currentColor('base', { alpha: 0.1 }), - color: currentColor('base'), + color: currentColor('foreground'), }, hover: { background: 'transparent', - color: currentColor('base'), + color: currentColor('foreground'), }, activated: { @@ -523,12 +523,12 @@ export const defaultTheme: DefaultTheme = { focus: { background: currentColor('base', { alpha: 0.1 }), - color: currentColor('base'), + color: currentColor('foreground'), }, hover: { background: 'transparent', - color: currentColor('base'), + color: currentColor('foreground'), }, activated: { diff --git a/core/src/themes/md/default.tokens.ts b/core/src/themes/md/default.tokens.ts index e4a0a226b8e..e5cb40a2e71 100644 --- a/core/src/themes/md/default.tokens.ts +++ b/core/src/themes/md/default.tokens.ts @@ -445,7 +445,7 @@ export const defaultTheme: DefaultTheme = { default: { background: 'transparent', - color: ionColor('primary', 'base'), + color: ionColor('primary', 'foreground'), border: { color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.12), @@ -460,18 +460,18 @@ export const defaultTheme: DefaultTheme = { focus: { background: ionColor('primary', 'base', { alpha: 0.12 }), - color: ionColor('primary', 'base'), + color: ionColor('primary', 'foreground'), }, hover: { background: ionColor('primary', 'base', { alpha: 0.04 }), - color: ionColor('primary', 'base'), + color: ionColor('primary', 'foreground'), }, // The ripple is the pressed feedback activated: { background: 'transparent', - color: ionColor('primary', 'base'), + color: ionColor('primary', 'foreground'), }, disabled: { @@ -491,7 +491,7 @@ export const defaultTheme: DefaultTheme = { semantic: { default: { background: 'transparent', - color: currentColor('base'), + color: currentColor('foreground'), border: { color: rgba('var(--ion-text-color-rgb, 0, 0, 0)', 0.12), @@ -500,17 +500,17 @@ export const defaultTheme: DefaultTheme = { focus: { background: currentColor('base', { alpha: 0.12 }), - color: currentColor('base'), + color: currentColor('foreground'), }, hover: { background: currentColor('base', { alpha: 0.04 }), - color: currentColor('base'), + color: currentColor('foreground'), }, activated: { background: 'transparent', - color: currentColor('base'), + color: currentColor('foreground'), }, disabled: { From df9450aebb512b3c8a4946f2cd1fca40dadf8712 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 27 Oct 2026 19:43:51 -0700 Subject: [PATCH 6/8] test(button): note the tests that don't vary by direction --- core/src/components/button/test/a11y/button.e2e.ts | 6 ++++++ core/src/components/button/test/hue/button.e2e.ts | 2 +- core/src/components/button/test/spinner/button.e2e.ts | 3 +++ core/src/components/button/test/states/button.e2e.ts | 3 +++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/core/src/components/button/test/a11y/button.e2e.ts b/core/src/components/button/test/a11y/button.e2e.ts index 52aa10b33c9..a3fececd4d9 100644 --- a/core/src/components/button/test/a11y/button.e2e.ts +++ b/core/src/components/button/test/a11y/button.e2e.ts @@ -4,6 +4,9 @@ import { configs, test } from '@utils/test/playwright'; import { ION_BUTTON_FILLS } from '../../button.interface'; +/** + * This behavior does not vary across directions. + */ configs({ directions: ['ltr'], palettes: ['light', 'dark'] }).forEach(({ title, config }) => { test.describe(title('button: a11y for ion-color()'), () => { test('should not have accessibility violations', async ({ page }) => { @@ -291,6 +294,9 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { */ const COLORS = ['primary', 'secondary', 'tertiary', 'danger', 'medium', 'dark']; +/** + * This behavior does not vary across directions. + */ configs({ directions: ['ltr'], palettes: ['light', 'dark'] }).forEach(({ title, config }) => { test.describe(title('button: subtle hue a11y'), () => { test('should not have accessibility violations', async ({ page }) => { diff --git a/core/src/components/button/test/hue/button.e2e.ts b/core/src/components/button/test/hue/button.e2e.ts index 808738a7226..22c7a0142ac 100644 --- a/core/src/components/button/test/hue/button.e2e.ts +++ b/core/src/components/button/test/hue/button.e2e.ts @@ -2,7 +2,7 @@ import { expect } from '@playwright/test'; import { configs, test } from '@utils/test/playwright'; /** - * This behavior does not vary across directions + * This behavior does not vary across directions. */ configs({ directions: ['ltr'], modes: ['ios', 'md', 'ionic-md'] }).forEach(({ title, screenshot, config }) => { test.describe(title('button: hue'), () => { diff --git a/core/src/components/button/test/spinner/button.e2e.ts b/core/src/components/button/test/spinner/button.e2e.ts index 627a3051d78..62c971bcc05 100644 --- a/core/src/components/button/test/spinner/button.e2e.ts +++ b/core/src/components/button/test/spinner/button.e2e.ts @@ -1,6 +1,9 @@ import { expect } from '@playwright/test'; import { configs, test } from '@utils/test/playwright'; +/** + * This behavior does not vary across directions. + */ configs({ modes: ['ionic-md', 'md', 'ios'], directions: ['ltr'] }).forEach(({ config, screenshot, title }) => { test.describe(title('button: spinner'), () => { test('should not have visual regressions', async ({ page }) => { diff --git a/core/src/components/button/test/states/button.e2e.ts b/core/src/components/button/test/states/button.e2e.ts index d68676545f0..4097342b59e 100644 --- a/core/src/components/button/test/states/button.e2e.ts +++ b/core/src/components/button/test/states/button.e2e.ts @@ -5,6 +5,9 @@ import { ION_BUTTON_FILLS } from '../../button.interface'; const STATES = ['activated', 'focused']; +/** + * This behavior does not vary across directions. + */ configs({ directions: ['ltr'], modes: ['ionic-md', 'md', 'ios'] }).forEach(({ title, screenshot, config }) => { test.describe(title('button: states'), () => { STATES.forEach((state) => { From 8c1b2178ed7ea86436ea94f84b417606c562d607 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Wed, 28 Oct 2026 16:37:11 -0700 Subject: [PATCH 7/8] test(button): render the hue test with setContent, one snapshot per hue --- .../components/button/test/hue/button.e2e.ts | 63 +++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/core/src/components/button/test/hue/button.e2e.ts b/core/src/components/button/test/hue/button.e2e.ts index 22c7a0142ac..498ba9a4dbd 100644 --- a/core/src/components/button/test/hue/button.e2e.ts +++ b/core/src/components/button/test/hue/button.e2e.ts @@ -1,17 +1,72 @@ import { expect } from '@playwright/test'; import { configs, test } from '@utils/test/playwright'; +import { HUES } from '../../../../themes/themes.interfaces'; +import { ION_BUTTON_FILLS } from '../../button.interface'; + +const states = ['', 'class="ion-focused"', 'class="ion-activated"', 'disabled']; + +const colors = ['', 'primary', 'secondary', 'tertiary', 'success', 'warning', 'danger', 'light', 'medium', 'dark']; + /** * This behavior does not vary across directions. */ configs({ directions: ['ltr'], modes: ['ios', 'md', 'ionic-md'] }).forEach(({ title, screenshot, config }) => { test.describe(title('button: hue'), () => { - test('should render each hue in each fill and state', async ({ page }) => { - await page.goto(`/src/components/button/test/hue`, config); + HUES.forEach((hue) => { + test(`should render ${hue} buttons in each fill and state`, async ({ page }) => { + const sections = ION_BUTTON_FILLS.map( + (fill) => ` +

${fill}

+ ${states + .map( + (state) => + `
${colors + .map( + (color) => + `${ + color || 'Default' + }` + ) + .join('')}
` + ) + .join('\n')} + ` + ).join('\n'); + + await page.setContent( + ` + + +
+ ${sections} +
+ `, + config + ); - await page.setIonViewport(); + const container = page.locator('.hue-container'); - await expect(page).toHaveScreenshot(screenshot(`button-hue`)); + await expect(container).toHaveScreenshot(screenshot(`button-hue-${hue}`)); + }); }); }); }); From dd12cad1939984b9833816b76fea497b077c1c0b Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Wed, 28 Oct 2026 19:37:36 -0700 Subject: [PATCH 8/8] test(button): size the hue viewport to the content --- core/src/components/button/test/hue/button.e2e.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/src/components/button/test/hue/button.e2e.ts b/core/src/components/button/test/hue/button.e2e.ts index 498ba9a4dbd..1a64bff6090 100644 --- a/core/src/components/button/test/hue/button.e2e.ts +++ b/core/src/components/button/test/hue/button.e2e.ts @@ -65,6 +65,10 @@ configs({ directions: ['ltr'], modes: ['ios', 'md', 'ionic-md'] }).forEach(({ ti const container = page.locator('.hue-container'); + // Firefox and Safari leave the area below the viewport transparent + const box = await container.boundingBox(); + await page.setViewportSize({ width: Math.ceil(box!.width), height: Math.ceil(box!.height) }); + await expect(container).toHaveScreenshot(screenshot(`button-hue-${hue}`)); }); });