Duncan Faulkner

@duncanfaulkner.bsky.social

Angular dev for 10 years, building NgBracket — signal-native, accessible Angular 22 components. Author of Modern Web Development with Angular. Norfolk, UK.

Re-skin NgBracket with one attribute, not 100 ::ng-deep overrides. It's all CSS custom properties — set data-ngbr-theme and accent/surface/text follow: light, dark or AAA high-contrast. 50% off: https://ngbracket.com?utm_source=bluesky&utm_medium=social&utm_campaign=launch

An accordion rebuilt with divs + ngIf is a screen-reader black hole: a clickable header, no state, no keyboard. ARIA disclosure: header is a real <button> with aria-expanded + aria-controls → its panel; Enter/Space toggle for free. Broken tell: "clickable", not "button, collapsed." #a11y

A WCAG 2.2 rule that catches dense UIs: target size. Interactive targets should be ≥24×24 CSS px (AAA wants 44×44), or have enough spacing. Those tiny icon buttons crammed in a toolbar are a miss-tap machine on a phone or with a tremor. Padding is an accessibility feature. #a11y

Angular route guards don't need classes: const authGuard: CanActivateFn = () => inject(Auth).isLoggedIn(). Just a function using inject() — no class, no Injectable, testable, composable. Same for ResolveFn + CanMatchFn. The class-based guard interfaces are deprecated. #Angular

We don't just ship the NgBracket packs — we run 5 real apps on them (admin, storefront, booking, editor, KB). Dogfooding catches the gap between "passes tests" and "nice to build with". 50% off: https://ngbracket.com?utm_source=bluesky&utm_medium=social&utm_campaign=launch

File upload: a styled div drop-zone with a hidden input is invisible to keyboard + screen-reader users. Keep a real labelled <input type=file> and style it — operable and announced for free. Drag-drop is an enhancement, never the only path. Announce files + progress via aria-live. #a11y

The cheapest a11y win you're skipping: a skip link. One anchor, first focusable thing on the page: <a href="#main">Skip to content</a>. Hidden until focused, it lets keyboard + screen-reader users jump past your nav instead of Tabbing through 30 links on every page. Ten minutes of work. #a11y

Touch the DOM in Angular safely: afterNextRender(() => initChart(host)). Runs only in the browser after paint — no isPlatformBrowser guards, no setTimeout(0), SSR-safe. afterNextRender once; afterRender every change. The sanctioned hook for measuring + non-Angular libs. #Angular

A quiet decision behind NgBracket: signals + OnPush everywhere. Signal inputs, computed() state, local change detection — no Zone.js thrash from a library you didn't write. 50% off: https://ngbracket.com?utm_source=bluesky&utm_medium=social&utm_campaign=launch

Pagination looks safe and is quietly one of the least accessible widgets hand-rolled: clickable numbers with no semantics. Wrap in nav aria-label, mark aria-current=page (not just colour), real links/buttons, name Prev/Next, announce "Page 3 of 10" via aria-live. #a11y

Two names must agree: the visible label (what you read) and the accessible name (what a screen reader says). Drift — button reads "Search", aria-label says "Submit" — and voice users say "click Search" to no effect. If there's visible text, it should BE the accessible name. #a11y

Angular signal queries replace the ViewChild/ContentChild decorators: header = viewChild('header'), rows = viewChildren(RowCmp). Result is a signal — read header(), derive with computed(), no AfterViewInit timing and no QueryList subscription. Template queries as reactive state. #Angular #Signals

The most common tooltip bug isn't ARIA — it only exists on hover. Keyboard users never trigger it, touch users never see it. Show on focus too, wire aria-describedby, dismiss with Escape, never hide essential info inside. If losing it breaks a task, it isn't a tooltip. #a11y

Roadmap: the 8 launch packs are done. Next, same AA bar — navigation, structure, a rich-text editor, overlays (dialog/drawer/toast/tooltip) and buttons. The stuff everyone hand-rolls and gets wrong. 50% off: https://ngbracket.com?utm_source=bluesky&utm_medium=social&utm_campaign=launch

Accessible theming > a dark-mode toggle. Every NgBracket pack runs on CSS custom properties; Themes ship free with any pack — including a proper AAA High-Contrast set. One data-ngbr-theme attribute. 50% off: https://ngbracket.com?utm_source=bluesky&utm_medium=social&utm_campaign=launch

Multi-step forms break for screen readers predictably: the step changes and they've no idea they moved. Announce each step in an aria-live region ("Step 2 of 4"), move focus to the new heading, make progress real text (aria-current=step), send focus to the first error. #a11y

prefers-reduced-motion isn't a nice-to-have — for vestibular disorders, a big parallax or zoom can cause real nausea. The floor: in a prefers-reduced-motion: reduce media query, cut every animation/transition duration to ~0ms with !important. Then re-enable only motion you've checked. #a11y

Retire the takeUntil(this.destroy$) dance: stream$.pipe(takeUntilDestroyed()).subscribe(...) wires cleanup to the lifecycle — no Subject, no ngOnDestroy. Imperative teardown? inject(DestroyRef).onDestroy(() => …). That boilerplate at the bottom of every component is gone. #Angular

"Locked in?" Fair question. NgBracket is standard, compiled Angular from npm — no custom CLI, no proprietary runtime, signals-native and standalone. Leave and what's left is ordinary Angular. 50% off: https://ngbracket.com?utm_source=bluesky&utm_medium=social&utm_campaign=launch

Autocomplete is where teams quietly give up on a11y — a floating div of results is a screen-reader dead end. ARIA combobox pattern: role=combobox + aria-expanded, role=option/listbox, aria-activedescendant (focus stays in input), an aria-live result count, Esc/Enter. #a11y

The fix for "the focus ring looks ugly on click" is not outline:none. It's :focus-visible — the browser shows the ring for keyboard focus and hides it for mouse, automatically. :focus{outline:none} breaks keyboard nav for everyone. One line, no trade-off. #a11y

toSignal() bridges RxJS and signals: x = toSignal(source$, {initialValue: 0}) — latest value as a signal you read with x(), auto-unsubscribe on destroy, composes into computed(). RxJS still owns streams; toSignal is the handoff to the template. toObservable() goes back. #Angular

Something new is landing this week ⏳ Angular v22.1 arrives on July 29th. Clear your calendars, check your dependencies, and get ready for the next set of updates to the framework. What features are you hoping to see in this minor release? Drop your predictions below. 👇

AXE passing is where NgBracket starts, not stops. Automated checks catch ~30–40% of real a11y bugs — so every component gets a manual keyboard + screen-reader pass too, before it's done. 50% off: https://ngbracket.com?utm_source=bluesky&utm_medium=social&utm_campaign=launch

A toast that fades in at the corner is, to a screen-reader user, an event that never happened. Render it into an aria-live region — polite for info, assertive only for interrupting errors. Make dismiss a real keyboard button, and never hide the only "Undo" inside it. #a11y

aria-live in one screen: polite = wait then announce (your default — totals, "saved", results, loading→done). assertive = interrupt now (errors only, use sparingly). off = silent. 99% of the time you want polite; assertive "to be safe" is the classic mistake. #a11y

Angular templates now have local variables — the new let-block. Name a value once (total = cart$ | async) and reuse it below, no double async pipe and no if-block just to alias it. Scoped, readable, no directive. Small feature, big cleanup for the unwrap-once-use-thrice pattern. #Angular #WebDev

Bugs automated tools never flag: focus that doesn't move to a dialog, arrow keys dead in a tablist, an aria-live region too chatty or silent, a visible label ≠ the accessible name, a control with no keyboard, reduced-motion ignored. AXE is a spell-checker, not a proofreader. #a11y

Tabs are one of the web's most-failed widgets. Divs with click handlers aren't a tablist to a screen reader. ARIA pattern: role=tablist/tab/tabpanel, roving tabindex (arrows move, not Tab), aria-selected, focus to the panel. Get it right → "tab 2 of 4, selected." #a11y #Angular