Surfaces
Modal
Edit a value in context and return to the underlying screen.
Import
import { NeuModalComponent } from '@neural-ui/native/modal';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.
Basic dialog
Open a short message and close it with the action or the close control.
View and copy the complete example
import { Component, signal } from '@angular/core';
import { NeuButtonComponent } from '@neural-ui/native/button';
import { NeuCardComponent } from '@neural-ui/native/card';
import { NeuModalComponent } from '@neural-ui/native/modal';
import { NeuModalFooterDirective } from '@neural-ui/native/modal';
import { View, Text, ScrollView } from '@ng-native/components';
@Component({
selector: 'app-example',
imports: [NeuButtonComponent, NeuCardComponent, NeuModalComponent, NeuModalFooterDirective, View, Text, ScrollView],
template: `
<scroll-view class="usage-scroll" keyboardShouldPersistTaps="handled"><view class="usage-stack">
<neu-card title="Basic dialog" variant="outlined"><view class="usage-section"><text class="usage-caption">Open a short message and close it with the action or the close control.</text><neu-button label="About this workspace" size="sm" (pressed)="open.set(true)" /></view></neu-card>
<neu-modal title="Product workspace" [(open)]="open">
<text>A shared place for your team’s plans and documents.</text>
<ng-template neuModalFooter><view class="dialog-actions"><neu-button label="Got it" size="sm" variant="text" (pressed)="open.set(false)" /></view></ng-template>
</neu-modal>
</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; }
.dialog-actions { flex-direction: row; justify-content: flex-end; }
`,
})
export class ExampleComponent {
readonly open = signal(false);
}Breakfast booking
Customize a breakfast in a modal with a radio choice, optional coffee and confirmation actions. Confirm updates the booking locally; dismissing or cancelling preserves the previous order.
View and copy the complete example
import { Component, signal, computed } from '@angular/core';
import { NeuButtonComponent } from '@neural-ui/native/button';
import { NeuSwitchComponent } from '@neural-ui/native/switch';
import { NeuCardComponent } from '@neural-ui/native/card';
import { NeuBadgeComponent } from '@neural-ui/native/badge';
import { NeuModalComponent } from '@neural-ui/native/modal';
import { NeuRadioGroupComponent } from '@neural-ui/native/radio';
import { NeuModalFooterDirective } from '@neural-ui/native/modal';
import { View, Text, ScrollView } from '@ng-native/components';
@Component({
selector: 'app-example',
imports: [NeuButtonComponent, NeuSwitchComponent, NeuCardComponent, NeuBadgeComponent, NeuModalComponent, NeuRadioGroupComponent, NeuModalFooterDirective, View, Text, ScrollView],
template: `
<scroll-view class="product-scroll" keyboardShouldPersistTaps="handled" [showsVerticalScrollIndicator]="true"><view class="product-stack"><view class="product-section"><text class="product-title" accessibilityRole="header">A good day starts here</text><text class="product-caption">Your team breakfast, just the way you like it</text></view>
<neu-card title="Friday breakfast" [description]="saved()" variant="outlined"><view class="product-section"><neu-badge label="09:00 · Neural Studio" tone="info" /><view class="product-actions"><neu-button label="Choose breakfast" size="sm" (pressed)="edit()" /></view></view></neu-card>
<neu-modal title="Your breakfast" [(open)]="open"><view class="product-section"><neu-radio label="Breakfast choice" [options]="choices" [(value)]="choice" /><neu-switch label="Add coffee" [(checked)]="coffee" /></view><ng-template neuModalFooter><view class="product-actions"><neu-button label="Confirm breakfast" size="sm" (pressed)="confirm()" /><neu-button label="Cancel" size="sm" variant="text" (pressed)="open.set(false)" /></view></ng-template></neu-modal></view></scroll-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 choices = [{ id: 'toast', label: 'Avocado toast' }, { id: 'yogurt', label: 'Yogurt and fruit' }];
readonly choice = signal('toast');
readonly coffee = signal(true);
readonly open = signal(false);
readonly savedChoice = signal('toast');
readonly savedCoffee = signal(true);
readonly saved = computed(() => (this.choices.find(item => item.id === this.savedChoice())?.label || '') + (this.savedCoffee() ? ' · With coffee' : ''));
edit() { this.choice.set(this.savedChoice()); this.coffee.set(this.savedCoffee()); this.open.set(true); }
confirm() { this.savedChoice.set(this.choice()); this.savedCoffee.set(this.coffee()); this.open.set(false); }
}Profile form
Edit the profile and check the saved name after closing the dialog.
View and copy the complete example
import { Component, signal } from '@angular/core';
import { NeuButtonComponent } from '@neural-ui/native/button';
import { NeuInputComponent } from '@neural-ui/native/input';
import { NeuCardComponent } from '@neural-ui/native/card';
import { NeuModalComponent } from '@neural-ui/native/modal';
import { NeuModalFooterDirective } from '@neural-ui/native/modal';
import { View, Text, ScrollView } from '@ng-native/components';
@Component({
selector: 'app-example',
imports: [NeuButtonComponent, NeuInputComponent, NeuCardComponent, NeuModalComponent, NeuModalFooterDirective, View, Text, ScrollView],
template: `
<scroll-view class="usage-scroll" keyboardShouldPersistTaps="handled"><view class="usage-stack">
<neu-card title="Profile form" variant="outlined"><view class="usage-section"><text class="usage-caption">Edit the profile and check the saved name after closing the dialog.</text><neu-button label="Edit profile" (pressed)="editing.set(true)" /></view></neu-card>
<neu-modal title="Edit profile" [(open)]="editing">
<neu-input label="Full name" [(value)]="name" />
<ng-template neuModalFooter><view class="dialog-actions"><neu-button label="Done" size="sm" variant="text" (pressed)="editing.set(false)" /></view></ng-template>
</neu-modal>
<text class="usage-caption">Current profile: {{ name() || 'No name' }}</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; }
.dialog-actions { flex-direction: row; justify-content: flex-end; }
`,
})
export class ExampleComponent {
readonly editing = signal(false);
readonly name = signal('Alex Morgan');
}Scrollable content
Keep a long brief scrollable inside the dialog while the closing action remains available.
View and copy the complete example
import { Component, signal } from '@angular/core';
import { NeuButtonComponent } from '@neural-ui/native/button';
import { NeuCardComponent } from '@neural-ui/native/card';
import { NeuModalComponent } from '@neural-ui/native/modal';
import { NeuModalFooterDirective } from '@neural-ui/native/modal';
import { View, Text, ScrollView } from '@ng-native/components';
@Component({
selector: 'app-example',
imports: [NeuButtonComponent, NeuCardComponent, NeuModalComponent, NeuModalFooterDirective, View, Text, ScrollView],
template: `
<scroll-view class="usage-scroll" keyboardShouldPersistTaps="handled"><view class="usage-stack">
<neu-card title="Scrollable content" variant="outlined"><view class="usage-section"><text class="usage-caption">Keep a long brief scrollable inside the dialog while the closing action remains available.</text><neu-button label="Read project brief" (pressed)="open.set(true)" /></view></neu-card>
<neu-modal title="Project brief" [(open)]="open"><view class="modal-long-content">@for (part of briefSections; track part.title) {
<view class="usage-section"><text class="usage-heading">{{ part.title }}</text><text>{{ part.description }}</text></view>
}</view><ng-template neuModalFooter><view class="dialog-actions"><neu-button label="Done" size="sm" variant="text" (pressed)="open.set(false)" /></view></ng-template></neu-modal>
</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; }
.modal-long-content { gap: 16px; } .dialog-actions { flex-direction: row; justify-content: flex-end; }
`,
})
export class ExampleComponent {
readonly open = signal(false);
readonly briefSections = [
{ title: 'Goal', description: 'Make the mobile onboarding flow clearer for new team members.' },
{ title: 'Audience', description: 'People who use the workspace while travelling and between meetings.' },
{ title: 'First session', description: 'Introduce the next action and keep important information easy to find.' },
{ title: 'Content', description: 'Use short labels, helpful hints and clear empty states.' },
{ title: 'Review', description: 'Check the complete flow on a small screen with the team.' },
{ title: 'Handoff', description: 'Share the final plan with the people responsible for the release.' },
{ title: 'Offline access', description: 'Explain which changes are saved on the device and which need a connection. Keep the current draft when the connection drops, and let the person decide when to retry.' },
{ title: 'Privacy choices', description: 'Ask only for information needed by the next step. Explain optional permissions in context and allow the person to continue when a permission is not essential to the task.' },
{ title: 'Accessible journeys', description: 'Read the entire journey with a screen reader, enlarge the text and check the keyboard focus order. Every action needs a clear name, and every error needs a useful recovery step.' },
{ title: 'Release checklist', description: 'Confirm the final copy, translated messages and empty states with the team. Test saving, cancellation and retry on both platforms before handing the build to the people who will review it.' },
];
}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 { NeuModalHeaderDirective, NeuModalFooterDirective } from '@neural-ui/native/modal';| Template or region | Context | Purpose |
|---|---|---|
ng-template[neuModalHeader] | — | Replaces the heading content; the close control stays available. title remains the fallback heading; mark your custom heading with accessibilityRole="header". |
ng-template[neuModalFooter] | — | Renders after the scrollable body. Use it for actions that must remain visible. |
ng-content | — | Content between the modal tags goes into its scrollable body. |
API reference
Inputs
| Name | Type | Default | Binding | Description |
|---|---|---|---|---|
| title | string | Required | Input | Heading for the surface, section or presentation. |
| dismissible | boolean | true | Input | Allows user dismissal of the presentation. The application can still close it through open. |
| showCloseButton | boolean | true | Input | Show the header close action when the dialog is dismissible. Hiding it does not disable native Back dismissal. |
| closeLabel | string | 'Close' | Input | Text or accessible name used for close. It does not perform the action by itself. |
Models
| Name | Type | Default | Binding | Description |
|---|---|---|---|---|
| open | boolean | false | Model · two-way | Whether the presentation or message is requested to be open. Bind a writable signal with [(open)]. |
Outputs
| Name | Payload | When it emits |
|---|---|---|
| openChange | boolean | Emits the updated open value. [(open)] binds this output together with its model input. |
| closed | "native" | "action" | "backdrop" | Close-request reason: native, action or backdrop. openChange synchronizes the visibility model. |
| opened | void | Reports that the native presentation has opened. |
| dismissed | void | Reports completion of native dismissal. |
Model state
Bind the signal with [(modelName)] without calling it. Use modelName() to read its value.
import { signal } from '@angular/core';
// Fields inside your component class
readonly open = signal<boolean>(false);