A Safari Web Extension that restores the classic 2015/2018 Twitter UI on twitter.com and x.com. This is a Safari port of the OldTwitter Chrome extension by dimdenGD.
OldTwitter is not a CSS skin — it completely replaces Twitter's React frontend with a pre-built classic layout:
- Blocks Twitter's JavaScript from loading entirely
- Strips Content-Security-Policy headers so the extension can inject its own UI
- Injects pre-built HTML/CSS/JS layouts for each page (home, profile, search, notifications, etc.)
- Calls Twitter's own internal APIs using your existing login session
- Restores the old UI: chronological timeline, classic tweet cards, old-style notifications, DMs, and more
All features from the original Chrome extension are preserved: themes, font options, custom CSS, tweet filtering, muting, translation, media viewer, and more.
- macOS 13 (Ventura) or later
- Safari 16.4 or later
- Xcode 14 or later (to build)
git clone https://github.com/aikios/oldtwitter-safari.git
cd oldtwitter-safariopen OldTwitterSafari.xcodeprojSelect the OldTwitterSafari scheme and press Run (⌘R). This builds the host app and the extension together.
- Open Safari → Settings → Extensions
- Enable Old Twitter Layout
- Click Always Allow on twitter.com and Always Allow on x.com when prompted
- Navigate to twitter.com or x.com
Note: The first time you load Twitter after enabling the extension, you may need to log in again. Your credentials are not stored by the extension — it uses your browser's existing Twitter session.
If you're building for personal use, set the team to your personal Apple ID in Signing & Capabilities. No paid developer account is required to run on your own Mac.
The extension runs in three stages on every Twitter page load:
-
blockBeforeInject.js— injected atdocument_start, attaches a MutationObserver that sets all incoming<script>tags totype="javascript/blocked", preventing Twitter's React app from mounting. -
ruleset.json— declarativeNetRequest rules stripContent-Security-PolicyandX-Frame-Optionsheaders, block Twitter's service worker, and fix CORS headers for media. -
injection.js— detects the current page URL, fetches the corresponding pre-built layout fromlayouts/[page]/, replacesdocument.documentElement.innerHTMLwith the layout HTML, then injects the layout's scripts and styles. The layout scripts call Twitter's own REST/GraphQL APIs viaapis.jsto load your actual timeline, notifications, etc.
OldTwitterSafariExtension/Resources/
├── manifest.json # Safari MV3 manifest
├── scripts/
│ ├── blockBeforeInject.js # Blocks Twitter's React at document_start
│ ├── injection.js # Routes pages and injects layouts
│ ├── apis.js # Twitter REST + GraphQL API wrappers
│ ├── helpers.js # Shared utilities
│ ├── tweetConstructor.js # Builds tweet DOM elements
│ ├── tweetviewer.js # Modal tweet detail view
│ ├── background.js # Service worker
│ └── ...
├── layouts/
│ ├── home/ # Timeline
│ ├── tweet/ # Individual tweet + replies
│ ├── profile/ # User profiles
│ ├── notifications/ # Notifications + mentions
│ ├── search/ # Search results
│ ├── bookmarks/
│ ├── lists/
│ └── ... # Each layout has index.html + style.css + script.js
├── images/ # Icons and assets
├── fonts/ # Custom icon font (JustBird, rosetta)
├── libraries/ # Third-party libs (twemoji, viewer.js, etc.)
└── ruleset.json # declarativeNetRequest rules
All core extension code is by dimdenGD. This repo contains only the Safari packaging, Xcode project, and Safari-specific compatibility patches.
Safari-specific changes made in this port:
- Xcode project and Swift host app wrapper
browser.runtime.getURLinstead ofchrome.runtime.getURLthroughout- Favicon delivered as embedded
data:URL (Safari ignoressafari-web-extension://URLs as favicons) history.replaceStatetrigger for Safari to re-evaluate favicon after DOM replacement- CSS URL rewriting (
chrome-extension://→safari-web-extension://) for font assets - Translate button shown inline on timeline (not just tweet detail page)
See the original project for license information.