Official support for CSS Shadow Parts in Ionic Framework has landed! Shadow parts make it easier to theme and customize Ionic Framework components.
They replace the need for a large amount of CSS variables by adding the ability to target any CSS property on an element inside of a shadow tree.
Why Shadow Parts? 🤔
To better understand what shadow parts are, we need to understand why we need them. The release of Ionic Framework 4 at the beginning of last year migrated all of our components to become a distributed set of Web Components. Web Components follow the Shadow DOM specification in order to encapsulate styles and markup.
Shadow DOM is useful for preventing styles from leaking out of components and unintentionally applying to other elements. For example, we assign a .button
class to our ion-button
component. If an Ionic Framework user were to set the class .button
on one of their own elements, it would inherit the Ionic button styles in past versions of the framework. Since ion-button
is now a Shadow Web Component, this is no longer a problem.
However, due to this encapsulation, styles aren’t able to bleed into inner elements of a Shadow component either. This means that if a Shadow component renders elements inside of its shadow tree, a user isn’t able to target the inner element with their CSS. Using the ion-select
component as an example, it renders the following markup:
<ion-select>
#shadow-root
<div class=”select-text select-placeholder”></div>
<div class=”select-icon”></div>
</ion-select>
The placeholder text and icon elements are inside of the #shadow-root
, which means the following CSS will NOT work to style the placeholder:
/* Does NOT work */
ion-select .select-placeholder {
color: blue;
}
CSS custom properties (variables) have been useful in solving this problem. We’ve created CSS variables to target many of the CSS properties on these inner elements. In this case, the placeholder color could be changed using:
/* DOES work */
ion-select {
--placeholder-color: blue;
}
The problem with this approach is that if we were to declare every possible property as a custom property it would cause bloat in the CSS files and slow performance. Instead, we have relied on style inheritance where possible and implemented custom properties based on commonly customized components and user requests. This has led us to discover that there are still many challenges with customizing our components.
This is where CSS Shadow Parts come in!
Shadow Parts Explained đź‘»
Shadow Parts give users the ability to target an element inside of a shadow tree from outside of it. In order for the element to be targeted, it has to be exposed by the library author, in this case, Ionic Framework.
How ::part works
By setting the part on an element inside of a shadow tree, the element will be exposed to the user for customization. The following change is made inside of the Ionic Framework and requires no action from an end user.
<ion-select>
#shadow-root
<div part=”placeholder” class=”select-text select-placeholder”></div>
<div part=”icon” class=”select-icon”></div>
</ion-select>
After adding an ion-select
in your app, you can customize the placeholder's color using the placeholder
part:
ion-select::part(placeholder) {
color: blue;
opacity: 1;
}
In addition to being able to target the part, you can also target pseudo-elements without them being explicitly exposed:
ion-select::part(placeholder)::first-letter {
font-size: 22px;
font-weight: 500;
}
Vendor prefixed pseudo-elements are not supported at this time. See this issue on GitHub for more information.
Most pseudo-classes are also supported, however, structural pseudo-classes can not be used with parts.
As you can see, shadow parts are very powerful because they allow you to edit any property you’d like on an element and/or pseudo-element, without each property being declared as a CSS variable.
Elements inside of a part
It’s important to note that you cannot target elements inside of a part. The following example does not work:
/* Does NOT work */
ion-select::part(icon) .select-icon-inner { ... }
Styling multiple parts
Components can have multiple parts that are added to a single element. For example, take a component with the following parts:
<my-example>
#shadow-root
<div part=”tab active”></div>
<div part=”tab”></div>
</my-example>
If you wanted to style the tab that is active, while leaving the inactive tab alone, you would use the following:
my-example::part(tab active) {
color: green;
}
Ionic Framework Parts đź’™
Like I said previously, this is very powerful and a HUGE win for customization. Being able to customize any property on an element inside of a shadow root (as long as it has been exposed as a part) takes away the need for CSS variables for every property possible.
With all of that said, let’s go over which parts have been added in Ionic Framework. Our main goal with this initial rollout was to make UI customization easier while discouraging users from modifying elements that are used structurally. As we learned when adding CSS custom properties, we will likely find that more parts are needed to meet more advanced customization needs. This is not the end-all API for Shadow Parts.
Exposed Parts
- Back Button (
native
,icon
,text
) — Back Button documentation - Button (
native
) — Button documentation - Card (
native
) — Card documentation - Checkbox (
container
,mark
) — Checkbox documentation - Content (
background
,scroll
) — Content documentation - Datetime (
text
,placeholder
) — Datetime documentation - Fab Button (
close-icon
,native
) — Fab Button documentation - Img (
image
) — Img documentation - Item (
native
,detail-icon
) — Item documentation - Item Option (
native
) — Item Option documentation - Menu (
container
,backdrop
) — Menu documentation - Menu Button (
native
,icon
) — Menu Button documentation - Radio (
container
,mark
) — Radio documentation - Range (
tick
,tick-active
,pin
,knob
,bar
,bar-active
) — Range documentation - Reorder (
icon
) — Reorder documentation - Select (
placeholder
,text
,icon
) — Select documentation - Segment Button (
native
,indicator
,indicator-background
) — Segment Button documentation - Tab Button (
native
) — Tab Button documentation - Toast (
button
,container
,header
,message
) — Toast documentation - Toggle (
track
,handle
) — Toggle documentation
You may notice a lot of components missing from this list. If a component is missing it is likely for one of these reasons:
- It isn't a Shadow component, thus no parts can be added. If it is a Scoped (such as overlay and input components) or Light DOM component, the child elements can be targeted directly.
- There are no children elements. For example,
ion-card-header
is a Shadow component, but all styles are applied to the host element. Since it has no child elements, there's no need for parts. - The children elements are mainly structural. In certain components, including
ion-title
, the child element is a structural element used to position the inner elements. - We didn't see an immediate need for parts on the component. If you believe there is one missing, please create a new GitHub issue with as much information as possible so we can make sure the level of customization needed can be achieved.
Browser Support đź’»
CSS Shadow Part support has recently landed in all major browsers. However, you can see that some of the browsers still have some usage on unsupported versions. This is something to keep in mind when implementing parts in an app.
Try it out đź‘Ť
To get started using CSS Shadow Parts, install the latest release of Ionic Framework:
# for an angular app
npm i @ionic/angular@latest --save
# for a react app
npm i @ionic/react@latest --save
npm i @ionic/react-router@latest --save
npm i ionicons@latest --save
# for a stencil / vanilla JS app
npm i @ionic/core@latest --save
A minimum version of
5.2.0
is required for all of the parts mentioned in this blog.
Future Work đź”®
Due to the usage of some of the older browser versions without support, we are continuing to support the large amount of CSS custom properties in our components. While we believe that Shadow parts are the way of the future, we will continue to provide and add CSS properties as needed.
If you believe that a CSS variable or part should be exposed that isn’t, please create a new GitHub issue letting us know.