Skip to content

Fix: Wrap hover styles in @media (hover: hover) for mobile

Problem

None of our interactive components guard hover styles behind @media (hover: hover). This causes "sticky hover" on touch devices - elements stay in their hover state after a tap until the user taps elsewhere.

Solution

Wrap visual hover effects in @media (hover: hover) media queries. This ensures hover styles only apply on devices with a true hover capability (mouse/trackpad), not on touch devices.

Note: cursor: pointer does NOT need wrapping - it has no visual side-effect on touch devices.


Files to change

1. src/components/blocks/basic/Choice.vue (~line 225)

Current:

js
'&:hover': {
  transform: 'scale3d(1.05,1.05,1)',
  filter: 'brightness(1.1) saturate(1.05)',
},k

Impact: High - scale + brightness is very visible as sticky hover after tap.


2. src/components/creative/CreativeFormBlock/CreativeFormBlock.vue

Multiple hover declarations:

a) Input fields (~line 511)

js
[`${this.toClassSelector(inputField)}:hover`]: pseudoClassStyles.hover,

b) Dropdown (~line 646)

js
'.dropdown-input-container select:hover': pseudoClassStyles.hover,

c) Stars/emoji rating (~line 828)

js
'.star:hover, .star.filled, .emoji:hover': {
  transform: 'scale(1.1)',
},

Note: .star.filled is not a hover state - keep it outside the media query.

js
'.clickout-link:hover': {
  textDecoration: 'none',
},

Low impact, but should be wrapped for consistency.

e) Range/checkbox/radio hover reset (~line 881)

js
[this.toMultiClassSelector(rangeField, checkboxField, radioField, countryCode, ':hover')]: {
  boxShadow: 'none !important',
},

This removes hover effects - still wrap it since it's only relevant when hover exists.

f) Submit button (~line 949)

js
[`${this.toClassSelector(submitButton)}:hover`]: submitBlock?.hoverStyle ? { ... } : {},

Impact: High - user-configured hover style stays stuck after tap.


3. src/components/creative/CreativeSliderBlock/CreativeSliderBlock.vue (~lines 637, 652)

js
[`&:hover ${this.toClassSelector(arrowInner)}`]: {
  transform: `translate3d(-3px, 0, 0)`, // left arrow
},
[`&:hover ${this.toClassSelector(arrowInner)}`]: {
  transform: `translate3d(3px, 0, 0)`,  // right arrow
},

4. src/components/blocks/rich/Slider.vue (~lines 429, 469)

js
'&:hover svg': {
  transform: 'translate3d(1px, 0, 0)',
  transition: 'transform 100ms',
},
'&:hover': {
  '& .slider_arrow_path_outer': { ... },
  '&.slider_arrow_path_inner': { ... },
},

5. src/components/blocks/rich/Slider/SliderSlide.vue (~lines 341, 364)

js
'&:hover': {
  'color': 'rgba(0,0,0,.9) !important',
  '& .encircled-arrow-background': { ... },
  '& .encircled-arrow-glow': { ... },
},
'&:hover': {
  'backgroundColor': 'white',
  '& .slider-custom-text:before': { ... },
},

Note: This component already partially handles mobile with :not(.mobile) on the second hover block. Verify that the .mobile class approach can be replaced/complemented with @media (hover: hover).


6. src/components/creative/CreativeVideoBlock/CreativeVideoControls.vue (~line 90)

js
'&:hover': {
  opacity: 1,
  transition: 'none',
},

Impact: Medium - video control icon stays fully opaque after tap.


7. src/components/creative/VisualElements/CreativeButtonBlock.vue (~line 101)

js
'&:hover': {
  cursor: 'pointer',
},

Skip this one - cursor: pointer has no visual effect on touch devices.


Implementation approach

Since all styles are CSS-in-JS, we can't use @media directly in the object. Two options:

Create a utility that conditionally includes hover styles based on a CSS media query wrapper:

js
// e.g. in src/utils/styleHelpers.js
export function hoverStyle(selector, styles) {
  return {
    [`@media (hover: hover)`]: {
      [selector]: styles,
    },
  }
}

Check if the CSS-in-JS library used (likely JSS or similar) supports @media nesting in objects.

Option B: Use matchMedia in JS

Check window.matchMedia('(hover: hover)').matches and conditionally include hover styles in the computed style objects. Simpler but less elegant - the check runs once and doesn't update if device capabilities change (e.g. connecting a mouse to a tablet).

Before implementing

  • Verify which CSS-in-JS approach the project uses and whether @media nesting is supported in style objects
  • Check if SliderSlide.vue's .mobile class can be fully replaced by the media query approach
  • Test on actual mobile devices after changes

Internal documentation