skip to content

In Cypress, what happens when you call cy.hover(), and what do you use instead?

level: middleimportance: must knowfreq 70%

answer

  1. one of the missing commands
  2. the error links to workarounds
  3. JavaScript events versus CSS state
  4. trigger, invoke show, or force
  5. realHover comes from a plugin

basics

~20 s

Cypress has no hover command. Calling cy.hover() fails with an error saying it is not implemented and links to the workarounds page. Use .trigger('mouseover'), .invoke('show'), a forced action, or the cypress-real-events plugin's realHover for genuine pointer input.

solid answer

~40 s

There is no `cy.hover()` in Cypress and, as of Cypress 16, there still is not. The name is kept as a placeholder so the failure is useful: calling it errors with "`cy.hover()` is not currently implemented" and links to a page titled *hover: workarounds*. What replaces it depends on what the hover drives. If a statement row reveals its Download control from a JavaScript `mouseover` handler, `.trigger('mouseover')` reproduces it. If the control is merely hidden, `.invoke('show')` or `{ force: true }` on the action gets past the visibility check. What none of those reach is CSS: a `:hover` rule is applied by the browser from real pointer position, not by a dispatched event, so nothing repaints. For that you need native input, which the third-party `cypress-real-events` plugin supplies through `realHover` on Chromium browsers.

code

javascript · 14 lines
javascript
// cypress/support/e2e.js
import 'cypress-real-events'

// cypress/e2e/statements.cy.js
it('reveals the download control on a statement row', () => {
  cy.visit('/statements')

  // realHover drives native input, so the CSS :hover rule applies
  cy.contains('tr', 'August 2026').realHover()

  cy.contains('tr', 'August 2026')
    .find('[data-cy="download-pdf"]')
    .should('be.visible')
})

go deeper

for a junior

Know that Cypress has no hover command and that the error tells you so. Being able to name .trigger('mouseover') as the usual stand-in is enough at this level.

for a middle

Explain why a dispatched event reaches JavaScript handlers but never the browser's CSS hover state, and pick the right stand-in for each way a control can be revealed.

for a senior

Show judgment about false greens: a forced action on a control a user cannot reveal passes the test and ships the bug. Say when a native-events plugin earns its place in the suite.

for a principal

Own the standard for the whole suite — one custom command wrapping one chosen workaround, and a clear position on taking a Chromium-only plugin dependency for hover fidelity.

## The command that exists only to fail Cypress has never shipped a `cy.hover()`, and as of Cypress 16 it still does not. What is interesting is that the name is not simply unknown. Cypress keeps `hover` (alongside `mount`) as a **placeholder command**, so that calling it produces a targeted message rather than a generic "unknown command" error: > `cy.hover()` is not currently implemented. However it is usually easy to > workaround. Read the following document for a detailed explanation. The error links to the hover workarounds page, whose title is literally *"hover: workarounds"*. Keeping the name reserved has a second effect that matters in real projects: because it is a placeholder rather than a real built-in, a team can register its own `hover` with `Cypress.Commands.add('hover', …)` without fighting Cypress over the name. ## What a hover actually is, and why that decides the workaround "Hover" is not one thing. On a bank statements table, the **Download PDF** control on each row can be revealed in three quite different ways, and only the first two are reachable with synthetic events: - A JavaScript `mouseover` or `mouseenter` handler toggles a class or React state. - The control is always in the DOM but hidden with `display: none` until something shows it. - A pure CSS rule — `tr:hover .download { opacity: 1 }` — with no JavaScript at all. The built-in stand-ins map onto those cases: - `.trigger('mouseover')` (or `'mouseenter'`) dispatches the event on the element, so any JavaScript listening for it runs. This is the workaround Cypress documents first. - `.invoke('show')` calls jQuery's `show` on the subject, which makes a hidden element visible so the next command can act on it. - `{ force: true }` on the action — `.click({ force: true })` — skips the actionability checks entirely and dispatches the click regardless of visibility. ## The line synthetic events cannot cross The third case is the one candidates get wrong. A dispatched `mouseover` is a JavaScript event object; the CSS `:hover` pseudo-class is applied by the browser from its own record of where the real pointer is. Dispatching an event does not move the pointer, so the browser never enters the hover state and never repaints. The Cypress docs say this outright: using `.trigger()` "will only affect events in JavaScript and will not trigger any effects in CSS." | how the control is revealed | `.trigger('mouseover')` | `.invoke('show')` | force the action | native events plugin | |---|---|---|---|---| | JavaScript `mouseover` handler | works | no | no | works | | hidden element, no hover logic | no | works | works | not needed | | CSS `:hover` rule only | no | no | works, but nothing repaints | works | "Works, but nothing repaints" is worth dwelling on: forcing the click on a CSS-hidden control can still exercise the handler, so the flow passes — while a real user staring at an unrevealed button would be stuck. That is a false green, and it is the reason hover is worth an interview question at all. ## Native events, and what they cost For genuine pointer input, the community plugin `cypress-real-events` adds commands such as `realHover`, `realClick`, `realType`, `realPress` and `realSwipe`. It drives the browser's own input channel rather than dispatching DOM events, so CSS pseudo-classes really apply. Two caveats: 1. It is Chromium-only, so a suite that also runs elsewhere needs a fallback path or a browser-conditional spec. 2. Its commands are not Cypress built-ins, which matters to anything that recognises a fixed set of interaction commands — Cypress UI Coverage, for example, has an `additionalInteractionCommands` setting precisely so `realClick` and `realHover` count as interactions. ## A pragmatic house rule 1. Prefer a hover that JavaScript owns, and cover it with `.trigger('mouseover')`. 2. If the reveal is CSS-only and the control matters, use the native-events plugin so the test proves what a user sees. 3. If you register a custom `cy.hover()` with `Cypress.Commands.add()`, make it one named step that wraps whichever workaround you chose, so a later change is one edit rather than a search across every spec. The trap to avoid is a custom `cy.hover()` that quietly forces the action. It makes every spec pass and hides the case where a user cannot reach the button at all.

  • Why does Cypress keep the name `hover` reserved instead of leaving it undefined?
    Cypress treats `hover` (and `mount`) as placeholder commands so that calling one produces a specific, documented error pointing at the workarounds page rather than a generic unknown-command failure. The same reservation is what lets a project register its own `hover` with `Cypress.Commands.add()` without colliding with a real built-in.
  • Your custom cy.hover() forces the click when the control is hidden. What risk have you taken on?
    A forced action dispatches regardless of visibility, so the spec passes even when a real user could never reveal the control. The suite then reports green on a broken interaction. Force is a deliberate escape hatch for a known-safe case, not a default to bake into a shared command.

saying these in an interview costs you the question

  • Says cy.hover() exists and works like a click
  • Claims Cypress removed cy.hover() in a recent version
  • Thinks a dispatched mouseover applies CSS :hover rules
  • Treats force: true as a real hover rather than a bypass
  • Presents realHover as a built-in Cypress command