Retheme Ionic Components with CSS Custom Properties and Shadow Parts
Restyle Ionic components without brittle deep selectors. Learn how to use documented CSS custom properties and ::part() to override ion-button styles safely across browser and device WebViews.
10 Jun 2026, 02:42 UTC

Use the Documented Contract, Not Internal Markup
If you have tried to restyle an Ionic component with a selector like ion-button .button-native, you already know the problem: it works until the next Ionic update, then the button silently reverts to the default look. Ionic components are Web Components with shadow DOM, so their internal markup is intentionally encapsulated. The supported way to change their appearance is a two-layer contract: set the component's documented CSS custom properties for themable values, and use ::part() to reach only the elements the component explicitly exposes.
How the Two Layers Work
Ionic components consume CSS custom properties (variables) through var() declarations in their own styles. When you redefine a variable such as --background on ion-button, the component's internal rules pick up the new value. This layer is safe because it only changes declared design tokens.
The second layer is the shadow-parts API. A component can expose internal elements with a part attribute. The ::part(name) pseudo-element lets you style exactly those exposed elements from the outside. If a part is not exposed, it is unreachable by design.
Worked Example: ion-button
Add the following to your global stylesheet (typically src/global.scss in an Ionic starter, or the global CSS file your project uses):
ion-button {
--background: #3880ff;
--background-hover: #3171e0;
--border-radius: 999px;
--color: #ffffff;
}
ion-button::part(native) {
letter-spacing: 0.04em;
text-transform: uppercase;
}
The variable block changes the host-level design tokens that ion-button already consumes. The ::part(native) rule styles the inner button element that Ionic exposes as native. Because these are documented contracts, they remain stable across minor Ionic releases.
Retheming the Palette
To rebrand the default color set, redefine the full token group on :root. Setting only --ion-color-primary leaves shade, tint, and contrast variables stale, because those are separate declarations, not computed values:
:root {
--ion-color-primary: #3880ff;
--ion-color-primary-rgb: 56,128,255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255,255,255;
--ion-color-primary-shade: #3171e0;
--ion-color-primary-tint: #4c8dff;
}
Components that reference var(--ion-color-primary) or its companion variables will update automatically.
Scoping with Inheritance
Custom properties inherit through the DOM. To retint buttons only inside a specific container, scope the variable to that subtree:
.checkout ion-button {
--background: var(--ion-color-success);
}
This changes the token for buttons inside .checkout without affecting the rest of the app. Ionic's own rules use host-scoped selectors, so a plain global selector usually wins, though attribute or host-context cases may need equal-or-higher specificity.
Limits and Common Mistakes
- ::part() reaches only exposed parts. If Ionic does not expose an internal element with a
partattribute, you cannot style it from outside the shadow root. - Variables only affect consumed properties. A custom property changes a value only if the component's internal CSS uses
var()for that property. Arbitrary properties on the host still need::partor a wrapper element. - Deep selectors are brittle. Selectors like
ion-button .button-native,::ng-deep, or::v-deeppierce the shadow DOM through framework-specific hacks and break when internal markup changes. - !important is a warning sign. Using
!importantor ever-more-specific selectors usually means you are fighting the wrong layer. Prefer variables and parts so activation states such as--background-hoverand--background-activatedkeep working. - Dark mode is version-sensitive. Recent Ionic versions ship dark palettes activated by a class on the root element or a
prefers-color-schememedia query. The exact selector and default behavior vary by version, so confirm the mechanism for your installed version before relying on automatic switching. - Old WebViews may lack ::part support. While broad in modern Chrome and WebViews,
::part()can be missing on very old Android System WebViews.
Verifying the Result
Confirm the theming works in the environments you support:
- Browser check. Add the rules to your global stylesheet, run
ionic servefrom your project root, and inspect the button in browser DevTools. Confirm the computed--backgroundand--border-radiuschange and that the::part(native)rules appear inside the shadow root. - Dark mode check. Toggle the dark-mode mechanism your version documents (add or remove the documented class on the
htmlelement, or emulateprefers-color-schemein DevTools) and confirm tokens swap without a visible flash on reload. - Device check. Build and run on a device or emulator using the Capacitor CLI from your project root to confirm variables and
::partrender correctly in the native WebView, especially on the oldest Android version you support. - Upgrade check. In a scratch branch, bump the Ionic package to the next minor version and re-run the same checks to detect any reliance on undocumented internals or changed defaults.
If a style does not apply, check whether the component actually consumes that variable or exposes that part. If it does not, the change belongs on a wrapper element or a different layer entirely.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.