It has taken you precious time to develop your carefully crafted list of items with variable height with Angular. Now, your only remaining task is to add virtual scroll support. You install and integrate @angular/cdk/scrolling
, but then you reach a dead end – Angular CDK virtual scroll can only handle items with fixed size. After some searching, you've came across this article. Hopefully, by the end of it, you should have a flawlessly-working list that supports virtual scrolling.
Harnessing @angular/cdk
Obviously, we are not going to reinvent the wheel by implementing our own virtual scroll. Surely, that's an option, but we would like to use the existing tools that have proven to work and Angular CDK actually offers a very capable instruments for that. What we are going to do is to implement a custom VirtualScrollStrategy
. But before we start ...
@angular/cdk-experimental
It's worth mentioning that the CDK team is currently developing an autosize strategy which is part of the experimental version of the Angular CDK. You might achieve very good results with it but be aware that the strategy is not ready for production yet (official documentation) as of the time of writing this article. So, you might potentially experience unexpected behavior. In short, you could give it a try, in case it achieves the results you desire. Anyhow, without further ado, let's move our focus to the custom strategy.
Chapters
Since I thought that it makes logical sense, I separated the article in several chapters. If I have to summarize, we are going to:
- Present our problem
- Explain what a
VirtualScrollStrategy
is and how to harness it - Employ the strategy
- Focus on the implementation of the internals
We'll try to skim throught some of these matters as quickly as possible, as granularity might not be important, but having a grasp of them would be essential for you to understand how things stitch together in the final product. The actual strategy implementation is going to be examined in the final chapter as the list suggests.
⚠️ NOTE: By just having a quick glance at the chapters and blindly copying the code examples, you might end up with a non-functional solution. If you are short on time and don't plan reading the text, you can head to the Final code section and explore the repository directly (the code is documented).
For the sake of convenience, here is a handy content:
Contents
- I. Case study – Hero Feed
- II.
VirtualScrollStrategy
– What is that? - III. Setting up the custom strategy
- IV. Strategy implementation
- Conclusion
I. Case study – Hero Feed
It is very likely that, if you develop Angular apps, you have came across the Hero tutorials in the official documentation. So, in order to be inline with that, I am going to build a Hero feed. In essence, it's just a hypothetical feed with randomly generated data that is trying to mimic a real-world example.
For that purpose, let's introduce the HeroMessage
interface. The message will represent our list item:
export interface HeroMessage {
id: string;
name: string;
date: Date;
text: string;
tags: string[];
}
... and this is how it will look in the app:
Respectively, I am going to create a component that renders that data which I'll name HeroMessageComponent
. For the sake of saving time, I am going to omit the details as they are not very relevant to our main goal.
💾: You can check the full component code at GitHub
Now, let's move on to the strategy interface.
II. VirtualScrollStrategy
– What is that?
VirtualScrollStrategy
represents an interface that we should implement in order to describe our desired scrolling behavior, or more specifically, define which items should be rendered in the viewport. Actually, if you've noticed, the standard cdk-virtual-scroll-viewport
component, as already mentioned, works with fixed-size items, the experimental CDK – variable-size items. These two modes are separate VirtualScrollStrategy
-ies – FixedSize
and Autosize
, respectively. Let's take a look at the methods:
interface VirtualScrollStrategy {
/** Emits when the index of the first element visible in the viewport changes. */
scrolledIndexChange: Observable<number>;
/**
* Attaches this scroll strategy to a viewport.
* @param viewport The viewport to attach this strategy to.
*/
attach(viewport: CdkVirtualScrollViewport): void;
/** Detaches this scroll strategy from the currently attached viewport. */
detach(): void;
/** Called when the viewport is scrolled (debounced using requestAnimationFrame). */
onContentScrolled(): void;
/** Called when the length of the data changes. */
onDataLengthChanged(): void;
/** Called when the range of items rendered in the DOM has changed. */
onContentRendered(): void;
/** Called when the offset of the rendered items changed. */
onRenderedOffsetChanged(): void;
/**
* Scroll to the offset for the given index.
* @param index The index of the element to scroll to.
* @param behavior The ScrollBehavior to use when scrolling.
*/
scrollToIndex(index: number, behavior: ScrollBehavior): void;
}
Generally, we won't have to implement all of them, unless you want any of the omitted behavior. I'll hold off the details for now. First, let's implement the interface and name our custom strategy "HeroMessageVirtualScrollStrategy
":
export class HeroMessageVirtualScrollStrategy implements VirtualScrollStrategy {
// [...]
}
In order to provide our custom strategy to the viewport component, we should use the VIRTUAL_SCROLL_STRATEGY
injection token. We can achieve this by introducing a new directive:
@Directive({
selector: '[appHeroMessageVirtualScroll]',
providers: [
{
provide: VIRTUAL_SCROLL_STRATEGY,
/* We will use `useFactory` and `deps` approach for providing the instance */
useFactory: (d: HeroMessageVirtualScrollDirective) => d._scrollStrategy,
deps: [forwardRef(() => HeroMessageVirtualScrollDirective)],
},
],
})
export class HeroMessageVirtualScrollDirective {
/* Create an instance of the custom scroll strategy that we are going to provide */
_scrollStrategy = new HeroMessageVirtualScrollStrategy();
}
After we are done, we would like to add some additional flavor – provide the list of messages, as an @Input
, that we are going to use later in the scroll strategy. You will notice that the scroll strategy has one additional method called updateMessages
. It will be added later by us but bare with me for now.
export class HeroMessageVirtualScrollDirective {
// [...]
private _messages: HeroMessage[] = [];
@Input()
set messages(value: HeroMessage[] | null) {
if (value && this._messages.length !== value.length) {
this._scrollStrategy.updateMessages(value);
this._messages = value;
}
}
}
What we've just done is to add a _messages
property that is going to be updated and respectively sent to the scroll strategy when it changes*
* A change can be very subjective. The current check is very primitive but it can be altered depending on the use case.
💾: You can check the final file at GitHub
Summary
To summarize what we've done so far:
- Got familiar with the
VirtualScrollStrategy
interface - Created our own strategy (although it's not implemented yet)
- Built a directive that will be used along with the virtual scroll component in order to plug our strategy, which bring us to the next chapter
III. Setting up the custom strategy
Since we now have the custom strategy, or at least the wireframe, let's plug it into the cdk-virtual-scroll-viewport
in the template of our desired host component. In my case, I am going to use app.component.html
.
<cdk-virtual-scroll-viewport
appHeroMessageVirtualScroll
[messages]="heroMsgs.messages$ | async"
>
<!-- We'll use the HeroMessageComponent in order to render our list items -->
<app-hero-message
*cdkVirtualFor="let msg of heroMsgs.messages$ | async"
[message]="msg"
[attr.data-hm-id]="msg.id"
></app-hero-message>
</cdk-virtual-scroll-viewport>
Data source
Because the generation of data is not a point of interest for this article, you can check the mocked API service that performs this task at GitHub. I prefer not to clutter the article with unnecessary source code. The mocked data is provided to the app.component.ts
via the service and respectively the template.
💾: Check the app component at GitHub
💾: Check the mocked API at GitHub
Infinite scrolling
Finally, since we want to be as close as possible to a real-world app, the mocked data is going to be continuously loaded while the user scrolls down the list. For this purpose, we will need to introduce infinite scrolling.
💾: You can check the final file at GitHub.
Summary
In this chapter we managed to:
- Plug the custom strategy to
CdkVirtualScrollViewport
- Briefly showcase our data generation
- Add some infinite scrolling for the sake of real-world-ness
IV. Strategy implementation
We reached the point where we can start the implementation of the strategy. Logically, let's start with the attach
and detach
methods:
export class HeroMessageVirtualScrollStrategy implements VirtualScrollStrategy {
private _viewport!: CdkVirtualScrollViewport | null;
attach(viewport: CdkVirtualScrollViewport): void {
this._viewport = viewport;
}
detach(): void {
this._viewport = null;
}
}
As a next step, let's add the updateMessages
custom method that will serve the HeroMessage
-s to the strategy class:
export class HeroMessageVirtualScrollStrategy implements VirtualScrollStrategy {
private _messages: HeroMessage[] = [];
// [...]
updateMessages(messages: HeroMessage[]) {
this._messages = messages;
if (this._viewport) {
this._viewport.checkViewportSize();
}
}
}
Now, it is time to introduce our main method which will dictate the rest of the implementation. The _updateRenderedRange
must contain our core logic for determining the range of hero messages that should be rendered in the viewport. In short, we should be able to tell few things in order to implement it:
- Measure the total height of the scrollable container
- Determine the scroll position (or offset)
- Determine the number of list items
As you can see, there is one key take here – we have to be able to distinguish and measure the size of the different list items. Of course, this task might become very tricky. This is why, we will go with a rough estimation. How will this happen? Let's examine the UI of the HeroMessage
.
Determining the height of the messages (list items)
As you can see, the UI is not that complex – we have a hero, a date, some text, labels. The message has a minimal height determined by the existing elements (i.e. a hero message with a single line of text). Nevertheless, the height is unconstrained when it comes to growth (unless we introduce max. size for the text but this is not what we want in this article). Since we've already mentioned that we need a rough estimation, not a precise one, we can examine the styled component and its child elements and write down their rough sizes. Without further ado, let's introduce our height predictor.
// hero-message-height-predictor.ts
const Padding = 24 * 2;
const NameHeight = 21;
const DateHeight = 14;
const MessageMarginTop = 14;
const MessageRowHeight = 24;
const MessageRowCharCount = 35;
const TagsMarginTop = 16;
const TagsRowHeight = 36;
const TagsPerRow = 3;
export const heroMessageHeightPredictor = (m: HeroMessage) => {
const textHeight =
Math.ceil(m.text.length / MessageRowCharCount) * MessageRowHeight;
const tagsHeight = m.tags.length
? TagsMarginTop + Math.ceil(m.tags.length / TagsPerRow) * TagsRowHeight
: 0;
return (
Padding +
NameHeight +
DateHeight +
MessageMarginTop +
textHeight +
tagsHeight
);
};
As you can see, it is a fairly simple function which estimates/predicts the height of a HeroMessage
based on its data. The text and tags height estimation is pretty much a ballpark figure based on an approximate row length. However, this is more than enough for our needs at this stage. I guess it's needless to mention, and obvious, that for your own particular case you will have to develop a similar function that matches your own UI. Now, let's go back to the strategy implementation.
💾: You can check the file at GitHub.
Incorporating height prediction
Due to the need of a height estimation, we can directly start with a new private method which uses the predictor:
export class HeroMessageVirtualScrollStrategy implements VirtualScrollStrategy {
private _heightCache = new Map<string, number>();
// [...]
private _getMsgHeight(m: HeroMessage): number {
let height = 0;
const cachedHeight = this._heightCache.get(m.id);
if (!cachedHeight) {
height = heroMessageHeightPredictor(m);
this._heightCache.set(m.id, height);
} else {
height = cachedHeight;
}
return height;
}
}
You can notice that we've also added a cache property. Basically, our method will be memoized so we can avoid recalculations. We might have a lot of them when the user scrolls.
Next, let's introduce several other methods that are going to be based on/use this particular one. As mentioned above, for the _updateRenderedRange
we will have to know things like total scroll height, message offset, etc. We will start with calculating the height of a set of messages, which is a fairly simple operation:
export class HeroMessageVirtualScrollStrategy implements VirtualScrollStrategy {
// [...]
private _measureMessagesHeight(messages: HeroMessage[]): number {
return messages
.map((m) => this._getMsgHeight(m))
.reduce((a, c) => a + c, 0);
}
}
After that we can introduce the following private methods:
export class HeroMessageVirtualScrollStrategy implements VirtualScrollStrategy {
// [...]
/**
* Returns the total height of the scrollable container
* given the size of the elements.
*/
private _getTotalHeight(): number {
return this._measureMessagesHeight(this._messages);
}
/**
* Returns the offset relative to the top of the container
* by a provided message index.
*
* @param idx
* @returns
*/
private _getOffsetByMsgIdx(idx: number): number {
return this._measureMessagesHeight(this._messages.slice(0, idx));
}
/**
* Returns the message index by a provided offset.
*
* @param offset
* @returns
*/
private _getMsgIdxByOffset(offset: number): number {
let accumOffset = 0;
for (let i = 0; i < this._messages.length; i++) {
const msg = this._messages[i];
const msgHeight = this._getMsgHeight(msg);
accumOffset += msgHeight;
if (accumOffset >= offset) {
return i;
}
}
return 0;
}
}
We are getting pretty close to the implementation of _updateRenderedRange
. In the code section above, we've added a method for measuring the total height, getting the scroll offset of a message index, and the reverse method – getting a message index by a scroll offset.
What is next is a method that determines the number of messages in the viewport given a start index of a message:
export class HeroMessageVirtualScrollStrategy implements VirtualScrollStrategy {
// [...]
private _determineMsgsCountInViewport(startIdx: number): number {
if (!this._viewport) {
return 0;
}
let totalSize = 0;
// That is the height of the scrollable container (i.e. viewport)
const viewportSize = this._viewport.getViewportSize();
for (let i = startIdx; i < this._messages.length; i++) {
const msg = this._messages[i];
totalSize += this._getMsgHeight(msg);
if (totalSize >= viewportSize) {
return i - startIdx + 1;
}
}
return 0;
}
}
After this short sprint of method implementations we are finally ready to introduce our key method that we've been mentioning since the beginning of this chapter – _updateRenderedRange
. Its purpose is to build a range object composed by a start and end indices which is then provided to the viewport object via its API. You will also notice the existence of two constants called PaddingAbove
and PaddingBelow
. What they do is to instruct the virtual scroll to render some more messages before and after the scroll viewport. This way, we will have some rendered content that is not visible but needed for a better scrolling experience (i.e. the items won't have to be rendered at the last moment):
typescript
const PaddingAbove = 5;
const PaddingBelow = 5;
export class HeroMessageVirtualScrollStrategy implements VirtualScrollStrategy {
_scrolledIndexChange$ = new Subject<number>();
scrolledIndexChange: Observable<number> = this._scrolledIndexChange$.pipe(
distinctUntilChanged(),
);
// [...]
private _updateRenderedRange() {
if (!this._viewport) {
return;
}
const scrollOffset = this._viewport.measureScrollOffset();
const scrollIdx = this._getMsgIdxByOffset(scrollOffset);
const dataLength = this._viewport.getDataLength();
const renderedRange = this._viewport.getRenderedRange();
const range = {
start: renderedRange.start,
end: renderedRange.end,
};
range.start = Math.max(0, scrollIdx - PaddingAbove);
range.end = Math.min(
dataLength,
scrollIdx + this._determineMsgsCountInViewport(scrollIdx) + PaddingBelow,
);
this._viewport.setRenderedRange(range);
this._viewport.setRenderedContentOffset(
this._getOffsetByMsgIdx(range.start),
);
this._scrolledIndexChange$.next(scrollIdx);
}
}
So far, so good. You may remember the attach
method that we implemented, right? We must update it now with the newly introduced _updateRenderedRange
:
attach(viewport: CdkVirtualScrollViewport): void {
this._viewport = viewport;
// New code
if (this._messages) {
this._viewport.setTotalContentSize(this._getTotalHeight());
this._updateRenderedRange();
}
}
We can now also add and onDataLengthChanged
to the strategy class which is very similar in terms of code:
export class HeroMessageVirtualScrollStrategy implements VirtualScrollStrategy {
// [...]
onDataLengthChanged(): void {
if (!this._viewport) {
return;
}
this._viewport.setTotalContentSize(this._getTotalHeight());
this._updateRenderedRange();
}
}
And finally, we need to update the rendered range when the user scrolls. This is simply managed by onContentScrolled
:
export class HeroMessageVirtualScrollStrategy implements VirtualScrollStrategy {
// [...]
onContentScrolled(): void {
if (this._viewport) {
this._updateRenderedRange();
}
}
}
At this stage we should have the internals of our new strategy ready. As a next step, we can implement some of the other methods – that are not so crucial for the operation of the virtual scroll – which are part of the VirtualScrollStrategy
interface.
Complementary methods
This section will be very short. We will focus on 3 methods from which only one will be implemented.
For our purposes, we won't benefit from onContentRendered
and onRenderedOffsetChanged
. We don't need to perform actions when the content is rendered or the offset has changed. So:
export class HeroMessageVirtualScrollStrategy implements VirtualScrollStrategy {
// [...]
onContentRendered(): void {
/** no-op */
}
onRenderedOffsetChanged(): void {
/** no-op */
}
}
On the other hand, having scrollToIndex
functionality supported by the virtual scroll strategy might be desired, so we can quickly implement it with the existing private methods that we added earlier in the process:
export class HeroMessageVirtualScrollStrategy implements VirtualScrollStrategy {
// [...]
scrollToIndex(index: number, behavior: ScrollBehavior): void {
if (!this._viewport) {
return;
}
const offset = this._getOffsetByMsgIdx(index);
this._viewport.scrollToOffset(offset, behavior);
}
}
With this final addition we can mark the implementation of the VirtualScrollStrategy
completed. In essence, we should now have a functional virtual scroll that is very likely sufficient for our needs. Anyway, this doesn't mean that we can't do some improvements.
Improving height measurement
With its current design, our virtual scroll strategy relies heavily on the message height predictor that we introduced for measuring the approximate height of the list item. This is fine, and will probably work good enough in a lot of situations. Nevertheless, there is an inherent flaw in this design – since the approximated heights of the list items are almost the same as – if not the same as in some cases – the real heights but never the same in all situations, the more items the virtual scroll renders, the more imprecise the measurement of the total height will become due to these small deviations. While they are insignificant when observed individually, their accumulation results in a significant height difference.
Our goal, of course, is to make these measurements as precise as possible. This can only happen, if we take the height of the already rendered list items – at least this is the easiest way unless we want to dwelve into more concrete calculations – and, if you think more about it, we actually already have the list items rendered at some point of the virtual scroll usage. To summarize: what will do is to update the approximate heights (predicted) with the real heights (actual) of the list items when they get rendered.
Let's start by modifying our existing code. First, we'll update the type of the _heightCache
map:
private _heightCache = new Map<string, MessageHeight>();
where the MessageHeight
is defined by a new interface:
interface MessageHeight {
value: number;
source: 'predicted' | 'actual';
}
We will need to differentiate between a predicted height and an actual height; hence, the change.
Next, let's fix _getMsgHeight
since the updated type will result in some errors.
private _getMsgHeight(m: HeroMessage): number {
// [...]
if (!cachedHeight) {
height = heroMessageHeightPredictor(m);
// Values from the height predictor will be marked as `predicted`
this._heightCache.set(m.id, { value: height, source: 'predicted' });
} else {
height = cachedHeight.value;
}
// [...]
}
Now, since we need to get the actual heights from the DOM somehow, we will have to obtain a reference of the scroll wrapper. It makes sense to do that in the attach
method of the strategy because that's the starting point of our code and that is where we set our viewport which can be used for that purpose.
export class HeroMessageVirtualScrollStrategy implements VirtualScrollStrategy {
private _wrapper!: ChildNode | null;
// [...]
attach(viewport: CdkVirtualScrollViewport): void {
this._viewport = viewport;
this._wrapper = viewport.getElementRef().nativeElement.childNodes[0];
// [...]
}
}
We are now getting even closer to the completion, but we need to add one additional private method to the strategy. We can name it _updateHeightCache
:
export class HeroMessageVirtualScrollStrategy implements VirtualScrollStrategy {
// [...]
private _updateHeightCache() {
if (!this._wrapper || !this._viewport) {
return;
}
// Get a reference of the child nodes/list items
const nodes = this._wrapper.childNodes;
let cacheUpdated: boolean = false;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i] as HTMLElement;
// Check if the node is actually an app-hero-message component
if (node && node.nodeName === 'APP-HERO-MESSAGE') {
// Get the message ID
const id = node.getAttribute('data-hm-id') as string;
const cachedHeight = this._heightCache.get(id);
// Update the height cache, if the existing height is predicted
if (!cachedHeight || cachedHeight.source !== 'actual') {
const height = node.clientHeight;
this._heightCache.set(id, { value: height, source: 'actual' });
cacheUpdated = true;
}
}
}
// Reset the total content size only if there has been a cache change
if (cacheUpdated) {
this._viewport.setTotalContentSize(this._getTotalHeight());
}
}
}
The method is a bit long but generally straightforward to grasp. You have probably noticed the existance of the data-hm-id
attribute. Actually, this attribute has been part of our code since the beginning of chapter III but I intentionally decided to not point to its existance until now when you should hopefully realize its relevance and importance to our implementation. If not – it's there to help us easily distinguish the different messages in the DOM.
Finally, we will have to call that method somewhere. A good place is actually _updateRenderedRange
, at the end after the rest of the statements.
export class HeroMessageVirtualScrollStrategy implements VirtualScrollStrategy {
// [...]
private _updateRenderedRange() {
// [...]
this._updateHeightCache();
}
}
At this point, we can now conclude that our implementation is complete. 🎉
💾: Check the implemented custom strategy at GitHub
Final code
Throughout this article, I've given some links to the GitHub repository containing the example code which is based on this article. To make it even easier for you, here is the link of the whole repository:
hawkgs/hero-feed-virtual-scroll
Conclusion
The presented problem in this article can be abstracted for a lot of other use cases. The height predictor can be injected instead. All of that can result in a fairly generic implementation. I intentionally decided to not approach the problem that way. Anyhow, in reality, this implementation can be further improved or tailored for other needs; hence, it probably does not take into account more specific use cases.
What I really hope though is that by reading this piece of text you now have a good understanding how to implement your own VirtualScrollStrategy
.
Top comments (13)
Hello !
What a piece of work !
I implemented it, and it took me quite some time to figure out the what and why :)
Actually, the last piece I missed was the CSS part where we need to have a declared
display
property for the host of the items, in order for the custom element to be actually calculated by the clientHeight.Anyway, thanks a million for the hard work.
Thank you! I am glad that the article helped you.
Indeed, this is something that I haven't explicitly mentioned since I intentionally omitted the details about the UI implementation in the article itself. I might add a separate comment in the repository that the container should be a block (Angular components use
display: inline
by default).Thanks for your answer.
Actually, could you tell me if the "predicted" item height calculation is important or maybe we could get rid of it in a rather simple situation ? I will experiment some variations on my actual project anyway.
Unfortunately, I cannot give you a clear-cut "yes" or "no". It really depends on your use case. At the very least, you will need to provide a ballpark figure that represents the average size of your list item so that the total scroll area can be calculated. This, however, might be problematic, if your items have the potential to vary greatly in size and these size differences are not evenly distributed across the list (averaging will be harder). But, by all means, you can experiment and fine tune/simplify the whole prediction part based on your needs. I've used a more general approach in the article.
I see, thanks a lot for taking your time on this.
My goal is to catch what is still quite "automagic" in the implementation.
I think that the official documentation is not enough to say the least.
Hi, thanks for the code and taking the time to document it so well. It's really a high quality article.
I've made some changes that might be interesting:
scroll-item
that replaceshero-message
to make it reusable, scroll item has 2 values:measuredHeight
andpredictedHeight
, thescrollStrategy
uses this instead ofhero-message
and you can implement it onto your objects.afterViewInit
and updates the scrollItem that was passed along. So no need to recalculate or check again in the scrollStrategyupdateHeightCache
and also can do away with thedata-hm-id
because its not needed anymore.measuredHeight
is available (set by directive in 2) and use that, elsepredictedHeight
.Cheers,
Elger
Thanks! Indeed, what you've proposed is the way to go, if you want a generic solution. I myself use almost the same approach with a scroll item directive on production since we need virtual scrolling in couple of places. Anyway, I intentionally wanted to go with a concrete example, as mentioned in the conclusion of the article, because it is easier to grasp the core concept this way rather than going generic. I believe/hope that the readers can conclude the rest themselves, as you did.
Few notes though:
afterViewInit
is definitely more inline with Angular compared to the DOM operations that I used. Have to keep in mind that the children of the scroll item might not be rendered at that time though. This might very well be valid for the direct DOM tree peek too, but it's probably harder to verify given you'll need to explore the internals of the virtual scroll viewport. The newafterRender
hook might come handy here, I think.O(1)
complexity, so we are good.Thanks for the input!
Hello. Thank you so much for the wonderful article. Thanks to you, I successfully implemented Dynamic Size Virtual Scroll! My approach seemed to be generalizable, so I’ve made it into a library. If you’re interested, please take a look.
github.com/rdlabo-team/ionic-angul...
That's great! For sure, your interpretation/approach is a valid and nice solution to the problem.
Amazing article. Thank you a lot
Thank you very much for the investment
An excellent article that explains in detail
a question
I want to apply the same thing only in reverse
Something like WhatsApp.
What is the direction I need to think about in order to implement this with this code you provided?
Thank you very much and much appreciation
Glad that it was helpful to you. As for your question – from what I understand, you need a chat/messages feed. I think that the strategy shouldn't matter in your case since it doesn't care about the scroll direction. So, it should be rather a general problem for the
cdk-virtual-scroll-viewport
. All that said, I guess you just have to scroll the viewport to the bottom upon initialization + potentially, reversing the data source list/array that you pass to the virtual scroll (you have to figure out whether that's needed at all depending on your order). Still, you can research that, in case there is a better approach, but it seems that something like this should be sufficient in your case.FInally found something interesting. My app needs this. How come the feature has not existed yet? You must be hired by angular team.