WWDC Quick Look 💓 By SwiftGGTeam
What's new in Safari extensions

What's new in Safari extensions

Watch original video

Highlight

Safari 17 continues to support Manifest v2 and v3 while Web Extensions now cover xrOS; content blockers gain :has() selector support, Declarative Net Request can modify request headers, App Extensions introduce per-site permissions, and extension behavior in Private Browsing and Profiles is more controllable.

Core Content

Cross-Platform Web Extensions

Safari extensions come in four types: Content Blockers, Share Extensions, App Extensions, and Web Extensions. Safari 17 continues to support all four, but Apple clearly states that Web Extensions are the future for browser customization.

Apple co-chairs the W3C WebExtensions Community Group and works with other browser vendors to drive standardization. That means a Web Extension written once can run on iOS, iPadOS, macOS, and xrOS.

Web Extensions on xrOS have the same capabilities as on iOS: script injection, background content, and popovers.

(00:45)

Safari 17 supports both Manifest v2 and v3, but new features continue to land in v3. Developers should migrate to v3 when the time is right.

Content Blocker Enhancements

Content blockers declaratively block or hide content using JSON rules without needing access to information about which sites users visit. This year adds support for the :has() selector.

:has() lets you match a parent element based on its children. For example, hiding every .post element that contains a .paid-promo child was difficult with pure CSS selectors before.

(02:50)

Declarative Net Request Updates

Declarative Net Request lets Web Extensions intercept and modify network requests declaratively. Safari executes rules at the system level, saving power and protecting privacy.

Since Safari 16.4, you can modify request headers—set, append, or delete header values, or remove a header entirely.

Using this feature requires declaring the declarativeNetRequestWithHostAccess permission in the manifest, and the extension must receive per-site authorization before rules take effect.

(03:37)

The new declarativeNetRequest.setExtensionActionOptions API can set badge text to automatically show an action count (such as blocked requests). This gives users a clear view of what the extension is doing while keeping browsing history private.

(05:09)

Script and Storage APIs

The registerContentScript family of APIs lets you programmatically register, update, and delete content scripts. This complements statically declared content scripts in the manifest—you can inject scripts dynamically for specific pages or conditions, and registrations persist across sessions.

(05:45)

Safari 16.4 added a session storage area. Unlike local storage, session storage data lives in memory and is cleared when Safari exits. This suits sensitive data such as decryption keys and authentication tokens.

(06:22)

Per-Site Permissions

Safari App Extensions now support per-site authorization, aligning with the Web Extension permission model. When an extension is first enabled, it has no site access. When it tries to access a page, Safari shows a prompt on the toolbar icon. Users can choose “Allow for One Day” or “Always Allow.”

Once enabled and authorized, the toolbar icon is tinted. Permissions can be revoked at any time.

When upgrading from older Safari versions, existing extension permissions are migrated. Users also see a banner offering “Ask for Each Website” to improve privacy.

(07:24)

All extensions now show a toolbar icon by default. Apple recommends providing PDF vector icons for correct tinting.

Private Browsing and Profiles

Safari 17 lets users control individually which extensions can access Private Browsing windows without disabling extensions in other browsing contexts.

Extensions that inject scripts or read page content are off by default in Private Browsing. Content blockers and similar extensions that do not access page content are allowed automatically.

Profiles are new in Safari this year for isolating browsing data (history, cookies, site data). Each Profile can have its own extension settings. When an extension is enabled in a Profile, it is a fresh instance with a different UUID, background page, and storage—but per-site permissions are shared across Profiles.

(09:02)

If an extension communicates with a native host app, it must handle messages from multiple Profiles. Decode userInfo in beginRequest(with context:)—when running in a Profile, SFExtensionProfileKey provides a profileIdentifier.

Safari 17’s Develop menu lists Web Extension background content grouped by extension, with inspectable background pages and service workers listed by Profile under each extension.

Detailed Content

Content Blocker :has() Rules

Content blocker rules are defined in JSON. The :has() selector enables child-based matching.

[
  {
    "action": {
      "type": "hide"
    },
    "trigger": {
      "url-filter": ".*",
      "if-domain": ["example.com"],
      "selector": ".post:has(.paid-promo)"
    }
  }
]

Key points:

  • "type": "hide" hides matching elements without blocking network requests
  • "url-filter": ".*" matches all URLs
  • "if-domain" limits the rule to specified domains
  • "selector": ".post:has(.paid-promo)" matches .post elements containing a .paid-promo child
  • :has() supports nesting and combination with other selectors

Declarative Net Request Header Modification

Declare permissions in manifest.json:

{
  "manifest_version": 3,
  "permissions": [
    "declarativeNetRequest",
    "declarativeNetRequestWithHostAccess"
  ],
  "host_permissions": [
    "*://example.com/*"
  ]
}

Key points:

  • declarativeNetRequestWithHostAccess is required to modify headers and redirect
  • host_permissions declares domains the extension can access
  • Rules take effect only after the user grants per-site permission

Rules definition (rules.json):

[
  {
    "id": 1,
    "priority": 1,
    "action": {
      "type": "modifyHeaders",
      "requestHeaders": [
        {
          "header": "User-Agent",
          "operation": "set",
          "value": "MyExtension/1.0"
        },
        {
          "header": "X-Custom-Header",
          "operation": "append",
          "value": "extra-value"
        },
        {
          "header": "X-Tracking-Id",
          "operation": "remove"
        }
      ]
    },
    "condition": {
      "urlFilter": "||example.com",
      "resourceTypes": ["main_frame"]
    }
  }
]

Key points:

  • "operation": "set" replaces an existing header value
  • "operation": "append" appends to the existing value
  • "operation": "remove" removes the header entirely
  • "resourceTypes" limits which resource types the rule applies to

Badge count setup (background script):

// Safari 16.4+
chrome.declarativeNetRequest.setExtensionActionOptions({
  displayActionCountAsBadgeText: true
});

Key points:

  • displayActionCountAsBadgeText: true automatically shows the action count as badge text
  • Currently this is the only option for this API
  • The count updates automatically based on rule triggers

Programmatic Content Script Registration

// Register a dynamic content script
await chrome.scripting.registerContentScripts([
  {
    id: "webkit-highlight",
    matches: ["https://webkit.org/*"],
    js: ["highlight.js"],
    runAt: "document_idle",
    persistAcrossSessions: true
  }
]);

// Update an existing script
await chrome.scripting.updateContentScripts([
  {
    id: "webkit-highlight",
    matches: ["https://webkit.org/*", "https://bugs.webkit.org/*"]
  }
]);

// Remove a script
await chrome.scripting.unregisterContentScripts({ ids: ["webkit-highlight"] });

Key points:

  • registerContentScripts registers content scripts dynamically
  • persistAcrossSessions: true keeps registration across sessions
  • updateContentScripts modifies existing script configuration
  • unregisterContentScripts removes scripts by ID
  • Complements statically declared content_scripts in the manifest

Session Storage Usage

// Store sensitive data in session storage
await chrome.storage.session.set({
  authToken: "eyJhbGciOiJIUzI1NiIs...",
  decryptionKey: "base64-encoded-key"
});

// Read data
const data = await chrome.storage.session.get(["authToken"]);
console.log(data.authToken);

// Compare with local storage
await chrome.storage.local.set({ settings: { theme: "dark" } });    // persisted to disk
await chrome.storage.session.set({ tempKey: "value" });              // in memory, cleared on exit

Key points:

  • chrome.storage.session is a storage area added in Safari 16.4
  • Data is stored in memory, not written to disk
  • Cleared automatically when Safari exits
  • Suitable for auth tokens, decryption keys, and other sensitive data
  • API matches local and sync storage areas

SVG Icon Configuration

{
  "manifest_version": 3,
  "icons": {
    "16": "icon.svg",
    "32": "icon.svg",
    "48": "icon.svg",
    "128": "icon.svg"
  }
}

Key points:

  • Safari 16.4+ supports SVG icons
  • One SVG file can replace PNGs at all sizes
  • Safari handles scaling automatically for crisp rendering
  • Vector icons are recommended for correct tinting

Profile-Aware Message Handling

import SafariServices

class ExtensionRequestHandler: NSObject, NSExtensionRequestHandling {
    func beginRequest(with context: NSExtensionContext) {
        guard let item = context.inputItems.first as? NSExtensionItem,
              let userInfo = item.userInfo else { return }
        
        // Check if running in a Profile
        if let profileId = userInfo[SFExtensionProfileKey] as? String {
            // Extension is running in a Profile
            // Isolate data by profileId
            handleRequest(forProfile: profileId, context: context)
        } else {
            // Default browsing mode
            handleRequest(context: context)
        }
    }
}

Key points:

  • SFExtensionProfileKey identifies the Profile the extension is running in
  • profileIdentifier is a unique ID for each Profile
  • Each Profile instance has its own UUID, background page, and storage
  • Native host apps must isolate data by profileIdentifier

Core Takeaways

1. Use :has() selectors for precise content blocking

  • What to do: Use :has() in content blocker rules to hide content based on page structure rather than URL alone
  • Why it matters: Precisely block complex cases like “article cards containing ad labels” with fewer false positives
  • How to start: Analyze target site DOM structure, identify parent element traits of ads or clutter, write and test :has() rules

2. Manage request headers with Declarative Net Request

  • What to do: Use Declarative Net Request to modify or clean HTTP request headers—unify User-Agent, remove tracking headers, and so on
  • Why it matters: Declarative handling is more power-efficient than intercepting all requests and does not require page content access
  • How to start: Declare declarativeNetRequestWithHostAccess in manifest v3, write modifyHeaders rules, test the per-site authorization flow

3. Move sensitive data from localStorage to sessionStorage

  • What to do: Migrate auth tokens and temporary decryption keys from localStorage to sessionStorage
  • Why it matters: Data is not written to disk and is cleared when Safari exits, reducing sensitive data exposure
  • How to start: Replace chrome.storage.local.set/get for sensitive data with chrome.storage.session.set/get

4. Adapt for Private Browsing and Profiles

  • What to do: Test extension behavior in Private Browsing and multiple Profiles to ensure correct data isolation
  • Why it matters: Safari 17 users may use work and personal Profiles simultaneously; extensions must run independently in each context
  • How to start: Create multiple Profiles in Safari Settings, install and test extensions in each. Verify native host apps handle SFExtensionProfileKey correctly

5. Use programmatic script registration for dynamic features

  • What to do: Register and unregister content scripts dynamically based on user settings or the current page
  • Why it matters: Avoid injecting scripts on every page—load only when needed to reduce overhead
  • How to start: Provide feature toggles in the extension options page; call registerContentScripts when enabled and unregisterContentScripts when disabled

Comments

GitHub Issues · utterances