Skip to main content
Neural Nativev0.1.0-beta.1Documentation

Import

component.ts
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
basic.component.ts
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
custom-items.component.ts
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()));
}
Interactive playground

Change the settings and try the result.

Device captures shown in English.

Virtual List · android · native capture
AndroidNative screenshot
Virtual List · ios · native capture
iOSNative screenshot

Personalization

Import these directives together with the component and add them to Component.imports. The examples above include the complete setup.

personalization-imports.ts
import { NeuVirtualListItemDirective } from '@neural-ui/native/virtual-list';
Template or regionContextPurpose
ng-template[neuVirtualListItem]$implicit: NeuOption; index: number; disabled: booleanCustom 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

template-contexts.ts
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

NameTypeDefaultBindingDescription
itemsreadonly NeuOption[][]InputOrdered data for the component. Use the data contract below and stable, unique item IDs.
disabledbooleanfalseInputDisables user interaction. Values can still be supplied by the application.
estimatedItemHeightnumber84InputEstimated row height used to calculate the virtualized window.
heightnumber400InputNative layout height of the viewport or specimen, depending on the component.
emptyLabelstring'No items'InputText or accessible name used for empty state. It does not perform the action by itself.

Models

This component declares no two-way models.

Outputs

NamePayloadWhen it emits
selectedstringEmits the selected item ID.
endReachedvoidReports 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

NeuOption.ts
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;
}
FieldTypeRequirementMeaning
idstringRequiredStable, unique identifier used for tracking and selection.
labelstringRequiredHuman-readable item name.
descriptionstringOptionalOptional supporting text; rendering depends on the control.
disabledbooleanOptionalPrevents activating or selecting this item.