# Mobile & responsive

> NGWR adapts to touch and small screens — overlays become sheets, hit areas grow on touch, content respects device safe areas, and key layouts reflow to their container. Most of it is automatic; the rest is a one-line opt-in.

Source: https://ngwr.dev/guides/mobile  
Kind: Guide

## Responsive overlays

On small viewports, dialog / select / dropdown / popover collapse from an anchored overlay to a bottom-sheet, and the command-palette goes full-screen — easier to reach with a thumb. Turn it on app-wide with `provideWrResponsiveOverlays()`, built on the existing overlay plumbing.

```angular-ts
import { provideWrOverlay, provideWrResponsiveOverlays } from 'ngwr/overlay';

bootstrapApplication(AppComponent, {
  providers: [
    provideWrOverlay(),
    // App-wide opt-in: on viewports at or below the breakpoint, dialog /
    // select / dropdown / popover collapse to a bottom-sheet, and the
    // command-palette goes full-screen. Defaults to 640px.
    provideWrResponsiveOverlays({ breakpoint: 640 }),
  ],
});
```

## Per-component opt-in

Prefer to keep it surgical? Skip the provider and set the `responsive` input on just the overlays you want adapted. The dialog is service-opened, so it takes a `responsive` option instead.

```angular-html
<!-- Or opt in one overlay at a time, without the global provider -->
<wr-select responsive [(value)]="size">…</wr-select>

// Dialog is service-opened, so pass it as an option:
this.dialog.open(EditProfile, { responsive: true });
```

## Adaptive layout components

`descriptions`, `stepper`, `page-header`, `toolbar`, `pagination` and `table` can reflow based on their own width — a container query, not the viewport — so a two-column descriptions stacks, a horizontal stepper turns vertical, a page-header drops its actions below the title, a toolbar wraps, pagination collapses to a compact `current / total`, and a table becomes labelled cards whenever the box they live in is narrow, on any screen size. Opt in with `responsive`. (Tabs need no flag — the strip scrolls and shows an edge fade automatically when its headers overflow.)

```angular-html
<!-- Reflows on its OWN width (a container query), not the viewport — so it
     adapts inside a narrow card or split pane even on a wide screen. -->
<wr-descriptions responsive inline bordered>…</wr-descriptions>
<wr-stepper responsive>…</wr-stepper>
<wr-page-header responsive title="Settings">…</wr-page-header>
<wr-toolbar responsive>…</wr-toolbar>
<wr-pagination responsive [(page)]="page" [total]="200" />
<wr-table responsive [columns]="cols" [items]="rows" />
```

## Touch targets

Small controls grow their hit area on touch devices, gated behind `@media (pointer: coarse)`. The shared `touch-target` mixin floors a full 44px with an invisible pseudo-element — the alert / dialog / drawer / lightbox close buttons, the slider thumb and knob dial, the colour-picker hue and alpha tracks, the splitter divider and the drag handle. The rest is hand-sized to stay clear of its neighbours: the select chip removes and clear, the cascader clear and the toast actions reach 32px and the tree toggle 28px, with their rows and chips opened up around them. Nothing to configure, and mouse / trackpad layouts stay untouched.

## Touch density

For touch-first screens, the `touch` density preset scales the padding of every density-aware control at once — vertical ×1.7, horizontal ×1.25 — so button, input, textarea, select, cascader, the tree-select trigger, list rows, table cells and tags grow together and keep a shared baseline. Controls with fixed geometry — checkbox, switch, radio, segmented, slider — are unaffected; reach for the touch targets above for those. `--wr-density-text` and `--wr-density-gap` are published for you to read in your own `calc()`, but no shipped component multiplies by either yet. Set it app-wide with `provideWrDensity`, or scope it to a subtree with the `wrDensity` directive.

```angular-ts
import { provideWrDensity } from 'ngwr/density';

// App-wide default — sm | md | lg | touch.
provideWrDensity({ defaultDensity: 'touch' });

// …or scope it to a subtree with the directive:
// <section wrDensity="touch">…</section>
```

## Swipe gestures

Touch dismissal and navigation feel native: drag a drawer's grab handle toward its edge to close it (`showHandle`), swipe a lightbox down to close, flick a toast sideways to dismiss it, and swipe a carousel left / right to change slides — the moving surface follows your finger and snaps back if you release before the threshold. Everything but the drawer handle is automatic.

```angular-html
<!-- Drawer: render a grab handle, then drag it toward the edge to close -->
<wr-drawer position="bottom" showHandle>…</wr-drawer>

<!-- The rest is automatic — no input needed:
       lightbox   swipe down       → close
       toast      swipe sideways   → dismiss
       carousel   swipe left/right → change slide -->
```

## Safe areas

Fixed surfaces honor `env(safe-area-inset-*)` so content clears notches, home indicators and rounded corners. The toast host, command-palette and back-top do this automatically; the drawer opts in with `safeArea`. `env()` resolves to 0 on devices without insets, so desktop is unaffected.

```angular-html
<!-- Edge-anchored drawers can pad the system safe-area inset -->
<wr-drawer position="bottom" safeArea>…</wr-drawer>
```

## Build your own

For your own responsive logic, the `WrMedia` service exposes signal-based breakpoint queries (and a breakpoints SCSS API covers styles). See the Media service page for the full surface.

```angular-ts
import { inject } from '@angular/core';
import { WrMedia } from 'ngwr/media';

export class Toolbar {
  private readonly media = inject(WrMedia);

  // Signals — recompute when the viewport crosses a breakpoint.
  protected readonly isMd = this.media.matches('md');
  protected readonly isWide = this.media.matches('(min-width: 1200px)');
}
```

## On the server there is no viewport

`WrMedia` is SSR-safe in the sense that it does not throw — not in the sense that it answers. There is no `matchMedia` to ask, so on the server `matches(q)` is always `false` and `current()` is always `'xs'`, and a template that branches on either prerenders the narrow arm for every visitor.

```angular-html
<!-- Reflows in CSS. Same markup on the server and in the browser, so
     hydration matches and nothing jumps. -->
<wr-table responsive [columns]="cols" [items]="rows" />

<!-- Branches in the template. `isMd()` is false on the server, so the
     prerendered HTML is always the narrow arm and the aside appears only
     once the bundle has booted. -->
@if (isMd()) {
  <aside class="filters">…</aside>
}
```

Which is why the `responsive` modifier is the first thing to reach for and `WrMedia` the second: a container query is CSS, so the markup is identical on both sides and the browser resolves the layout after hydration has already matched. Where you do need the signal, treat the server's answer as the FIRST layout rather than the wrong one — render the mobile arm, and let the wide one arrive. The full server/client table is on the [server-side rendering guide](https://ngwr.dev/guides/ssr).
