DDI

ScrollRevealY

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:

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.

The state is exposed as three mutually exclusive boolean attributes on the element, for styling (ddi-scroll-reveal-y[attached] .bar { … }):

Finally:

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").

The yellow rectangle is put in a ddi-scroll-reveal-y container. Scroll up and down and observe its behavior
---
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} />
The same thing anchored to the bottom — the blue rectangle reveals from the bottom edge of the viewport instead
---
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} />