Skip to content

Commit abde4c9

Browse files
Only apply hover on devices that support hover (#14500)
This PR updates the `hover` variant to only apply when `@media (hover: hover)` matches. ```diff .hover\:bg-black { &:hover { + @media (hover: hover) { background: black; + } } } ``` This is technically a breaking change because you may have built your site in a way where some interactions depend on hover (like opening a dropdown menu), and were relying on the fact that tapping on mobile triggers hover. To bring back the old hover behavior, users can override the `hover` variant in their CSS file back to the simpler implementation: ```css @import "tailwindcss"; @variant hover (&:hover); ``` I've opted to go with just `@media (hover: hover)` for this because it seems like the best trade-off between the available options. Using `(any-hover: hover)` would mean users would get sticky hover states when tapping on an iPad if they have a mouse or trackpad connected, which feels wrong to me because in those cases touch is still likely the primary method of interaction. Sites built with this feature in mind will be treating hover styles as progressive enhancement, so it seems better to me that using an iPad with a mouse would not have hover styles, vs. having sticky hover styles in the same situation. Of course users can always override this with whatever they want, so making this the default isn't locking anyone in to a particular choice. --------- Co-authored-by: Adam Wathan <[email protected]> Co-authored-by: Robin Malfait <[email protected]>
1 parent a270e2c commit abde4c9

File tree

7 files changed

+249
-153
lines changed

7 files changed

+249
-153
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3636
- Preserve explicit transition duration and timing function when overriding transition property ([#14490](https://github.com/tailwindlabs/tailwindcss/pull/14490))
3737
- Change the implementation for `@import` resolution to speed up initial builds ([#14446](https://github.com/tailwindlabs/tailwindcss/pull/14446))
3838
- Remove automatic `var(…)` injection ([#13657](https://github.com/tailwindlabs/tailwindcss/pull/13657))
39+
- Only apply `:hover` states on devices that support `@media (hover: hover)` ([#14500](https://github.com/tailwindlabs/tailwindcss/pull/14500))
3940

4041
## [4.0.0-alpha.24] - 2024-09-11
4142

packages/tailwindcss/playwright.config.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,31 @@ export default defineConfig({
4242
},
4343
{
4444
name: 'firefox',
45-
use: { ...devices['Desktop Firefox'] },
45+
use: {
46+
...devices['Desktop Firefox'],
47+
// https://playwright.dev/docs/test-use-options#more-browser-and-context-options
48+
launchOptions: {
49+
// https://playwright.dev/docs/api/class-browsertype#browser-type-launch-option-firefox-user-prefs
50+
firefoxUserPrefs: {
51+
// By default, headless Firefox runs as though no pointers
52+
// capabilities are available.
53+
// https://github.com/microsoft/playwright/issues/7769#issuecomment-966098074
54+
//
55+
// This impacts our `hover` variant implementation which uses an
56+
// '(hover: hover)' media query to determine if hover is available.
57+
//
58+
// Available values for pointer capabilities:
59+
// NO_POINTER = 0x00;
60+
// COARSE_POINTER = 0x01;
61+
// FINE_POINTER = 0x02;
62+
// HOVER_CAPABLE_POINTER = 0x04;
63+
//
64+
// Setting to 0x02 | 0x04 says the system supports a mouse
65+
'ui.primaryPointerCapabilities': 0x02 | 0x04,
66+
'ui.allPointerCapabilities': 0x02 | 0x04,
67+
},
68+
},
69+
},
4670
},
4771

4872
/* Test against mobile viewports. */

packages/tailwindcss/src/compat/plugin-api.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1919,8 +1919,10 @@ describe('matchVariant', () => {
19191919
"@layer utilities {
19201920
@media (width >= 100px) {
19211921
@media (width <= 200px) {
1922-
.testmin-\\[100px\\]\\:testmax-\\[200px\\]\\:hover\\:underline:hover {
1923-
text-decoration-line: underline;
1922+
@media (hover: hover) {
1923+
.testmin-\\[100px\\]\\:testmax-\\[200px\\]\\:hover\\:underline:hover {
1924+
text-decoration-line: underline;
1925+
}
19241926
}
19251927
}
19261928
}

packages/tailwindcss/src/index.test.ts

Lines changed: 89 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ describe('compiling CSS', () => {
3535
display: flex;
3636
}
3737
38-
.hover\\:underline:hover {
39-
text-decoration-line: underline;
38+
@media (hover: hover) {
39+
.hover\\:underline:hover {
40+
text-decoration-line: underline;
41+
}
4042
}
4143
4244
@media (width >= 768px) {
@@ -193,8 +195,10 @@ describe('@apply', () => {
193195
text-decoration-line: underline;
194196
}
195197
196-
.foo:hover {
197-
background-color: var(--color-blue-500, #3b82f6);
198+
@media (hover: hover) {
199+
.foo:hover {
200+
background-color: var(--color-blue-500, #3b82f6);
201+
}
198202
}
199203
200204
@media (width >= 768px) {
@@ -390,16 +394,20 @@ describe('arbitrary variants', () => {
390394
describe('variant stacking', () => {
391395
it('should stack simple variants', async () => {
392396
expect(await run(['focus:hover:flex'])).toMatchInlineSnapshot(`
393-
".focus\\:hover\\:flex:focus:hover {
394-
display: flex;
397+
"@media (hover: hover) {
398+
.focus\\:hover\\:flex:focus:hover {
399+
display: flex;
400+
}
395401
}"
396402
`)
397403
})
398404

399405
it('should stack arbitrary variants and simple variants', async () => {
400406
expect(await run(['[&_p]:hover:flex'])).toMatchInlineSnapshot(`
401-
".\\[\\&_p\\]\\:hover\\:flex p:hover {
402-
display: flex;
407+
"@media (hover: hover) {
408+
.\\[\\&_p\\]\\:hover\\:flex p:hover {
409+
display: flex;
410+
}
403411
}"
404412
`)
405413
})
@@ -420,13 +428,17 @@ describe('variant stacking', () => {
420428
content: var(--tw-content);
421429
}
422430
423-
.before\\:hover\\:flex:before:hover {
424-
display: flex;
431+
@media (hover: hover) {
432+
.before\\:hover\\:flex:before:hover {
433+
display: flex;
434+
}
425435
}
426436
427-
.hover\\:before\\:flex:hover:before {
428-
content: var(--tw-content);
429-
display: flex;
437+
@media (hover: hover) {
438+
.hover\\:before\\:flex:hover:before {
439+
content: var(--tw-content);
440+
display: flex;
441+
}
430442
}
431443
432444
@supports (-moz-orient: inline) {
@@ -627,22 +639,24 @@ describe('sorting', () => {
627639
),
628640
),
629641
).toMatchInlineSnapshot(`
630-
".pointer-events-none {
631-
pointer-events: none;
632-
}
642+
".pointer-events-none {
643+
pointer-events: none;
644+
}
633645
634-
.flex {
635-
display: flex;
636-
}
646+
.flex {
647+
display: flex;
648+
}
637649
650+
@media (hover: hover) {
638651
.hover\\:flex:hover {
639652
display: flex;
640653
}
654+
}
641655
642-
.focus\\:pointer-events-none:focus {
643-
pointer-events: none;
644-
}"
645-
`)
656+
.focus\\:pointer-events-none:focus {
657+
pointer-events: none;
658+
}"
659+
`)
646660
})
647661

648662
/**
@@ -672,16 +686,20 @@ describe('sorting', () => {
672686
display: flex;
673687
}
674688
675-
.hover\\:flex:hover {
676-
display: flex;
689+
@media (hover: hover) {
690+
.hover\\:flex:hover {
691+
display: flex;
692+
}
677693
}
678694
679695
.focus\\:flex:focus {
680696
display: flex;
681697
}
682698
683-
.hover\\:focus\\:flex:hover:focus {
684-
display: flex;
699+
@media (hover: hover) {
700+
.hover\\:focus\\:flex:hover:focus {
701+
display: flex;
702+
}
685703
}
686704
687705
.disabled\\:flex:disabled {
@@ -715,44 +733,64 @@ describe('sorting', () => {
715733
].sort(() => Math.random() - 0.5),
716734
),
717735
).toMatchInlineSnapshot(`
718-
".group-hover\\:flex:is(:where(.group):hover *) {
719-
display: flex;
736+
"@media (hover: hover) {
737+
.group-hover\\:flex:is(:where(.group):hover *) {
738+
display: flex;
739+
}
720740
}
721741
722742
.group-focus\\:flex:is(:where(.group):focus *) {
723743
display: flex;
724744
}
725745
726-
.peer-hover\\:flex:is(:where(.peer):hover ~ *) {
727-
display: flex;
746+
@media (hover: hover) {
747+
.peer-hover\\:flex:is(:where(.peer):hover ~ *) {
748+
display: flex;
749+
}
728750
}
729751
730-
.group-hover\\:peer-hover\\:flex:is(:where(.group):hover *):is(:where(.peer):hover ~ *) {
731-
display: flex;
752+
@media (hover: hover) {
753+
@media (hover: hover) {
754+
.group-hover\\:peer-hover\\:flex:is(:where(.group):hover *):is(:where(.peer):hover ~ *) {
755+
display: flex;
756+
}
757+
}
732758
}
733759
734-
.peer-hover\\:group-hover\\:flex:is(:where(.peer):hover ~ *):is(:where(.group):hover *) {
735-
display: flex;
760+
@media (hover: hover) {
761+
@media (hover: hover) {
762+
.peer-hover\\:group-hover\\:flex:is(:where(.peer):hover ~ *):is(:where(.group):hover *) {
763+
display: flex;
764+
}
765+
}
736766
}
737767
738-
.group-focus\\:peer-hover\\:flex:is(:where(.group):focus *):is(:where(.peer):hover ~ *) {
739-
display: flex;
768+
@media (hover: hover) {
769+
.group-focus\\:peer-hover\\:flex:is(:where(.group):focus *):is(:where(.peer):hover ~ *) {
770+
display: flex;
771+
}
740772
}
741773
742-
.peer-hover\\:group-focus\\:flex:is(:where(.peer):hover ~ *):is(:where(.group):focus *) {
743-
display: flex;
774+
@media (hover: hover) {
775+
.peer-hover\\:group-focus\\:flex:is(:where(.peer):hover ~ *):is(:where(.group):focus *) {
776+
display: flex;
777+
}
744778
}
745779
746780
.peer-focus\\:flex:is(:where(.peer):focus ~ *) {
747781
display: flex;
748782
}
749783
750-
.group-hover\\:peer-focus\\:flex:is(:where(.group):hover *):is(:where(.peer):focus ~ *) {
751-
display: flex;
784+
@media (hover: hover) {
785+
.group-hover\\:peer-focus\\:flex:is(:where(.group):hover *):is(:where(.peer):focus ~ *) {
786+
display: flex;
787+
}
752788
}
753789
754-
.peer-focus\\:group-hover\\:flex:is(:where(.peer):focus ~ *):is(:where(.group):hover *) {
755-
display: flex;
790+
@media (hover: hover) {
791+
.peer-focus\\:group-hover\\:flex:is(:where(.peer):focus ~ *):is(:where(.group):hover *) {
792+
display: flex;
793+
}
756794
}
757795
758796
.group-focus\\:peer-focus\\:flex:is(:where(.group):focus *):is(:where(.peer):focus ~ *) {
@@ -763,8 +801,10 @@ describe('sorting', () => {
763801
display: flex;
764802
}
765803
766-
.hover\\:flex:hover {
767-
display: flex;
804+
@media (hover: hover) {
805+
.hover\\:flex:hover {
806+
display: flex;
807+
}
768808
}"
769809
`)
770810
})
@@ -2104,8 +2144,10 @@ describe('@variant', () => {
21042144
expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`
21052145
"@layer utilities {
21062146
@media (any-hover: hover) {
2107-
.any-hover\\:hover\\:underline:hover {
2108-
text-decoration-line: underline;
2147+
@media (hover: hover) {
2148+
.any-hover\\:hover\\:underline:hover {
2149+
text-decoration-line: underline;
2150+
}
21092151
}
21102152
}
21112153
}"

packages/tailwindcss/src/utilities.test.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15660,10 +15660,12 @@ describe('custom utilities', () => {
1566015660
display: flex;
1566115661
}
1566215662
15663-
.hover\\:foo:hover {
15664-
flex-direction: column;
15665-
text-decoration-line: underline;
15666-
display: flex;
15663+
@media (hover: hover) {
15664+
.hover\\:foo:hover {
15665+
flex-direction: column;
15666+
text-decoration-line: underline;
15667+
display: flex;
15668+
}
1566715669
}"
1566815670
`)
1566915671
})

0 commit comments

Comments
 (0)