Skip to content

Conversation

alex-snezhko
Copy link
Contributor

fix #13025 for class

Match behavior of style overriding present in normalizeStyle for normalizeClass.

Example: <div class="foo" :class="{ foo: false }"></div> -> <div class=""></div>

I have assumed that the discrepancy between class and style is a bug hence listing this as a fix.

Copy link

Size Report

Bundles

File Size Gzip Brotli
runtime-dom.global.prod.js 101 kB (+289 B) 38.6 kB (+108 B) 34.7 kB (+81 B)
vue.global.prod.js 159 kB (+289 B) 58.7 kB (+119 B) 52.2 kB (+54 B)

Usages

Name Size Gzip Brotli
createApp (CAPI only) 46.9 kB (+290 B) 18.3 kB (+98 B) 16.8 kB (+82 B)
createApp 54.9 kB (+290 B) 21.4 kB (+95 B) 19.5 kB (+89 B)
createSSRApp 59.2 kB (+290 B) 23.1 kB (+103 B) 21.1 kB (+81 B)
defineCustomElement 59.9 kB (+290 B) 23 kB (+113 B) 20.9 kB (+80 B)
overall 69 kB (+290 B) 26.6 kB (+111 B) 24.2 kB (+48 B)

Copy link

pkg-pr-new bot commented Sep 21, 2025

Open in StackBlitz

@vue/compiler-core

npm i https://pkg.pr.new/@vue/compiler-core@13911

@vue/compiler-dom

npm i https://pkg.pr.new/@vue/compiler-dom@13911

@vue/compiler-sfc

npm i https://pkg.pr.new/@vue/compiler-sfc@13911

@vue/compiler-ssr

npm i https://pkg.pr.new/@vue/compiler-ssr@13911

@vue/reactivity

npm i https://pkg.pr.new/@vue/reactivity@13911

@vue/runtime-core

npm i https://pkg.pr.new/@vue/runtime-core@13911

@vue/runtime-dom

npm i https://pkg.pr.new/@vue/runtime-dom@13911

@vue/server-renderer

npm i https://pkg.pr.new/@vue/server-renderer@13911

@vue/shared

npm i https://pkg.pr.new/@vue/shared@13911

vue

npm i https://pkg.pr.new/vue@13911

@vue/compat

npm i https://pkg.pr.new/@vue/compat@13911

commit: fe15e02

@edison1105 edison1105 added the 🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed. label Sep 23, 2025
@skirtles-code
Copy link
Contributor

This change adds quite a few bytes to the build. Not necessarily a problem, but it's something I think we need to take into account when deciding whether this fix is worth it.

The underlying problem described in #13025 is interesting, but I don't think changing how we merge classes is the correct way to address that use case.

I think changing how normalizeClass behaves will break a lot of existing applications. It's common for elements to accrue classes from a variety of sources, including attribute inheritance.

This change would alter the semantics of falsey values from 'do nothing' to 'remove this class'. Currently, :class="{ a: b }" is equivalent to :class="b ? 'a' : ''". It's purely an additive process, adding the class a if required. It doesn't block that same class from being added by an earlier source.

class and style are fundamentally different and their handling reflects that. I don't see this as an inconsistency, they're just different.

I think the correct mental model for style is a map, with key/value pairs. Merging those maps needs a way to handle clashing keys. Browsers already need to handle this for HTML, e.g. style="color: red; color: blue" will use blue as the value for color. Our handling reflects that and gives priority to the last value. We allow an object to be used with :style as its a natural fit for specifying a map.

The mental model for class is different, that behaves more like a set. When we merge those sets we essentially just take the union of those sets. (I'm glossing over duplicate classes, but I don't think they matter from the perspective of the mental model). When we use an object with :class it should be seen as a convenient way to specify one of those sets. Allowing the truthiness of the value to determine inclusion is useful for conditionals, but a falsey value is only intended to indicate that the key should not be added to current set. That information isn't supposed to be retained beyond the creation of the current set and shouldn't be taken into account when taking the union of the sets.

@alex-snezhko
Copy link
Contributor Author

@skirtles-code Thanks for the feedback!

I completely agree about the map vs. set mental models for styles and classes, but I don't necessarily think that this set mental model is at odds with the class removal logic i.e. the mental model assumed by this PR is that there is also a set difference operation for the new set of falsy classes instead of only a union with the added ones.

From some testing I'm seeing that some other frameworks that have a class truthiness-inclusion feature also utilize this class removal logic on merging. Of course that's not to say that Vue should also follow suit just because other projects are doing it or that their behavior is inherently better, but just some data points about this removal logic not being too far-fetched:

  • Angular via single-class binds and also with the ngClass directive
  • Svelte via class directives. It is a little different since the ordering of the directive vs the base class on the HTML element does not make a difference, but the override-removal behavior is still there

There are also some counterexamples I found to be fair:

  • clsx is purely-additive when merging
  • classnames is also purely-additive (though the classnames/dedupe subpackage does do removals on subsequent falsy values)

That being said, I understand the concern of potentially breaking existing applications with this change, especially if you otherwise feel that the value proposition of this PR isn't worth it. I suppose a compiler feature flag could be used to avoid the breaking change, but that has its own set of cons.

Do you have a different idea for how #13025 should be addressed (or if it's even a problem at all)? If so then I'd be happy to rework this PR to accommodate for it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Overriding existing HTML for the sake hydration isn't consistent
3 participants