Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Delete Your Dependencies: What the Browser Can ...

Avatar for Paul Conroy Paul Conroy
October 19, 2026

Delete Your Dependencies: What the Browser Can Do Now

The browser has changed. Most of us just haven’t caught up.

Modern web APIs now cover ground that used to require heavy libraries, complex workarounds, or even native apps. In this talk, we’ll explore a selection of these capabilities in action: richer UI primitives built into HTML and CSS, direct access to the local file system, real-time screen capture and processing, and even interaction with physical devices from a web page.

Each example is grounded in practical use cases, showing how these APIs can simplify your architecture, reduce reliance on third-party dependencies, and unlock new ways of building on the web. Along the way, we’ll highlight what’s production-ready today, where the rough edges still are, and how to think about adopting these features safely.

Every dependency is a tax on bundle size, on security, and on the next developer who inherits your code. This talk is about paying less of it.

The browser isn’t a thin client anymore, and treating it like one is costing you more than you think!

Avatar for Paul Conroy

Paul Conroy

October 19, 2026

More Decks by Paul Conroy

Other Decks in Technology

Transcript

  1. Delete Your Dependencies: What the Browser Can Do Now Paul

    Conroy / @conroyp 22nd October, 2026
  2. Paul Conroy 🇮🇪 From Dublin, Ireland 👴 Started playing with

    the web 30+ years ago (Notepad, table layouts, spacer GIFs, & FTP deploys!) CTO at Square1 🌍 conroyp.com / @conroyp
  3. I have a problem! • I’m working on an android

    app • Client call coming up • They want a demo • The call is on google meet
  4. Making the connection • I need my browser to show

    a partial desktop view • What lives in the middle? ◦ Screen capture software? ◦ Virtual camera? ◦ External plugin?
  5. Making the connection • I need my browser to show

    a partial desktop view • What lives in the middle? ◦ Screen capture software? ◦ Virtual camera? ◦ External plugin?
  6. ✂

  7. The page: three elements <div id="setup-screen"> <!-- 1. live capture

    of your screen --> <video id="video" autoplay></video> <!-- 2. drag a rectangle on top --> <div id="overlay"></div> </div> <div id="portal-screen" hidden> <!-- 3. the cropped copy you share --> <canvas id="canvas"></canvas> </div>
  8. The page: three elements <div id="setup-screen"> <!-- 1. live capture

    of your screen --> <video id="video" autoplay></video> <!-- 2. drag a rectangle on top --> <div id="overlay"></div> </div> <div id="portal-screen" hidden> <!-- 3. the cropped copy you share --> <canvas id="canvas"></canvas> </div>
  9. LIMITED Capture the screen const stream = await navigator.mediaDevices.getDisplayMedia({ video:

    { frameRate: 30 }, audio: false, }); video.srcObject = stream;
  10. LIMITED Capture the screen const stream = await navigator.mediaDevices.getDisplayMedia({ video:

    { frameRate: 30 }, audio: false, }); video.srcObject = stream;
  11. BROAD Select: pick an area of the screen // The

    video is shrunk to fit (object-fit: contain) // so work out where it sits const box = overlay.getBoundingClientRect(); const scale = Math.min(box.width / video.videoWidth, box.height / video.videoHeight); const offsetX = (box.width - video.videoWidth * scale) / 2; const offsetY = (box.height - video.videoHeight * scale) / 2; // sel = rectangle the user dragged on the overlay const sx = (sel.x - offsetX) / scale; const sy = (sel.y - offsetY) / scale; const sw = sel.w / scale; const sh = sel.h / scale;
  12. Crop: copy part of the video into canvas // Swap

    views: the canvas is what the user sees now portalScreen.hidden = false; canvas.width = sw; canvas.height = sh; function draw() { ctx.drawImage(video, sx, sy, sw, sh, // from: this rectangle of the video 0, 0, canvas.width, canvas.height); // to: whole canvas requestAnimationFrame(draw); } draw();
  13. Crop: copy part of the video into canvas // Swap

    views: the canvas is what the user sees now portalScreen.hidden = false; canvas.width = sw; canvas.height = sh; function draw() { ctx.drawImage(video, sx, sy, sw, sh, // from: this rectangle of the video 0, 0, canvas.width, canvas.height); // to: whole canvas requestAnimationFrame(draw); } draw();
  14. Crop: copy part of the video into canvas // Swap

    views: the canvas is what the user sees now portalScreen.hidden = false; canvas.width = sw; canvas.height = sh; function draw() { ctx.drawImage(video, sx, sy, sw, sh, // from: this rectangle of the video 0, 0, canvas.width, canvas.height); // to: whole canvas requestAnimationFrame(draw); } draw();
  15. BROAD Back to the three elements <div id="setup-screen"> <!-- 1.

    live capture of your screen --> <video id="video" autoplay></video> <!-- 2. drag a rectangle on top --> <div id="overlay"></div> </div> <div id="portal-screen" hidden> <!-- 3. the cropped copy you share --> <canvas id="canvas"></canvas> </div>
  16. BROAD Back to the three elements <div id=“setup-screen" hidden> <!--

    1. live capture of your screen --> <video id="video" autoplay></video> <!-- 2. drag a rectangle on top --> <div id="overlay"></div> </div> <div id="portal-screen"> <!-- 3. the cropped copy you share --> <canvas id="canvas"></canvas> </div>
  17. BROAD Gotcha: You can’t hide the video! // ❌ The

    obvious way to hide the setup screen setupScreen.style.display = 'none'; // ✅ Invisible to the user, still "rendered" for the browser Object.assign(setupScreen.style, { position: 'absolute', width: '1px', height: '1px', opacity: '0', overflow: 'hidden', pointerEvents: 'none', });
  18. BROAD Gotcha: You can’t hide the video! // ❌ The

    obvious way to hide the setup screen setupScreen.style.display = 'none'; // ✅ Invisible to the user, still "rendered" for the browser Object.assign(setupScreen.style, { position: 'absolute', width: '1px', height: '1px', opacity: '0', overflow: 'hidden', pointerEvents: 'none', });
  19. Success! • Video inside tab • Share tab on call

    • It works! • Interactive while sharing
  20. Success! • Video inside tab • Share tab on call

    • It works! • Interactive while sharing https://github.com/conroyp/screen-portal/
  21. 💡 Get an idea! 🚀 Deploy code 🔎 📦 Install

    package / library Look for solution
  22. 💡 Get an idea! 🚀 Deploy code 🔎 📦 Install

    package / library Look for solution
  23. 😱 🚀 upply s t u o ab Panic ulnerability

    v chain Deploy code 💡 Get an idea! 🔎 📦 Install package / library Look for solution
  24. Every dependency is a tax • Larger bundle size •

    Increased maintenance overhead • Additional complexity • Security ◦ “Slop-squatting” is a new AI risk
  25. Where each example works BROAD Current major browsers LIMITED A

    browser or device gap CHROMIUM Chrome, Edge and related browsers PREVIEW Requires a flag or preview setup CHROME Chrome-only
  26. A very detailed order • Textarea for freetext input •

    What about very long entry? • Can’t see it all! • Manually make it bigger? • Plugin to detect when it’s full ◦ Resize?
  27. A very detailed order • Textarea for freetext input •

    What about very long entry? • Can’t see it all! • Manually make it bigger? • Plugin to detect when it’s full ◦ Resize?
  28. BROAD Relative positioning, no JS! <button popovertarget="actions">More actions …</button> <div

    id="actions" popover> <a href="/orders/150">Copy order link</a> <a href="/orders/150/pickup">Copy pickup code</a> </div> <style> #actions { margin: 0; position-area: right; position-try-fallbacks: flip-inline; } </style>
  29. BROAD Relative positioning, no JS! <button popovertarget="actions">More actions …</button> <div

    id="actions" popover> <a href="/orders/150">Copy order link</a> <a href="/orders/150/pickup">Copy pickup code</a> </div> <style> #actions { margin: 0; position-area: right; position-try-fallbacks: flip-inline; } </style>
  30. BROAD Relative positioning, no JS! <button popovertarget="actions">More actions …</button> <div

    id="actions" popover> <a href="/orders/150">Copy order link</a> <a href="/orders/150/pickup">Copy pickup code</a> </div> <style> #actions { margin: 0; position-area: right; position-try-fallbacks: flip-inline; } </style>
  31. BROAD Relative positioning, no JS! <button popovertarget="actions">More actions …</button> <div

    id="actions" popover> <a href="/orders/150">Copy order link</a> <a href="/orders/150/pickup">Copy pickup code</a> </div> <style> #actions { margin: 0; position-area: right; position-try-fallbacks: flip-inline; } </style>
  32. BROAD Relative positioning, no JS! <button popovertarget="actions">More actions …</button> <div

    id="actions" popover> <a href="/orders/150">Copy order link</a> <a href="/orders/150/pickup">Copy pickup code</a> </div> <style> #actions { margin: 0; position-area: right; position-try-fallbacks: flip-inline; } </style>
  33. Orders • Open one order from list • Start editing

    • Go to do something else • Open same order from list • Start editing…
  34. Orders • Open one order from list • Start editing

    • Go to do something else • Open same order from list • Start editing…
  35. Orders • Open one order from list • Start editing

    • Go to do something else • Open same order from list • Start editing…
  36. Orders • Open one order from list • Start editing

    • Go to do something else • Open same order from list • Start editing…
  37. Orders • Open one order from list • Start editing

    • Go to do something else • Open same order from list • Start editing…
  38. Broadcast Channel • Share communication across tabs, windows, iframes, workers

    ◦ Same origin, same browser profile, same device • No backend server needed • No polling • No localstorage workaround needed
  39. BROAD Broadcast Channel // Something got updated const orders =

    new BroadcastChannel('orders'); // Something got updated orders.postMessage({ type: 'order-updated', id: 150 });
  40. BROAD Broadcast Channel // Something got updated const orders =

    new BroadcastChannel('orders'); // Something got updated orders.postMessage({ type: 'order-updated', id: 150 }); // Something changed? Update from source orders.onmessage = ({ data }) => { if (data.type === 'order-updated') { refreshOrder(data.id); } };
  41. One message, every tab #150 #150 #150 • Each browser

    knows what has changed locally • Tell any other open browsers • No backend management required #150
  42. Web locks • Make one instance responsible for polling •

    If the tab dies, another takes over automatically • Held only until the promise settles • When that tab gets the update, broadcast it • Same scope as BroadcastChannel • The web got mutexes! 🔒
  43. BROAD // Every tab queues up to be the poller.

    navigator.locks.request('orders-poller', async () => { // We've the lock until this function returns while (true) { const fresh = await fetch('/api/orders?new=1') .then(r => r.json()); renderOrders(fresh); orders.postMessage({ type: 'orders-updated', orders: fresh }); } }); await new Promise(r => setTimeout(r, 10_000));
  44. BROAD // Every tab queues up to be the poller.

    navigator.locks.request('orders-poller', async () => { // We've the lock until this function returns while (true) { const fresh = await fetch('/api/orders?new=1') .then(r => r.json()); renderOrders(fresh); orders.postMessage({ type: 'orders-updated', orders: fresh }); } }); await new Promise(r => setTimeout(r, 10_000));
  45. BROAD // Every tab queues up to be the poller.

    navigator.locks.request('orders-poller', async () => { // We've the lock until this function returns while (true) { const fresh = await fetch('/api/orders?new=1') .then(r => r.json()); renderOrders(fresh); orders.postMessage({ type: 'orders-updated', orders: fresh }); } }); await new Promise(r => setTimeout(r, 10_000));
  46. Picture in Picture • Important content on a long page

    (order list, video call controls) • Floats above everything while the tab is open • Parent can keep a reference and communicate with it • Only one at a time per browser!
  47. LIMITED const pip = await documentPictureInPicture.requestWindow({ width: 320, height: 480

    }); // Bring our styles over document.querySelectorAll('link[rel=stylesheet], style') .forEach(node => pip.document.head.append(node.cloneNode(true))); // Move the order in pip.document.body.append(orderList); // Move it back when we close pip.addEventListener('pagehide', () => { document.querySelector('#orders-slot').append(orderList); });
  48. LIMITED const pip = await documentPictureInPicture.requestWindow({ width: 320, height: 480

    }); // Bring our styles over document.querySelectorAll('link[rel=stylesheet], style') .forEach(node => pip.document.head.append(node.cloneNode(true))); // Move the order in pip.document.body.append(orderList); // Move it back when we close pip.addEventListener('pagehide', () => { document.querySelector('#orders-slot').append(orderList); });
  49. LIMITED const pip = await documentPictureInPicture.requestWindow({ width: 320, height: 480

    }); // Bring our styles over document.querySelectorAll('link[rel=stylesheet], style') .forEach(node => pip.document.head.append(node.cloneNode(true))); // Move the order in pip.document.body.append(orderList); // Move it back when we close pip.addEventListener('pagehide', () => { document.querySelector('#orders-slot').append(orderList); });
  50. LIMITED const pip = await documentPictureInPicture.requestWindow({ width: 320, height: 480

    }); // Bring our styles over document.querySelectorAll('link[rel=stylesheet], style') .forEach(node => pip.document.head.append(node.cloneNode(true))); // Move the order in pip.document.body.append(orderList); // Move it back when we close pip.addEventListener('pagehide', () => { document.querySelector('#orders-slot').append(orderList); });
  51. LIMITED const pip = await documentPictureInPicture.requestWindow({ width: 320, height: 480

    }); // Bring our styles over document.querySelectorAll('link[rel=stylesheet], style') .forEach(node => pip.document.head.append(node.cloneNode(true))); // Move the order in pip.document.body.append(orderList); // Move it back when we close pip.addEventListener('pagehide', () => { document.querySelector('#orders-slot').append(orderList); });
  52. Smooth transitions? • Store manager has been playing with their

    phone • Some very nice animations between screens! • Can we do it in our SSR web app? ◦ Move to React? ◦ Vue? ◦ Client-side routing?
  53. LIMITED /* In the stylesheet shared by both pages */

    @view-transition { navigation: auto; } {{-- list.html --}} /* Unique name per item */ <div style="view-transition-name: order-150">#150</div> <div style="view-transition-name: order-151">#151</div> <div style="view-transition-name: order-152">#152</div> {{-- detail.html --}} <h1 style="view-transition-name: order-150">#150</h1>
  54. LIMITED /* In the stylesheet shared by both pages */

    @view-transition { navigation: auto; } {{-- list.html --}} /* Unique name per item */ <div style="view-transition-name: order-150">#150</div> <div style="view-transition-name: order-151">#151</div> <div style="view-transition-name: order-152">#152</div> {{-- detail.html --}} <h1 style="view-transition-name: order-150">#150</h1>
  55. PREVIEW const video = document.querySelector('video'); video.srcObject = await navigator.mediaDevices.getUserMedia({ video:

    { facingMode: 'user' }, audio: false, }); await video.play(); if (!('FaceDetector' in window)) { throw new Error('No native FaceDetector here!'); } const detector = new FaceDetector({ fastMode: true, maxDetectedFaces: 1 });
  56. PREVIEW const video = document.querySelector('video'); video.srcObject = await navigator.mediaDevices.getUserMedia({ video:

    { facingMode: 'user' }, audio: false, }); await video.play(); if (!('FaceDetector' in window)) { throw new Error('No native FaceDetector here!'); } const detector = new FaceDetector({ fastMode: true, maxDetectedFaces: 1 });
  57. PREVIEW const ctx = canvas.getContext('2d'); let hits = 0; async

    function tick() { ctx.clearRect(0, 0, canvas.width, canvas.height); const [face] = await detector.detect(video); // Count consecutive frames with a face hits = face ? hits + 1 : 0; if (face && hits >= 10) { const { x, y, width, height } = face.boundingBox; // draw box... } video.requestVideoFrameCallback(tick); } video.requestVideoFrameCallback(tick);
  58. PREVIEW const ctx = canvas.getContext('2d'); let hits = 0; async

    function tick() { ctx.clearRect(0, 0, canvas.width, canvas.height); const [face] = await detector.detect(video); // Count consecutive frames with a face hits = face ? hits + 1 : 0; if (face && hits >= 10) { const { x, y, width, height } = face.boundingBox; // draw box... } video.requestVideoFrameCallback(tick); } video.requestVideoFrameCallback(tick);
  59. PREVIEW const ctx = canvas.getContext('2d'); let hits = 0; async

    function tick() { ctx.clearRect(0, 0, canvas.width, canvas.height); const [face] = await detector.detect(video); // Count consecutive frames with a face hits = face ? hits + 1 : 0; if (face && hits >= 10) { const { x, y, width, height } = face.boundingBox; // draw box... } video.requestVideoFrameCallback(tick); } video.requestVideoFrameCallback(tick);
  60. PREVIEW const ctx = canvas.getContext('2d'); let hits = 0; async

    function tick() { ctx.clearRect(0, 0, canvas.width, canvas.height); const [face] = await detector.detect(video); // Count consecutive frames with a face hits = face ? hits + 1 : 0; if (face && hits >= 10) { const { x, y, width, height } = face.boundingBox; // draw box... } video.requestVideoFrameCallback(tick); } video.requestVideoFrameCallback(tick); Run in web worker to save main threads!
  61. const [face] = await detector.detect(video); Smile detection? const mouth =

    face.landmarks.find(l => l.type === 'mouth'); // We have a mouth, but don't know the shape it makes!
  62. const [face] = await detector.detect(video); Smile detection? const mouth =

    face.landmarks.find(l => l.type === 'mouth'); // We have a mouth, but don't know the shape it makes! • We can get coords for eyes and mouth • Missing mouth geometry • MediaPipe - Google AI & ML tooling ◦ All run locally on client side ◦ A new dependency! https://developers.google.com/edge/mediapipe/solutions/guide
  63. const vision = await FilesetResolver.forVisionTasks( '/mediapipe/wasm' ); const landmarker =

    await FaceLandmarker.createFromOptions( vision, { baseOptions: { modelAssetPath: '/face_landmarker.task', delegate: 'GPU' }, runningMode: 'VIDEO', // We want the expression scores outputFaceBlendshapes: true, } );
  64. const vision = await FilesetResolver.forVisionTasks( '/mediapipe/wasm' ); const landmarker =

    await FaceLandmarker.createFromOptions( vision, { baseOptions: { modelAssetPath: '/face_landmarker.task', delegate: 'GPU' }, runningMode: 'VIDEO', // We want the expression scores outputFaceBlendshapes: true, } );
  65. const result = landmarker.detectForVideo( video, performance.now() ); // Check the

    scores for each recognised face shape const score = name => result.faceBlendshapes[0]?.categories .find(c => c.categoryName === name)?.score ?? 0; const smile = (score('mouthSmileLeft') + score('mouthSmileRight')) / 2; if (smile > 0.65) { unlockDiscount(); } // SMILE10 ☕
  66. const result = landmarker.detectForVideo( video, performance.now() ); // Check the

    scores for each recognised face shape const score = name => result.faceBlendshapes[0]?.categories .find(c => c.categoryName === name)?.score ?? 0; const smile = (score('mouthSmileLeft') + score('mouthSmileRight')) / 2; if (smile > 0.65) { unlockDiscount(); } // SMILE10 ☕
  67. LIMITED Shape detection • Face detection is early • QR

    code detection is more stable video.srcObject = await navigator.mediaDevices.getUserMedia({ video: true }); const detector = new BarcodeDetector({ formats: ['qr_code'] }); const [code] = await detector.detect(video); if (code) { console.log(code.rawValue); // CUSTOMER_123_LOYALTY }
  68. LIMITED Shape detection • Face detection is early • QR

    code detection is more stable video.srcObject = await navigator.mediaDevices.getDisplayMedia({ video: true }); const detector = new BarcodeDetector({ formats: ['qr_code'] }); const [code] = await detector.detect(video); if (code) { console.log(code.rawValue); // CUSTOMER_123_LOYALTY } Swap from camera to screen-sharing
  69. Physical Connection? • Web Bluetooth - Bluetooth, but on the

    web! • A web page can talk to real hardware ◦ BLE (Bluetooth Low Energy) ▪ Printers ▪ Thermometers ▪ Smart scales ◦ Not all Bluetooth devices support connection
  70. CHROMIUM Bluetooth Printer // 1. User picks the printer from

    the browser's chooser const device = await navigator.bluetooth.requestDevice(); // 2. Connect and find the characteristic we write to const server = await device.gatt.connect(); const PRNT_WRITE = 0x2af1; // Demo printer UUIDs const printer = await service.getCharacteristic(PRNT_WRITE);
  71. CHROMIUM // 3. Build bytes array const bytes = new

    Uint8Array([ new TextEncoder().encode('1 x Flat white for Aoife\n'), ]); // 4. Send in small chunks: BLE packets are tiny for (let i = 0; i < bytes.length; i += 20) { await printer.writeValueWithoutResponse( bytes.slice(i, i + 20) ); } device.gatt.disconnect();
  72. CHROMIUM // 3. Build bytes array const bytes = new

    Uint8Array([ new TextEncoder().encode('1 x Flat white for Aoife\n'), ]); // 4. Send in small chunks: BLE packets are tiny for (let i = 0; i < bytes.length; i += 20) { await printer.writeValueWithoutResponse( bytes.slice(i, i + 20) ); } device.gatt.disconnect();
  73. CHROMIUM // 3. Build bytes array const bytes = new

    Uint8Array([ new TextEncoder().encode('1 x Flat white for Aoife\n'), ]); // 4. Send in small chunks: BLE packets are tiny for (let i = 0; i < bytes.length; i += 20) { await printer.writeValueWithoutResponse( bytes.slice(i, i + 20) ); } device.gatt.disconnect();
  74. CHROMIUM const device = await navigator.bluetooth.requestDevice({ filters: [ { namePrefix:

    'PLAYBULB' }, // partial name match { services: [0xff0d] }, // ...or this service ] });
  75. CHROMIUM const device = await navigator.bluetooth.requestDevice({ filters: [ { namePrefix:

    'PLAYBULB' }, // partial name match { services: [0xff0d] }, // ...or this service ] });
  76. CHROMIUM const device = await navigator.bluetooth.requestDevice({ filters: [ { namePrefix:

    'PLAYBULB' }, // partial name match { services: [0xff0d] }, // ...or this service ] });
  77. CHROMIUM const device = await navigator.bluetooth.requestDevice({ filters: [ { namePrefix:

    'PLAYBULB' }, // partial name match { services: [0xff0d] }, // ...or this service ] }); const server = await device.gatt.connect(); const service = await server.getPrimaryService(0xff0d); const colour = await service.getCharacteristic(0xfffc); // Mipow Playbulb: 4 bytes, [white, red, green, blue] await colour.writeValueWithResponse( new Uint8Array([0, 255, 120, 0]) );
  78. CHROMIUM const device = await navigator.bluetooth.requestDevice({ filters: [ { namePrefix:

    'PLAYBULB' }, // partial name match { services: [0xff0d] }, // ...or this service ] }); const server = await device.gatt.connect(); const service = await server.getPrimaryService(0xff0d); const colour = await service.getCharacteristic(0xfffc); // Mipow Playbulb: 4 bytes, [white, red, green, blue] await colour.writeValueWithResponse( new Uint8Array([0, 255, 120, 0]) );
  79. CHROMIUM const device = await navigator.bluetooth.requestDevice({ filters: [ { namePrefix:

    'PLAYBULB' }, // partial name match { services: [0xff0d] }, // ...or this service ] }); const server = await device.gatt.connect(); const service = await server.getPrimaryService(0xff0d); const colour = await service.getCharacteristic(0xfffc); // Mipow Playbulb: 4 bytes, [white, red, green, blue] await colour.writeValueWithResponse( new Uint8Array([0, 255, 120, 0]) );
  80. CHROME AI Assistance • The model ships with the browser

    ◦ Downloads on first run ◦ No remote server or API key 🧑💻 • Data stays on the device • Tasks APIs, plus a general-purpose one • Chrome desktop, decent hardware (GPU, disk space) • ⚠ Firefox and Safari currently opposed ⚠
  81. CHROME const session = await LanguageModel.create({ initialPrompts: [{ role: 'system',

    content: 'Summarise this review for me.' }], }); const result = await session.prompt( `<review>${review}</review>`, { responseConstraint: { type: 'object', properties: { summary: { type: 'string' }, sentiment: { enum: ['positive', 'neutral', 'negative']}, }, }}, );
  82. • Task APIs to simplify common tasks ◦ Summarizer ◦

    Rewriter ◦ Translator ◦ Proofreader • Prompt API for more bespoke requirements const summarizer = await Summarizer.create({ type: 'tldr', length: 'short' }); const summary = await summarizer.summarize(review); CHROME
  83. CHROME Go multi-modal! const session = await LanguageModel.create({ expectedInputs: [{

    type: 'text' }, { type: 'image' }], }); await session.prompt([{ role: 'user', content: [ { type: 'text', value: `Describe the photo` }, { type: 'image', value: photo }, ]} ]);
  84. Screen Capture + Canvas: share one area getUserMedia: use the

    camera field-sizing: grow textarea with content BarcodeDetector: scan an order code CSS Anchoring: keep a menu by its button Web Bluetooth: connect real-world devices BroadcastChannel: alert another tab Web Locks: give one tab the polling job Document PiP: float the live queue Built-in AI: summarise locally View Transitions: animate server-rendered navigation WebMCP: Simplify agent access to sites Availability, permissions and fallbacks still matter!
  85. 😱 🚀 upply s t u o ab Panic ulnerability

    v chain Deploy code 💡 Get an idea! 🔎 📦 Install package / library Look for solution
  86. 😱 🚀 upply s t u o ab Panic ulnerability

    v chain Deploy code 💡 Get an idea! 🔎 Look for solution
  87. 😱 🚀 upply s t u o ab Panic ulnerability

    v chain Deploy code 💡 Get an idea! 🔎 Look for solution