
In responsive web design, hiding or showing elements based on screen size is something we all do. But simply using CSS to hide elements (like display: none
) doesn’t remove them from the DOM — which can cause performance issues, unnecessary rendering, and even accessibility problems.
So, I built a set of Angular structural directives that let me dynamically control what gets rendered based on device type — mobile, tablet, or desktop. The best part? They’re clean, reusable, and powered by signals for reactive updates. Let’s walk through how I set it up.
🤔 Why Not Just Use CSS?
Sure, media queries are great for styling. But hiding elements with CSS means they’re still in the DOM, still being evaluated, and still draining memory. That’s where structural directives come in — they allow us to actually remove or insert elements into the DOM based on device resolution. Win-win for performance and code clarity.
🛠️ Step-by-Step: Building Custom Responsive Directives
1. A Utility Function to Manage DOM Visibility
First, I created a utility that decides whether to render a template based on a reactive Signal
. This works great with Angular’s new effect()
API:
const createVisibilityEffect = (
predicate: Signal<boolean>,
viewContainerRef: ViewContainerRef,
templateRef: TemplateRef<any>,
cd: ChangeDetectorRef,
shouldShow: boolean
) =>
effect(() => {
untracked(() => {
shouldShow === predicate()
? viewContainerRef.createEmbeddedView(templateRef)
: viewContainerRef.clear();
});
cd.markForCheck();
});
Here’s what’s happening:
- We watch a reactive signal (like
isMobileRes
). - If it matches our condition (
shouldShow
), we inject the template. - If not, we clear it.
untracked()
is used to prevent signal-related Angular warnings.
2. A Base Class to Avoid Repeating Logic
Next, I made a reusable abstract directive that other visibility directives can extend:
@Directive()
abstract class BaseVisibilityDirective {
protected readonly commonService = inject(CommonService);
protected readonly templateRef = inject(TemplateRef);
protected readonly viewContainerRef = inject(ViewContainerRef);
protected readonly cd = inject(ChangeDetectorRef);
constructor(predicate: Signal<boolean>, shouldShow: boolean) {
createVisibilityEffect(predicate, this.viewContainerRef, this.templateRef, this.cd, shouldShow);
}
}
This base class wires everything up — all the child directives have to do is provide the right signal and visibility flag.
3. Creating Device-Specific Directives
Now for the fun part — I created individual directives for mobile, tablet, and desktop visibility:
@Directive({ selector: '[hideItMobile]' })
export class HideItMobileDirective extends BaseVisibilityDirective {
constructor() {
super(inject(CommonService).isMobileRes, false);
}
}
@Directive({ selector: '[showItMobile]' })
export class ShowItMobileDirective extends BaseVisibilityDirective {
constructor() {
super(inject(CommonService).isMobileRes, true);
}
}
And I repeated that pattern for tablets and desktops:
@Directive({ selector: '[hideItTablet]' })
export class HideItTabletDirective extends BaseVisibilityDirective {
constructor() {
super(inject(CommonService).isTabletRes, false);
}
}
@Directive({ selector: '[showItTablet]' })
export class ShowItTabletDirective extends BaseVisibilityDirective {
constructor() {
super(inject(CommonService).isTabletRes, true);
}
}
@Directive({ selector: '[hideItDesktop]' })
export class HideItDesktopDirective extends BaseVisibilityDirective {
constructor() {
super(inject(CommonService).isDesktopRes, false);
}
}
@Directive({ selector: '[showItDesktop]' })
export class ShowItDesktopDirective extends BaseVisibilityDirective {
constructor() {
super(inject(CommonService).isDesktopRes, true);
}
}
đź§ Note:
CommonService
exposes the reactive screen resolution signals likeisMobileRes
.
4. Using the Directives in Templates
Now using them is dead simple:
<div *hideItMobile>
This will NOT appear on mobile.
</div>
<div *showItTablet>
This will ONLY appear on tablets.
</div>
<div *hideItDesktop>
Hidden on desktops.
</div>
No messy conditionals, no cluttered templates — just drop the directive and it works.
âś… Why I Like This Approach
- Cleaner Templates – No more
*ngIf="isMobile"
repeated everywhere. - Better Performance – We’re not just hiding elements, we’re removing them entirely from the DOM.
- Reusability – Once written, these directives work anywhere with a consistent API.
- Reactive – Because it’s powered by signals, the visibility updates automatically with screen size changes.
🚀 Next Steps I’m Exploring
- Adding animations when elements appear/disappear.
- Combining this with feature flags for conditional feature rollout.
- Using similar patterns for user roles, app states, or A/B testing.
đź’¬ Over to You
I’ve found this pattern super helpful for building clean, responsive UIs in Angular. If you’re tired of tangled template logic or bloated DOM trees, give this approach a shot!
Got a better way to handle responsive logic in Angular? I’d love to hear it — let’s swap ideas in the comments 👇