Skip to content

Commit 6564b41

Browse files
authored
Merge branch 'main' into fix-14815
2 parents 7e78082 + 7f8acb8 commit 6564b41

File tree

50 files changed

+489
-82
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+489
-82
lines changed

documentation/docs/03-template-syntax/16-class.md

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
---
2+
title: class
3+
---
4+
5+
There are two ways to set classes on elements: the `class` attribute, and the `class:` directive.
6+
7+
## Attributes
8+
9+
Primitive values are treated like any other attribute:
10+
11+
```svelte
12+
<div class={large ? 'large' : 'small'}>...</div>
13+
```
14+
15+
> [!NOTE]
16+
> 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.
17+
18+
### Objects and arrays
19+
20+
Since Svelte 5.16, `class` can be an object or array, and is converted to a string using [clsx](https://github.com/lukeed/clsx).
21+
22+
If the value is an object, the truthy keys are added:
23+
24+
```svelte
25+
<script>
26+
let { cool } = $props();
27+
</script>
28+
29+
<!-- results in `class="cool"` if `cool` is truthy,
30+
`class="lame"` otherwise -->
31+
<div class={{ cool, lame: !cool }}>...</div>
32+
```
33+
34+
If the value is an array, the truthy values are combined:
35+
36+
```svelte
37+
<!-- if `faded` and `large` are both truthy, results in
38+
`class="saturate-0 opacity-50 scale-200"` -->
39+
<div class={[faded && 'saturate-0 opacity-50', large && 'scale-200']}>...</div>
40+
```
41+
42+
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.
43+
44+
Arrays can contain arrays and objects, and clsx will flatten them. This is useful for combining local classes with props, for example:
45+
46+
```svelte
47+
<!--- file: Button.svelte --->
48+
<script>
49+
let props = $props();
50+
</script>
51+
52+
<button {...props} class={['cool-button', props.class]}>
53+
{@render props.children?.()}
54+
</button>
55+
```
56+
57+
The user of this component has the same flexibility to use a mixture of objects, arrays and strings:
58+
59+
```svelte
60+
<!--- file: App.svelte --->
61+
<script>
62+
import Button from './Button.svelte';
63+
let useTailwind = $state(false);
64+
</script>
65+
66+
<Button
67+
onclick={() => useTailwind = true}
68+
class={{ 'bg-blue-700 sm:w-1/2': useTailwind }}
69+
>
70+
Accept the inevitability of Tailwind
71+
</Button>
72+
```
73+
74+
## The `class:` directive
75+
76+
Prior to Svelte 5.16, the `class:` directive was the most convenient way to set classes on elements conditionally.
77+
78+
```svelte
79+
<!-- These are equivalent -->
80+
<div class={{ cool, lame: !cool }}>...</div>
81+
<div class:cool={cool} class:lame={!cool}>...</div>
82+
```
83+
84+
As with other directives, we can use a shorthand when the name of the class coincides with the value:
85+
86+
```svelte
87+
<div class:cool class:lame={!cool}>...</div>
88+
```
89+
90+
> [!NOTE] Unless you're using an older version of Svelte, consider avoiding `class:`, since the attribute is more powerful and composable.

documentation/docs/98-reference/.generated/client-errors.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,19 @@ A component is attempting to bind to a non-bindable property `%key%` belonging t
2121
### component_api_changed
2222

2323
```
24-
%parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes for more information
24+
%parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5
2525
```
2626

27+
See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more information.
28+
2729
### component_api_invalid_new
2830

2931
```
30-
Attempted to instantiate %component% with `new %name%`, which is no longer valid in Svelte 5. If this component is not under your control, set the `compatibility.componentApi` compiler option to `4` to keep it working. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes for more information
32+
Attempted to instantiate %component% with `new %name%`, which is no longer valid in Svelte 5. If this component is not under your control, set the `compatibility.componentApi` compiler option to `4` to keep it working.
3133
```
3234

35+
See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more information.
36+
3337
### derived_references_self
3438

3539
```

documentation/docs/98-reference/.generated/client-warnings.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Your `console.%method%` contained `$state` proxies. Consider using `$inspect(...
5252
5353
When logging a [proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), browser devtools will log the proxy itself rather than the value it represents. In the case of Svelte, the 'target' of a `$state` proxy might not resemble its current value, which can be confusing.
5454
55-
The easiest way to log a value as it changes over time is to use the [`$inspect`](https://svelte.dev/docs/svelte/$inspect) rune. Alternatively, to log things on a one-off basis (for example, inside an event handler) you can use [`$state.snapshot`](https://svelte.dev/docs/svelte/$state#$state.snapshot) to take a snapshot of the current value.
55+
The easiest way to log a value as it changes over time is to use the [`$inspect`](/docs/svelte/$inspect) rune. Alternatively, to log things on a one-off basis (for example, inside an event handler) you can use [`$state.snapshot`](/docs/svelte/$state#$state.snapshot) to take a snapshot of the current value.
5656
5757
### event_handler_invalid
5858

documentation/docs/98-reference/.generated/compile-warnings.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Enforce that `autofocus` is not used on elements. Autofocusing elements can caus
6262
### a11y_click_events_have_key_events
6363

6464
```
65-
Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details
65+
Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate
6666
```
6767

6868
Enforce that visible, non-interactive elements with an `onclick` event are accompanied by a keyboard event handler.

packages/svelte/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# svelte
22

3+
## 5.16.0
4+
5+
### Minor Changes
6+
7+
- feat: allow `class` attribute to be an object or array, using `clsx` ([#14714](https://github.com/sveltejs/svelte/pull/14714))
8+
9+
### Patch Changes
10+
11+
- fix: don't include keyframes in global scope in the keyframes to rename ([#14822](https://github.com/sveltejs/svelte/pull/14822))
12+
313
## 5.15.0
414

515
### Minor Changes

packages/svelte/elements.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -741,7 +741,7 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
741741
accesskey?: string | undefined | null;
742742
autocapitalize?: 'characters' | 'off' | 'on' | 'none' | 'sentences' | 'words' | undefined | null;
743743
autofocus?: boolean | undefined | null;
744-
class?: string | undefined | null;
744+
class?: string | import('clsx').ClassArray | import('clsx').ClassDictionary | undefined | null;
745745
contenteditable?: Booleanish | 'inherit' | 'plaintext-only' | undefined | null;
746746
contextmenu?: string | undefined | null;
747747
dir?: 'ltr' | 'rtl' | 'auto' | undefined | null;
@@ -1522,7 +1522,7 @@ export interface SvelteWindowAttributes extends HTMLAttributes<Window> {
15221522
export interface SVGAttributes<T extends EventTarget> extends AriaAttributes, DOMAttributes<T> {
15231523
// Attributes which also defined in HTMLAttributes
15241524
className?: string | undefined | null;
1525-
class?: string | undefined | null;
1525+
class?: string | import('clsx').ClassArray | import('clsx').ClassDictionary | undefined | null;
15261526
color?: string | undefined | null;
15271527
height?: number | string | undefined | null;
15281528
id?: string | undefined | null;

packages/svelte/messages/client-errors/errors.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,15 @@
1212
1313
## component_api_changed
1414

15-
> %parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes for more information
15+
> %parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5
16+
17+
See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more information.
1618

1719
## component_api_invalid_new
1820

19-
> Attempted to instantiate %component% with `new %name%`, which is no longer valid in Svelte 5. If this component is not under your control, set the `compatibility.componentApi` compiler option to `4` to keep it working. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes for more information
21+
> Attempted to instantiate %component% with `new %name%`, which is no longer valid in Svelte 5. If this component is not under your control, set the `compatibility.componentApi` compiler option to `4` to keep it working.
22+
23+
See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more information.
2024

2125
## derived_references_self
2226

packages/svelte/messages/client-warnings/warnings.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ function add() {
4242
4343
When logging a [proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), browser devtools will log the proxy itself rather than the value it represents. In the case of Svelte, the 'target' of a `$state` proxy might not resemble its current value, which can be confusing.
4444
45-
The easiest way to log a value as it changes over time is to use the [`$inspect`](https://svelte.dev/docs/svelte/$inspect) rune. Alternatively, to log things on a one-off basis (for example, inside an event handler) you can use [`$state.snapshot`](https://svelte.dev/docs/svelte/$state#$state.snapshot) to take a snapshot of the current value.
45+
The easiest way to log a value as it changes over time is to use the [`$inspect`](/docs/svelte/$inspect) rune. Alternatively, to log things on a one-off basis (for example, inside an event handler) you can use [`$state.snapshot`](/docs/svelte/$state#$state.snapshot) to take a snapshot of the current value.
4646
4747
## event_handler_invalid
4848

packages/svelte/messages/compile-warnings/a11y.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Enforce that `autofocus` is not used on elements. Autofocusing elements can caus
4949

5050
## a11y_click_events_have_key_events
5151

52-
> Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details
52+
> Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate
5353
5454
Enforce that visible, non-interactive elements with an `onclick` event are accompanied by a keyboard event handler.
5555

packages/svelte/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "svelte",
33
"description": "Cybernetically enhanced web apps",
44
"license": "MIT",
5-
"version": "5.15.0",
5+
"version": "5.16.0",
66
"type": "module",
77
"types": "./types/index.d.ts",
88
"engines": {
@@ -153,6 +153,7 @@
153153
"acorn-typescript": "^1.4.13",
154154
"aria-query": "^5.3.1",
155155
"axobject-query": "^4.1.0",
156+
"clsx": "^2.1.1",
156157
"esm-env": "^1.2.1",
157158
"esrap": "^1.3.2",
158159
"is-reference": "^3.0.3",

packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,19 @@ function is_global_block_selector(simple_selector) {
2828
);
2929
}
3030

31+
/**
32+
*
33+
* @param {Array<AST.CSS.Node>} path
34+
*/
35+
function is_in_global_block(path) {
36+
return path.some((node) => node.type === 'Rule' && node.metadata.is_global_block);
37+
}
38+
3139
/** @type {CssVisitors} */
3240
const css_visitors = {
3341
Atrule(node, context) {
3442
if (is_keyframes_node(node)) {
35-
if (!node.prelude.startsWith('-global-')) {
43+
if (!node.prelude.startsWith('-global-') && !is_in_global_block(context.path)) {
3644
context.state.keyframes.push(node.prelude);
3745
}
3846
}

packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,7 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv
731731
/** @type {string[]} */
732732
let prev_values = [];
733733
for (const chunk of chunks) {
734-
const current_possible_values = get_possible_values(chunk);
734+
const current_possible_values = get_possible_values(chunk, name === 'class');
735735

736736
// impossible to find out all combinations
737737
if (!current_possible_values) return true;
@@ -784,7 +784,7 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv
784784
prev_values.push(current_possible_value);
785785
}
786786
});
787-
if (prev_values.length < current_possible_values.size) {
787+
if (prev_values.length < current_possible_values.length) {
788788
prev_values.push(' ');
789789
}
790790
if (prev_values.length > 20) {

packages/svelte/src/compiler/phases/2-analyze/css/utils.js

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,95 @@ const UNKNOWN = {};
44

55
/**
66
* @param {Node} node
7+
* @param {boolean} is_class
78
* @param {Set<any>} set
9+
* @param {boolean} is_nested
810
*/
9-
function gather_possible_values(node, set) {
11+
function gather_possible_values(node, is_class, set, is_nested = false) {
12+
if (set.has(UNKNOWN)) {
13+
// no point traversing any further
14+
return;
15+
}
16+
1017
if (node.type === 'Literal') {
1118
set.add(String(node.value));
1219
} else if (node.type === 'ConditionalExpression') {
13-
gather_possible_values(node.consequent, set);
14-
gather_possible_values(node.alternate, set);
20+
gather_possible_values(node.consequent, is_class, set, is_nested);
21+
gather_possible_values(node.alternate, is_class, set, is_nested);
22+
} else if (node.type === 'LogicalExpression') {
23+
if (node.operator === '&&') {
24+
// && is a special case, because the only way the left
25+
// hand value can be included is if it's falsy. this is
26+
// a bit of extra work but it's worth it because
27+
// `class={[condition && 'blah']}` is common,
28+
// and we don't want to deopt on `condition`
29+
const left = new Set();
30+
gather_possible_values(node.left, is_class, left, is_nested);
31+
32+
if (left.has(UNKNOWN)) {
33+
// add all non-nullish falsy values, unless this is a `class` attribute that
34+
// will be processed by cslx, in which case falsy values are removed, unless
35+
// they're not inside an array/object (TODO 6.0 remove that last part)
36+
if (!is_class || !is_nested) {
37+
set.add('');
38+
set.add(false);
39+
set.add(NaN);
40+
set.add(0); // -0 and 0n are also falsy, but stringify to '0'
41+
}
42+
} else {
43+
for (const value of left) {
44+
if (!value && value != undefined && (!is_class || !is_nested)) {
45+
set.add(value);
46+
}
47+
}
48+
}
49+
50+
gather_possible_values(node.right, is_class, set, is_nested);
51+
} else {
52+
gather_possible_values(node.left, is_class, set, is_nested);
53+
gather_possible_values(node.right, is_class, set, is_nested);
54+
}
55+
} else if (is_class && node.type === 'ArrayExpression') {
56+
for (const entry of node.elements) {
57+
if (entry) {
58+
gather_possible_values(entry, is_class, set, true);
59+
}
60+
}
61+
} else if (is_class && node.type === 'ObjectExpression') {
62+
for (const property of node.properties) {
63+
if (
64+
property.type === 'Property' &&
65+
!property.computed &&
66+
(property.key.type === 'Identifier' || property.key.type === 'Literal')
67+
) {
68+
set.add(
69+
property.key.type === 'Identifier' ? property.key.name : String(property.key.value)
70+
);
71+
} else {
72+
set.add(UNKNOWN);
73+
}
74+
}
1575
} else {
1676
set.add(UNKNOWN);
1777
}
1878
}
1979

2080
/**
2181
* @param {AST.Text | AST.ExpressionTag} chunk
22-
* @returns {Set<string> | null}
82+
* @param {boolean} is_class
83+
* @returns {string[] | null}
2384
*/
24-
export function get_possible_values(chunk) {
85+
export function get_possible_values(chunk, is_class) {
2586
const values = new Set();
2687

2788
if (chunk.type === 'Text') {
2889
values.add(chunk.data);
2990
} else {
30-
gather_possible_values(chunk.expression, values);
91+
gather_possible_values(chunk.expression, is_class, values);
3192
}
3293

3394
if (values.has(UNKNOWN)) return null;
34-
return values;
95+
return [...values].map((value) => String(value));
3596
}
3697

3798
/**

packages/svelte/src/compiler/phases/2-analyze/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,8 @@ export function analyze_component(root, source, options) {
773773

774774
if (attribute.type !== 'Attribute') continue;
775775
if (attribute.name.toLowerCase() !== 'class') continue;
776+
// The dynamic class method appends the hash to the end of the class attribute on its own
777+
if (attribute.metadata.needs_clsx) continue outer;
776778

777779
class_attribute = attribute;
778780
}

0 commit comments

Comments
 (0)