11# Web Component Styling Cookbook — Common Recipes
22
3- Concrete styling recipes for common patterns. Use these as-is or adapt them. Every recipe follows Shadow DOM rules (CSS custom properties + ::part selectors only).
3+ Concrete styling recipes for common patterns. Every recipe follows Shadow DOM rules (CSS custom properties + ::part selectors only). Examples use generic ` my-* ` prefixes — replace with your library's actual tag names and tokens.
4+
5+ ## Before Writing Any CSS
6+
7+ ** Always call ` styling_preflight ` first** to discover the component's actual API:
8+
9+ ```
10+ styling_preflight({
11+ cssText: "my-button::part(base) { border-radius: 8px; }",
12+ tagName: "my-button"
13+ })
14+ ```
15+
16+ This returns: available parts, tokens, slots, validation issues, and a correct CSS snippet. Never guess part or token names — verify them against the CEM.
417
518## Recipe 1: Customize Button Colors
619
720``` css
8- /* Override the button's color scheme */
9- sl -button {
10- --sl-color-primary-600 : #2563eb ;
11- --sl -color-primary-500 : #3b82f6 ;
12- --sl-color-primary-700 : #1d4ed8 ;
21+ /* Override the button's color tokens on the host */
22+ my -button {
23+ --button-bg : #2563eb ;
24+ --button -color : #ffffff ;
25+ --button-hover-bg : #1d4ed8 ;
1326}
1427
1528/* Style the button's internal structure via ::part */
16- sl -button ::part(base ) {
29+ my -button ::part(base ) {
1730 border-radius : 9999px ; /* pill shape */
1831 text-transform : uppercase ;
1932 letter-spacing : 0.05em ;
2033}
2134
2235/* Hover state on the part */
23- sl -button ::part(base ):hover {
36+ my -button ::part(base ):hover {
2437 box-shadow : 0 4px 12px rgba (37 , 99 , 235 , 0.4 );
2538}
2639```
2740
2841## Recipe 2: Card with Custom Header Styling
2942
3043``` css
31- /* Card container */
32- sl -card {
33- --sl- card-border-color : transparent ;
34- --sl- card-border-radius : 12px ;
44+ /* Card container tokens */
45+ my -card {
46+ --card-border-color : transparent ;
47+ --card-border-radius : 12px ;
3548}
3649
3750/* Card's internal regions via ::part */
38- sl -card ::part(base ) {
51+ my -card ::part(base ) {
3952 box-shadow : 0 1px 3px rgba (0 , 0 , 0 , 0.1 );
4053}
4154
42- sl -card ::part(header ) {
55+ my -card ::part(header ) {
4356 background : var (--surface-secondary , #f8fafc );
4457 border-bottom : 1px solid var (--border-default , #e2e8f0 );
4558 padding : 1rem 1.5rem ;
4659}
4760
48- sl -card ::part(body ) {
61+ my -card ::part(body ) {
4962 padding : 1.5rem ;
5063}
5164
52- /* Slotted content ( light DOM — normal selectors work) */
53- sl -card img [slot = " image" ] {
65+ /* Slotted content — light DOM selectors work on slotted elements */
66+ my -card > img [slot = " image" ] {
5467 width : 100% ;
5568 height : 200px ;
5669 object-fit : cover ;
@@ -61,67 +74,67 @@ sl-card img[slot="image"] {
6174## Recipe 3: Form Input with Validation States
6275
6376``` css
64- /* Base input styling via tokens */
65- sl -input {
66- --sl- input-border-radius : 8px ;
67- --sl- input-font-size-medium : 16px ;
77+ /* Base input tokens */
78+ my -input {
79+ --input-border-radius : 8px ;
80+ --input-font-size : 16px ;
6881}
6982
70- /* Error state — scope tokens to the invalid attribute */
71- sl -input [data-user-invalid ] {
72- --sl- input-border-color : var (--sl- color-danger-600 );
73- --sl- input-focus-ring-color : var (--sl- color-danger-300 );
83+ /* Error state — scope tokens to a state attribute */
84+ my -input [data-user-invalid ] {
85+ --input-border-color : var (--color-danger , #dc2626 );
86+ --input-focus-ring-color : var (--color-danger-light , #fca5a5 );
7487}
7588
7689/* Success state */
77- sl -input [data-user-valid ] {
78- --sl- input-border-color : var (--sl- color-success-600 );
90+ my -input [data-user-valid ] {
91+ --input-border-color : var (--color-success , #16a34a );
7992}
8093
81- /* Help text styling (if exposed as a slot — it's light DOM) */
82- sl -input .help-text {
94+ /* Slotted help text ( light DOM) */
95+ my -input > .help-text {
8396 font-size : 0.875rem ;
84- color : var (--sl-color-neutral-500 );
97+ color : var (--text-secondary , #6b7280 );
8598}
8699```
87100
88101## Recipe 4: Dialog/Modal Overlay
89102
90103``` css
91- /* Dialog overlay and panel */
92- sl -dialog {
93- --sl-panel-background-color : white ;
94- --sl-panel -border-radius : 16px ;
104+ /* Dialog tokens */
105+ my -dialog {
106+ --dialog-width : 560 px ;
107+ --dialog -border-radius : 16px ;
95108}
96109
97- sl -dialog ::part(overlay) {
110+ my -dialog ::part(overlay) {
98111 background : rgba (0 , 0 , 0 , 0.5 );
99112 backdrop-filter : blur (4px );
100113}
101114
102- sl -dialog ::part(panel) {
103- max-width : 560px ;
115+ my -dialog ::part(panel) {
116+ max-width : var ( --dialog-width , 560px ) ;
104117 box-shadow : 0 20px 60px rgba (0 , 0 , 0 , 0.3 );
105118}
106119
107- sl -dialog ::part(header ) {
120+ my -dialog ::part(header ) {
108121 padding : 1.5rem ;
109- border-bottom : 1px solid var (--sl-color-neutral-200 );
122+ border-bottom : 1px solid var (--border-default , #e2e8f0 );
110123}
111124
112- sl -dialog ::part(body ) {
125+ my -dialog ::part(body ) {
113126 padding : 1.5rem ;
114127}
115128
116- sl -dialog ::part(footer ) {
129+ my -dialog ::part(footer ) {
117130 padding : 1rem 1.5rem ;
118131 display : flex ;
119132 justify-content : flex-end ;
120133 gap : 0.75rem ;
121134}
122135```
123136
124- ## Recipe 5: Dark Mode Toggle
137+ ## Recipe 5: Dark Mode with Token Switching
125138
126139``` css
127140/* Define both themes via CSS custom properties */
@@ -132,11 +145,6 @@ sl-dialog::part(footer) {
132145 --text-primary : #0f172a ;
133146 --text-secondary : #475569 ;
134147 --border-default : #e2e8f0 ;
135-
136- /* Override component library tokens */
137- --sl-color-neutral-0 : #ffffff ;
138- --sl-color-neutral-900 : #0f172a ;
139- --sl-color-neutral-1000 : #000000 ;
140148}
141149
142150.theme-dark {
@@ -145,11 +153,6 @@ sl-dialog::part(footer) {
145153 --text-primary : #f1f5f9 ;
146154 --text-secondary : #94a3b8 ;
147155 --border-default : #334155 ;
148-
149- /* Override component library tokens for dark */
150- --sl-color-neutral-0 : #0f172a ;
151- --sl-color-neutral-900 : #f1f5f9 ;
152- --sl-color-neutral-1000 : #ffffff ;
153156}
154157
155158/* Respect system preference */
@@ -160,60 +163,61 @@ sl-dialog::part(footer) {
160163 --text-primary : #f1f5f9 ;
161164 --text-secondary : #94a3b8 ;
162165 --border-default : #334155 ;
163-
164- --sl-color-neutral-0 : #0f172a ;
165- --sl-color-neutral-900 : #f1f5f9 ;
166- --sl-color-neutral-1000 : #ffffff ;
167166 }
168167}
168+
169+ /* Components consume YOUR tokens, not hardcoded colors */
170+ my-card {
171+ --card-bg : var (--surface-primary );
172+ --card-text : var (--text-primary );
173+ --card-border : var (--border-default );
174+ }
169175```
170176
171- ## Recipe 6: Navigation/Tab Styling
177+ ## Recipe 6: Slot Styling (Light DOM CSS)
172178
173179``` css
174- /* Tab group container */
175- sl-tab-group {
176- --sl-spacing-small : 0.25rem ;
180+ /* Default slot — style all direct children */
181+ my-card > * {
182+ font-family : inherit ;
183+ line-height : 1.5 ;
177184}
178185
179- /* Individual tabs via ::part */
180- sl-tab ::part( base ) {
181- padding : 0.75 rem 1.25rem ;
182- font-weight : 500 ;
183- border-radius : 8 px 8 px 0 0 ;
186+ /* Named slot — target by slot attribute */
187+ my-card > [ slot = " header " ] {
188+ font-size : 1.25rem ;
189+ font-weight : 600 ;
190+ margin-bottom : 0.5 rem ;
184191}
185192
186- sl-tab ::part(base ):hover {
187- background : var (--surface-secondary , #f1f5f9 );
188- }
189-
190- /* Active tab indicator */
191- sl-tab-group ::part(active-tab-indicator ) {
192- background : var (--sl-color-primary-600 );
193- height : 3px ;
194- border-radius : 3px 3px 0 0 ;
193+ my-card > [slot = " footer" ] {
194+ display : flex ;
195+ gap : 0.5rem ;
196+ justify-content : flex-end ;
197+ padding-top : 1rem ;
198+ border-top : 1px solid var (--border-default , #e2e8f0 );
195199}
196200
197- /* Tab panel content */
198- sl-tab-panel ::part(base ) {
199- padding : 1.5rem ;
200- }
201+ /*
202+ Remember: font styles (color, font-size, line-height) INHERIT through
203+ Shadow DOM. Layout styles (margin, padding, display, width) do NOT —
204+ they must be set here in light DOM CSS.
205+ */
201206```
202207
203- ## Recipe 7: High Contrast / Forced Colors Mode
208+ ## Recipe 7: High Contrast / Forced Colors
204209
205210``` css
206- /* Some libraries handle forced-colors internally, but you can enhance: */
207211@media (forced-colors: active) {
208- sl -button ::part(base ) {
212+ my -button ::part(base ) {
209213 border : 2px solid ButtonText ;
210214 }
211-
212- sl -button ::part(base ):hover {
215+
216+ my -button ::part(base ):hover {
213217 border-color : Highlight ;
214218 }
215-
216- sl -button ::part(base ):focus-visible {
219+
220+ my -button ::part(base ):focus-visible {
217221 outline : 2px solid Highlight ;
218222 outline-offset : 2px ;
219223 }
@@ -224,22 +228,21 @@ sl-tab-panel::part(base) {
224228
225229| What you wrote | Why it fails | What to write instead |
226230| ---| ---| ---|
227- | ` sl -button .label { }` | Descendant selector can't pierce Shadow DOM | ` sl -button::part(label) { }` |
228- | ` sl -button { color: red; }` | Only affects host, not internal text | ` sl -button { --sl-color-primary-600 : red; }` |
229- | ` sl -input input { }` | Internal ` <input> ` is in Shadow DOM | ` sl -input::part(input) { }` or use CSS custom properties |
230- | ` document.querySelector('sl-button') .shadowRoot.style` | Bypasses encapsulation | Use CSS custom properties |
231- | ` sl -button::part(base) .icon { }` | Can't nest selectors after ::part | ` sl -button::part(base) { }` (style the whole part) |
231+ | ` my -button .label { }` | Descendant selector can't pierce Shadow DOM | ` my -button::part(label) { }` |
232+ | ` my -button { color: red; }` | Only affects host box , not internal text | ` my -button { --button-text-color : red; }` |
233+ | ` my -input input { }` | Internal ` <input> ` is in Shadow DOM | ` my -input::part(input) { }` or use tokens |
234+ | ` el .shadowRoot.style` | Bypasses encapsulation | Use CSS custom properties |
235+ | ` my -button::part(base) .icon { }` | Can't nest selectors after ::part | ` my -button::part(base) { }` (style the whole part) |
232236| ` ::slotted(div span) { } ` | ::slotted only selects direct children | ` ::slotted(div) { } ` (direct child only) |
233- | ` .wrapper sl-button { all: unset; } ` | ` all: unset ` doesn't reach Shadow DOM | Override specific CSS custom properties |
237+ | ` .wrapper my-button { all: unset; } ` | ` all: unset ` doesn't reach Shadow DOM | Override specific CSS custom properties |
238+ | ` my-button { --token: var(--token); } ` | Self-referential — resolves to empty | ` my-button { --token: #value; } ` |
239+ | ` my-button { --token: initial; } ` | ` initial ` resets to empty for custom props | ` my-button { --token: #value; } ` |
234240
235- ## Debugging Checklist
241+ ## Validation Workflow
236242
237- If a style isn't working on a web component:
243+ After writing CSS for any component:
238244
239- 1 . ** Open DevTools** → Elements panel → find the component → expand its #shadow-root
240- 2 . ** Check if the element you're targeting is inside Shadow DOM** — if yes, normal selectors won't work
241- 3 . ** Check available CSS parts** — look for ` part="..." ` attributes on internal elements
242- 4 . ** Check available CSS custom properties** — use ` getComputedStyle(element).getPropertyValue('--property-name') `
243- 5 . ** Verify your custom property name** — typos are the #1 cause of "it's not working"
244- 6 . ** Check inheritance** — is another ancestor overriding the same property?
245- 7 . ** Check specificity** — ` :host ` styles inside the component may need higher specificity to override
245+ 1 . ** Preflight check** : ` styling_preflight({ cssText, tagName }) ` — one call validates everything
246+ 2 . ** If issues found** : Fix based on the ` issues ` array and ` correctSnippet `
247+ 3 . ** Full validation** : ` validate_component_code({ html, css, code, tagName }) ` — runs 20 validators
248+ 4 . ** Get auto-fixes** : ` suggest_fix({ type, issue, original }) ` — copy-pasteable corrected code
0 commit comments