Show It Once, Then Never Again… One-Time UI in Angular
Have you ever wanted to show a banner, tooltip, or onboarding message just once, and then hide it forever? Like… "We get it. Thanks for the message. Please don’t show it again." In this tutorial, I’ll show you a clean, modern Angular 19+ approach for one-time UI using local storage, signals, and finally a reusable structural directive you can drop anywhere in your app.
Let’s See the Problem in Action
So, here’s our basic dashboard app:

Towards the top we have this yellow welcome banner:

What we want is for this to only show once per user.
After they dismiss it, they should no longer see it.
Let’s look at the template for the main app component.
Okay here, we’ve got the welcome-banner component showing directly:
<app-welcome-banner (dismiss)="dismissBanner()"></app-welcome-banner>
Notice the “dismiss” output, that’s a custom output event that fires when the “dismiss” button is clicked.
In this case, it lets us hide the banner when someone clicks the button.
But, it’s not doing anything yet, so let’s see why. Let’s look at the code for this component.
Okay here it is, the “dismiss” method that’s called when the button is clicked, doesn’t do anything yet:
export class App {
protected dismiss() {}
}
So, let’s look at how we might make this more dynamic with a signal.
Quick Fix: Hiding the Banner with a Signal
Let’s start by adding a new property called “hideBanner”.
It will be a signal, and it will be initialized to false:
import { ..., signal } from '@angular/core';
export class App {
protected hideBanner = signal(false);
...
}
Then let’s update the “dismiss” function to flip that signal:
export class App {
...
protected dismiss() {
this.hideBanner.set(true);
}
}
Now back in the template, we’ll wrap the welcome-banner in an @if block using that signal:
@if (!hideBanner()) {
<app-welcome-banner (dismiss)="dismiss()"></app-welcome-banner>
}
Okay, let’s save and see how it works now:

Okay, the banner still shows initially, and then when we click “dismiss,” it hides.
So that’s cool, but we have a problem here don’t we?
If we refresh, the banner comes back.
This is because we haven’t done anything to store the “dismissed” state of this banner that will allow it to persist.
Make It Stick: Saving Dismissal with localStorage
What we really want is for our app to remember:
“Hey, this user already dismissed the banner — don’t show it again.”
So, let’s fix it with localStorage.
Let’s switch back to the code.
First, we’ll update the “dismiss” method to write a flag to localStorage, let’s call it “bannerDismissed”, and we’ll give it a value of “true”:
export class App {
...
protected dismiss() {
this.hideBanner.set(true);
localStorage.setItem('bannerDismissed', 'true');
}
}
Now, when the “dismiss” button is clicked, a “bannerDismissed” property will be added to localStorage and will persist.
Now, we need to add the logic to determine whether or not it should display when initialized.
Let’s add a constructor.
Then let’s add a “dismissed” variable that we’ll set based on the value of our new “bannerDismissed” property from localStorage, and we’ll check if it’s set to “true”.
Then we update our “hideBanner” signal based on this value:
export class App {
...
constructor() {
const dismissed = localStorage.getItem('bannerDismissed') === 'true';
this.hideBanner.set(dismissed);
}
}
Okay, that should be all we need, so let’s save and try it out:

Nice. To start the banner shows, and there’s nothing in localStorage.
Then, when we click “dismiss,” the banner hides and in the dev tools, we can see the key: “bannerDismissed” set to “true”.
Then, when we refresh, the banner remains hidden.
Pretty cool, right?
This works well, but… the logic is starting to feel too custom and too local.
Let’s make it reusable.
Upgrade Time: Reusable UI with a Structural Directive
Let’s create a structural directive that takes in a localStorage key and controls whether to show an item or not based on whether that key is set.
I’ve already got this directive stubbed out. It’s called “show-once”:
import { Directive } from '@angular/core';
@Directive({
selector: '[showOnce]'
})
export class ShowOnceDirective {
}
Right now, this file is empty, so let’s build it step-by-step.
First, we need to add an input for the localStorage key:
import { ..., input } from '@angular/core';
export class ShowOnceDirective {
key = input('', { alias: 'showOnce' });
}
We’re using Angular’s input() function and giving it an alias that matches the directive name “showOnce”.
Next, we need to create a couple more properties and inject both TemplateRef and ViewContainerRef:
import { ..., inject, TemplateRef, ViewContainerRef } from '@angular/core';
export class ShowOnceDirective {
...
private templateRef = inject(TemplateRef<unknown>);
private viewContainerRef = inject(ViewContainerRef);
}
These will be used to conditionally render the content placed within this directive.
Now, let’s add a constructor.
Inside the constructor, we’ll set up an effect().
This will run whenever the “key” input changes.
Within the effect(), we call the clear() method from the ViewContainerRef to reset the view.
Then, we read the localStorage key passed into the directive from the “key” input.
If the key isn’t set, we call the createEmbeddedView() method from the ViewContainerRef to render the content, like our banner:
import { ..., effect } from '@angular/core';
export class ShowOnceDirective {
...
constructor() {
effect(() => {
this.viewContainerRef.clear();
const value = localStorage.getItem(this.key());
if (!value) {
this.viewContainerRef.createEmbeddedView(this.templateRef);
}
});
}
}
This effect() handles conditional display on initialization.
Next, we need to add a custom “clear” method that can be used to dismiss the message.
This method updates localStorage with the “key” and sets its value to “true”.
Then, it clears the ViewContainerRef to hide the content:
export class ShowOnceDirective {
...
protected clear() {
localStorage.setItem(this.key(), 'true');
this.viewContainerRef.clear();
}
}
This becomes part of the directive’s public API.
And, in order to access it from the template, we need to add an alias with exportAs:
@Directive({
selector: '[showOnce]',
exportAs: 'showOnce'
})
Using the Directive in Our Component
Now we can remove the old logic from the app component. t
The “hideBanner” signal, the constructor, the “dismiss” function, and imports can all go.
Then, we need to import the new show-once directive so we can use it in the template:
import { ShowOnceDirective } from './show-once.directive';
@Component({
selector: 'app-root',
...,
imports: [ ..., ShowOnceDirective ]
})
Then, we replace the @if block with an <ng-template>
.
The template is where we apply the directive passing it the localStorage key, “bannerDismissed”.
Then, we add a template reference variable using our “showOnce” alias to access the directive right within the template.
Finally, we use that reference to call the directive’s clear() method when the “dismiss” button is clicked:
<ng-template showOnce="bannerDismissed" #showOnce="showOnce">
<app-welcome-banner (dismiss)="showOnce.clear()"></app-welcome-banner>
</ng-template>
Okay, now let’s save and test it out:

After we clear localStorage and refresh, the banner is properly displayed again.
Then, when we dismiss it, it hides and the key is stored in localStorage again.
And, when we refresh again, the banner remains hidden.
Everything works great, and now we have a reusable concept.
Wrap-Up & When to Use This Pattern
So, that’s it. You now have a clean, reusable directive that you can drop into any Angular app.
It’s perfect for banners, tooltips, modals, or anything you only want users to see once.
No global state. No services. Just declarative, Angular-idiomatic code.
But remember: it’s per-browser and not tied to user auth.
Also, it needs manual clearing if you want to reset it.
Just a couple things to keep in mind.
If you found this helpful, don’t forget to subscribe, check out my other Angular tutorials for more tips and tricks, and maybe buy some Angular swag from my shop!
Additional Resources
- The demo app BEFORE any changes
- The demo app AFTER making changes
- Angular Signals Guide
- Angular Structural Directives Explained
- JavaScript Local Storage Documentation
- My course: “Styling Angular Applications”
Want to See It in Action?
Want to experiment with the final version? Explore the full StackBlitz demo below. If you have any questions or thoughts, don’t hesitate to leave a comment.