Hack Trello for a Better User Experience
Having come back to Trello For Individuals (free) after several years, I see it was bought by Atlassian and has “Try Premium Free” banners in too many places, as well as little nags to the same effect. Also, the columns are all gray and saddening.
Trello Pain Points:
- Trello has too many distracting “free trial” buttons and banners.
- The column colours are gray and boring.
- There is no black or charcoal background option.
Here are some of the distracting “free trial” call-to-action (CTA) buttons:
My company pays enterprise fees per seat already, but I just need something simple for home organization without the clutter and nagging, as well as a black background.
I’d like to just use Trello like I did before Atlassian bought it and not see all the nag banners; can it just be a board of simple sticky notes again? Is that possible?
1. Make the Background Black
We’re given the choice of pastel colours or some default backgrounds, but black is missing. If I put Trello on a side monitor all day, it would be nice to have a low-light background such as black. A black PNG could be used as a background, but one line of CSS would solve this, right?
The first Tampermonkey script just adds changes to the root background colour on body hover. Why on body hover? This is a React website so HTML elements come and go and change while on the same page. We could use the CSS !important
directive, but using hover
works well too.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // ==UserScript== // @name Trello | Black Backgrounds on Boards // @namespace https://ericdraken.com/ // @version 1.0 // @description Give Trello boards a black background // @author Eric Draken (ericdraken.com) // @match https://trello.com/b/* // @require https://code.jquery.com/jquery-3.4.1.min.js // @icon https://www.google.com/s2/favicons?sz=64&domain=trello.com // @grant none // ==/UserScript== /*global jQuery*/ /*eslint no-multi-spaces: "off"*/ (function($) { 'use strict'; const action_fn = () => $('#trello-root').css('background-color', '#000'); $(action_fn); // Once on document ready $('html').hover(action_fn); // Again on hover in case the page transitions })(jQuery.noConflict()); |
2. Colour the Columns
It would be nice if the columns had colours: instead of having many boards that I’d forget about, I’d instead have one board with several columns of projects I can bounce around and between – kind of like a single physical wall of sticky notes.
The plan is to find the column text and then work backward to colour the column and the holder of the cards. In my example, [Setup]
in the column name will cause a pastel purple theme on that column. Edit the script to add/change keywords in the columns.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | // ==UserScript== // @name Trello | Colour Board Columns // @namespace https://ericdraken.com/ // @version 1.0 // @description Give Trello board columns some colour // @author Eric Draken (ericdraken.com) // @match https://trello.com/b/* // @require https://code.jquery.com/jquery-3.4.1.min.js // @icon https://www.google.com/s2/favicons?sz=64&domain=trello.com // @grant none // ==/UserScript== /*global jQuery*/ /*eslint no-multi-spaces: "off"*/ (function($) { 'use strict'; const style_map = { 'green':{ // Just a unique key text: 'Complete', // Search for this text in the column name target: 'h2', parent: '.js-list.list-wrapper', children: { '.list.js-list-content': {backgroundColor: '#D7FDDF'}, // Column card holder '.list-card-details': {backgroundColor: 'white'}, // Card in the column } }, 'yellow':{ text: 'In Progress', target: 'h2', parent: '.js-list.list-wrapper', children: { '.list.js-list-content': {backgroundColor: '#FBFFDE'}, '.list-card-details': {backgroundColor: 'white'}, } }, 'blue':{ text: '[Misc]', target: 'h2', parent: '.js-list.list-wrapper', children: { '.list.js-list-content': {backgroundColor: '#E0FFFD'}, '.list-card-details': {backgroundColor: 'white'}, } }, 'red':{ text: 'Blocked', target: 'h2', parent: '.js-list.list-wrapper', children: { '.list.js-list-content': {backgroundColor: '#FFD1D1'}, '.list-cards': {backgroundColor: 'white'}, } }, 'purple':{ text: '[Setup]', target: 'h2', parent: '.js-list.list-wrapper', children: { '.list.js-list-content': {backgroundColor: '#D0D0FE'}, '.list-card-details': {backgroundColor: 'white'}, } }, }; const action_fn = () => { for (const [_, style] of Object.entries(style_map)) { let $targets = $(`${style.target}:contains('${style.text}')`); $targets.each((_, elem) => { let $parent = $(elem).parents(style.parent); if ($parent.length) { for (const [selector, css] of Object.entries(style.children)) { if (selector != '.') { $parent.find(selector).css(css); } else { $parent.css(css); } } } }); } }; $(action_fn); // Once on document ready $('html').hover(action_fn); // Again on hover in case the page transitions })(jQuery.noConflict()); |
This script is quite crude, but it sets RGB hex colours in the desired places.
3. Remove Free-Trial and Upgrade Distractions
Here is what Trello without nag banners looks like:
Using the same technique of searching for phrases like “Premium free” and “upgrade”, we can find those target <span>
, <h2>
, etc. tags and walk the HTML DOM backwards to find juicy container tags and obliterate them (well, display: none
).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | // ==UserScript== // @name Trello | Remove Free-Trial Distractions // @namespace https://ericdraken.com/ // @version 1.0 // @description Remove free trial nag buttons and banners // @author Eric Draken (ericdraken.com) // @match https://trello.com/b/* // @require https://code.jquery.com/jquery-3.4.1.min.js // @icon https://www.google.com/s2/favicons?sz=64&domain=trello.com // @grant none // ==/UserScript== /*global jQuery*/ /*eslint no-multi-spaces: "off"*/ (function($) { 'use strict'; const debug = true; const fuzzy_matches = { 'left nav > Try Premium free': { // Just a unique key text: 'Premium free', // Search for this text target: 'button div', closest: '.js-react-root', children: [{ target: '.', css: {display: 'none'}, }] }, 'left nav > Free': { text: 'Free', target: 'js-react-root nav p', closest: '.', children: [{ target: '.', css: {display: 'none'}, }] }, }; const exact_matches = { 'board menu > custom fields': { target: '.disabled', css: {display: 'none'} } }; const action_fn = () => { // Fuzzy matches first for (const [_, entry] of Object.entries(fuzzy_matches)) { let $targets = $(`${entry.target}:contains('${entry.text}')`); $targets.each((_, elem) => { let $parent = entry.closest != '.' ? $(elem).closest(entry.closest) : $(elem); if ($parent.length) { for (const child of entry.children) { if (child.target != '.') { $parent.find(child.target).css(child.css); } else { $parent.css(child.css); } } } else if(debug) { console.log(`Closest ancestor (${entry.closest}) not found from ${elem.tagName}`); } }); } // Exact matches for (const [_, entry] of Object.entries(exact_matches)) { let $target = $(entry.target); if($target.length) { $target.css(entry.css); } else { console.log(`Exact match (${entry.target}) not found`); } } }; $(action_fn); // Once on document ready $('html').hover(action_fn); // Again on hover in case the page transitions })(jQuery.noConflict()); |
4. Trello Awesomeness in One Script
After working on individual improvements for Trello, and since there is overlap in the handling of the CSS search algorithm, I figure why not roll everything into one script and add some more zhuzh and use HSL colour format (e.g. color: hsl(74deg 13% 49%)
) to make some very cool themes.
Why use HSL-colour notation? It stands for Hue, Saturation, and Lightness (HSL) and is very cool for this reason: If I find a sweet colour and want a complementary colour that is either lighter or darker, I’m pretty much SOL if I’m looking at a colour wheel for RGB colours: what usually happens is I can only find ugly colours that sort of match.
Instead, if I find a nice colour using HSL, I can simply adjust the Lightness to make it darker or lighter and still retain the quality of the original colour. Could I just use alpha
on RGB? No, because HTML containers stack on top of each other.
HSL Example
- HSL(35deg 100% 69%) = #FFBD61
- HSL(35deg 100% 49%) = #945600
You can see that by dropping the lightness by 20%, the RGB hex code changes considerably. HSL gives us a neat way to create themes without the trial and error of the RGB colour wheel. Chrome DevTools has an HSL toggle: click the little arrows in the screenshot above to move between colour formats.
Customize to taste. The only additional feature is that exact CSS rules will be added with the !important
directive and will be added to the DOM’s stylesheet globally. Also, to make this more robust, a setInterval(..., 1000)
of one second is used to hunt for new elements to be modified because global rules are not possible.
MutationObserver
on the DOM? You bet. However, because this is a React script, there is a shadow DOM, plus nodes in the DOM are constantly changing. That means a high-level observer would be required, like on <body>
and forevermore we’d have to iterate on the whole DOM to see what changed, then perform the ancestor traversal to apply the CSS. In sum, when the page changes, we’d have to walk down the DOM, then walk it back up, for every CSS override we want – very inefficient.How to Use
A keyword in the column controls the colour. See the screenshot below.
To adjust, please play with the fields in the JS below. For example, if the column contains the word Complete
, then the green theme is applied. To limit false matches, I like to use square brackets. For example, [Misc]
triggers the blue theme.
1 2 3 4 5 6 7 8 9 10 11 12 13 | ... 'green column': { // Just a unique key text: 'Complete', // Search for this text in the column name ... children: [{ target: '.list.js-list-content', // Column card holder css: {'background-color': 'hsl(74deg 13% 49%)'} },{ target: '.list-card-details', // Card in the column css: {'background-color': 'hsl(74deg 13% 79%)', 'border': 'solid 1px hsl(74deg 13% 39%)'} }] }, ... |
The Trello Awesomeness Script
Feel free to fork this script on GitHub.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 | // ==UserScript== // @name Trello | Make Trello Awesome Again // @namespace https://ericdraken.com/ // @version 1.0.1.20220427 // @description Remove free trial nags; use earthtones for columns; set a dark background // @author Eric Draken (ericdraken.com) // @match https://trello.com/b/* // @require https://code.jquery.com/jquery-3.4.1.min.js // @icon https://www.google.com/s2/favicons?sz=64&domain=trello.com // @grant none // ==/UserScript== /*global jQuery*/ /*eslint no-multi-spaces: "off"*/ (function($) { 'use strict'; const debug = false; // Turn on for detailed console logs const fuzzy_matches = { /***** Add column colours *****/ 'complete column': { // Just a unique key text: 'Complete', // Search for this text in the column name target: 'h2', closest: '.js-list.list-wrapper', children: [{ target: '.list.js-list-content', // Column card holder css: {'background-color': 'hsl(74deg 13% 49%)'} },{ target: '.list-card-details', // Card in the column css: {'background-color': 'hsl(74deg 13% 79%)', 'border': 'solid 1px hsl(74deg 13% 39%)'} }] }, 'progress column':{ text: 'In Progress', target: 'h2', closest: '.js-list.list-wrapper', children: [{ target: '.list.js-list-content', css: {'background-color': 'hsl(35deg 100% 39%)'} },{ target: '.list-card-details', css: {'background-color': 'hsl(35deg 100% 69%)', 'border': 'solid 1px hsl(35deg 100% 29%)'} }] }, 'misc column':{ text: '[Misc]', target: 'h2', closest: '.js-list.list-wrapper', children: [{ target: '.list.js-list-content', css: {'background-color': 'hsl(207deg 15% 32%)'} },{ target: '.list-card-details', css: {'background-color': 'hsl(207deg 15% 62%)', 'border': 'solid 1px hsl(207deg 15% 22%)'} }] }, 'blocked column':{ text: 'Blocked', target: 'h2', closest: '.js-list.list-wrapper', children: [{ target: '.list.js-list-content', css: {'background-color': 'hsl(13deg 48% 43%)'} },{ target: '.list-card-details', css: {'background-color': 'hsl(13deg 48% 73%)', 'border': 'solid 1px hsl(13deg 48% 33%)'} }] }, 'setup column':{ text: '[Setup]', target: 'h2', closest: '.js-list.list-wrapper', children: [{ target: '.list.js-list-content', css: {'background-color': 'hsl(214deg 9% 52%)'} },{ target: '.list-card-details', css: {'background-color': 'hsl(214deg 9% 82%)', 'border': 'solid 1px hsl(214deg 9% 42%)'} }] }, 'project column':{ text: '[Project]', target: 'h2', closest: '.js-list.list-wrapper', children: [{ target: '.list.js-list-content', css: {'background-color': 'hsl(191deg 100% 28%)'} },{ target: '.list-card-details', css: {'background-color': 'hsl(191deg 100% 58%)', 'border': 'solid 1px hsl(191deg 100% 8%)'} }] }, 'patent column':{ text: '[Patent]', target: 'h2', closest: '.js-list.list-wrapper', children: [{ target: '.list.js-list-content', css: {'background-color': 'hsl(290deg 100% 68%)'} },{ target: '.list-card-details', css: {'background-color': 'hsl(290deg 100% 88%)', 'border': 'solid 1px hsl(290deg 100% 48%)'} }] }, /***** Remove nag elements *****/ 'left nav > Try Premium free': { // Just a unique key text: 'Premium free', // Search for this text target: 'button div', closest: '.js-react-root', children: [{ target: '.', css: {'display': 'none'} }] }, 'left nav > Free': { text: 'Free', target: '.js-react-root > nav p', closest: '.', children: [{ target: '.', css: {'display': 'none'} }] }, }; const exact_matches = { /***** Add column colours *****/ 'Add a card': { target: 'a.open-card-composer.js-open-card-composer > .js-add-a-card', css: {'color': '#172b4d'} }, 'board icons': { target: '#board .icon-sm, #board .icon-md, #board .icon-lg', css: {'color': '#172b4d'} }, /***** Black board background *****/ 'black board background': { target: '#trello-root', css: {'background-color': 'hsl(204deg 19% 16%)'} }, /***** Remove nag elements *****/ 'board menu > custom fields': { target: '.board-menu-navigation-item.disabled', css: {'display': 'none'} }, 'card edit screen > Start free trial': { target: '.button-link.disabled', css: {'display': 'none'} }, 'anything disabled': { target: '.disabled', css: {'display': 'none'} }, 'card edit screen > Disabled custom fields': { target: '.js-card-back-custom-fields-prompt', css: {'display': 'none'} }, /***** Enhancements *****/ 'show label labels by default': { target: '.card-label.mod-card-front', css: { 'height': '16px', 'line-height': '16px', 'max-width': '198px', 'padding': '0 8px', } } }; /***** Business logic *****/ const addCSSRule = (selector, css_obj) => { if ( typeof css_obj !== 'object' ) throw "Use objects"; const style_id = 'newTrelloStyle'; const style = document.getElementById(style_id) || (function() { const style = document.createElement('style'); style.type = 'text/css'; style.id = style_id; document.head.appendChild(style); return style; })(); const sheet = style.sheet; let css = selector + ' {'; for (const [key, val] of Object.entries(css_obj)) { css += `${key}: ${val} !important`; } css += '}'; sheet.insertRule(css, (sheet.rules || sheet.cssRules || []).length); debug && console.log(`[Debug:addCSSRule] ${css} at ${(sheet.rules || sheet.cssRules || []).length}`) } const getCSSSelector = ($this, path) => { if ( typeof $this.get(0) === 'undefined' ) return ''; if ( typeof path === 'undefined' ) path = ''; if ( $this.is('html') ) return 'html' + path; let cur = $this.get(0).tagName.toLowerCase(); let id = $this.attr('id') let clazz = $this.attr('class'); debug && console.log(`[Debug:getCSSSelector]> ${cur}, ${id}, ${clazz}, (${path})`); // Build a selector with the highest specifity if ( typeof id !== 'undefined' ) { return cur + '#' + id + path; } else if ( typeof clazz !== 'undefined' ) { cur += '.' + clazz.split(/[\s\n]+/).join('.'); } // Recurse up the DOM return getCSSSelector($this.parent(), ' > ' + cur + path ); }; const actionFn = () => { // Exact matches for (const [rule, entry] of Object.entries(exact_matches)) { let $target = $(entry.target); if($target.length) { debug && console.log(`[Debug:actionFn] ${getCSSSelector($target)} css: ${JSON.stringify(entry.css)}`); // Only run these CSS rules once, ever addCSSRule(entry.target, entry.css); delete exact_matches[rule]; } } // Fuzzy matches first for (const [_, entry] of Object.entries(fuzzy_matches)) { let $targets = $(`${entry.target}:contains('${entry.text}')`); $targets.each((_, elem) => { let $parent = entry.closest != '.' ? $(elem).closest(entry.closest) : $(elem); if ($parent.length) { debug && console.log(`[Debug:actionFn] Closest ancestor (${entry.closest}) found from ${elem.tagName}`); for (const child of entry.children) { let $div = child.target !== '.' ? $parent.find(child.target) : $parent; if ($div.length) { debug && console.log(`[Debug:actionFn] ${getCSSSelector($div)} css: ${JSON.stringify(child.css)}`); $div.css(child.css); } } } }); } }; let timer = setInterval(actionFn, 1000); // No choice but to apply periodically as this is a React app $(actionFn); // Run once when the DOM is ready })(jQuery.noConflict()); |
Installation
You can copy and paste the above script into a new Tampermonkey Editor you can find on the Chrome store. This works for Chrome and Brave, presently.
Conclusion
With a bit of patience and DevTools magic, we are able to figure out what elements need to be adjusted or hidden on a React page with mangled CSS class names and a moving DOM structure to apply column colours and remove “free trial” distractions.