mirror of https://github.com/sveltejs/svelte
feat: allow objects/arrays for class attribute (#14714)
* WIP * missed * fix * fix * rename, smooth over incompatibilities * spread support + test * docs * types * implement CSS pruning for array/object expressions * beefier static analysis * lint * rename doc * move class after all directive docs * tweak docs - clarify top-level falsy values, stagger examples, demonstrate composition, discourage class: more strongly * changeset * fix * Update documentation/docs/03-template-syntax/18-class.md Co-authored-by: Conduitry <git@chor.date> * Apply suggestions from code review --------- Co-authored-by: Rich Harris <rich.harris@vercel.com> Co-authored-by: Conduitry <git@chor.date>pull/14825/head
parent
38a3ae321f
commit
015210a1a8
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': minor
|
||||
---
|
||||
|
||||
feat: allow `class` attribute to be an object or array, using `clsx`
|
@ -1,23 +0,0 @@
|
||||
---
|
||||
title: class:
|
||||
---
|
||||
|
||||
The `class:` directive is a convenient way to conditionally set classes on elements, as an alternative to using conditional expressions inside `class` attributes:
|
||||
|
||||
```svelte
|
||||
<!-- These are equivalent -->
|
||||
<div class={isCool ? 'cool' : ''}>...</div>
|
||||
<div class:cool={isCool}>...</div>
|
||||
```
|
||||
|
||||
As with other directives, we can use a shorthand when the name of the class coincides with the value:
|
||||
|
||||
```svelte
|
||||
<div class:cool>...</div>
|
||||
```
|
||||
|
||||
Multiple `class:` directives can be added to a single element:
|
||||
|
||||
```svelte
|
||||
<div class:cool class:lame={!cool} class:potato>...</div>
|
||||
```
|
@ -0,0 +1,90 @@
|
||||
---
|
||||
title: class
|
||||
---
|
||||
|
||||
There are two ways to set classes on elements: the `class` attribute, and the `class:` directive.
|
||||
|
||||
## Attributes
|
||||
|
||||
Primitive values are treated like any other attribute:
|
||||
|
||||
```svelte
|
||||
<div class={large ? 'large' : 'small'}>...</div>
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> For historical reasons, falsy values (like `false` and `NaN`) are stringified (`class="false"`), though `class={undefined}` (or `null`) cause the attribute to be omitted altogether. In a future version of Svelte, all falsy values will cause `class` to be omitted.
|
||||
|
||||
### Objects and arrays
|
||||
|
||||
Since Svelte 5.16, `class` can be an object or array, and is converted to a string using [clsx](https://github.com/lukeed/clsx).
|
||||
|
||||
If the value is an object, the truthy keys are added:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { cool } = $props();
|
||||
</script>
|
||||
|
||||
<!-- results in `class="cool"` if `cool` is truthy,
|
||||
`class="lame"` otherwise -->
|
||||
<div class={{ cool, lame: !cool }}>...</div>
|
||||
```
|
||||
|
||||
If the value is an array, the truthy values are combined:
|
||||
|
||||
```svelte
|
||||
<!-- if `faded` and `large` are both truthy, results in
|
||||
`class="saturate-0 opacity-50 scale-200"` -->
|
||||
<div class={[faded && 'saturate-0 opacity-50', large && 'scale-200']}>...</div>
|
||||
```
|
||||
|
||||
Note that whether we're using the array or object form, we can set multiple classes simultaneously with a single condition, which is particularly useful if you're using things like Tailwind.
|
||||
|
||||
Arrays can contain arrays and objects, and clsx will flatten them. This is useful for combining local classes with props, for example:
|
||||
|
||||
```svelte
|
||||
<!--- file: Button.svelte --->
|
||||
<script>
|
||||
let props = $props();
|
||||
</script>
|
||||
|
||||
<button {...props} class={['cool-button', props.class]}>
|
||||
{@render props.children?.()}
|
||||
</button>
|
||||
```
|
||||
|
||||
The user of this component has the same flexibility to use a mixture of objects, arrays and strings:
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import Button from './Button.svelte';
|
||||
let useTailwind = $state(false);
|
||||
</script>
|
||||
|
||||
<Button
|
||||
onclick={() => useTailwind = true}
|
||||
class={{ 'bg-blue-700 sm:w-1/2': useTailwind }}
|
||||
>
|
||||
Accept the inevitability of Tailwind
|
||||
</Button>
|
||||
```
|
||||
|
||||
## The `class:` directive
|
||||
|
||||
Prior to Svelte 5.16, the `class:` directive was the most convenient way to set classes on elements conditionally.
|
||||
|
||||
```svelte
|
||||
<!-- These are equivalent -->
|
||||
<div class={{ cool, lame: !cool }}>...</div>
|
||||
<div class:cool={cool} class:lame={!cool}>...</div>
|
||||
```
|
||||
|
||||
As with other directives, we can use a shorthand when the name of the class coincides with the value:
|
||||
|
||||
```svelte
|
||||
<div class:cool class:lame={!cool}>...</div>
|
||||
```
|
||||
|
||||
> [!NOTE] Unless you're using an older version of Svelte, consider avoiding `class:`, since the attribute is more powerful and composable.
|
@ -0,0 +1,20 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
warnings: [
|
||||
{
|
||||
code: 'css_unused_selector',
|
||||
message: 'Unused CSS selector ".unused"\nhttps://svelte.dev/e/css_unused_selector',
|
||||
start: {
|
||||
line: 24,
|
||||
column: 1,
|
||||
character: 548
|
||||
},
|
||||
end: {
|
||||
line: 24,
|
||||
column: 8,
|
||||
character: 555
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
@ -0,0 +1,12 @@
|
||||
|
||||
.used1.svelte-xyz { color: green; }
|
||||
.used2.svelte-xyz { color: green; }
|
||||
.used3.svelte-xyz { color: green; }
|
||||
.used4.svelte-xyz { color: green; }
|
||||
.used5.svelte-xyz { color: green; }
|
||||
.used6.svelte-xyz { color: green; }
|
||||
.used7.svelte-xyz { color: green; }
|
||||
.used8.svelte-xyz { color: green; }
|
||||
.used9.svelte-xyz { color: green; }
|
||||
|
||||
/* (unused) .unused { color: red; }*/
|
@ -0,0 +1,25 @@
|
||||
<script>
|
||||
let condition = Math.random() < 0.5;
|
||||
</script>
|
||||
|
||||
<p class={['used1']}></p>
|
||||
<p class={[{ used2: true }]}></p>
|
||||
<p class={{ used3: true }}></p>
|
||||
<p class={{ 'used4 used5': true }}></p>
|
||||
<p class={{ used6 }}></p>
|
||||
<p class={[condition ? 'used7' : 'used8']}></p>
|
||||
<p class={[condition && 'used9']}></p>
|
||||
|
||||
<style>
|
||||
.used1 { color: green; }
|
||||
.used2 { color: green; }
|
||||
.used3 { color: green; }
|
||||
.used4 { color: green; }
|
||||
.used5 { color: green; }
|
||||
.used6 { color: green; }
|
||||
.used7 { color: green; }
|
||||
.used8 { color: green; }
|
||||
.used9 { color: green; }
|
||||
|
||||
.unused { color: red; }
|
||||
</style>
|
@ -0,0 +1,2 @@
|
||||
|
||||
.x.svelte-xyz { color: green; }
|
@ -0,0 +1,5 @@
|
||||
<h1 class={[foo]}>hello world</h1>
|
||||
|
||||
<style>
|
||||
.x { color: green; }
|
||||
</style>
|
@ -0,0 +1,2 @@
|
||||
|
||||
.x.svelte-xyz { color: green; }
|
@ -0,0 +1,5 @@
|
||||
<h1 class={{ foo: true, ...rest }}>hello world</h1>
|
||||
|
||||
<style>
|
||||
.x { color: green; }
|
||||
</style>
|
@ -0,0 +1,2 @@
|
||||
|
||||
.x.svelte-xyz { color: green; }
|
@ -0,0 +1,5 @@
|
||||
<h1 class={{ [foo]: true }}>hello world</h1>
|
||||
|
||||
<style>
|
||||
.x { color: green; }
|
||||
</style>
|
@ -0,0 +1,43 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
html: `
|
||||
<div class="foo svelte-owbekl"></div>
|
||||
<div class="foo svelte-owbekl"></div>
|
||||
<div class="foo svelte-owbekl"></div>
|
||||
<div class="foo svelte-owbekl"></div>
|
||||
<div class="foo svelte-owbekl"></div>
|
||||
|
||||
<div class="foo">child</div>
|
||||
<div class="foo">child</div>
|
||||
<div class="foo">child</div>
|
||||
<div class="foo">child</div>
|
||||
<div class="foo">child</div>
|
||||
|
||||
<button>update</button>
|
||||
`,
|
||||
test({ assert, target }) {
|
||||
const button = target.querySelector('button');
|
||||
|
||||
button?.click();
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`
|
||||
<div class="foo svelte-owbekl"></div>
|
||||
<div class="foo svelte-owbekl"></div>
|
||||
<div class="foo svelte-owbekl"></div>
|
||||
<div class="foo svelte-owbekl"></div>
|
||||
<div class="foo svelte-owbekl"></div>
|
||||
|
||||
<div class="foo">child</div>
|
||||
<div class="foo">child</div>
|
||||
<div class="foo">child</div>
|
||||
<div class="foo">child</div>
|
||||
<div class="foo">child</div>
|
||||
|
||||
<button>update</button>
|
||||
`
|
||||
);
|
||||
}
|
||||
});
|
@ -0,0 +1,5 @@
|
||||
<script>
|
||||
let { class: cls } = $props();
|
||||
</script>
|
||||
|
||||
<div class={cls}>child</div>
|
@ -0,0 +1,33 @@
|
||||
<script>
|
||||
import Child from "./child.svelte";
|
||||
|
||||
let foo = $state('foo');
|
||||
let bar = $state(null);
|
||||
let spread = { class: { foo: true, bar: false } };
|
||||
</script>
|
||||
|
||||
<div class={{ foo: true, bar: false }}></div>
|
||||
<div class={['foo', false && 'bar']}></div>
|
||||
<div class={{ foo, bar }}></div>
|
||||
<div class={[ foo, bar ]}></div>
|
||||
<div {...spread}></div>
|
||||
|
||||
<Child class={{ foo: true, bar: false }} />
|
||||
<Child class={['foo', false && 'bar']} />
|
||||
<Child class={{ foo, bar }} />
|
||||
<Child class={[ foo, bar ]} />
|
||||
<Child {...spread} />
|
||||
|
||||
<button onclick={() => {
|
||||
foo = null;
|
||||
bar = 'bar';
|
||||
}}>update</button>
|
||||
|
||||
<style>
|
||||
.foo {
|
||||
color: red;
|
||||
}
|
||||
.bar {
|
||||
color: blue;
|
||||
}
|
||||
</style>
|
Loading…
Reference in new issue