A reveal has to hide its content before it can show it. That gives the CSS two jobs the rest of a snippet’s styles do not have: it must apply before the first paint, and it must let go if the script that ends the hidden state never runs.
Load the CSS in the head
Scripts run late. A module script is deferred, so the browser has usually painted the HTML from the server before it executes. A rule that arrives with the script hides content the reader has already seen: the image shows, vanishes, then comes back. That flash is worse than no effect.
So every snippet ships its CSS as a file, and every version loads it in the head:
---// Imported in the frontmatter, so Astro bundles it into the head.import "../styles/word-wipe.css";---
<div data-word-wipe> <slot /></div>
<script> import { initWordWipes } from "../scripts/word-wipe"; initWordWipes();</script>// A CSS import: the bundler puts it in the head before the text paints.import "../styles/word-wipe.css";A snippet that hides nothing, such as marquee or pixelate-image, loads its CSS the same way. Nothing would flash if it arrived late, but one rule for every snippet is easier to follow than two.
Gotcha
Check the built HTML, not the source. The stylesheet needs to be a <link> or <style> in the
<head> of the server response. If a framework lazy loads the component and its CSS with it, the
rule arrives late and the flash is back.
Never hide content on CSS alone
Head CSS that hides content can fail in its own way: if the script never runs, the content stays hidden. The snippets guard against that in three ways. Use the first where you can, and the other two together where the hiding has to start at first paint.
- Hide only once the script has claimed the element.
word-wipedims text only when it hasdata-word-wipe-state="pending", which the script sets. Without the script the attribute never appears, and the text shows at full strength. - Hide only when scripting is on.
block-image-revealandscroll-revealmust hide from first paint, so their rules sit inside@media (scripting: enabled). With JavaScript switched off, the rules do not apply. The same query adds(prefers-reduced-motion: no-preference), so nothing is hidden for people who opt out of motion. - Add a failsafe for a script that fails to load. Scripting can be on and the file can still fail to arrive. While the element has no state attribute, a CSS animation with a three second delay sets the content back to visible. The script sets a state as soon as it runs, which switches the failsafe off.
@media (scripting: enabled) and (prefers-reduced-motion: no-preference) { /* Hidden until the panel carries it in, unless the script stood down. */ [data-block-image-reveal]:not([data-block-image-reveal-state="off"]) > :not([data-block-image-reveal-panel]) { opacity: 0; }
/* The script has not claimed the element after 3s, so show the content. */ [data-block-image-reveal]:not([data-block-image-reveal-state]) > :not([data-block-image-reveal-panel]) { animation: block-image-reveal-failsafe 0s 3s forwards; }}A browser that does not know the scripting query skips the whole block. The content then shows from the start and the effect plays over it, which is a fair result for an old browser.
Warning
Hidden content costs Largest Contentful Paint. Chrome does not count an element at opacity: 0 as
painted, so a reveal on the biggest image above the fold pushes LCP back until the animation runs.
Keep reveal effects below the fold, and say so in the snippet’s seo field.
Scope it, don’t layer it
Leave snippet CSS out of cascade layers, and scope every rule to the snippet’s data-* attributes. A snippet goes into projects you don’t control, and many of them have an unlayered reset. Unlayered CSS beats every layer whatever its specificity, so a rule such as svg { display: block } would undo a layered snippet. Scoped rules beat a reset’s element rules on specificity. The project changes the look through the snippet’s custom properties, not by overriding its rules.
Which guard to use
| The CSS… | Guard |
|---|---|
| hides nothing | None |
| can wait for the script before it hides or dims | A state attribute that the script sets |
| must hide from first paint, before the script runs | @media (scripting: enabled) and a three second failsafe |