Skip to content

Commit

Permalink
feat: add style expression support (#1058)
Browse files Browse the repository at this point in the history
Co-authored-by: Joe Davidson <[email protected]>
  • Loading branch information
a-h and joerdav authored Jan 30, 2025
1 parent 0474dd9 commit fea7372
Show file tree
Hide file tree
Showing 12 changed files with 1,084 additions and 104 deletions.
2 changes: 1 addition & 1 deletion .version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.3.832
0.3.833
219 changes: 214 additions & 5 deletions docs/docs/03-syntax-and-usage/11-css-style-management.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,230 @@
# CSS style management

## HTML class attribute
## HTML class and style attributes

The standard HTML `class` attribute can be added to components to set class names.
The standard HTML `class` and `style` attributes can be added to components. Note the use of standard quotes to denote a static value.

```templ
templ button(text string) {
<button class="button is-primary">{ text }</button>
<button class="button is-primary" style="background-color: red">{ text }</button>
}
```

```html title="Output"
<button class="button is-primary">
<button class="button is-primary" style="background-color: red">
Click me
</button>
```

## Class expression
## Style attribute

To use a variable in the style attribute, use braces to denote the Go expression.

```templ
templ button(style, text string) {
<button style={ style }>{ text }</button>
}
```

You can pass multiple values to the `style` attribute. The results are all added to the output.

```templ
templ button(style1, style2 string, text string) {
<button style={ style1, style2 }>{ text }</button>
}
```

The style attribute supports use of the following types:

* `string` - A string containing CSS properties, e.g. `background-color: red`.
* `templ.SafeCSS` - A value containing CSS properties and values that will not be sanitized, e.g. `background-color: red; text-decoration: underline`
* `map[string]string` - A map of string keys to string values, e.g. `map[string]string{"color": "red"}`
* `map[string]templ.SafeCSSProperty` - A map of string keys to values, where the values will not be sanitized.
* `templ.KeyValue[string, string]` - A single CSS key/value.
* `templ.KeyValue[string, templ.SafeCSSProperty` - A CSS key/value, but the value will not be sanitized.
* `templ.KeyValue[string, bool]` - A map where the CSS in the key is only included in the output if the boolean value is true.
* `templ.KeyValue[templ.SafeCSS, bool]` - A map where the CSS in the key is only included if the boolean value is true.

Finally, a function value that returns any of the above types can be used.

Go syntax allows you to pass a single function that returns a value and an error.

```templ
templ Page(userType string) {
<div style={ getStyle(userType) }>Styled</div>
}
func getStyle(userType string) (string, error) {
//TODO: Look up in something that might error.
return "background-color: red", errors.New("failed")
}
```

Or multiple functions and values that return a single type.

```templ
templ Page(userType string) {
<div style={ getStyle(userType), "color: blue" }>Styled</div>
}
func getStyle(userType string) (string) {
return "background-color: red"
}
```

### Style attribute examples

#### Maps

Maps are useful when styles need to be dynamically computed based on component state or external inputs.

```templ
func getProgressStyle(percent int) map[string]string {
return map[string]string{
"width": fmt.Sprintf("%d%%", percent),
"transition": "width 0.3s ease",
}
}
templ ProgressBar(percent int) {
<div style={ getProgressStyle(percent) } class="progress-bar">
<div class="progress-fill"></div>
</div>
}
```

```html title="Output (percent=75)"
<div style="transition:width 0.3s ease;width:75%;" class="progress-bar">
<div class="progress-fill"></div>
</div>
```

#### KeyValue pattern

The `templ.KV` helper provides conditional style application in a more compact syntax.

```templ
templ TextInput(value string, hasError bool) {
<input
type="text"
value={ value }
style={
templ.KV("border-color: #ff3860", hasError),
templ.KV("background-color: #fff5f7", hasError),
"padding: 0.5em 1em;",
}
>
}
```

```html title="Output (hasError=true)"
<input
type="text"
value=""
style="border-color: #ff3860; background-color: #fff5f7; padding: 0.5em 1em;">
```

#### Bypassing sanitization

By default, dynamic CSS values are sanitized to protect against dangerous CSS values that might introduce vulnerabilities into your application.

However, if you're sure, you can bypass sanitization by marking your content as safe with the `templ.SafeCSS` and `templ.SafeCSSProperty` types.

```templ
func calculatePositionStyles(x, y int) templ.SafeCSS {
return templ.SafeCSS(fmt.Sprintf(
"transform: translate(%dpx, %dpx);",
x*2, // Example calculation
y*2,
))
}
templ DraggableElement(x, y int) {
<div style={ calculatePositionStyles(x, y) }>
Drag me
</div>
}
```

```html title="Output (x=10, y=20)"
<div style="transform: translate(20px, 40px);">
Drag me
</div>
```

### Pattern use cases

| Pattern | Best For | Example Use Case |
|---------|----------|------------------|
| **Maps** | Dynamic style sets requiring multiple computed values | Progress indicators, theme switching |
| **KeyValue** | Conditional style toggling | Form validation, interactive states |
| **Functions** | Complex style generation | Animations, data visualizations |
| **Direct Strings** | Simple static styles | Basic formatting, utility classes |

### Sanitization behaviour

By default, dynamic CSS values are sanitized to protect against dangerous CSS values that might introduce vulnerabilities into your application.

```templ
templ UnsafeExample() {
<div style={ "background-image: url('javascript:alert(1)')" }>
Dangerous content
</div>
}
```

```html title="Output"
<div style="background-image:zTemplUnsafeCSSPropertyValue;">
Dangerous content
</div>
```

These protections can be bypassed with the `templ.SafeCSS` and `templ.SafeCSSProperty` types.

```templ
templ SafeEmbed() {
<div style={ templ.SafeCSS("background-image: url(/safe.png);") }>
Trusted content
</div>
}
```

```html title="Output"
<div style="background-image: url(/safe.png);">
Trusted content
</div>
```

:::note
HTML attribute escaping is not bypassed, so `<`, `>`, `&` and quotes will always appear as HTML entities (`&lt;` etc.) in attributes - this is good practice, and doesn't affect how browsers use the CSS.
:::

### Error Handling

Invalid values are automatically sanitized:

```templ
templ InvalidButton() {
<button style={
map[string]string{
"": "invalid-property",
"color": "</style>",
}
}>Click me</button>
}
```

```html title="Output"
<button style="zTemplUnsafeCSSPropertyName:zTemplUnsafeCSSPropertyValue;color:zTemplUnsafeCSSPropertyValue;">
Click me
</button>
```

Go's type system doesn't support union types, so it's not possible to limit the inputs to the style attribute to just the supported types.

As such, the attribute takes `any`, and executes type checks at runtime. Any invalid types will produce the CSS value `zTemplUnsupportedStyleAttributeValue:Invalid;`.

## Class attributes

To use a variable as the name of a CSS class, use a CSS expression.

Expand All @@ -42,6 +250,7 @@ templ button(text string, className string) {

Toggle addition of CSS classes to an element based on a boolean value by passing:

* A `string` containing the name of a class to apply.
* A `templ.KV` value containing the name of the class to add to the element, and a boolean that determines whether the class is added to the attribute at render time.
* `templ.KV("is-primary", true)`
* `templ.KV("hover:red", true)`
Expand Down
Loading

0 comments on commit fea7372

Please sign in to comment.