|
| 1 | +# CSS Shadow Parts Guidelines |
| 2 | + |
| 3 | +<sub><b>TABLE OF CONTENTS</b></sub> |
| 4 | +- [Definitions](#definitions) |
| 5 | +- [Scope](#scope) |
| 6 | +- [General Guidelines](#general-guidelines) |
| 7 | +- [Standard Parts](#standard-parts) |
| 8 | +- [Specialized Parts](#specialized-parts) |
| 9 | +- [Documentation](#documentation) |
| 10 | + |
| 11 | +## Definitions |
| 12 | + |
| 13 | +**CSS Shadow Parts:** The CSS shadow parts module defines the [::part()](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/::part) pseudo-element that can be set on a [shadow host](https://developer.mozilla.org/en-US/docs/Glossary/Shadow_tree). Using this pseudo-element, you can enable shadow hosts to expose the selected element in the shadow tree to the outside page for styling purposes. [^1] |
| 14 | + |
| 15 | +## Scope |
| 16 | + |
| 17 | +Ionic Framework components that use Shadow DOM expose CSS Shadow Parts to enable custom styling by end users. |
| 18 | + |
| 19 | +This document establishes a standardized naming convention for CSS Shadow Parts in Ionic Framework components. |
| 20 | + |
| 21 | +## General Guidelines |
| 22 | + |
| 23 | +1. **Attempt to use standard parts first**: Use `native`, `wrapper`, `inner`, `container`, and `content` wherever they apply before inventing new names. |
| 24 | +2. **Use semantic, kebab-case names**: Choose descriptive names that communicate the role of the element (for example, `detail-icon`, `supporting-text`). |
| 25 | +3. **Reuse names for the same concept**: Use the same part name across components when the element serves the same role (for example, `backdrop`, `handle`, `label`). |
| 26 | + |
| 27 | +## Standard Parts |
| 28 | + |
| 29 | +**Name parts by what the element does, not where it appears.** Ask what role the element plays: |
| 30 | + |
| 31 | +| Name | Role | |
| 32 | +| --- | --- | |
| 33 | +| `native` | Is it a native HTML element that the user interacts with (e.g. `<button>`, `<a>`, `<input>`, `<textarea>`)? | |
| 34 | +| `wrapper` | Is it a native `<label>` element that wraps the whole form control? | |
| 35 | +| `inner` | Is it the inner layout wrapper around the main content? It may wrap only the default slot (e.g. `ion-list-header`), or a container plus slot(s) (e.g. `ion-item`, `ion-item-divider`, `ion-item-option`) when present. | |
| 36 | +| `container` | Does it wrap the main content itself (default slot or native control)? | |
| 37 | +| `content` | Is it the main user-content area of an overlay or primary content region? | |
| 38 | + |
| 39 | +The following examples show the correct usage for the standard parts. |
| 40 | + |
| 41 | +### `native` |
| 42 | + |
| 43 | +**What it does:** The element the user directly interacts with - the native button, anchor, or form control (e.g. `<button>`, `<a>`, `<textarea>`, `<input>`). |
| 44 | + |
| 45 | +- **Use when**: The element receives click/focus/input from the user. |
| 46 | +- **Examples**: `ion-item`, `ion-button`, `ion-textarea`. |
| 47 | + |
| 48 | +**ion-item** - the interactive element is the `<button>`, `<a>`, or `<div>` (`TagType`): |
| 49 | + |
| 50 | +```tsx |
| 51 | +const TagType = clickable ? (href === undefined ? 'button' : 'a') : ('div' as any); |
| 52 | + |
| 53 | +return ( |
| 54 | + <Host> |
| 55 | + <TagType class="item-native" part="native"> |
| 56 | + <slot name="start"></slot> |
| 57 | + <div class="item-inner" part="inner"> |
| 58 | + <div class="input-wrapper" part="container"> |
| 59 | + <slot></slot> |
| 60 | + </div> |
| 61 | + <slot name="end"></slot> |
| 62 | + </div> |
| 63 | + </TagType> |
| 64 | + </Host> |
| 65 | +); |
| 66 | +``` |
| 67 | + |
| 68 | +**ion-textarea** - the interactive element is the native `<textarea>`: |
| 69 | + |
| 70 | +```tsx |
| 71 | +<div class="native-wrapper" part="container"> |
| 72 | + <textarea class="native-textarea" part="native"> |
| 73 | + {value} |
| 74 | + </textarea> |
| 75 | +</div> |
| 76 | +``` |
| 77 | + |
| 78 | +### `wrapper` |
| 79 | + |
| 80 | +**What it does:** The HTML `<label>` element that wraps the entire form control. Clicking anywhere on it focuses the control. |
| 81 | + |
| 82 | +- **Use when**: The element is the `<label>` that wraps the form control. |
| 83 | +- **Examples**: `ion-select`, `ion-textarea`, `ion-input`, `ion-checkbox`, `ion-toggle`, `ion-radio`, `ion-range`. |
| 84 | + |
| 85 | +**ion-select** - the `<label>` has `part="wrapper"`: |
| 86 | + |
| 87 | +```tsx |
| 88 | +<label class="select-wrapper" part="wrapper"> |
| 89 | + {this.renderLabelContainer()} |
| 90 | + <div class="select-wrapper-inner" part="inner"> |
| 91 | + <slot name="start"></slot> |
| 92 | + <div class="native-wrapper" part="container">...</div> |
| 93 | + <slot name="end"></slot> |
| 94 | + </div> |
| 95 | +</label> |
| 96 | +``` |
| 97 | + |
| 98 | +**ion-textarea** - the `<label>` has `part="wrapper"`: |
| 99 | + |
| 100 | +```tsx |
| 101 | +<label class="textarea-wrapper" part="wrapper"> |
| 102 | + {this.renderLabelContainer()} |
| 103 | + <div class="textarea-wrapper-inner" part="inner"> |
| 104 | + <slot name="start"></slot> |
| 105 | + <div class="native-wrapper" part="container">...</div> |
| 106 | + <slot name="end"></slot> |
| 107 | + </div> |
| 108 | +</label> |
| 109 | +``` |
| 110 | + |
| 111 | +### `inner` |
| 112 | + |
| 113 | +**What it does:** The inner layout wrapper around the main content. It may wrap only the default slot (e.g. `ion-list-header`), or it may wrap a container and the slot(s) (e.g. `start`, `end`) that sit alongside the main content. In `ion-item`, and `ion-item-divider`, the `start` slot is a sibling of this element. In `ion-select`, both `start` and `end` slots are inside this element. |
| 114 | + |
| 115 | +- **Use when**: The element is the inner layout wrapper (with or without a separate container and `start`/`end` slots). |
| 116 | +- **Examples**: `ion-list-header` (`.list-header-inner` wraps only the default slot), `ion-item` (`.item-inner`), `ion-item-divider` (`.item-divider-inner`), `ion-select` (`.select-wrapper-inner`). |
| 117 | + |
| 118 | +**ion-list-header** - `.list-header-inner` wraps only the default slot (no container, no `start`/`end` slots): |
| 119 | + |
| 120 | +```tsx |
| 121 | +<div class="list-header-inner" part="inner"> |
| 122 | + <slot></slot> |
| 123 | +</div> |
| 124 | +``` |
| 125 | + |
| 126 | +**ion-item** - `.item-inner` wraps the container and `end` slot (`start` slot is a sibling): |
| 127 | + |
| 128 | +```tsx |
| 129 | +<slot name="start"></slot> |
| 130 | +<div class="item-inner" part="inner"> |
| 131 | + <div class="input-wrapper" part="container"> |
| 132 | + <slot></slot> |
| 133 | + </div> |
| 134 | + <slot name="end"></slot> |
| 135 | +</div> |
| 136 | +``` |
| 137 | + |
| 138 | +**ion-item-divider** - `.item-divider-inner` wraps the container and `end` slot (`start` slot is a sibling): |
| 139 | + |
| 140 | +```tsx |
| 141 | +<slot name="start"></slot> |
| 142 | +<div class="item-divider-inner" part="inner"> |
| 143 | + <div class="item-divider-wrapper" part="container"> |
| 144 | + <slot></slot> |
| 145 | + </div> |
| 146 | + <slot name="end"></slot> |
| 147 | +</div> |
| 148 | +``` |
| 149 | + |
| 150 | +**ion-select** - `.select-wrapper-inner` arranges `start` slot, container, `end` slot: |
| 151 | + |
| 152 | +```tsx |
| 153 | +<div class="select-wrapper-inner" part="inner"> |
| 154 | + <slot name="start"></slot> |
| 155 | + <div class="native-wrapper" part="container"></div> |
| 156 | + <slot name="end"></slot> |
| 157 | +</div> |
| 158 | +``` |
| 159 | + |
| 160 | +### `container` |
| 161 | + |
| 162 | +**What it does:** Wraps the main content - either the default slot (for item-like components) or the native control and its immediate content (for form controls like select, textarea). |
| 163 | + |
| 164 | +- **Use when**: The element wraps the default slot, or wraps the native control (and any immediate content like listbox or slots inside it). |
| 165 | +- **Don’t use when**: The element is the main content area of an overlay (use `content` instead). |
| 166 | +- **Examples**: `ion-item` (`.input-wrapper` around default slot), `ion-item-divider` (`.item-divider-wrapper`), `ion-select` (`.native-wrapper` around select text + listbox), `ion-textarea` (`.native-wrapper` around `<textarea>`). |
| 167 | + |
| 168 | +From the examples above: |
| 169 | + |
| 170 | +**ion-item** - `.input-wrapper` wraps the default `<slot>`: |
| 171 | + |
| 172 | +```tsx |
| 173 | +<slot name="start"></slot> |
| 174 | +<div class="item-inner" part="inner"> |
| 175 | + <div class="input-wrapper" part="container"> |
| 176 | + <slot></slot> |
| 177 | + </div> |
| 178 | + <slot name="end"></slot> |
| 179 | +</div> |
| 180 | +``` |
| 181 | + |
| 182 | +**ion-select** - `.native-wrapper` wraps the select text and listbox: |
| 183 | + |
| 184 | +```tsx |
| 185 | +<div class="select-wrapper-inner" part="inner"> |
| 186 | + <slot name="start"></slot> |
| 187 | + <div class="native-wrapper" part="container"> |
| 188 | + {this.renderSelectText()} |
| 189 | + {this.renderListbox()} |
| 190 | + </div> |
| 191 | + <slot name="end"></slot> |
| 192 | +</div> |
| 193 | +``` |
| 194 | + |
| 195 | +**ion-textarea** - `.native-wrapper` wraps the `<textarea>`: |
| 196 | + |
| 197 | +```tsx |
| 198 | +<label class="textarea-wrapper" part="wrapper"> |
| 199 | + {this.renderLabelContainer()} |
| 200 | + <div class="textarea-wrapper-inner" part="inner"> |
| 201 | + <slot name="start"></slot> |
| 202 | + <div class="native-wrapper" part="container"> |
| 203 | + <textarea class="native-textarea" part="native"> |
| 204 | + {value} |
| 205 | + </textarea> |
| 206 | + </div> |
| 207 | + <slot name="end"></slot> |
| 208 | + </div> |
| 209 | +</label> |
| 210 | +``` |
| 211 | + |
| 212 | +### `content` |
| 213 | + |
| 214 | +**What it does:** The main user-content area of an overlay or the primary content region (e.g. modal body, toolbar’s main slot). |
| 215 | + |
| 216 | +- **Use when**: The element is the primary content area where users see the main content (overlay body, or primary slot inside something like a toolbar). |
| 217 | +- **Examples**: `ion-modal`, `ion-popover`, `ion-accordion`, `ion-toolbar` (the div that wraps the default slot inside the toolbar container). |
| 218 | + |
| 219 | +**ion-modal** - `content` wraps the default `<slot>` which is the primary content: |
| 220 | + |
| 221 | +```tsx |
| 222 | +<div class="modal-content" part="content"> |
| 223 | + <slot></slot> |
| 224 | +</div> |
| 225 | +``` |
| 226 | + |
| 227 | +**ion-toolbar** - `content` wraps the default `<slot>` which is the primary content: |
| 228 | + |
| 229 | +```tsx |
| 230 | +<div class="toolbar-container" part="container"> |
| 231 | + <slot name="start"></slot> |
| 232 | + <slot name="secondary"></slot> |
| 233 | + <div class="toolbar-content" part="content"> |
| 234 | + <slot></slot> |
| 235 | + </div> |
| 236 | + <slot name="primary"></slot> |
| 237 | + <slot name="end"></slot> |
| 238 | +</div> |
| 239 | +``` |
| 240 | + |
| 241 | +## Specialized Parts |
| 242 | + |
| 243 | +Components may also expose specialized parts for specific elements. The following parts are reused across multiple components: |
| 244 | + |
| 245 | +| Name | Description | |
| 246 | +| --- | --- | |
| 247 | +| `background` | Background elements (e.g., `ion-content`, `ion-toolbar`) | |
| 248 | +| `backdrop` | Backdrop elements. **Must only be used on `<ion-backdrop>` components.** (e.g., `ion-modal`, `ion-popover`, `ion-menu`) | |
| 249 | +| `label` | Label text elements - not the HTML `<label>` (see standard part `wrapper`) | |
| 250 | +| `supporting-text` | Supporting text elements | |
| 251 | +| `helper-text` | Helper text elements | |
| 252 | +| `error-text` | Error text elements | |
| 253 | +| `icon` | Icon elements. **Must only be used on `<ion-icon>` components.** Use specific names like `detail-icon`, `close-icon` when the icon serves a distinct purpose (e.g., `ion-item` uses `detail-icon`, `ion-fab-button` uses `close-icon`) | |
| 254 | +| `handle` | Handle elements (e.g., `ion-modal`, `ion-toggle`) | |
| 255 | +| `track` | Track elements (e.g., `ion-toggle`, `ion-progress-bar`) | |
| 256 | +| `mark` | Checkmark or indicator marks (e.g., `ion-checkbox`, `ion-radio`) | |
| 257 | + |
| 258 | +**When to create new specialized parts:** |
| 259 | +- Use standard parts (`native`, `wrapper`, `inner`, `container`, `content`) when they apply |
| 260 | +- Reuse existing specialized parts (listed above) when they match the element's role |
| 261 | +- Create component-specific specialized parts for elements that don't fit standard patterns or existing specialized parts |
| 262 | +- Use descriptive, semantic names (e.g., `header`, `text`, `arrow`, `scroll` for component-specific elements) |
| 263 | + |
| 264 | +## Documentation |
| 265 | + |
| 266 | +Shadow parts must be documented in the component's JSDoc comments using the `@part` tag. The following example demonstrates the proper documentation format: |
| 267 | + |
| 268 | +```tsx |
| 269 | +/** |
| 270 | + * @part native - The native HTML button, anchor or div element that wraps all child elements. |
| 271 | + * @part inner - The inner wrapper element that arranges the item content. |
| 272 | + * @part container - The wrapper element that contains the default slot. |
| 273 | + * @part detail-icon - The chevron icon for the item. Only applies when `detail="true"`. |
| 274 | + */ |
| 275 | +``` |
| 276 | + |
| 277 | +[^1]: MDN Documentation - CSS shadow parts, https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Shadow_parts |
0 commit comments