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
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.
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
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
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
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