feat(card): convert card-content to shadow DOM (#30759)

Issue number: internal

---------

## What is the current behavior?
Card content has no encapsulation.

## What is the new behavior?
Converted `ion-card-content` to Shadow DOM which improves consistency among components & CSP compatibility.

## Does this introduce a breaking change?

- [x] Yes
- [ ] No

BREAKING CHANGE:

The `ion-card-content` component has been updated to Shadow DOM. With this update, all card-related components now use Shadow DOM for style encapsulation. There should not be any breaking changes related to targeting inner elements since `ion-card-content` does not have any internal elements of its own. However, some user styles may break due to the removal of the `card-content-{mode}` class or changes in selector specificity.

The default styles for heading elements inside `ion-card-content` have been removed. If you need custom styling for headings, you can add your own CSS targeting these elements.

For example:

```css
ion-card-content h1 {
  margin-top: 0;
  margin-bottom: 2px;

  font-size: 1.5rem;
}

ion-card-content h2 {
  margin-top: 2px;
  margin-bottom: 2px;

  font-size: 1rem;
}

ion-card-content h3,
ion-card-content h4,
ion-card-content h5,
ion-card-content h6 {
  margin-top: 2px;
  margin-bottom: 2px;

  font-size: 0.875rem;
}
```

---------

Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
This commit is contained in:
Brandy Smith
2026-02-05 12:17:42 -05:00
committed by GitHub
parent 17b8468b04
commit 4aaece0bec
17 changed files with 75 additions and 106 deletions

View File

@@ -28,7 +28,35 @@ This is a comprehensive list of the breaking changes introduced in the major ver
<h4 id="version-9x-card">Card</h4>
- The `border-radius` of the `ios` and `md` card now defaults to `14px` and `12px` instead of `8px` and `4px`, respectively, in accordance with the iOS and Material Design 3 guidelines. To revert to the previous appearance, set the `shape` to `"soft"`, or override the `--border-radius` CSS variable to specify a different value.
- **ion-card**: The `border-radius` of the `ios` and `md` card now defaults to `14px` and `12px` instead of `8px` and `4px`, respectively, in accordance with the iOS and Material Design 3 guidelines. To revert to the previous appearance, set the `shape` to `"soft"`, or override the `--border-radius` CSS variable to specify a different value.
- **ion-card-content**: The `ion-card-content` component has been updated to Shadow DOM. With this update, all card-related components now use Shadow DOM for style encapsulation. The default styles for heading elements inside `ion-card-content` have been removed. If you need custom styling for headings, you can add your own CSS targeting these elements. For example:
```css
ion-card-content h1 {
margin-top: 0;
margin-bottom: 2px;
font-size: 1.5rem;
}
ion-card-content h2 {
margin-top: 2px;
margin-bottom: 2px;
font-size: 1rem;
}
ion-card-content h3,
ion-card-content h4,
ion-card-content h5,
ion-card-content h6 {
margin-top: 2px;
margin-bottom: 2px;
font-size: 0.875rem;
}
```
<h4 id="version-9x-chip">Chip</h4>
@@ -47,6 +75,7 @@ Additionally, the `radio-group-wrapper` div element has been removed, causing sl
<h5>Example 1: Swap two columns</h5>
**Version up to 8.x**
```html
<ion-grid>
<ion-row>
@@ -56,7 +85,9 @@ Additionally, the `radio-group-wrapper` div element has been removed, causing sl
</ion-row>
</ion-grid>
```
**Version 9.x+**
```html
<ion-grid>
<ion-row>
@@ -68,9 +99,11 @@ Additionally, the `radio-group-wrapper` div element has been removed, causing sl
```
<h5>Example 2: Reorder columns with specific sizes</h5>
To reorder two columns where column 1 has `size="9" push="3"` and column 2 has `size="3" pull="9"`:
**Version up to 8.x**
```html
<ion-grid>
<ion-row>
@@ -79,7 +112,9 @@ To reorder two columns where column 1 has `size="9" push="3"` and column 2 has `
</ion-row>
</ion-grid>
```
**Version 9.x+**
```html
<ion-grid>
<ion-row>
@@ -88,7 +123,9 @@ To reorder two columns where column 1 has `size="9" push="3"` and column 2 has `
</ion-row>
</ion-grid>
```
<h5>Example 3: Push</h5>
```html
<ion-grid>
<ion-row>
@@ -101,7 +138,9 @@ To reorder two columns where column 1 has `size="9" push="3"` and column 2 has `
</ion-row>
</ion-grid>
```
**Version 9.x+**
```html
<ion-grid>
<ion-row>
@@ -116,6 +155,7 @@ To reorder two columns where column 1 has `size="9" push="3"` and column 2 has `
```
<h5>Example 4: Push and Pull</h5>
```html
<ion-grid>
<ion-row>
@@ -128,7 +168,9 @@ To reorder two columns where column 1 has `size="9" push="3"` and column 2 has `
</ion-row>
</ion-grid>
```
**Version 9.x+**
```html
<ion-grid>
<ion-row>
@@ -140,4 +182,4 @@ To reorder two columns where column 1 has `size="9" push="3"` and column 2 has `
</ion-col>
</ion-row>
</ion-grid>
```
```

View File

@@ -504,7 +504,7 @@ ion-card,css-prop,--color,ios
ion-card,css-prop,--color,md
ion-card,part,native
ion-card-content,none
ion-card-content,shadow
ion-card-content,prop,mode,"ios" | "md",undefined,false,false
ion-card-content,prop,theme,"ios" | "md" | "ionic",undefined,false,false

View File

@@ -1,6 +1,6 @@
// Card Content
// --------------------------------------------------
ion-card-content {
:host {
position: relative;
}

View File

@@ -4,7 +4,7 @@
// Ionic Card Content
// --------------------------------------------------
.card-content-ionic {
:host {
@include globals.padding(globals.$ion-space-400);
@include globals.typography(globals.$ion-body-md-regular);
@@ -13,12 +13,8 @@
flex-direction: column;
gap: globals.$ion-space-400;
img {
@include globals.margin(globals.$ion-space-200, 0, globals.$ion-space-200, 0);
}
}
ion-card-header + .card-content-ionic {
padding-top: 0;
::slotted(img) {
@include globals.margin(globals.$ion-space-200, 0, globals.$ion-space-200, 0);
}

View File

@@ -4,44 +4,16 @@
// iOS Card Header
// --------------------------------------------------
.card-content-ios {
:host {
@include padding($card-ios-padding-top, $card-ios-padding-end, $card-ios-padding-bottom, $card-ios-padding-start);
font-size: $card-ios-font-size;
line-height: 1.4;
h1 {
@include margin(0, 0, 2px);
font-size: dynamic-font(24px);
font-weight: normal;
}
h2 {
@include margin(2px, 0);
font-size: dynamic-font(16px);
font-weight: normal;
}
h3,
h4,
h5,
h6 {
@include margin(2px, 0);
font-size: dynamic-font(14px);
font-weight: normal;
}
p {
@include margin(0, 0, 2px);
font-size: dynamic-font(14px);
}
}
ion-card-header + .card-content-ios {
padding-top: 0;
::slotted(p) {
@include margin(0, 0, 2px);
font-size: dynamic-font(14px);
}

View File

@@ -4,47 +4,19 @@
// Material Design Card Content
// --------------------------------------------------
.card-content-md {
:host {
@include padding($card-md-padding-top, $card-md-padding-end, $card-md-padding-bottom, $card-md-padding-start);
font-size: $card-md-font-size;
line-height: $card-md-line-height;
h1 {
@include margin(0, 0, 2px);
font-size: dynamic-font(24px);
font-weight: normal;
}
h2 {
@include margin(2px, 0);
font-size: dynamic-font(16px);
font-weight: normal;
}
h3,
h4,
h5,
h6 {
@include margin(2px, 0);
font-size: dynamic-font(14px);
font-weight: normal;
}
p {
@include margin(0, 0, 2px);
font-size: dynamic-font(14px);
font-weight: normal;
line-height: 1.5;
}
}
ion-card-header + .card-content-md {
padding-top: 0;
::slotted(p) {
@include margin(0, 0, 2px);
font-size: dynamic-font(14px);
font-weight: normal;
line-height: 1.5;
}

View File

@@ -4,6 +4,6 @@
// Card Content
// --------------------------------------------------
ion-card-content {
:host {
display: block;
}

View File

@@ -14,6 +14,7 @@ import { getIonTheme } from '../../global/ionic-global';
md: 'card-content.md.scss',
ionic: 'card-content.ionic.scss',
},
shadow: true,
})
export class CardContent implements ComponentInterface {
render() {
@@ -22,11 +23,10 @@ export class CardContent implements ComponentInterface {
<Host
class={{
[theme]: true,
// Used internally for styling
[`card-content-${theme}`]: true,
}}
></Host>
>
<slot></slot>
</Host>
);
}
}

View File

@@ -91,27 +91,6 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
const card = page.locator('ion-card');
await expect(card).toHaveScreenshot(screenshot(`card-color`));
});
test('headings should have correct size in card', async ({ page }) => {
await page.setContent(
`
<ion-card>
<ion-card-content>
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>
<p>Paragraph</p>
</ion-card-content>
</ion-card>
`,
config
);
const card = page.locator('ion-card');
await expect(card).toHaveScreenshot(screenshot(`card-headings`));
});
test('should render even without header or content elements', async ({ page }) => {
await page.setContent(
`

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

View File

@@ -244,6 +244,10 @@ ion-card-header.ion-color .ion-inherit-color {
color: inherit;
}
ion-card-header + ion-card-content {
padding-top: 0;
}
// Menu Styles
// --------------------------------------------------

View File

@@ -238,6 +238,10 @@ ion-card-header.ion-color .ion-inherit-color {
color: inherit;
}
ion-card-header + ion-card-content {
padding-top: 0;
}
// Menu Styles
// --------------------------------------------------