A stray shadow, and what it taught us about CSS specificity

A new click effect on our site picked up a drop shadow it was never given. The cause was an older CSS rule reaching further than intended, and the lesson applies to any custom code that grows over time.

Most interface work isn't building something from nothing. It's adding one more piece to something already running: a site with months or years of styles, scripts, and small decisions layered on top of each other. The craft isn't only in what you write. It's in understanding what's already there well enough that your addition doesn't collide with it in ways neither piece intended.

A recent example, small in scope but useful as a lesson: while extending a custom cursor effect on our own site with two new click animations, one of them picked up a black drop shadow it was never given. The new code was correct. The bug came from a rule written earlier, for something else entirely, that reached further than anyone had noticed. The cursor has since been simplified back to a plain arrow, but the lesson outlasted the effect.

Why this is worth explaining, not just fixing

It would have been easy to patch this by adding !important and moving on. That's the tempting shortcut with CSS bugs: override the symptom, skip the diagnosis. But the actual cause was a specificity issue, and specificity issues don't stay contained. If one old rule can silently affect a new element, the same pattern can resurface the next time anyone adds to that code, and each fix-by-override makes the stylesheet harder to reason about, not easier.

Understanding why the shadow appeared is what prevents the next three bugs like it.

How CSS decides which rule wins

When two CSS rules could both apply to the same element, the browser doesn't pick the one written most recently. It calculates specificity, a score based on what kind of selector each rule uses, and the higher score wins regardless of order in the file.

The rough hierarchy, from strongest to weakest:

  • Inline styles
  • ID selectors, such as #agent-cursor
  • Class selectors, attribute selectors, and pseudo-classes, such as .agent-cursor-outline-flash, [data-state], or :hover
  • Element selectors, such as svg or div

An ID selector always outranks a class selector, even if the class-based rule appears later in the stylesheet and even if it looks more specific to a person reading it. Source order only breaks ties between rules of equal specificity. It never overrides the hierarchy itself.

In this case, an existing rule styled the cursor's arrow icon as #agent-cursor svg: an ID followed by a plain element selector, with a space between them. That space matters as much as the ID does. A space is a descendant combinator, meaning "any svg, anywhere inside #agent-cursor, no matter how deeply nested." It wasn't written to be greedy. It was written when the container held exactly one svg, and it was accurate at the time.

Later, a new click effect added a second, unrelated svg inside that same container. The old rule, still technically correct in what it said, now matched something its author never anticipated.

The fix, and the more useful version of the fix

The immediate repair was narrow: scope the original rule to #agent-cursor > svg. The > restricts it to a direct child rather than any descendant, so it only ever touches the one arrow icon it was meant for. A one-character change.

The more durable move was adding a small backstop on the new element itself: an explicit filter: none on the new effect's own selector. It wasn't strictly required once the scoping was fixed, but it's cheap insurance against the same kind of surprise arriving from a different direction later.

That combination is the actual takeaway: narrow the old rule, and don't assume a new element is safe just because nothing currently targets it.

A short framework for adding to code that's already live

This applies well beyond one site or one bug. Any team maintaining a Webflow site's custom code, a shared stylesheet, or a component library accumulates rules over time, usually written correctly for the moment they were added. A few habits keep that accumulation from turning into hidden hazards.

Scope selectors to what you mean, not to what happens to work today. A descendant combinator (a space) matches anything, anywhere inside. A direct-child combinator (>) or a dedicated class matches only what you intended. The narrower selector is slightly more typing and considerably less risk.

Treat ID selectors as heavy, and use them accordingly. IDs are useful as anchors for JavaScript or as unique page landmarks. As a styling tool, their specificity is hard to outrank later without resorting to !important, which is usually a sign the selector structure needs attention rather than a real solution.

When something new behaves strangely, look at what's already there. The instinct is to re-read what you just wrote. Often the more productive question is which existing rule could be reaching your new element. Check the container's other selectors before doubting your own code.

Keep tunable values in custom properties. When colors, durations, and sizes live in declared variables rather than buried in logic, the next person adjusting one edits a single value instead of hunting through a script.

Re-test the whole block after adding to it. A style block that has grown over months behaves as one system, even if it was written as many small additions.

Carry accessibility guards forward automatically. The original cursor effect already respected the prefers-reduced-motion setting, and the new click effects needed to inherit that same behavior rather than reopen the question. Anything layered onto an existing interactive pattern should assume the original's accessibility decisions were intentional and keep them.

What to do with this

If your site's custom code, embedded scripts, or component overrides have grown for more than a few months without a review, a short audit is worth the time. Not a rewrite, just a read-through with one question: what does this selector actually match, compared with what it was meant to match? Most of the time the answer is fine. Occasionally it finds the next stray shadow before a visitor does.

This kind of review is part of how we approach Webflow development. If you'd like a second set of eyes on a site that has grown in layers, we're glad to talk it through.

Sign up for our newsletter.

Thoughts and reflections from our creative journey.
Learn how we handle your data in our privacy policy.
Thank you. Your message has been sent.
Something went wrong with your submission.