The Journey So Far

I’m building a web app/Chrome extension pair that allows your bookmarks to “decay” (grow old and disappear) over time; the intention is to encourage you to actually read those tabs you have open as opposed to letting them linger until your browser crashes. The basic core of the webapp is done (data fetched and displayed, simply), so now it’s time to apply some styles to visually identify decaying bookmarks.


Missed another part of this project series? Check out all the Bookmark Decay posts here!


Current Progress

Check it out!

Gif of the Bookmark Decay webapp showing decaying and dying from the list.
Decay -> Death

In this GIF the first bookmark is fresh (not “dead” or past the decay rate), while the rest have all decayed and are considered dead.

I’m certainly no designer, but I am really happy with how this turned out!

Technical Decisions

Let’s run through my decisions & obstacles during this project phase:

Decay design

While I really wanted this to look cool and “decayed”, I didn’t want to get in over my head with the styling or animations (or at least have them prevent me from finishing the functionality). After some tinkering around, I designed the decay and death visuals thusly:

  • Reduced opacity the closer the bookmark date is to the decay time (default of 7 days)
  • For fully “dead” bookmarks (any bookmark date past the set decay time), it:
    • Has very low opacity
    • Initially flashes red
    • “Winks” out of existence
  • Staggered animation times so all dead bookmarks wouldn’t “die” at the same exact time

Learning Tailwind animations

I got to continue my Tailwind learnings during this phase of the project, including animations. Simple transforms can be done via class additions (which I also utilized), but slightly more complex animations require updates to your Tailwind config by extending your theme:


export default {
    ...
    theme: {
        extend: {
            keyframes: {
                deathFlash: {
                    '0%, 100%': { 'background-color': 'red' },
                    '50%': { 'background-color': 'white'}
                },
                deathCollapse: {
                    '0%': { transform: 'scale(1)' },
                    '100%': { transform: 'scale(1, 0)' }
                }
            },
            animation: {
                deathFlash: 'deathFlash 1s infinite',
                deathCollapse: '.3s ease-in 0s 1 normal forwards running deathCollapse'
            }
        }
    },
    ...
}

These crafted keyframes and animations I added via classes, animate-deathFlash and animate-deathCollapse.

Note: you might have noticed here that deathCollapse uses the full animation notation, as opposed to deathFlash, which uses the shorthand. This is because deathCollapse needs the animation-direction property set explicitly to forwards to prevent it from resetting once the animation is complete.

Tailwind Opacity issues

So, I ran into this super weird problem for awhile with applying opacity with Tailwind - basically, I couldn’t get it to work!

At a glance, controlling opacity with Tailwind should have been easy: opacity-{number}, but for some reason only opacity-70 displayed correctly (random!). All other opacities, while the class was being correctly added in the markup, never received any style update.

This was a weird one to research, not being sure how to describe the root of the problem, so I eventually sought out suggestions from my group of fellow devs and former coworkers. While no one had ever experienced my specific issue, one person (thanks, Brian!) threw out there that it might have something to do with tree-shaking: maybe the styles I was expecting simply weren’t being built.

Since I’d been using Tailwind with Vite and PostCSS I never actually saw the compiled CSS Vite produces, so to test this theory out I manually built the styles to see if, in fact, the classes weren’t being created - and they weren’t! In fact only .opacity-70 was being built, which made sense based on what I’d been seeing.

So why was my CSS not being built as I expected? It was because I was…

Incorrectly applying dynamic CSS

I was missing a piece of knowledge when I applied Tailwind styles to my React components.

Tailwind doesn’t include all its default classes when it’s building your CSS (which makes sense, given there’s so many of them) - it reviews all the files you have listed in your tailwind.config.js and only builds the classes thatf it actually sees in your code. This means that if you’ve built them dynamically in your code, Tailwind won’t “see” them and won’t build them into your styles. This is actually delineated well in their docs, you just had to have read them (and not make assumptions about how to use Tailwind in your app).

This, for example, won’t work:


const BadTailwindComponent = () => {
    const getMyOpacityValue = () => {
        return '80';
    };

    return <div className={`opacity-${getMyOpacityValue()}} />
};

In this example the full Tailwind class doesn’t exist in the code, so Tailwind won’t pick up that opacity-80 needs to be built into the styles.

Per the Tailwind docs suggestions, I changed my tactic and built all the opacity values I needed into an object. It made the code far less streamlined than I wanted (making Tailwind perhaps not the best choice for styling in this situation), but the change worked perfectly:


const GoodTailwindComponent = () => {
    const getMyOpacityValue = () => {
        const opacityValues = {
            '80': 'opacity-80'
        }

        return opacityValues[80];
    };

    return <div className={getMyOpacityValue()} />
};

In this example, the full string opacity-80 lives in the code so Tailwind will recognize it during its scanning and build it into the compiled CSS.

The only detail that has me stumped is why opacity-70 was originally included in my styles at all…I hadn’t used it anywhere in my code, and I couldn’t find it anywhere else in the codebase. This one I’m chalking up to the man - at least I know more about Tailwind now!

Next Steps

Next up for the webapp is handling the data behind the dead bookmarks. The extension is the source of truth for all of our data, so when the webapp determines some bookmarks should shuffle off this mortal coil, we need to update the data to do the same. 🪦