Customising
Customising
the web:
browsers as user agents
delan azabani (she/her)
november 2023
Browsers as user agents
Empower users to interact with the web on their own terms!
my general process is that every time i am feeling complainthoughts about a website, i think, wait, i could do something about that.
Why customise the web?
- [cohost, mastodon, twitter]
- Accessibility (26)
- Readability and layout (31)
- Automation and user rights (26)
- Blocking non-ad content (18)
- Bugs and features (18)
- Personalisation (11)
- Other (2)
More survey responses
- Blocking ads (∞)
- Accessibility (26)
- Readability and layout (31)
- Automation and user rights (26)
- Blocking other content (18)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
- Redirecting to preferred websites (6)
[1, 2, 3, 4, 5, 6]
- Improving media controls (6) [1, 2, 3, 4, 5, 6]
- Organising information (5) [1, 2, 3, 4, 5]
- Fixing website bugs (4) [1, 2, 3, 4]
- Personalising websites (4) [1, 2, 3, 4]
- Improving notifications/replies (3) [1, 2, 3]
- Game modding(!)
- Other changes (2) [1, 2]
looking through my user styles it's a lot of making things more accessible.
Accessibility
- Dark/light mode (9) [1, 2, 3, 4, 5, 6, 7, 8, 9]
(see also ‘prefers-color-scheme’)
- Removing animations/distractions
(7) [1, 2, 3, 4, 5, 6, 7]
(see also ‘prefers-reduced-motion’)
- Surfacing alt text (5) [1, 2, 3, 4, 5]
- More/less contrast (2) [1, 2]
(see also ‘forced-colors’)
- Keyboard navigation (2) [1, 2]
(see also ‘:focus-visible’)
- Other accessibility changes (1) [1]
cohost is really difficult to use with a screen reader, so i made a userscript that adds aria labels and fixes incorrect roles. for me when my eyes are dying
Readability and layout
- Fonts and font settings (12) [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
(see also built-in reader modes)
- Wider/narrower layout (5) [1, 2, 3, 4, 5]
(see also built-in reader modes)
- More/less spacing (3) [1, 2, 3]
- Expanding/collapsing views (3) [1, 2, 3]
- Resizing or moving videos (2) [1, 2]
(see also built-in picture-in-picture modes)
- Unsticking headers/footers (2) [1, 2]
- Other layout changes (4) [1, 2, 3, 4]
Automation & user rights
- Data entry (6) [1, 2, 3, 4, 5, 6]
- Bulk downloading/archiving (5) [1, 2, 3, 4, 5]
- Bypassing {pay,signup}walls (4) [1, 2, 3, 4]
- Canned responses (2) [1, 2]
- Exercising cookie consent (2) [1, 2]
- Go to next/parent URL (2) [1, 2]
- Disabling security theatre (2) [1, 2]
- Other such changes (3) [1, 2, 3]
i wrote scripts to automatically deny all on various GDPR “we respect your privacy” consent popups, [and a] script that automatically enables captions on youtube videos (if available) because i like having them
I made a userscript that injected a button […] into the CMS that opened an auto-populated AIM message [to my micromanaging boss].
Tools for customisation
- Author tools
- styles
- scripts
- interactivity
⇔
- User tools
- user styling
- user scripting
- bookmarklets
- User styling: when loading some URL, add custom CSS
- User scripting: when loading some URL, run JS in page context
- Bookmarklets: when clicking bookmark, run JS in page context
User styling
- Epiphany, Firefox: Custom user stylesheet (built-in)
When loading any page, add CSS in user origin
- Chrome, Firefox: Userstyles (
Stylish → Stylus)
When loading matching URL, inject CSS into author origin
I don't really do my own coding or writing my own scripts or anything, but I DO […] write my own custom CSS […] for pretty much every website I use frequently.
i think i have a userstyle for almost every site i use regularly
User scripting
- Epiphany: Custom user script (built-in)
When loading any page, run JS in page context
- Epiphany, Chrome, Firefox Userscripts ({Grease,Tamper,Violent}Monkey)
When loading matching URL, run JS in page context
Content blocking
AdBlock Plus → uBlock Origin
- Remove elements matching CSS selectors
- uBlock Origin’s picker UI is pretty useful
- Left picker is ancestors ↔ descendants
- Right picker is general ↔ specific
We use cookies and other tracking technologies to improve your browsing experience on our website, to show you personalised content and targeted ads, to analyse our website traffic, and to understand where our visitors are coming from.
Hi, how can I help you today?
👩💼
Igalia NFT now selling!
Limited edition! Don’t wait!
Userstyle gotchas
(author origin)
- You will sometimes need
!important
- You will sometimes need specificity hacks:
*
→ *:not(#specificity)
- You can’t style elements under shadow roots! (stylus#739)
- Walk shadowRoot tree (open) or hook attachShadow (closed),
then inject styles with adoptedStyleSheets (userscript)
User stylesheet gotchas
(user origin)
- Epiphany doesn’t support
@-moz-document
- Firefox only reloads userContent.css on restart
- Most custom styles in the wild are userstyles (author origin)
- You will almost always need
!important
- You will never need specificity hacks
- Styling elements under shadow roots is trivial
No more GitHub scrapers
(see also full version, which rewrites Google search results too)
// ==UserScript==
// @match https://githubmemory.com/repo/*
// @match https://gitmemory.com/issue/*
// @match https://issueexplorer.com/issue/*
// @match https://uonfu.com/q/*
// ==/UserScript==
const BAD = {
"githubmemory.com": {pathPrefix: "/repo/", userIndex: 2, repoIndex: 3, issueIndex: 5},
"gitmemory.com": {pathPrefix: "/issue/", userIndex: 2, repoIndex: 3, issueIndex: 4},
"issueexplorer.com": {pathPrefix: "/issue/", userIndex: 2, repoIndex: 3, issueIndex: 4},
"uonfu.com": {pathPrefix: "/q/", userIndex: 2, repoIndex: 3, issueIndex: 4},
};
if (BAD.hasOwnProperty(location.hostname)) {
const {pathPrefix, userIndex, repoIndex, issueIndex} = BAD[location.hostname];
if (location.pathname.startsWith(pathPrefix)) {
const components = location.pathname.split("/");
const [user, repo, issue] = [userIndex, repoIndex, issueIndex].map(x => components[x]);
location.replace(`https://github.com/${user}/${repo}/issues/${issue}`);
}
}
No more GitHub scrapers
(see also full version, which rewrites Google search results too)
// ==UserScript==
// @match https://githubmemory.com/repo/*
// @match https://gitmemory.com/issue/*
// @match https://issueexplorer.com/issue/*
// @match https://uonfu.com/q/*
// ==/UserScript==
const BAD = {
"githubmemory.com": {pathPrefix: "/repo/", userIndex: 2, repoIndex: 3, issueIndex: 5},
"gitmemory.com": {pathPrefix: "/issue/", userIndex: 2, repoIndex: 3, issueIndex: 4},
"issueexplorer.com": {pathPrefix: "/issue/", userIndex: 2, repoIndex: 3, issueIndex: 4},
"uonfu.com": {pathPrefix: "/q/", userIndex: 2, repoIndex: 3, issueIndex: 4},
};
if (BAD.hasOwnProperty(location.hostname)) {
const {pathPrefix, userIndex, repoIndex, issueIndex} = BAD[location.hostname];
if (location.pathname.startsWith(pathPrefix)) {
const components = location.pathname.split("/");
const [user, repo, issue] = [userIndex, repoIndex, issueIndex].map(x => components[x]);
location.replace(`https://github.com/${user}/${repo}/issues/${issue}`);
}
}
No more GitHub scrapers
(see also full version, which rewrites Google search results too)
// ==UserScript==
// @match https://githubmemory.com/repo/*
// @match https://gitmemory.com/issue/*
// @match https://issueexplorer.com/issue/*
// @match https://uonfu.com/q/*
// ==/UserScript==
const BAD = {
"githubmemory.com": {pathPrefix: "/repo/", userIndex: 2, repoIndex: 3, issueIndex: 5},
"gitmemory.com": {pathPrefix: "/issue/", userIndex: 2, repoIndex: 3, issueIndex: 4},
"issueexplorer.com": {pathPrefix: "/issue/", userIndex: 2, repoIndex: 3, issueIndex: 4},
"uonfu.com": {pathPrefix: "/q/", userIndex: 2, repoIndex: 3, issueIndex: 4},
};
if (BAD.hasOwnProperty(location.hostname)) {
const {pathPrefix, userIndex, repoIndex, issueIndex} = BAD[location.hostname];
if (location.pathname.startsWith(pathPrefix)) {
const components = location.pathname.split("/");
const [user, repo, issue] = [userIndex, repoIndex, issueIndex].map(x => components[x]);
location.replace(`https://github.com/${user}/${repo}/issues/${issue}`);
}
}
Universal next button
- Find any integer in the URL and bump it
((pathname) => {
console.log(pathname);
})(location.pathname.split("/"))
Universal next button
- Find any integer in the URL and bump it
((pathname) => {
for (const [i,name] of pathname.entries()) {
if (name == parseInt(name,10)) {
pathname[i]++;
location.pathname = pathname.join("/");
return;
}
}
})(location.pathname.split("/"))
Copying text
Who would win?
203 characters of clipboard API
(c=>{document.addEventListener("copy",c);document.execCommand("copy");document.removeEventListener("copy",c)})(e=>{e.clipboardData.setData("text/plain","your text");e.preventDefault();e.stopImmediatePropagation()})
- or -