Skip to content

Commit 2c55df5

Browse files
authored
docs: make styling cookbook library-agnostic + add validation workflow
docs: make styling cookbook library-agnostic + add validation workflow
2 parents a19bf71 + 0b0fdf8 commit 2c55df5

File tree

2 files changed

+108
-100
lines changed

2 files changed

+108
-100
lines changed
Lines changed: 103 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,69 @@
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: 560px;
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.75rem 1.25rem;
182-
font-weight: 500;
183-
border-radius: 8px 8px 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.5rem;
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
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'helixir': patch
3+
---
4+
5+
docs: make styling cookbook library-agnostic and add validation workflow references

0 commit comments

Comments
 (0)