Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
dcc2b55
feat(title): add recipe and tokens
thetaPC Oct 6, 2026
0163d99
chore(): run lint
thetaPC Oct 6, 2026
86fca06
fix(header): detect a large title set through config
thetaPC Oct 10, 2026
8c8937d
refactor(title): remove unused element and destructure color
thetaPC Oct 10, 2026
82ce8b2
docs(title): explain the translateZ transform
thetaPC Oct 10, 2026
03d09ce
test(title): cover hue classes and config defaults
thetaPC Oct 10, 2026
a74205a
refactor(title): move the ionic title padding out of the toolbar
thetaPC Oct 12, 2026
3b8c53e
refactor(toolbar): remove the unused ios large title rule
thetaPC Oct 12, 2026
f6d82ac
docs(toolbar): fix a typo in the slot placement comment
thetaPC Oct 19, 2026
7a1ecb5
docs(breaking): render code in the title headings
thetaPC Oct 19, 2026
9c693d7
docs(breaking): move the toolbar title classes to the toolbar section
thetaPC Oct 19, 2026
c986975
test(title): remove the color test now that hue covers it
thetaPC Oct 19, 2026
c8977ef
test(title): render the hue test with setContent and include ios
thetaPC Oct 19, 2026
3fbbd93
refactor(toolbar): give the ios large title its own row from the toolbar
thetaPC Oct 19, 2026
4b265bb
refactor(title): set box-sizing in the stylesheet instead of a token
thetaPC Oct 19, 2026
05d1349
refactor(title): set pointer-events in the stylesheet instead of a token
thetaPC Oct 19, 2026
a111599
docs(title): mark the temporary position tokens for FW-6864
thetaPC Oct 19, 2026
819c21c
docs(title): mark the ios medium side padding for FW-6864
thetaPC Oct 19, 2026
f492478
fix(title): match the ios small title inset to the iOS design kits
thetaPC Oct 19, 2026
e376b50
fix(title): match the md title letter spacing to Material Design 2
thetaPC Oct 20, 2026
941687c
chore(): add updated snapshots
Ionitron Oct 20, 2026
0d9570b
test(title): move the snapshot updates to their own PR
thetaPC Oct 20, 2026
2e2df52
chore(): add updated snapshots
Ionitron Oct 20, 2026
42ff788
refactor(title): set width and height in the stylesheet instead of to…
thetaPC Oct 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
79 changes: 79 additions & 0 deletions BREAKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ This is a comprehensive list of the breaking changes introduced in the major ver
- [Text](#version-10x-text)
- [Textarea](#version-10x-textarea)
- [Thumbnail](#version-10x-thumbnail)
- [Title](#version-10x-title)
- [Toolbar](#version-10x-toolbar)
- [Framework Specific](#version-10x-framework-specific)
- [Angular](#version-10x-angular)

Expand Down Expand Up @@ -529,6 +531,83 @@ Use the **parent's** thumbnail tokens instead:

Remove any instances that target the theme classes: `ion-thumbnail.md`, `ion-thumbnail.ios`.

<h4 id="version-10x-title">Title</h4>

The following breaking changes apply to `ion-title`:

1. `--color` has been replaced. <sup>[1](#version-10x-title-replaced-css-variables)</sup>
2. The color applied by the `color` prop is now driven by the centralized Ionic Theming system, scoped to the new `hue` property. <sup>[2](#version-10x-title-hue)</sup>
3. The size classes now include the property name, and a title without `size` is now `medium`. <sup>[3](#version-10x-title-size-classes)</sup>
4. The `title-rtl` class has been removed. <sup>[4](#version-10x-title-rtl-class)</sup>
5. Theme classes (`ion-title.md`, `ion-title.ios`) are no longer supported. <sup>[5](#version-10x-title-theme-classes)</sup>
6. In the `md` theme, a tap on the title's padding no longer counts as a tap on the title. <sup>[6](#version-10x-title-padding-taps)</sup>

<h5 id="version-10x-title-replaced-css-variables">Replaced CSS variables</h5>

Use the new token structure for global styles, or the corresponding CSS variable for component-specific overrides:

| Old (9.x) | New token (global) | New CSS variable (component-specific) |
|---|---|---|
| `--color` | `IonTitle.color` | `--ion-title-color` |

<h5 id="version-10x-title-hue">New <code>hue</code> property and color tokens</h5>

A new `hue` property selects between vibrant and muted color variants. It defaults to `"bold"`, which preserves prior behavior when `color` is set.

When `color` is set, the title color now reads from a token instead of `--ion-color-base` directly. Global overrides should use the theme tokens; component-specific overrides use the corresponding CSS variables:

| Hue | Token (global) | CSS variable (component-specific) |
|---|---|---|
| `bold` | `IonTitle.hue.bold.semantic.default.color` | `--ion-title-hue-bold-semantic-default-color` |
| `subtle` | `IonTitle.hue.subtle.semantic.default.color` | `--ion-title-hue-subtle-semantic-default-color` |

<h5 id="version-10x-title-size-classes">Size classes</h5>

The classes `ion-title` adds for its size now include the property name. A title without `size` is now `medium`, which can also be set explicitly:

| Old (9.x) | New |
|---|---|
| `title-default` | `title-size-medium` |
| `title-small` | `title-size-small` |
| `title-large` | `title-size-large` |

`ion-toolbar` builds its own classes from these, so they change as well. Refer to [Toolbar](#version-10x-toolbar-title-size-classes).

<h5 id="version-10x-title-rtl-class"><code>title-rtl</code> class</h5>

`ion-title` no longer adds the `title-rtl` class. Use the `:dir(rtl)` pseudo-class instead:

```diff
-ion-title.title-rtl { }
+ion-title:dir(rtl) { }
```

<h5 id="version-10x-title-theme-classes">Theme classes</h5>

Remove any instances that target the theme classes: `ion-title.md`, `ion-title.ios`.

<h5 id="version-10x-title-padding-taps">Taps on the title's padding</h5>

In the `md` theme, a tap on the padding at the start or end of `ion-title` now reaches the element behind it, usually `ion-toolbar`, instead of the title. This matches the `ios` and `ionic` themes. Taps on the text, and on the rest of the title, still reach `ion-title`.

If an app listens for taps on `ion-title` and needs them to include the padding, listen on `ion-toolbar` instead.

<h4 id="version-10x-toolbar">Toolbar</h4>

The following breaking changes apply to `ion-toolbar`:

1. The classes it adds for a slotted title's size now include the property name. <sup>[1](#version-10x-toolbar-title-size-classes)</sup>

<h5 id="version-10x-toolbar-title-size-classes">Title size classes</h5>

The classes `ion-toolbar` adds for a slotted title's size now include the property name, following the title's own size classes. Refer to [Title](#version-10x-title-size-classes).

| Old (9.x) | New |
|---|---|
| `toolbar-title-default` | `toolbar-title-size-medium` |
| `toolbar-title-small` | `toolbar-title-size-small` |
| `toolbar-title-large` | `toolbar-title-size-large` |

<h2 id="version-10x-framework-specific">Framework Specific</h2>

<h4 id="version-10x-angular">Angular</h4>
Expand Down
46 changes: 41 additions & 5 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2811,12 +2811,48 @@ ion-thumbnail,css-prop,--ion-thumbnail-width

ion-title,shadow
ion-title,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
ion-title,prop,hue,"bold" | "subtle" | undefined,undefined,false,false
ion-title,prop,mode,"ios" | "md",undefined,false,false
ion-title,prop,size,"large" | "small" | undefined,undefined,false,false
ion-title,prop,theme,"ios" | "md" | "ionic",undefined,false,false
ion-title,css-prop,--color,ionic
ion-title,css-prop,--color,ios
ion-title,css-prop,--color,md
ion-title,prop,size,"large" | "medium" | "small" | undefined,undefined,false,false
ion-title,css-prop,--ion-title-color
ion-title,css-prop,--ion-title-font-family
ion-title,css-prop,--ion-title-hue-bold-semantic-default-color
ion-title,css-prop,--ion-title-hue-subtle-semantic-default-color
ion-title,css-prop,--ion-title-letter-spacing
ion-title,css-prop,--ion-title-position-bottom
ion-title,css-prop,--ion-title-position-end
ion-title,css-prop,--ion-title-position-start
ion-title,css-prop,--ion-title-position-top
ion-title,css-prop,--ion-title-size-large-align-items
ion-title,css-prop,--ion-title-size-large-font-size
ion-title,css-prop,--ion-title-size-large-font-weight
ion-title,css-prop,--ion-title-size-large-inner-width
ion-title,css-prop,--ion-title-size-large-line-height
ion-title,css-prop,--ion-title-size-large-padding-bottom
ion-title,css-prop,--ion-title-size-large-padding-end
ion-title,css-prop,--ion-title-size-large-padding-start
ion-title,css-prop,--ion-title-size-large-padding-top
ion-title,css-prop,--ion-title-size-large-position
ion-title,css-prop,--ion-title-size-large-text-align
ion-title,css-prop,--ion-title-size-large-transform-origin-x
ion-title,css-prop,--ion-title-size-medium-font-size
ion-title,css-prop,--ion-title-size-medium-font-weight
ion-title,css-prop,--ion-title-size-medium-line-height
ion-title,css-prop,--ion-title-size-medium-padding-bottom
ion-title,css-prop,--ion-title-size-medium-padding-end
ion-title,css-prop,--ion-title-size-medium-padding-start
ion-title,css-prop,--ion-title-size-medium-padding-top
ion-title,css-prop,--ion-title-size-medium-position
ion-title,css-prop,--ion-title-size-medium-text-align
ion-title,css-prop,--ion-title-size-small-font-size
ion-title,css-prop,--ion-title-size-small-font-weight
ion-title,css-prop,--ion-title-size-small-line-height
ion-title,css-prop,--ion-title-size-small-padding-bottom
ion-title,css-prop,--ion-title-size-small-padding-end
ion-title,css-prop,--ion-title-size-small-padding-start
ion-title,css-prop,--ion-title-size-small-padding-top
ion-title,css-prop,--ion-title-size-small-position
ion-title,css-prop,--ion-title-size-small-text-align

ion-toast,shadow
ion-toast,prop,animated,boolean,true,false,false
Expand Down
29 changes: 16 additions & 13 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { SelectPopoverOption } from "./components/select-popover/select-popover-
import { SpinnerSize } from "./components/spinner/spinner.interfaces";
import { TabBarChangedEventDetail, TabBarScrollEffect, TabButtonClickEventDetail, TabButtonLayout } from "./components/tab-bar/tab-bar-interface";
import { TextareaChangeEventDetail, TextareaInputEventDetail } from "./components/textarea/textarea-interface";
import { IonTitleSize } from "./components/title/title.interface";
import { ToastButton, ToastDismissOptions, ToastLayout, ToastPosition, ToastPresentOptions, ToastSwipeGestureDirection } from "./components/toast/toast-interface";
import { ToggleChangeEventDetail } from "./components/toggle/toggle-interface";
export { AccordionGroupChangeEventDetail } from "./components/accordion-group/accordion-group-interface";
Expand Down Expand Up @@ -95,6 +96,7 @@ export { SelectPopoverOption } from "./components/select-popover/select-popover-
export { SpinnerSize } from "./components/spinner/spinner.interfaces";
export { TabBarChangedEventDetail, TabBarScrollEffect, TabButtonClickEventDetail, TabButtonLayout } from "./components/tab-bar/tab-bar-interface";
export { TextareaChangeEventDetail, TextareaInputEventDetail } from "./components/textarea/textarea-interface";
export { IonTitleSize } from "./components/title/title.interface";
export { ToastButton, ToastDismissOptions, ToastLayout, ToastPosition, ToastPresentOptions, ToastSwipeGestureDirection } from "./components/toast/toast-interface";
export { ToggleChangeEventDetail } from "./components/toggle/toggle-interface";
export namespace Components {
Expand Down Expand Up @@ -4172,17 +4174,17 @@ export namespace Components {
*/
"color"?: Color;
/**
* The mode determines the platform behaviors of the component.
* Set to `"bold"` for a title with vibrant, bold colors or to `"subtle"` for a title with muted, subtle colors. Defaults to `"bold"` if both the hue property and theme config are unset.
*/
"mode"?: "ios" | "md";
"hue"?: Hue;
/**
* The size of the toolbar title.
* The mode determines the platform behaviors of the component.
*/
"size"?: 'large' | 'small';
"mode"?: "ios" | "md";
/**
* The theme determines the visual appearance of the component.
* The size of the toolbar title. Defaults to `"medium"` if both the size property and theme config are unset.
*/
"theme"?: "ios" | "md" | "ionic";
"size"?: IonTitleSize;
}
interface IonToast {
/**
Expand Down Expand Up @@ -10136,6 +10138,10 @@ declare namespace LocalJSX {
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
*/
"color"?: Color;
/**
* Set to `"bold"` for a title with vibrant, bold colors or to `"subtle"` for a title 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.
*/
Expand All @@ -10145,13 +10151,9 @@ declare namespace LocalJSX {
*/
"onIonStyle"?: (event: IonTitleCustomEvent<StyleEventDetail>) => void;
/**
* The size of the toolbar title.
* The size of the toolbar title. Defaults to `"medium"` if both the size property and theme config are unset.
*/
"size"?: 'large' | 'small';
/**
* The theme determines the visual appearance of the component.
*/
"theme"?: "ios" | "md" | "ionic";
"size"?: IonTitleSize;
}
interface IonToast {
/**
Expand Down Expand Up @@ -11146,7 +11148,8 @@ declare namespace LocalJSX {
}
interface IonTitleAttributes {
"color": Color;
"size": 'large' | 'small';
"hue": Hue;
"size": IonTitleSize;
}
interface IonToastAttributes {
"overlayIndex": number;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 8 additions & 5 deletions core/src/components/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
handleContentScroll,
handleHeaderFade,
handleToolbarIntersection,
isLargeTitle,
setHeaderActive,
setToolbarBackgroundOpacity,
getRoleType,
Expand Down Expand Up @@ -234,6 +235,10 @@ export class Header implements ComponentInterface {
}
}

private hasLargeTitle() {
return Array.from(this.el.querySelectorAll('ion-title')).some(isLargeTitle);
}

private async setupCondenseHeader(contentEl: HTMLElement | null, pageEl: Element | null): Promise<boolean> {
if (!contentEl || !pageEl) {
printIonContentErrorMsg(this.el);
Expand All @@ -258,8 +263,7 @@ export class Header implements ComponentInterface {
* in the condense header. Without it, there is nothing to
* collapse into the main header toolbar.
*/
const hasLargeTitle = this.el.querySelector('ion-title[size="large"]') !== null;
if (!hasLargeTitle) {
if (!this.hasLargeTitle()) {
printIonWarning(
'[ion-header] - The condense scroll effect requires an <ion-title size="large"> in the condense header.',
this.el
Expand Down Expand Up @@ -329,12 +333,11 @@ export class Header implements ComponentInterface {
// condense/fade via the deprecated `collapse` prop are iOS-only.
const isModeRestricted = scrollEffect === undefined && theme !== 'ios';
const hasHide = effect === 'hide';
const hasCondense =
effect === 'condense' && !isModeRestricted && this.el.querySelector('ion-title[size="large"]') !== null;
const hasCondense = effect === 'condense' && !isModeRestricted && this.hasLargeTitle();
const hasFade = effect === 'fade' && !isModeRestricted;
// The condense header should be hidden when
// - deprecated collapse prop is used on non-iOS (mode restricted), or
// - scrollEffect="condense" is set but no ion-title[size="large"] is present
// - scrollEffect="condense" is set but no large ion-title is present
const isHiddenCondense = effect === 'condense' && !hasCondense;
// banner role must be at top level, so remove role if inside a menu
const roleType = isHiddenCondense ? 'none' : getRoleType(hostContext('ion-menu', this.el));
Expand Down
21 changes: 20 additions & 1 deletion core/src/components/header/header.utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { readTask, writeTask } from '@stencil/core';
import { clamp } from '@utils/helpers';

import { config } from '../../global/config';
import type { IonTitleSize } from '../title/title.interface';

const TRANSITION = 'all 0.2s ease-in-out';
const ROLE_NONE = 'none';
const ROLE_BANNER = 'banner';
Expand Down Expand Up @@ -211,11 +214,27 @@ export const setHeaderActive = (headerIndex: HeaderIndex, active = true) => {
}
};

/**
* Checks the `size` prop, then the attribute (the header can render before
* the title is defined), then the `IonTitle.size` config.
*
* @internal
* @param ionTitleEl - The title to check.
* @returns Whether the title is large.
*/
export const isLargeTitle = (ionTitleEl: HTMLIonTitleElement) => {
const attributeSize = ionTitleEl.getAttribute('size') as IonTitleSize | null;
const configSize = config.getObjectValue('IonTitle.size') as IonTitleSize | undefined;
Comment thread
brandyscarney marked this conversation as resolved.
const size = ionTitleEl.size ?? attributeSize ?? configSize;

return size === 'large';
};

export const scaleLargeTitles = (toolbars: ToolbarIndex[] = [], scale = 1, transition = false) => {
toolbars.forEach((toolbar) => {
const ionTitle = toolbar.ionTitleEl;
const titleDiv = toolbar.innerTitleEl;
if (!ionTitle || ionTitle.size !== 'large') {
if (!ionTitle || !isLargeTitle(ionTitle)) {
return;
}

Expand Down
43 changes: 43 additions & 0 deletions core/src/components/header/test/header.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { newSpecPage } from '@stencil/core/testing';

import { config } from '../../../global/config';
import { ToolbarTitle } from '../../title/title';
import { Header } from '../header';

describe('header: condense', () => {
afterEach(() => {
config.reset({});
});

it('should condense when the title size is large', async () => {
const page = await newSpecPage({
components: [Header, ToolbarTitle],
html: `<ion-header scroll-effect="condense"><ion-title size="large">Title</ion-title></ion-header>`,
});

expect(page.root!.classList.contains('header-collapse-condense')).toBe(true);
});

it('should condense when the config sets a large title size', async () => {
config.reset({
customTheme: { config: { components: { IonTitle: { size: 'large' } } } },
});

const page = await newSpecPage({
components: [Header, ToolbarTitle],
html: `<ion-header scroll-effect="condense"><ion-title>Title</ion-title></ion-header>`,
});

expect(page.root!.classList.contains('header-collapse-condense')).toBe(true);
});

it('should hide the header when the title is not large', async () => {
const page = await newSpecPage({
components: [Header, ToolbarTitle],
html: `<ion-header scroll-effect="condense"><ion-title>Title</ion-title></ion-header>`,
});

expect(page.root!.classList.contains('header-collapse-condense')).toBe(false);
expect(page.root!.classList.contains('header-collapse-condense-hidden')).toBe(true);
});
});
Loading
Loading