Back to posts

Reverse Infinite Scroll in FastHTML

July 08, 2025

Today I learned, how to build a reverse infinite scroll in FastHTML. I needed it to create a logviewer, where you want to see the latest logs, but also want to scroll up to see older entries.

Below you can see the final result, and after that I explain how I built it.

Live Demo

Here's the final working result - newest entries at the bottom, scroll up for older ones:

Name ID

How it works

There's three parts to this solution.

1. Dynamically adding new rows as you scroll with HTMX

The standard pattern, scroll down, new items append at bottom, is easy with HTMX. Phihung's excellent FastHTML + HTMX examples has a live example. Super useful! Check it out if you haven't seen it before!

The key parts are the hx_trigger="intersect once" and hx_swap="afterbegin" which means that when the last row is revealed, the next page is loaded.

2. Reverse the container in CSS

To start at the bottom and scroll up, we need to add display: flex; flex-direction: column-reverse; to the container. Below you can see an example that shows the effect of adding the CSS. You can toggle the CSS reversal by clicking the button.

Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10
Line 11
Line 12
Line 13
Line 14
Line 15
Line 16
Line 17
Line 18
Line 19
Line 20
Line 21
Line 22
Line 23
Line 24
Line 25
Line 26
Line 27
Line 28
Line 29
Line 30
Line 31
Line 32
Line 33
Line 34
Line 35
Line 36
Line 37
Line 38
Line 39
Line 40
Line 41
Line 42
Line 43
Line 44
Line 45
Line 46
Line 47
Line 48
Line 49
Line 50
Line 51
Line 52
Line 53
Line 54
Line 55
Line 56
Line 57
Line 58
Line 59
Line 60
Line 61
Line 62
Line 63
Line 64
Line 65
Line 66
Line 67
Line 68
Line 69
Line 70
Line 71
Line 72
Line 73
Line 74
Line 75
Line 76
Line 77
Line 78
Line 79
Line 80
Line 81
Line 82
Line 83
Line 84
Line 85
Line 86
Line 87
Line 88
Line 89
Line 90
Line 91
Line 92
Line 93
Line 94
Line 95
Line 96
Line 97
Line 98
Line 99
Line 100
Line 101
Line 102
Line 103
Line 104
Line 105
Line 106
Line 107
Line 108
Line 109
Line 110
Line 111
Line 112
Line 113
Line 114
Line 115
Line 116
Line 117
Line 118
Line 119
Line 120
Line 121
Line 122
Line 123
Line 124
Line 125
Line 126
Line 127
Line 128
Line 129
Line 130
Line 131
Line 132
Line 133
Line 134
Line 135
Line 136
Line 137
Line 138
Line 139
Line 140
Line 141
Line 142
Line 143
Line 144
Line 145
Line 146
Line 147
Line 148
Line 149
Line 150
Line 151
Line 152
Line 153
Line 154
Line 155
Line 156
Line 157
Line 158
Line 159
Line 160
Line 161
Line 162
Line 163
Line 164
Line 165
Line 166
Line 167
Line 168
Line 169
Line 170
Line 171
Line 172
Line 173
Line 174
Line 175
Line 176
Line 177
Line 178
Line 179
Line 180
Line 181
Line 182
Line 183
Line 184
Line 185
Line 186
Line 187
Line 188
Line 189
Line 190
Line 191
Line 192
Line 193
Line 194
Line 195
Line 196
Line 197
Line 198
Line 199

3. Reverse the data in Python

However, the CSS also flips the order of the items on the page. So we need to fix that.

We can do this by reversing the data in Python. Here's how I did it:

def load_logs(page: int, limit: int = 5):
    page_logs = logs[-(page * limit):-(page - 1) * limit]  # <-- slice the logs array in reverse order
    if not page_logs: return []
    rows = [Tr(Td(name), Td(id)) for name, id in page_logs]
    rows.append(Tr(hx_trigger="intersect once", hx_swap="afterbegin", hx_get=load_logs.to(page=page + 1), hx_target="#logs-body", style="height: 1px; opacity: 0;"))
    return rows

Putting it all together

Here you can see the full code all together.

from fasthtml.common import *

app, rt = fast_app()
logs = [(f"Log", i) for i in range(1, 201)]  # <-- ok I lied, this example is not really "infinite"

@rt("/")
def get():
    return Container(
        H1("Reverse Infinite Scroll"),
        Div(
            Table(
                Thead(Tr(Th("Name"), Th("ID"))),
                Tbody(id="logs-body", hx_get="/load_logs?page=1", hx_trigger="load")
            ),
            # Key: CSS reversal to start at bottom
            style="max-height: 300px; overflow-y: scroll; border: 1px solid #ccc; display: flex; flex-direction: column-reverse;"
        )
    )

@rt("/load_logs")
def load_logs(page: int, limit: int = 5):
    # Key: Reverse data to get newest entries at the bottom
    page_logs = logs[-(page * limit):-(page - 1) * limit if page > 1 else None]
    if not page_logs: return []
    
    rows = [Tr(Td(name), Td(id)) for name, id in page_logs]
    # Key: `intersect once` and `afterbegin` for infinite scroll
    rows.append(Tr(hx_trigger="intersect once", hx_swap="afterbegin", 
                   hx_get=f"/load_logs?page={page + 1}", hx_target="#logs-body", 
                   style="height: 1px; opacity: 0;"))
    return rows

serve()