@zio.dev/zio-blocks 0.0.33 → 0.0.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/guides/compile-time-resource-safety-with-scope.md +16 -17
  2. package/guides/getting-started-with-mux.md +1507 -0
  3. package/guides/query-dsl-extending.md +161 -102
  4. package/guides/query-dsl-fluent-builder.md +217 -157
  5. package/guides/query-dsl-reified-optics.md +12 -10
  6. package/guides/query-dsl-sql.md +246 -165
  7. package/guides/telemetry-guide.md +1069 -0
  8. package/guides/zio-schema-migration.md +29 -22
  9. package/index.md +292 -50
  10. package/package.json +1 -1
  11. package/plans/config-follow-up-prs.md +188 -0
  12. package/plans/config-pr-assessment-roadmap.md +310 -0
  13. package/reference/MuxDataFlow.jsx +250 -0
  14. package/reference/async.md +651 -0
  15. package/reference/chunk.md +3533 -308
  16. package/reference/codegen/case-class.md +436 -0
  17. package/reference/codegen/emitter-config.md +383 -0
  18. package/reference/codegen/examples.md +664 -0
  19. package/reference/codegen/field.md +316 -0
  20. package/reference/codegen/index.md +317 -0
  21. package/reference/codegen/scala-emitter.md +392 -0
  22. package/reference/codegen/scala-file.md +276 -0
  23. package/reference/codegen/sealed-trait.md +408 -0
  24. package/reference/codegen/type-definition.md +340 -0
  25. package/reference/codegen/type-ref.md +201 -0
  26. package/reference/combinators.md +347 -117
  27. package/reference/config.md +158 -0
  28. package/reference/context.md +4 -4
  29. package/reference/datastar.md +346 -0
  30. package/reference/docs.md +1461 -345
  31. package/reference/endpoint/auth-type.md +146 -0
  32. package/reference/endpoint/endpoint.md +297 -0
  33. package/reference/endpoint/http-codec.md +249 -0
  34. package/reference/endpoint/index.md +825 -0
  35. package/reference/endpoint/path-codec.md +237 -0
  36. package/reference/endpoint/route-pattern.md +196 -0
  37. package/reference/endpoint/route-tree.md +111 -0
  38. package/reference/endpoint/segment-codec.md +212 -0
  39. package/reference/html.md +1120 -0
  40. package/reference/htmx/attribute-values.md +359 -0
  41. package/reference/htmx/hx-encoding.md +111 -0
  42. package/reference/htmx/hx-params.md +204 -0
  43. package/reference/htmx/hx-swap.md +276 -0
  44. package/reference/htmx/hx-sync.md +251 -0
  45. package/reference/htmx/hx-target.md +314 -0
  46. package/reference/htmx/hx-trigger.md +457 -0
  47. package/reference/htmx/hx-url-update.md +239 -0
  48. package/reference/htmx/index.md +855 -0
  49. package/reference/http-model/index.md +47 -0
  50. package/reference/http-model/model.md +1481 -0
  51. package/reference/http-model/schema.md +747 -0
  52. package/reference/maybe.md +826 -0
  53. package/reference/media-type.md +2 -2
  54. package/reference/mux.mdx +823 -0
  55. package/reference/openapi.md +1351 -0
  56. package/reference/resource-management/defer-handle.md +1 -1
  57. package/reference/resource-management/resource.md +31 -2
  58. package/reference/resource-management/scope.md +28 -12
  59. package/reference/resource-management/wire.md +3 -7
  60. package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
  61. package/reference/ringbuffer/MpscDiagram.jsx +618 -0
  62. package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
  63. package/reference/ringbuffer/SpscDiagram.jsx +677 -0
  64. package/reference/ringbuffer/advanced.mdx +109 -0
  65. package/reference/ringbuffer/index.mdx +145 -0
  66. package/reference/ringbuffer/mpmc.mdx +151 -0
  67. package/reference/ringbuffer/mpsc.mdx +132 -0
  68. package/reference/ringbuffer/spmc.mdx +108 -0
  69. package/reference/ringbuffer/spsc.mdx +344 -0
  70. package/reference/{allows.md → schema/allows.md} +4 -4
  71. package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
  72. package/reference/{binding.md → schema/binding.md} +2 -3
  73. package/reference/schema/built-in-codecs/avro.md +451 -0
  74. package/reference/schema/built-in-codecs/bson.md +480 -0
  75. package/reference/schema/built-in-codecs/csv.md +564 -0
  76. package/reference/schema/built-in-codecs/index.md +77 -0
  77. package/reference/schema/built-in-codecs/json/index.md +295 -0
  78. package/reference/schema/built-in-codecs/json/json-config.md +217 -0
  79. package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
  80. package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
  81. package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
  82. package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
  83. package/reference/schema/built-in-codecs/messagepack.md +508 -0
  84. package/reference/schema/built-in-codecs/thrift.md +433 -0
  85. package/reference/schema/built-in-codecs/toon.md +1078 -0
  86. package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
  87. package/reference/schema/built-in-codecs/yaml.md +552 -0
  88. package/reference/{codec.md → schema/codec.md} +10 -10
  89. package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +151 -5
  90. package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
  91. package/reference/schema/format.md +92 -0
  92. package/reference/schema/index.md +50 -0
  93. package/reference/schema/migration.md +297 -0
  94. package/reference/{modifier.md → schema/modifier.md} +58 -7
  95. package/reference/{optics.md → schema/optics.md} +2 -2
  96. package/reference/{patch.md → schema/patch.md} +1 -1
  97. package/{path-interpolator.md → reference/schema/path-interpolator.md} +165 -72
  98. package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
  99. package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
  100. package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
  101. package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
  102. package/reference/{schema.md → schema/schema.md} +12 -0
  103. package/reference/{structural-types.md → schema/structural-types.md} +1 -1
  104. package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
  105. package/reference/smithy.md +533 -0
  106. package/reference/sql/db-codec-deriver.md +71 -0
  107. package/reference/sql/db-codec.md +687 -0
  108. package/reference/sql/db-con.md +271 -0
  109. package/reference/sql/db-connection.md +153 -0
  110. package/reference/sql/db-param-writer.md +77 -0
  111. package/reference/sql/db-param.md +66 -0
  112. package/reference/sql/db-result-reader.md +146 -0
  113. package/reference/sql/db-tx.md +82 -0
  114. package/reference/sql/db-value.md +41 -0
  115. package/reference/sql/ddl.md +85 -0
  116. package/reference/sql/frag.md +254 -0
  117. package/reference/sql/index.md +341 -0
  118. package/reference/sql/repo.md +600 -0
  119. package/reference/sql/sql-dialect.md +73 -0
  120. package/reference/sql/sql-logger.md +62 -0
  121. package/reference/sql/sql-name-mapper.md +70 -0
  122. package/reference/sql/table-metadata.md +134 -0
  123. package/reference/sql/table.md +448 -0
  124. package/reference/sql/transactor-zio.md +399 -0
  125. package/reference/sql/transactor.md +353 -0
  126. package/reference/sql-zio.md +112 -0
  127. package/reference/streams/concurrent-operators.md +106 -0
  128. package/reference/streams/index.md +653 -0
  129. package/reference/streams/pipeline.md +718 -0
  130. package/reference/streams/reader.md +1284 -0
  131. package/reference/streams/scala-2-compatibility.md +55 -0
  132. package/reference/streams/sink.md +1426 -0
  133. package/reference/streams/stream.md +2526 -0
  134. package/reference/streams/writer.md +1045 -0
  135. package/reference/streams/zero-boxing.md +275 -0
  136. package/reference/telemetry.md +693 -0
  137. package/reference/typeid.md +5 -19
  138. package/sidebars.js +238 -43
  139. package/reference/formats.md +0 -694
  140. package/reference/http-model.md +0 -1716
  141. package/reference/streams.md +0 -989
  142. package/ringbuffer.md +0 -249
  143. /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
  144. /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
  145. /package/reference/{lazy.md → schema/lazy.md} +0 -0
  146. /package/reference/{reflect.md → schema/reflect.md} +0 -0
  147. /package/reference/{registers.md → schema/registers.md} +0 -0
  148. /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
  149. /package/reference/{syntax.md → schema/syntax.md} +0 -0
  150. /package/reference/{validation.md → schema/validation.md} +0 -0
@@ -0,0 +1,1120 @@
1
+ ---
2
+ id: html
3
+ title: "HTML"
4
+ ---
5
+
6
+ `zio-blocks-html` is a **type-safe HTML templating library** providing immutable data structures and a fluent DSL for building HTML, CSS, and JavaScript. It offers compile-time safety, automatic XSS protection, and zero-dependency simplicity across Scala 2.13, 3.x, and both JVM and Scala.js platforms.
7
+
8
+ Core types: `Dom` (HTML tree), `CssSelector` (CSS queries), `DomSelection` (DOM navigation), `Css` (stylesheets), `Js` (JavaScript expressions).
9
+
10
+ The module is structured around these core types:
11
+
12
+ ```
13
+ import zio.blocks.html._
14
+ import zio.blocks.chunk.Chunk
15
+
16
+ // Dom variants
17
+ sealed trait Dom
18
+ case class Dom.Text(content: String) extends Dom
19
+ sealed trait Dom.Element extends Dom
20
+ case class Dom.Element.Generic(tag: String, attributes: Chunk[Dom.Attribute], children: Chunk[Dom]) extends Dom.Element
21
+ case class Dom.Element.Script(attributes: Chunk[Dom.Attribute], children: Chunk[Dom]) extends Dom.Element
22
+ case class Dom.Element.Style(attributes: Chunk[Dom.Attribute], children: Chunk[Dom]) extends Dom.Element
23
+
24
+ // Attribute variants
25
+ sealed trait Dom.Attribute
26
+ case class Dom.Attribute.KeyValue(name: String, value: Dom.AttributeValue) extends Dom.Attribute
27
+ case class Dom.Attribute.AppendValue(name: String, value: Dom.AttributeValue, separator: Dom.AttributeSeparator) extends Dom.Attribute
28
+ case class Dom.Attribute.BooleanAttribute(name: String, enabled: Boolean) extends Dom.Attribute
29
+
30
+ // CssSelector variants (additional types omitted for brevity)
31
+ sealed trait CssSelector
32
+ case class CssSelector.Element(tag: String) extends CssSelector
33
+ case class CssSelector.Class(name: String) extends CssSelector
34
+
35
+ // Css variants
36
+ sealed trait Css
37
+ case class Css.Rule(selector: CssSelector, declarations: Chunk[Css.Declaration]) extends Css
38
+ case class Css.Declaration(property: String, value: String) extends Css
39
+ ```
40
+
41
+ ## Motivation
42
+
43
+ Why use `zio-blocks-html`?
44
+
45
+ String concatenation for HTML is error-prone:
46
+ - No type safety — typos in tag names or attributes only surface at runtime
47
+ - Manual escaping is tedious and easy to forget
48
+ - Tightly couples HTML structure to data serialization
49
+
50
+ Template engines with runtime parsing add overhead and complexity:
51
+ - Parsing validation on every render
52
+ - Separate template syntax to learn
53
+ - Difficult to compose programmatically
54
+
55
+ `zio-blocks-html` provides:
56
+ - **Type-safe construction** via DSL functions (`div(id := "main", p("Hello"))`)
57
+ - **Automatic XSS protection** through position-aware typeclasses and HTML escaping
58
+ - **Scala 3 compile-time optimizations** for string interpolators (constant folding, macro validation)
59
+ - **Zero dependencies** — pure Scala, works with any HTTP framework
60
+ - **Cross-platform** — identical API on JVM and Scala.js
61
+ - **Structured querying** — CSS selectors for DOM navigation and testing
62
+
63
+ ## Installation
64
+
65
+ Add to `build.sbt`:
66
+
67
+ ```
68
+ libraryDependencies += "dev.zio" %% "zio-blocks-html" % "0.0.51"
69
+ ```
70
+
71
+ For Scala.js projects, use `%%%`:
72
+
73
+ ```
74
+ libraryDependencies += "dev.zio" %%% "zio-blocks-html" % "0.0.51"
75
+ ```
76
+
77
+ Supported Scala versions: 2.13.x and 3.x
78
+
79
+ :::note
80
+ The `html` module depends transitively on `zio-blocks-schema` and `zio-blocks-chunk`. The schema dependency enables `ToJs` auto-derivation via JSON encoding — if your type has a `Schema` instance, it automatically becomes usable in `js"..."` expressions.
81
+ :::
82
+
83
+ ## Overview: How They Work Together
84
+
85
+ The module is organized around five core subsystems that compose together:
86
+
87
+ ```
88
+ ┌─────────────────────────────────────────────────────┐
89
+ │ DSL Functions (div, p, span, ...) │
90
+ │ + Attribute Builders (id :=, className +=) │
91
+ │ └─> Produces: Dom.Element.Generic │
92
+ │ │
93
+ ├─────────────────────────────────────────────────────┤
94
+ │ String Interpolators (html"", css"", js"") │
95
+ │ └─> Position-aware typeclasses │
96
+ │ └─> html"" → Dom.Element (attribute escaping) │
97
+ │ └─> css"" → Css (CSS value types) │
98
+ │ └─> js"" → Js (JavaScript with </script> guard) │
99
+ │ │
100
+ ├─────────────────────────────────────────────────────┤
101
+ │ Dom ADT (sealed trait Dom) │
102
+ │ ├─> Dom.Text (HTML-escaped text content) │
103
+ │ ├─> Dom.Element (Generic, Script, Style) │
104
+ │ ├─> Dom.Doctype (<!DOCTYPE html>) │
105
+ │ └─> Dom.Empty (renders to nothing) │
106
+ │ │
107
+ ├─────────────────────────────────────────────────────┤
108
+ │ Rendering │
109
+ │ ├─> dom.render → minified HTML String │
110
+ │ ├─> dom.render(indent=2) → pretty-printed String │
111
+ │ └─> Used in HTTP responses, testing │
112
+ │ │
113
+ ├─────────────────────────────────────────────────────┤
114
+ │ CSS Selectors + DOM Querying │
115
+ │ ├─> CssSelector ADT (Element, Class, Id, etc.) │
116
+ │ ├─> Fluent combinators (>, >>, +, ~, &, |) │
117
+ │ ├─> DomSelection API (select, filter, extract) │
118
+ │ └─> Used for testing, transformation, navigation │
119
+ └─────────────────────────────────────────────────────┘
120
+ ```
121
+
122
+ **Typical workflow:**
123
+
124
+ 1. **Build** — use DSL (`div(className := "card", ...)`) or interpolators (`html"<div>$content</div>"`)
125
+ 2. **Compose** — nest elements and use typeclasses to extend for custom types
126
+ 3. **Query** — use CSS selectors (`page.select(div.hover).texts`) for testing or transformation
127
+ 4. **Render** — call `Dom#render` or `Dom#render(indent)` for HTTP response or testing
128
+ 5. **Style** — use `css"..."` and `Css.Rule` to define or embed stylesheets
129
+
130
+ ## The Dom ADT
131
+
132
+ The `Dom` sealed trait is the core data model. Everything in the module works with or produces `Dom` nodes.
133
+
134
+ ### The Four Node Types
135
+
136
+ A `Dom` tree is composed of four node types:
137
+
138
+ #### Dom.Text
139
+
140
+ A text node containing string content. Text is automatically HTML-escaped when rendered to prevent XSS injection. Escaping happens at render time (not construction time), so `Dom.Text` stores the raw string.
141
+
142
+ To create text nodes, use the DSL (strings are converted via `ToModifier[String]`) or explicitly. Call `Dom#render` to convert a text node to an HTML string:
143
+
144
+ ```scala
145
+ import zio.blocks.html._
146
+
147
+ val text = Dom.Text("Hello, world!")
148
+ // text: Text = Text("Hello, world!")
149
+ text.render
150
+ // res0: String = "Hello, world!"
151
+ ```
152
+
153
+ #### Dom.Element.Generic
154
+
155
+ A standard HTML element. The tag is the element name, attributes is a `Chunk` of key-value pairs and modifiers, and children is a `Chunk` of DOM nodes. Text content in children is HTML-escaped during rendering.
156
+
157
+ The DSL functions (`div`, `p`, `span`, etc.) construct these:
158
+
159
+ ```scala
160
+ import zio.blocks.html._
161
+
162
+ val elem = div(id := "main", p("Content"))
163
+ ```
164
+
165
+ #### Dom.Element.Script
166
+
167
+ A specialized script element with `tag = "script"`. Unlike `Generic`, Script renders its text children **without HTML escaping**, allowing inline JavaScript to be emitted as-is. This is the mechanism that enables safe `js"..."` interpolation — the interpolator escapes the JavaScript, but the Script element does not double-escape.
168
+
169
+ Use `script().inlineJs(js"...")` or `script().externalJs(url)`:
170
+
171
+ ```scala
172
+ import zio.blocks.html._
173
+
174
+ val inlineScript = script().inlineJs(js"console.log('Hello');")
175
+ val externalScript = script().externalJs("/app.js")
176
+ ```
177
+
178
+ #### Dom.Element.Style
179
+
180
+ A specialized style element with `tag = "style"`. Like Script, Style renders text children **without escaping**, allowing raw CSS to be emitted. Use `style().inlineCss(css"...")`:
181
+
182
+ ```scala
183
+ import zio.blocks.html._
184
+
185
+ val inlineStyle = style().inlineCss(css"body { margin: 0; }")
186
+ ```
187
+
188
+ #### Dom.Doctype
189
+
190
+ A DOCTYPE declaration node. Renders as `<!DOCTYPE value>`. The singleton `doctype` value renders as `<!DOCTYPE html>`:
191
+
192
+ ```scala
193
+ import zio.blocks.html._
194
+
195
+ doctype.render
196
+ // res4: String = "<!DOCTYPE html>"
197
+ ```
198
+
199
+ #### Dom.Empty
200
+
201
+ A no-op node that renders to empty string. Useful for conditional rendering (e.g., `if (condition) element else Dom.Empty`).
202
+
203
+ ### Attributes and Values
204
+
205
+ Elements carry a `Chunk[Dom.Attribute]` where each attribute is one of:
206
+
207
+ - **`Dom.Attribute.KeyValue(name: String, value: Dom.AttributeValue)`** — A standard `name="value"` attribute. The value can be a string, multi-value, boolean, or JavaScript expression.
208
+
209
+ - **`Dom.Attribute.AppendValue(name: String, value: Dom.AttributeValue, separator: Dom.AttributeSeparator)`** — Declares a value to be concatenated with any existing value for the same attribute. Provides accumulation of multi-valued attributes like `class` and `rel`. The separator appears between values (Space, Comma, Semicolon, or Custom).
210
+
211
+ - **`Dom.Attribute.BooleanAttribute(name: String, enabled: Boolean)`** — A standalone attribute (like `disabled`, `checked`, `required`) that renders as just the name when enabled, or is omitted when disabled.
212
+
213
+ :::tip
214
+ The DSL provides `id := value` (`:=`) for single-valued attributes and `className += "class"` (`+=`) for appending to multi-valued attributes. Both are easier to use than constructing `Dom.Attribute` directly.
215
+ :::
216
+
217
+ ### Tree Traversal Operations
218
+
219
+ `Dom` provides four pure tree transformation methods:
220
+
221
+ **`Dom#collect(pf: PartialFunction[Dom, Dom]): List[Dom]`** — Applies a partial function to every node in the tree (depth-first) and collects the results. Useful for extracting or transforming specific nodes:
222
+
223
+ ```scala
224
+ import zio.blocks.html._
225
+
226
+ val tree = div(p("A"), span("B"), p("C"))
227
+ val paragraphs = tree.collect { case el: Dom.Element if el.tag == "p" => el }
228
+ ```
229
+
230
+ **`Dom#filter(predicate: Dom => Boolean): Dom`** — Removes any node for which the predicate returns false. Non-matching nodes are replaced with `Dom.Empty`, and their children are lost. Matching elements have their children recursively filtered:
231
+
232
+ ```scala
233
+ import zio.blocks.html._
234
+
235
+ val tree = div(p("A"), span("Keep"), p("B"), span("Also keep"))
236
+ val filtered = tree.filter {
237
+ case el: Dom.Element => el.tag == "div" || el.tag == "span"
238
+ case _ => true
239
+ }
240
+ ```
241
+
242
+ **`Dom#find(predicate: Dom => Boolean): Option[Dom]`** — Returns the first node (depth-first) matching the predicate, or `None`:
243
+
244
+ ```scala
245
+ import zio.blocks.html._
246
+
247
+ val tree = div(p("First"), span("Second"), p("Third"))
248
+ tree.find { case el: Dom.Element => el.tag == "p"; case _ => false }
249
+ ```
250
+
251
+ **`Dom#transform(f: Dom => Dom): Dom`** — Applies a transformation function to every node in pre-order. Each node receives the transformation first, and if it is an Element, its children are recursed on the transformed node, so a transformation that changes the child list affects what gets recursed into:
252
+
253
+ ```scala
254
+ import zio.blocks.html._
255
+
256
+ val tree = div(h3("Old"), p("Content"))
257
+ val upgraded = tree.transform {
258
+ case el: Dom.Element.Generic if el.tag == "h3" => el.copy(tag = "h2")
259
+ case other => other
260
+ }
261
+ ```
262
+
263
+ ## The HTML DSL
264
+
265
+ The DSL provides functions for all HTML5 elements, allowing fluent construction with a modifier pattern.
266
+
267
+ ### Element Functions
268
+
269
+ All standard HTML elements are available as lowercase functions (or backtick-quoted if they shadow Scala keywords):
270
+
271
+ ```scala
272
+ import zio.blocks.html._
273
+
274
+ // Basic elements
275
+ val paragraph = p("Hello, world!")
276
+ val heading = h1("Welcome")
277
+ val container = div("Content here")
278
+
279
+ // Void elements (auto self-closing)
280
+ val image = img(src := "photo.jpg", alt := "A photo")
281
+ val lineBreak = br
282
+ val horizontalRule = hr
283
+
284
+ // Keyword-shadowed elements (use backticks)
285
+ val objElement = `object`("data.bin")
286
+ ```
287
+
288
+ ### Setting Attributes
289
+
290
+ Attributes are set using the `:=` operator on `AttributeKey` builders:
291
+
292
+ ```scala
293
+ import zio.blocks.html._
294
+
295
+ val link = a(
296
+ href := "https://example.com",
297
+ target := "_blank",
298
+ titleAttr := "Visit Example",
299
+ "Visit Example"
300
+ )
301
+ ```
302
+
303
+ ### Multi-Valued Attributes: Override vs Accumulate
304
+
305
+ For attributes that can have multiple values (`class`, `rel`, `accept`), you can override or accumulate:
306
+
307
+ - **`:=` (override)** — Replaces any previous value. Last assignment wins:
308
+
309
+ ```scala
310
+ import zio.blocks.html._
311
+
312
+ val div1 = div(className := "a", className := "b")
313
+ ```
314
+
315
+ - **`+=` (append)** — Concatenates with the previous value using a separator (space for `class`):
316
+
317
+ ```scala
318
+ import zio.blocks.html._
319
+
320
+ val div2 = div(className += "card", className += "active")
321
+ ```
322
+
323
+ - **Mixed** — Set a base value, then append:
324
+
325
+ ```scala
326
+ import zio.blocks.html._
327
+
328
+ val div3 = div(className := "base").when(true)(className += "extra")
329
+ ```
330
+
331
+ ### Boolean Attributes
332
+
333
+ Use `BooleanAttribute.:=` to conditionally include attributes like `disabled`, `required`, or `checked`:
334
+
335
+ ```scala
336
+ import zio.blocks.html._
337
+
338
+ val isDisabled = true
339
+ val submitBtn = button(disabled := isDisabled, "Submit")
340
+ ```
341
+
342
+ ### Data and ARIA Attributes
343
+
344
+ Use `dataAttr(name)` and `aria(name)` builders for HTML5 data attributes and ARIA accessibility attributes:
345
+
346
+ ```scala
347
+ import zio.blocks.html._
348
+
349
+ val userDiv = div(
350
+ dataAttr("user-id") := "42",
351
+ dataAttr("action") := "edit",
352
+ "User Card"
353
+ )
354
+
355
+ val closeBtn = button(
356
+ aria("label") := "Close",
357
+ aria("expanded") := "false",
358
+ "×"
359
+ )
360
+ ```
361
+
362
+ For custom attributes not provided by the DSL, use the generic `attr(name)` builder:
363
+
364
+ ```scala
365
+ import zio.blocks.html._
366
+
367
+ val alpineDiv = div(attr("x-data") := "{open: false}", "Alpine.js component")
368
+ val htmxDiv = div(attr("hx-get") := "/api/data", "HTMX target")
369
+ ```
370
+
371
+ ### Programmatic Multi-Valued Attributes
372
+
373
+ For programmatic construction of multi-valued attributes (when the DSL `+=` operator is not available), use `Dom.multiAttr(name)` or `Dom.multiAttr(name, separator)`:
374
+
375
+ ```scala
376
+ import zio.blocks.html._
377
+
378
+ // Create a custom multi-valued attribute builder (alternative to DSL's className)
379
+ val classBuilder = Dom.multiAttr("class")
380
+ val div1 = div(classBuilder := "base", classBuilder += "active")
381
+
382
+ // Create a multi-valued attribute with explicit separator
383
+ val rel = Dom.multiAttr("rel", Dom.AttributeSeparator.Space)
384
+ val link = a(rel := "prev", rel += "first", href := "/previous")
385
+ ```
386
+
387
+ The `MultiAttributeKey` class handles accumulation of values with configurable separators (Space, Comma, Semicolon, or Custom).
388
+
389
+ For constructing multi-valued attributes directly from collections (without a builder chain), use the `Iterable[String]` overload:
390
+
391
+ ```scala
392
+ import zio.blocks.html._
393
+
394
+ // Directly create a multi-valued attribute from a collection
395
+ val customClasses = Dom.multiAttr("class", List("card", "active", "large"))
396
+ val div1 = div(customClasses)
397
+ ```
398
+
399
+ This approach is useful when programmatically building multi-valued attributes outside the DSL's builder pattern.
400
+
401
+ ### Children
402
+
403
+ Children can be strings, elements, or collections. The DSL uses the `ToModifier` typeclass to convert values into DOM nodes:
404
+
405
+ ```scala
406
+ import zio.blocks.html._
407
+
408
+ // String children are converted to Dom.Text
409
+ val simple = p("Plain text")
410
+
411
+ // Element children are nested
412
+ val nested = div(p("First"), p("Second"))
413
+
414
+ // Lists of children — elements append directly, no wrapper
415
+ val items = List("Apple", "Banana", "Cherry")
416
+ val listEl = ul(items.map(item => li(item)))
417
+ ```
418
+
419
+ ### Conditional Rendering
420
+
421
+ Use `when(condition)` to apply modifiers conditionally:
422
+
423
+ ```scala
424
+ import zio.blocks.html._
425
+
426
+ val isHighlighted = true
427
+ val box = div(
428
+ className := "box"
429
+ ).when(isHighlighted)(
430
+ className += "highlighted",
431
+ titleAttr := "This is highlighted"
432
+ )
433
+ ```
434
+
435
+ Use `whenSome(option)` to apply modifiers based on an `Option`:
436
+
437
+ ```scala
438
+ import zio.blocks.html._
439
+
440
+ val maybeTitle: Option[String] = Some("Important")
441
+ val card = div(
442
+ className := "card",
443
+ p("Content")
444
+ ).whenSome(maybeTitle)(t => Seq(
445
+ titleAttr := t,
446
+ className += "has-title"
447
+ ))
448
+ ```
449
+
450
+ ### Void Elements
451
+
452
+ Void elements (self-closing tags) automatically render with the correct syntax:
453
+
454
+ ```scala
455
+ import zio.blocks.html._
456
+
457
+ val voidElements = div(
458
+ br,
459
+ hr,
460
+ img(src := "photo.jpg", alt := "A photo"),
461
+ input(`type` := "text", placeholder := "Enter text")
462
+ )
463
+ ```
464
+
465
+ ## String Interpolators
466
+
467
+ The module provides four string interpolators: `html""`, `css""`, `js""`, and `selector""`. All offer compile-time safety (on Scala 3) and automatic escaping appropriate to their context.
468
+
469
+ ### `html""` Interpolator
470
+
471
+ The `html""` interpolator is **position-aware**: it detects whether each interpolated argument is in an attribute-value position or content position, then summons the appropriate typeclass.
472
+
473
+ The `html""` interpolator determines position based on the preceding string:
474
+
475
+ **Position detection:**
476
+ - **Attribute position**: if the preceding string part ends with `=`, `='`, or `="`, the next argument is treated as an attribute value and uses `ToAttrValue[A]`
477
+ - **Content position**: otherwise, the argument is treated as element content and uses `ToElements[A]`
478
+
479
+ Position-aware interpolation enables safe attribute values and content:
480
+
481
+ ```scala
482
+ import zio.blocks.html._
483
+
484
+ val name = "Alice"
485
+ val age = 30
486
+
487
+ // name is in content position → ToElements[String]
488
+ // age is in attribute position → ToAttrValue[Int]
489
+ val elem = html"""<div id="user-$age" class="profile">User: $name</div>"""
490
+ ```
491
+
492
+ **Single-root requirement**: The `html""` interpolator requires a **single root element**. Multiple top-level nodes cause an exception at runtime (Scala 2) or compile error for static templates (Scala 3):
493
+
494
+ ```scala
495
+ import zio.blocks.html._
496
+
497
+ // This compiles (single root)
498
+ val page = html"<div><p>A</p><p>B</p></div>"
499
+ ```
500
+
501
+ **XSS Protection:**
502
+
503
+ Content interpolated into `html""` is stored as `Dom.Text` nodes, which are HTML-escaped during `Dom#render` to prevent XSS:
504
+
505
+ ```scala
506
+ import zio.blocks.html._
507
+
508
+ val userInput = "<script>alert('XSS')</script>"
509
+ val safe = html"<p>$userInput</p>"
510
+ ```
511
+
512
+ :::danger
513
+ The `html""` interpolator requires a **single root element**. On Scala 3, pure-static templates with multiple roots fail at compile time. On Scala 2 or for dynamic templates, the error occurs at evaluation time. Always wrap multiple elements in a container (e.g., `<div>`).
514
+ :::
515
+
516
+ ### `css""` Interpolator
517
+
518
+ The `css""` interpolator returns a `Css` value with `ToCss` typeclass dispatch for interpolated parts:
519
+
520
+ ```scala
521
+ import zio.blocks.html._
522
+
523
+ val color = "blue"
524
+ val size = 16
525
+
526
+ val styles = css"color: $color; font-size: ${size}px;"
527
+ ```
528
+
529
+ Use `CssLength` and `CssColor` types for type-safe CSS values:
530
+
531
+ ```scala
532
+ import zio.blocks.html._
533
+
534
+ val width = css"width: ${CssLength(300.0, "px")};"
535
+ val background = css"background: ${CssColor.Hex.unsafe("ff0000")};"
536
+ ```
537
+
538
+ :::note
539
+ `CssColor.Hex` returns `Option[CssColor]` because hex validation may fail. Use `CssColor.Hex.apply()` (or just `CssColor.Hex(...)`) for safe validation, or `CssColor.Hex.unsafe()` for known-valid hex strings.
540
+ :::
541
+
542
+ ### `js""` Interpolator
543
+
544
+ The `js""` interpolator returns a `Js` value. Strings are automatically quoted and escaped; numerics and booleans are rendered unquoted:
545
+
546
+ ```scala
547
+ import zio.blocks.html._
548
+
549
+ val message = "Hello, world!"
550
+ val count = 42
551
+
552
+ val code = js"console.log($message); alert($count);"
553
+ ```
554
+
555
+ :::warning
556
+ Never interpolate untrusted user input directly into `js""`. The interpolator escapes string values, but the `Js` value is rendered without additional escaping in script elements. Always use `ToJs[String]` (which automatically quotes and escapes) for any untrusted input.
557
+ :::
558
+
559
+ The interpolator protects against `</script>` injection by escaping `<` and `>` as Unicode escapes:
560
+
561
+ ```scala
562
+ import zio.blocks.html._
563
+
564
+ val userInput = "if (x < y) alert('<script>');"
565
+ val code = js"let check = $userInput"
566
+
567
+ println(code.value)
568
+ // let check = "if (x < y) alert('<script>');"
569
+ // (with < and > characters escaped as < and > in actual output)
570
+ ```
571
+
572
+ ### `selector""` Interpolator
573
+
574
+ The `selector""` interpolator returns a `CssSelector`:
575
+
576
+ ```scala
577
+ import zio.blocks.html._
578
+
579
+ val className = "active"
580
+ val selector = selector".$className"
581
+ ```
582
+
583
+ ## CSS Selectors and the DSL
584
+
585
+ The `CssSelector` ADT provides a fluent DSL for building CSS selectors with combinator methods.
586
+
587
+ ### Basic Selectors
588
+
589
+ Create selectors for elements, classes, IDs, or universal matches:
590
+
591
+ ```scala
592
+ import zio.blocks.html._
593
+
594
+ val divSel = CssSelector.Element("div")
595
+ val classSel = CssSelector.Class("container")
596
+ val idSel = CssSelector.Id("header")
597
+ val universal = CssSelector.Universal
598
+ ```
599
+
600
+ ### Combinators
601
+
602
+ All HTML elements (`div`, `span`, `p`, etc.) implement `CssSelectable`, so you can use them directly as selectors:
603
+
604
+ ```scala
605
+ import zio.blocks.html._
606
+
607
+ // Element selectors via DSL elements
608
+ val divSel = div.selector
609
+ val childSel = div > span // div > span
610
+ val descendantSel = div >> span // div span (descendant)
611
+ val adjacentSel = div + span // div + span
612
+ val siblingSel = div ~ span // div ~ span
613
+ val andSel = div & CssSelector.Class("active") // div.active
614
+ val orSel = div | span // div, span
615
+ ```
616
+
617
+ ### Pseudo-Classes and Pseudo-Elements
618
+
619
+ Pseudo-classes match elements by their state, and pseudo-elements create dynamic content:
620
+
621
+ ```scala
622
+ import zio.blocks.html._
623
+
624
+ val hoverSel = a.hover // a:hover
625
+ val firstChild = li.firstChild // li:first-child
626
+ val nthChild = tr.nthChild(2) // tr:nth-child(2)
627
+ val before = div.before // div::before
628
+ val after = span.after // span::after
629
+ ```
630
+
631
+ ### Attribute Selectors
632
+
633
+ Select elements by their attribute values using built-in matchers:
634
+
635
+ ```scala
636
+ import zio.blocks.html._
637
+
638
+ val input = CssSelector.Element("input")
639
+
640
+ val hasType = input.withAttribute("type") // input[type]
641
+ val exactType = input.withAttribute("type", "text") // input[type="text"]
642
+ val containsClass = input.withAttributeContaining("class", "btn") // input[class*="btn"]
643
+ val startsWithHref = a.withAttributeStarting("href", "https") // a[href^="https"]
644
+ val endsWithPng = img.withAttributeEnding("src", ".png") // img[src$=".png"]
645
+ ```
646
+
647
+ ## DOM Querying with DomSelection
648
+
649
+ The `DomSelection` API lets you query and navigate DOM trees using CSS selectors. It is useful for testing, transforming templates, and extracting information.
650
+
651
+ ### Selecting Elements
652
+
653
+ Call `Dom#select(selector)` to query the tree:
654
+
655
+ ```scala
656
+ import zio.blocks.html._
657
+
658
+ val page = div(
659
+ header(nav(a(href := "/", "Home"), a(href := "/about", "About"))),
660
+ main(p(className += "intro", "Hello"), p("World")),
661
+ footer(p("© 2026"))
662
+ )
663
+
664
+ // Query by tag
665
+ val paragraphs = page.select(CssSelector.Element("p"))
666
+ paragraphs.length
667
+
668
+ // Query by class
669
+ val intros = page.select(CssSelector.Class("intro"))
670
+ intros.texts
671
+ ```
672
+
673
+ ### Navigation
674
+
675
+ The `DomSelection` API returned by `Dom#select` provides navigation methods like `.children` and `.first` to traverse the selection results:
676
+
677
+ ```scala
678
+ import zio.blocks.html._
679
+
680
+ val page = div(
681
+ nav(a(href := "/", "Home"), a(href := "/about", "About")),
682
+ main(p("Content"))
683
+ )
684
+
685
+ // Direct children
686
+ val navLinks = page.select(CssSelector.Element("nav")).children
687
+ ```
688
+
689
+ ### Extraction
690
+
691
+ The `DomSelection` API provides methods like `.attrs` and `.texts` to extract attribute values and text content from selected elements:
692
+
693
+ ```scala
694
+ import zio.blocks.html._
695
+
696
+ val page = div(
697
+ a(href := "/home", "Home"),
698
+ a(href := "/about", "About"),
699
+ a(href := "/contact", "Contact")
700
+ )
701
+
702
+ val hrefs = page.select(CssSelector.Element("a")).attrs("href")
703
+ ```
704
+
705
+ ### Filtering
706
+
707
+ The `DomSelection` API provides `.filter` and `.withClass` methods to filter selections by predicate or attribute:
708
+
709
+ ```scala
710
+ import zio.blocks.html._
711
+
712
+ val page = div(
713
+ p(className += "visible", "A"),
714
+ p(className += "hidden", "B"),
715
+ p("C")
716
+ )
717
+
718
+ // Filter by predicate
719
+ val visible = page.select(CssSelector.Element("p")).filter {
720
+ case el: Dom.Element => el.attributes.exists {
721
+ case attr: Dom.Attribute.KeyValue if attr.name == "class" => true
722
+ case _ => false
723
+ }
724
+ case _ => false
725
+ }
726
+
727
+ // Filter by class
728
+ val withClass = page.select(CssSelector.Element("p")).withClass("visible")
729
+ ```
730
+
731
+ ### Modifying Selections
732
+
733
+ :::warning
734
+ `DomSelection` returns new copies with modifications; the original DOM tree remains unchanged. The `DomSelection` API provides functional transformations. To modify the original DOM tree, use `Dom#transform` with a tree-rewriting function, or rebuild the tree from scratch using the DSL.
735
+ :::
736
+
737
+ The `DomSelection` API provides methods like `.modifyAll`, `.replaceAll`, and `.removeAll` to transform or replace selected nodes:
738
+
739
+ ```scala
740
+ import zio.blocks.html._
741
+
742
+ val page = div(
743
+ p("Old 1"),
744
+ p("Old 2"),
745
+ span("Keep")
746
+ )
747
+
748
+ // Transform all Element nodes in the selection; non-Element nodes pass through unchanged
749
+ val modifiedSelection = page.select(CssSelector.Element("p")).modifyAll {
750
+ case el: Dom.Element.Generic if el.tag == "p" => el.copy(tag = "div")
751
+ case other => other
752
+ }
753
+
754
+ // Replace all selected nodes (returns DomSelection of replacement nodes)
755
+ val replacedSelection = page.select(CssSelector.Element("p")).replaceAll(p("New"))
756
+
757
+ // Remove all selected nodes (returns empty DomSelection)
758
+ val removedSelection = page.select(CssSelector.Element("p")).removeAll
759
+ ```
760
+
761
+ :::note
762
+ Pseudo-class selectors (`:hover`, `:focus`, etc.) match elements structurally by their underlying element selector only — they cannot detect browser interaction state in a static DOM tree. `div.hover` matches the same elements as `CssSelector.Element("div")`.
763
+
764
+ Adjacent sibling (`+`) and general sibling (`~`) selectors are supported in CSS output but not in DOM querying — `DomSelection.select` with these combinators returns empty results.
765
+ :::
766
+
767
+ ## The CSS ADT
768
+
769
+ The `Css` ADT represents structured stylesheets as a typed data structure, separate from strings.
770
+
771
+ All `Css` subtypes support `.render()` for minified output and `.render(indent: Int)` for indented pretty-printing:
772
+ - `Css.Rule` — single CSS rule with selector and declarations
773
+ - `Css.Sheet` — collection of rules
774
+ - `Css.Raw` — raw CSS string
775
+ - `Css.Comment` — CSS comment
776
+
777
+ ### Declarations and Rules
778
+
779
+ A `Css.Declaration` is a property-value pair:
780
+
781
+ ```scala
782
+ import zio.blocks.html._
783
+ import zio.blocks.chunk.Chunk
784
+
785
+ val marginDecl = Css.Declaration("margin", "10px")
786
+ val colorDecl = Css.Declaration("color", "blue")
787
+
788
+ val rule = Css.Rule(
789
+ CssSelector.Element("p"),
790
+ Chunk(marginDecl, colorDecl)
791
+ )
792
+
793
+ rule.render
794
+ ```
795
+
796
+ ### Stylesheets
797
+
798
+ A `Css.Sheet` is a collection of rules:
799
+
800
+ ```scala
801
+ import zio.blocks.html._
802
+ import zio.blocks.chunk.Chunk
803
+
804
+ val bodyRule = Css.Rule(
805
+ CssSelector.Element("body"),
806
+ Chunk(
807
+ Css.Declaration("margin", "0"),
808
+ Css.Declaration("font-family", "sans-serif")
809
+ )
810
+ )
811
+
812
+ val stylesheet = Css.Sheet(Chunk(bodyRule))
813
+
814
+ stylesheet.render(indent = 2)
815
+ ```
816
+
817
+ ### Embedding Stylesheets
818
+
819
+ Embed stylesheets in HTML via the `style()` element:
820
+
821
+ ```scala
822
+ import zio.blocks.html._
823
+ import zio.blocks.chunk.Chunk
824
+
825
+ val page = html(
826
+ head(
827
+ style().inlineCss(
828
+ Css.Sheet(Chunk(
829
+ Css.Rule(
830
+ CssSelector.Element("body"),
831
+ Chunk(Css.Declaration("background", "#f0f0f0"))
832
+ )
833
+ ))
834
+ )
835
+ ),
836
+ body(p("Styled"))
837
+ )
838
+
839
+ page.render
840
+ ```
841
+
842
+ ### Raw CSS
843
+
844
+ For CSS features not expressible via the ADT (media queries, keyframes, at-rules), use `Css.Raw`:
845
+
846
+ ```scala
847
+ import zio.blocks.html._
848
+
849
+ val mediaQuery = Css.Raw("""
850
+ |@media (max-width: 600px) {
851
+ | body { font-size: 14px; }
852
+ |}
853
+ """.stripMargin)
854
+
855
+ println(mediaQuery.render)
856
+ ```
857
+
858
+ :::tip
859
+ Prefer `Css.Rule` and `Css.Sheet` over `Css.Raw` when possible — structured CSS enables future optimization and prevents CSS injection. Use `Css.Raw` only for trusted, hardcoded CSS.
860
+ :::
861
+
862
+ ### CSS Comments
863
+
864
+ Add comments to stylesheets using `Css.Comment`:
865
+
866
+ ```scala
867
+ import zio.blocks.html._
868
+ import zio.blocks.chunk.Chunk
869
+
870
+ val stylesheet = Css.Sheet(Chunk(
871
+ Css.Comment("Mobile-first responsive design"),
872
+ Css.Rule(
873
+ CssSelector.Element("body"),
874
+ Chunk(Css.Declaration("font-size", "16px"))
875
+ ),
876
+ Css.Comment("Tablet and desktop breakpoints"),
877
+ Css.Raw("@media (min-width: 768px) { body { font-size: 18px; } }")
878
+ ))
879
+
880
+ stylesheet.render(indent = 2)
881
+ ```
882
+
883
+ ## Rendering
884
+
885
+ All `Dom` and `Css` values support multiple rendering modes.
886
+
887
+ ### Minified Rendering
888
+
889
+ `Dom#render` produces compact HTML with no extra whitespace. Use `Dom#renderMinified` as an explicit alias for the same operation:
890
+
891
+ ```scala
892
+ import zio.blocks.html._
893
+
894
+ val page = div(h1("Title"), p("Content"))
895
+ page.render
896
+ ```
897
+
898
+ ### Pretty-Printed Rendering
899
+
900
+ `render(indent: Int)` produces indented, readable output:
901
+
902
+ ```scala
903
+ import zio.blocks.html._
904
+
905
+ val page = div(h1("Title"), p("Content"))
906
+ page.render(indent = 2)
907
+ ```
908
+
909
+ ### Performance Notes
910
+
911
+ - `Dom#render` uses a pre-allocated `StringBuilder` and while-loop rendering — zero allocations for iteration
912
+ - Indentation strings get cached in an array of pre-built space strings (up to 128 characters) to prevent repeated string allocation
913
+ - Void elements automatically self-close with no children
914
+ - Script and Style elements render their children without escaping (with `</` → `<\/` protection for scripts)
915
+
916
+ ## Security: XSS Protection
917
+
918
+ The module provides multiple layers of automatic XSS protection:
919
+
920
+ ### HTML Text Escaping
921
+
922
+ All `Dom.Text` nodes are HTML-escaped during rendering:
923
+ - `&` → `&amp;`
924
+ - `<` → `&lt;`
925
+ - `>` → `&gt;`
926
+ - `"` → `&quot;`
927
+ - `'` → `&#x27;`
928
+
929
+ Untrusted content is always escaped to prevent XSS:
930
+
931
+ ```scala
932
+ import zio.blocks.html._
933
+
934
+ val userInput = "<script>alert('XSS')</script>"
935
+ val safe = div(p(userInput))
936
+
937
+ safe.render
938
+ ```
939
+
940
+ ### JavaScript String Escaping
941
+
942
+ The `ToJs[String]` typeclass escapes strings to prevent breaking out of script contexts:
943
+ - `<` → backslash-u-0-0-3-c (the six-character Unicode escape sequence `<`)
944
+ - `>` → backslash-u-0-0-3-e (the six-character Unicode escape sequence `>`)
945
+ - `&` → backslash-u-0-0-2-6 (the six-character Unicode escape sequence `&`)
946
+ - `"` → `\"`, `'` → `\'`, `\` → `\\`
947
+ - Newlines, carriage returns, and Unicode line/paragraph separators are escaped
948
+
949
+ This protects against `</script>` injection:
950
+
951
+ ```scala
952
+ import zio.blocks.html._
953
+
954
+ val userInput = "</script><script>alert('XSS');</script>"
955
+ val code = js"let payload = $userInput"
956
+
957
+ println(code.value)
958
+ // let payload = "</script><script>alert('XSS');</script>"
959
+ // (with < and > characters escaped as Unicode in actual output)
960
+ ```
961
+
962
+ ### URL Sanitization
963
+
964
+ Attributes named `href`, `src`, `action`, or `formaction` are checked for dangerous schemes at render time:
965
+ - `javascript:`, `vbscript:`, `data:text/html` → prefixed with `unsafe:`
966
+
967
+ Dangerous URLs are automatically sanitized in HTML output:
968
+
969
+ ```scala
970
+ import zio.blocks.html._
971
+
972
+ val dangerous = a(href := "javascript:alert('XSS')", "Click me")
973
+ ```
974
+
975
+ ### No Raw HTML Escape Hatch
976
+
977
+ The module intentionally provides no `Dom.Raw` type for embedding arbitrary HTML. This prevents XSS by ensuring all dynamic content is either constructed via the DSL or interpolated through context-aware interpolators.
978
+
979
+ ## Common Patterns
980
+
981
+ The module supports several architectural patterns for code organization and reuse:
982
+
983
+ ### Building Reusable Components
984
+
985
+ Define functions that return `Dom.Element` to create reusable components:
986
+
987
+ ```scala
988
+ import zio.blocks.html._
989
+
990
+ def card(title: String, content: String): Dom.Element =
991
+ div(
992
+ className := "card",
993
+ h2(title),
994
+ p(content)
995
+ )
996
+
997
+ val page = div(
998
+ card("Card 1", "Content A"),
999
+ card("Card 2", "Content B")
1000
+ )
1001
+ ```
1002
+
1003
+ ### Conditional Rendering
1004
+
1005
+ Use `when` and `whenSome` for conditional modifiers:
1006
+
1007
+ ```scala
1008
+ import zio.blocks.html._
1009
+
1010
+ def userCard(name: String, isAdmin: Boolean): Dom.Element =
1011
+ div(
1012
+ className := "user-card"
1013
+ ).when(isAdmin)(
1014
+ className += "admin",
1015
+ span(className := "badge", "Admin")
1016
+ )
1017
+ ```
1018
+
1019
+ ### Rendering Collections
1020
+
1021
+ Map over collections to create child elements:
1022
+
1023
+ ```scala
1024
+ import zio.blocks.html._
1025
+
1026
+ def userList(users: List[String]): Dom.Element =
1027
+ ul(users.map(user => li(user)))
1028
+
1029
+ val page = userList(List("Alice", "Bob", "Charlie"))
1030
+ ```
1031
+
1032
+ ### Template Composition
1033
+
1034
+ Combine interpolators and DSL for flexibility:
1035
+
1036
+ ```scala
1037
+ import zio.blocks.html._
1038
+
1039
+ val title = "My Page"
1040
+ val content = "Welcome to my site"
1041
+
1042
+ val page = html"""
1043
+ <html>
1044
+ <head><title>$title</title></head>
1045
+ <body>
1046
+ ${div(p(content))}
1047
+ </body>
1048
+ </html>
1049
+ """
1050
+ ```
1051
+
1052
+ ### Querying for Tests
1053
+
1054
+ Use `DomSelection` to write structural assertions:
1055
+
1056
+ ```scala
1057
+ import zio.blocks.html._
1058
+
1059
+ val page = div(
1060
+ ul(
1061
+ li(a(href := "/home", "Home")),
1062
+ li(a(href := "/about", "About"))
1063
+ )
1064
+ )
1065
+
1066
+ // Test: check number of links
1067
+ val links = page.select(CssSelector.Element("a"))
1068
+ assert(links.length == 2)
1069
+
1070
+ // Test: check specific href
1071
+ val aboutLink = page.select(a.withAttribute("href", "/about"))
1072
+ assert(aboutLink.texts.contains("About"))
1073
+ ```
1074
+
1075
+ ## Complete Example: A Dashboard Page
1076
+
1077
+ Here is a complete, self-contained example building a full dashboard page with navigation, metadata, styling, and content sections:
1078
+
1079
+ ```scala
1080
+ import zio.blocks.html._
1081
+ import zio.blocks.chunk.Chunk
1082
+
1083
+ val userName = "Alice"
1084
+ val items = List("Dashboard", "Settings", "Logout")
1085
+
1086
+ val pieces: Chunk[Dom] = Chunk(
1087
+ doctype,
1088
+ html(
1089
+ head(
1090
+ meta(charset := "utf-8"),
1091
+ meta(name := "viewport", content := "width=device-width, initial-scale=1.0"),
1092
+ title("My App"),
1093
+ link(rel := "stylesheet", href := "/style.css"),
1094
+ style().inlineCss(
1095
+ Css.Sheet(Chunk(
1096
+ Css.Rule(CssSelector.Element("body"), Chunk(
1097
+ Css.Declaration("margin", "0"),
1098
+ Css.Declaration("font-family", "sans-serif")
1099
+ ))
1100
+ ))
1101
+ )
1102
+ ),
1103
+ body(
1104
+ header(
1105
+ nav(items.map(item => a(href := "#", item)))
1106
+ ),
1107
+ main(
1108
+ h1(s"Welcome, $userName!"),
1109
+ p("This is your dashboard.")
1110
+ ),
1111
+ footer(
1112
+ p("© 2026 My App")
1113
+ ),
1114
+ script().inlineJs(js"console.log('Page loaded');")
1115
+ )
1116
+ )
1117
+ )
1118
+
1119
+ pieces.map(_.render(indent = 2)).mkString("\n")
1120
+ ```