Data & media
Virtual List
Browse a long collection inside a bounded native viewport.
Import
import { NeuVirtualListComponent } from '@neural-ui/native/virtual-list';Examples
Import ExampleComponent into its parent and place <app-example /> inside your application’s root neu-theme. The preview supplies the device frame and safe areas; use the root setup from Installation in your app.
All projects
Browse a long project collection with recycled rows. Select a project to reveal its summary below the list.
View and copy the complete example
import { Component, signal, computed } from '@angular/core';
import { NeuAvatarComponent } from '@neural-ui/native/avatar';
import { NeuVirtualListComponent } from '@neural-ui/native/virtual-list';
import { View, Text } from '@ng-native/components';
import { NeuVirtualListItemDirective } from '@neural-ui/native/virtual-list';
import type { NeuOption } from '@neural-ui/native';
@Component({
selector: 'app-example',
imports: [NeuAvatarComponent, NeuVirtualListComponent, NeuVirtualListItemDirective, View, Text],
template: `
<view class="product-section"><view class="product-section"><text class="product-title" accessibilityRole="header">All projects</text><text class="product-caption">100 projects · One workspace</text></view>
<neu-virtual-list [items]="projects" [height]="320" [estimatedItemHeight]="100" (selected)="selected.set($event)" (endReached)="reachedEnd.set(true)">
<ng-template neuVirtualListItem let-project><view class="product-row"><neu-avatar [label]="project.label" size="sm" /><view class="product-copy"><text class="product-heading">{{ project.label }}</text><text class="product-caption">{{ project.description }}</text></view></view></ng-template>
</neu-virtual-list>
@if (selectedProject(); as project) { <view class="product-surface"><text class="product-heading">{{ project.label }}</text><text class="product-caption">{{ project.description }}</text></view> }
<text class="product-caption" accessibilityLiveRegion="polite">{{ reachedEnd() ? 'You have reached the last project' : 'Select a project to see its summary' }}</text></view>
`,
styles: `
.actions { gap: 12px; align-items: flex-start; } text { color: var(--neu-text, #172033); font-size: 16px; }
:host { flex: 1; flex-shrink: 1; min-height: 0; }
.product-scroll { flex: 1; flex-shrink: 1; height: 0px; min-height: 0; }
.product-stack { gap: 24px; padding-bottom: 20px; }
.product-section { gap: 12px; }
.product-row { flex-direction: row; align-items: center; gap: 12px; }
.product-copy { flex: 1; min-width: 0; gap: 4px; }
.product-title { color: var(--neu-text, #172033); font-size: 24px; line-height: 30px; font-weight: 600; }
.product-heading { color: var(--neu-text, #172033); font-size: 17px; line-height: 23px; font-weight: 600; }
.product-caption { color: var(--neu-muted, #596477); font-size: 14px; line-height: 20px; }
.product-actions { flex-direction: row; flex-wrap: wrap; align-items: center; gap: 8px; }
.product-surface { gap: 16px; padding: 16px; border-radius: 16px; background-color: var(--neu-surface, #ffffff); border-width: 1px; border-color: var(--neu-border, #a5afbf); }
`,
})
export class ExampleComponent {
readonly projects: readonly NeuOption[] = Array.from({ length: 100 }, (_, index) => ({
id: 'project-' + index,
label: ['Mobile launch', 'Website refresh', 'Customer research', 'Design system', 'Team onboarding', 'Accessibility review', 'Release notes', 'Customer portal', 'Offline workspace', 'Product analytics'][index % 10] + ' · ' + ['Discovery', 'Prototype', 'Pilot', 'Launch', 'Growth', 'Insights', 'Polish', 'Rollout', 'Feedback', 'Next chapter'][Math.floor(index / 10)],
description: ['Product team · In progress', 'Design team · In review', 'Customer team · Planning'][index % 3],
}));
readonly selected = signal('');
readonly selectedProject = computed(() => this.projects.find(item => item.id === this.selected()));
readonly reachedEnd = signal(false);
}Custom recycled rows
Scroll through custom rows with a header and footer. Each template receives the original item index even when the list recycles its views.
View and copy the complete example
import { Component, signal, computed } from '@angular/core';
import { NeuAvatarComponent } from '@neural-ui/native/avatar';
import { NeuVirtualListComponent } from '@neural-ui/native/virtual-list';
import { View, Text, ScrollView } from '@ng-native/components';
import { NeuVirtualListItemDirective } from '@neural-ui/native/virtual-list';
@Component({
selector: 'app-example',
imports: [NeuAvatarComponent, NeuVirtualListComponent, NeuVirtualListItemDirective, View, Text, ScrollView],
template: `
<scroll-view class="usage-scroll" keyboardShouldPersistTaps="handled"><view class="usage-stack">
<neu-virtual-list [items]="members" [height]="360" [estimatedItemHeight]="80" (selected)="selected.set($event)">
<view listHeader class="list-heading"><text class="usage-heading">Workspace directory</text><text class="usage-caption">24 team members</text></view>
<ng-template neuVirtualListItem let-member let-index="index" let-disabled="disabled">
<view class="directory-row"><neu-avatar [label]="member.label" size="md" /><view class="directory-copy"><text class="usage-heading">{{ member.label }}</text><text class="usage-caption">{{ disabled ? 'Unavailable' : member.description }}</text></view><text class="usage-caption">{{ index + 1 }}</text></view>
</ng-template>
<view listFooter class="list-heading"><text class="usage-caption">End of directory</text></view>
</neu-virtual-list>
<text class="usage-caption">{{ selectedMember() ? 'Selected member: ' + selectedMember()?.label : 'Select a member' }}</text>
</view></scroll-view>
`,
styles: `
.actions { gap: 12px; align-items: flex-start; } text { color: var(--neu-text, #172033); font-size: 16px; }
.usage-scroll { flex: 1; }
.usage-stack { gap: 20px; padding-bottom: 20px; }
.usage-section { gap: 12px; }
.usage-heading { color: var(--neu-text, #172033); font-size: 16px; font-weight: 600; }
.usage-caption { color: var(--neu-muted, #64748b); font-size: 14px; }
.usage-row { flex-direction: row; flex-wrap: wrap; gap: 12px; align-items: center; }
.usage-actions { flex-direction: row; flex-wrap: wrap; gap: 8px; align-items: center; }
.directory-row { flex-direction: row; gap: 12px; align-items: center; } .directory-copy { flex: 1; min-width: 0; gap: 3px; } .list-heading { gap: 4px; padding: 12px 0; }
`,
})
export class ExampleComponent {
readonly members = Array.from({ length: 24 }, (_, index) => ({ id: 'member-' + index, label: ['Alex', 'Sam', 'Jordan', 'Taylor', 'Robin', 'Casey'][index % 6] + ' ' + ['Morgan', 'Rivera', 'Chen', 'Patel'][Math.floor(index / 6)], description: ['Product designer', 'Mobile developer', 'Design engineer', 'Product manager'][index % 4], disabled: index === 3 }));
readonly selected = signal('');
readonly selectedMember = computed(() => this.members.find(member => member.id === this.selected()));
}Change the settings and try the result.
Device captures shown in English.


Personalization
Import these directives together with the component and add them to Component.imports. The examples above include the complete setup.
import { NeuVirtualListItemDirective } from '@neural-ui/native/virtual-list';| Template or region | Context | Purpose |
|---|---|---|
ng-template[neuVirtualListItem] | $implicit: NeuOption; index: number; disabled: boolean | Custom visual row content. The list keeps recycling, measurement and selection. index is the item position, not the recycled slot; keep item-specific state keyed by item.id. |
[listHeader] | — | Native view before the scrolling rows; no additional Neural directive is needed. |
[listFooter] | — | Native view after the scrolling rows; no additional Neural directive is needed. |
Template contexts
import type { NeuVirtualListItemContext } from '@neural-ui/native/virtual-list';let-item reads $implicit; named fields use let-index="index". Use the exported context types for reusable TemplateRef declarations. Keep nested content presentational: the parent retains its accessible action and selection behavior.
API reference
Inputs
| Name | Type | Default | Binding | Description |
|---|---|---|---|---|
| items | readonly NeuOption[] | [] | Input | Ordered data for the component. Use the data contract below and stable, unique item IDs. |
| disabled | boolean | false | Input | Disables user interaction. Values can still be supplied by the application. |
| estimatedItemHeight | number | 84 | Input | Estimated row height used to calculate the virtualized window. |
| height | number | 400 | Input | Native layout height of the viewport or specimen, depending on the component. |
| emptyLabel | string | 'No items' | Input | Text or accessible name used for empty state. It does not perform the action by itself. |
Models
This component declares no two-way models.
Outputs
| Name | Payload | When it emits |
|---|---|---|
| selected | string | Emits the selected item ID. |
| endReached | void | Reports reaching the end of the visible list. Guard concurrent loading in your application. |
Types and data interfaces
Use the imports in your application. The definitions below are provided for reference.
NeuOption
import type { NeuOption } from '@neural-ui/native';
// Public definition for reference only. Import this type; do not redeclare it.
export interface NeuOption {
readonly id: string;
readonly label: string;
readonly description?: string;
readonly disabled?: boolean;
}| Field | Type | Requirement | Meaning |
|---|---|---|---|
| id | string | Required | Stable, unique identifier used for tracking and selection. |
| label | string | Required | Human-readable item name. |
| description | string | Optional | Optional supporting text; rendering depends on the control. |
| disabled | boolean | Optional | Prevents activating or selecting this item. |