explain-interface
Answers how was this built about an interface. Give it a URL and name the thing you're curious about, and it reads the layers that produce the effect. Reads the whole frontend instead when you don't name a thing. From a screenshot it reconstructs rather than reads and says so.
Install
npx skills add https://github.com/jakubkrehel/skills --skill explain-interfaceFinding the layers behind an effect
Search by property signature, not by guessing the element. You rarely know the markup, but you always know which CSS properties could produce what you see.
Three things that cost you the answer
Pseudo-elements carry the effect more often than elements do. A gradient, a noise overlay, a hairline border or a glow commonly lives on ::before or ::after. getComputedStyle(el) alone never sees them, so pass the pseudo as the second argument and read the element and both pseudos.
Idle values are not effects. filter: blur(0px), opacity: 1 and transform: none are what an animation library leaves on every element it touches. Filter them out first, or they bury the few hits that matter.
A generated stop list is one technique, not twelve stops. Stops at 0%, 9.99%, 19.07%, ... came from a utility following an easing curve, and the extra stops keep the gradient from banding. Name the technique, never paste the stops.
The layer search
Read the result for the stack, not for one row:
- Compare
boxagainstviewport. An element wider than the viewport, or with a negative offset, is oversized on purpose so its edges never show. - Rows come back in DOM order. Within one stacking context a later row roughly paints over an earlier one, and a positioned row with a higher
zpaints over both. Confirm the order at a pixel with Narrowing to a region. backdropFilteron any row. That layer frosts something beneath it, so the layer beneath is part of the answer.mixBlendModeon any row. The layer's color depends on what it covers, so you cannot explain it without naming what is underneath.
When CSS is not the answer
Where the layer search comes back empty for the region you care about, the effect is not CSS. Run this after the page has drawn:
A canvas returns null for a context type other than the one it already holds, so webgl-or-other means a WebGL context is likely. Probing an unused canvas creates a 2D context on it, which is why this runs only after the page has drawn. A WebGL canvas plus a matching library is a shader. Say so and describe roughly what it looks like, rather than describing CSS that is not there. An svg may carry <filter> primitives worth reading directly.
Narrowing to a region
Where the page is large, sample what paints at one point instead of walking everything. Set x and y inside the function, since evaluate_script passes element references as arguments, not numbers:
elementsFromPoint returns front to back, which is the paint stack at that pixel in reverse. It skips elements with pointer-events: none, and a pseudo-element comes back as its host. Decorative layers are often pointer-events: none, so a missing layer here is not an absent one. Fall back to the layer search and match on box.
Is it animated?
getAnimations() catches CSS animations, transitions and Web Animations API playback in one call, which a stylesheet walk misses. Read easing on the keyframes as well as on the timing. A CSS animation carries its timing function on each keyframe, and its effect-level easing reads linear. delay across sibling targets is where a stagger shows.
It returns nothing on a page at rest, because a one-shot reveal has either finished or never started. Reload and run it at once, or scroll the target into view for a reveal on scroll. A headless page may not produce frames until something captures it, so where it stays empty, take a screenshot and run it again.