ScrollRevealY wraps a single child that starts in normal static flow. Once
the user has scrolled past it and then scrolls back up, the child “escapes”
flow — staying position: fixed for the whole escaped stretch and tracking
the scroll 1:1 via a transform, until it settles flush with the viewport
edge once fully revealed. Scrolling back up further, past the child’s own
original position, returns it to normal flow. Hovering or clicking the child
while it’s still partway revealed commits it fully open immediately.
That describes the default, anchor="top". Set anchor="bottom" for a bar
that belongs at the bottom of the screen. The direction is the same — scroll
down hides, scroll up reveals — but a bottom bar rests somewhere else, and
that changes its whole shape:
- It never sits in normal flow. The place a bottom bar would return to
is the bottom edge it reveals to, so there’s no flow position to go back
to. It rests shown, pinned, from the start;
initial-placementnever applies to it and it starts outattached. - It’s pinned to the bottom of its container’s visible part — the bottom
of the viewport, except where the container ends above it (it slides in and
out of the container’s bottom edge instead, and rides it away) or hasn’t
been reached yet (it waits just inside the container’s top). So it arrives
and leaves with the container it belongs to, and never renders outside it,
instead of floating over the rest of the page. With no
data-ddi-scroll-reveal-y-containerancestor, that container isdocument.body, i.e. the whole page. - Against a container edge it still slides in from below at the same rate as it does against the viewport’s edge. That edge scrolls with the page, so it comes down to meet the child; the reveal keeps pace with it, which means uncovering twice as fast. Without that the two motions cancel out and the child appears to unfold downward rather than slide up.
- Its own position in the flow doesn’t affect its behaviour — only the container’s bounds do. The wrapper still reserves the child’s height where you put it, though, so put it where the bar should leave a gap: usually at the end of the container, so nothing ends up hidden underneath it.
Always use the ScrollRevealY component (@dynamic-type/ddi/server) rather
than the underlying <ddi-scroll-reveal-y> custom element directly — it
provides the display: block the element needs to lay out at all.
Attributes
These are the custom attributes.
anchor—"top"(the default) or"bottom": which edge the child belongs to. See above — this is more than a mirror image.reveal-threshold— extra px of scroll-up required before the child begins to appear at all. Defaults to0, i.e. it starts appearing on the first pixel scrolled back up.data-ddi-scroll-reveal-y-container— goes on the ancestor that bounds the child, found viaclosest("[data-ddi-scroll-reveal-y-container]")fromScrollRevealYitself — so it doesn’t have to be the immediate parent, just an ancestor. Falls back todocument.bodyif omitted. Where the container runs out of room, the child docks to its edge asposition: absoluteat a document coordinate, so it scrolls away with the container rather than floating past it.
The state is exposed as three mutually exclusive boolean attributes on the
element, for styling (ddi-scroll-reveal-y[attached] .bar { … }):
initial-placement— the child is in normal static flow, not yet escaped. Top-anchored bars only.attaching— the child has escaped flow and is partially revealed.attached— the child is fully revealed, flush with the anchored edge.
Finally:
inactivated— a boolean attribute that pins the element atinitial-placement, even once scrolled past. Removing it resyncs from the current scroll position.
Programmatic scrolling
ScrollRevealY treats scrolling as user intent, so an app that moves the page
itself (repositioning content, scroll-into-view, restoring a saved position)
would make the child reveal or hide when it shouldn’t. Wrap those scrolls in
doProgrammaticScroll (from @dynamic-type/ddi/client) and the element ignores
exactly that scroll while still reacting to real user scrolls:
import { doProgrammaticScroll } from "@dynamic-type/ddi/client";
doProgrammaticScroll(() => window.scrollTo(0, targetY));
doProgrammaticScroll(() => selectedRow.scrollIntoView());
Only instant scrolls are covered — pass a function that performs the scroll
synchronously (no behavior: "smooth").
---
import { ScrollRevealY, Style, css } from "@dynamic-type/ddi/server";
const [{ pane, container }, rules] = css`
.container {
u: height:200vh width:90cqw p:4xs mbe:150vh bg;
}
.pane {
u: width:100% height:100px bg:rgb(240,200,0);
}
[initial-placement] .pane {
u: b:light;
}
[attaching] .pane {
u: drop-shadow bg:rgba(240,200,0,0.5)
}
[attached] .pane {
u: drop-shadow bg:rgba(240,200,0,0.75);
}
`;
---
<div class:list={[container, "box"]} data-ddi-scroll-reveal-y-container>
<ScrollRevealY>
<div class:list={pane}></div>
</ScrollRevealY>
</div>
<Style rules={rules} />---
import { ScrollRevealY, Style, css } from "@dynamic-type/ddi/server";
const [{ pane, container }, rules] = css`
/* The bar never occupies its slot in the flow, so put the slot where the
gap belongs: at the end of the container. */
.container {
u: height:200vh width:90cqw p:4xs mbe:150vh bg;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.pane {
u: width:100% height:100px bg:rgb(0,180,200);
}
[initial-placement] .pane {
u: b:light;
}
[attaching] .pane {
u: drop-shadow bg:rgba(0,180,200,0.5)
}
[attached] .pane {
u: drop-shadow bg:rgba(0,180,200,0.75);
}
`;
---
<div class:list={[container, "box"]} data-ddi-scroll-reveal-y-container>
<ScrollRevealY anchor="bottom">
<div class:list={pane}></div>
</ScrollRevealY>
</div>
<Style rules={rules} />