Safari Web Extensions allow you to customize the Safari browsing experience. They can modify web pages, add toolbar buttons, and interact with the browser using the Web Extensions API.
Safari extensions use standard Web Extensions APIs compatible with Chrome and Firefox extensions, with some Safari-specific features.
Getting Started
Generate the Safari extension
This creates targets/safari/ with:
expo-target.config.js — Target configuration
SafariWebExtensionHandler.swift — Native message handler
assets/ — Web extension files (HTML, CSS, JavaScript)
Configure the target
targets/safari/expo-target.config.js
/** @type {import('@bacons/apple-targets/app.plugin').Config} */
module . exports = {
type: "safari" ,
icon: "../../assets/icon.png" ,
deploymentTarget: "15.0" ,
};
Run prebuild
npx expo prebuild -p ios --clean
Enable the extension
Run your app, then go to Safari Settings > Extensions and enable your extension.
Extension Structure
The generated extension includes these files:
targets/safari/assets/
├── manifest.json # Extension manifest
├── background.js # Background script
├── content.js # Content script (runs on web pages)
├── popup.html # Toolbar button popup
├── popup.js # Popup script
├── popup.css # Popup styles
├── images/ # Extension icons
└── _locales/en/ # Localization
└── messages.json
Manifest File
targets/safari/assets/manifest.json
{
"manifest_version" : 3 ,
"name" : "My Extension" ,
"version" : "1.0" ,
"description" : "A Safari Web Extension" ,
"icons" : {
"48" : "images/icon-48.png" ,
"96" : "images/icon-96.png" ,
"128" : "images/icon-128.png" ,
"256" : "images/icon-256.png"
},
"action" : {
"default_popup" : "popup.html" ,
"default_title" : "My Extension" ,
"default_icon" : {
"16" : "images/toolbar-icon-16.png" ,
"19" : "images/toolbar-icon-19.png" ,
"32" : "images/toolbar-icon-32.png" ,
"38" : "images/toolbar-icon-38.png"
}
},
"background" : {
"service_worker" : "background.js"
},
"content_scripts" : [
{
"matches" : [ "*://*/*" ],
"js" : [ "content.js" ]
}
],
"permissions" : [
"activeTab" ,
"storage"
]
}
Content Scripts
Content scripts run in the context of web pages:
targets/safari/assets/content.js
// Run when the page loads
( function () {
'use strict' ;
console . log ( 'Extension loaded on:' , window . location . href );
// Example: Highlight all links
const links = document . querySelectorAll ( 'a' );
links . forEach ( link => {
link . style . outline = '2px solid red' ;
});
// Listen for messages from background script
browser . runtime . onMessage . addListener (( request , sender , sendResponse ) => {
if ( request . action === 'highlightLinks' ) {
highlightLinks ( request . color );
sendResponse ({ success: true });
}
return true ;
});
function highlightLinks ( color ) {
const links = document . querySelectorAll ( 'a' );
links . forEach ( link => {
link . style . backgroundColor = color ;
});
}
})();
Background Scripts
Background scripts handle extension logic:
targets/safari/assets/background.js
// Listen for extension installation
browser . runtime . onInstalled . addListener (() => {
console . log ( 'Extension installed' );
// Set default storage values
browser . storage . local . set ({
highlightColor: '#ffff00' ,
enabled: true
});
});
// Listen for toolbar button clicks
browser . action . onClicked . addListener (( tab ) => {
// Send message to content script
browser . tabs . sendMessage ( tab . id , {
action: 'highlightLinks' ,
color: '#ffff00'
});
});
// Listen for messages from content scripts
browser . runtime . onMessage . addListener (( request , sender , sendResponse ) => {
console . log ( 'Received message:' , request );
if ( request . action === 'getData' ) {
browser . storage . local . get ( 'highlightColor' ). then ( result => {
sendResponse ({ color: result . highlightColor });
});
return true ; // Keep channel open for async response
}
});
targets/safari/assets/popup.html
targets/safari/assets/popup.js
targets/safari/assets/popup.css
<! DOCTYPE html >
< html >
< head >
< meta charset = "UTF-8" >
< link rel = "stylesheet" href = "popup.css" >
</ head >
< body >
< div class = "container" >
< h1 > My Extension </ h1 >
< p > Customize your browsing experience </ p >
< div class = "setting" >
< label for = "color" > Highlight Color: </ label >
< input type = "color" id = "color" value = "#ffff00" >
</ div >
< button id = "apply" > Apply </ button >
< button id = "reset" > Reset </ button >
</ div >
< script src = "popup.js" ></ script >
</ body >
</ html >
Native Messaging
Communicate between your web extension and native Swift code:
Swift Handler
targets/safari/SafariWebExtensionHandler.swift
import SafariServices
import os . log
class SafariWebExtensionHandler : NSObject , NSExtensionRequestHandling {
func beginRequest ( with context : NSExtensionContext) {
let item = context. inputItems [ 0 ] as! NSExtensionItem
let message = item. userInfo ? [SFExtensionMessageKey]
os_log (. default , "Received message from browser.runtime.sendNativeMessage: %@" , message as! CVarArg )
let response = NSExtensionItem ()
response. userInfo = [ SFExtensionMessageKey : [ "Response to" : message ] ]
context. completeRequest ( returningItems : [response], completionHandler : nil )
}
}
JavaScript Caller
// Send message to native code
browser . runtime . sendNativeMessage ( 'com.yourapp.safari' , {
action: 'getData' ,
key: 'someKey'
}, response => {
console . log ( 'Native response:' , response );
});
Permissions
Common permissions for Safari extensions:
{
"permissions" : [
"activeTab" , // Access current tab
"storage" , // Use browser.storage API
"tabs" , // Query and manage tabs
"webRequest" , // Intercept network requests
"webNavigation" , // Monitor page navigation
"contextMenus" // Add context menu items
],
"host_permissions" : [
"*://*/*" // Access all websites
]
}
Content Security Policy
For Manifest V3, configure CSP:
{
"content_security_policy" : {
"extension_pages" : "script-src 'self'; object-src 'self';"
}
}
Debugging
Enable Web Inspector
On iOS: Settings > Safari > Advanced > Web Inspector
On Mac: Safari > Settings > Advanced > Show Develop menu
Debug Content Scripts
Open Safari and navigate to a page
Right-click and select “Inspect Element”
Go to the Sources tab to see your content scripts
Debug Background Scripts
Open Safari > Develop > Web Extension Background Content
Select your extension from the list
Right-click your extension’s toolbar icon
Select “Inspect Popup”
Testing
Run your app
Build and run your app in Xcode or on a device.
Enable the extension
iOS: Settings > Safari > Extensions > Your Extension (toggle on)
macOS: Safari > Settings > Extensions > Your Extension (check the box)
Test functionality
Open Safari and navigate to a website to test your content scripts.
Distribution
Safari extensions must be distributed through the App Store as part of an iOS or macOS app.
Your extension is packaged with your app and automatically available when users install your app.
Localization
Add translations in assets/_locales/:
assets/_locales/
├── en/
│ └── messages.json
├── es/
│ └── messages.json
└── fr/
└── messages.json
_locales/en/messages.json
{
"extensionName" : {
"message" : "My Extension"
},
"extensionDescription" : {
"message" : "A great Safari extension"
}
}
Reference in manifest.json:
{
"name" : "__MSG_extensionName__" ,
"description" : "__MSG_extensionDescription__"
}
Best Practices
Keep it lightweight: Content scripts run on every page load. Minimize their impact on page performance.
Use storage wisely: Store settings in browser.storage.local or browser.storage.sync (syncs across devices).
Respect privacy: Only request permissions you actually need. Users can see and must approve all permissions.
Next Steps