Stripe homepage section

I’d like to write about each homepage section individually, since each one had separate considerations when implementing, and the older the code, the more work was required to “embed” the animation on the site. Fortunately, I built the Stripe Capital animation only a few months ago. Because of this, I coded it with my most up-to-date tricks and made sure it was 100% portable, so I could easily include it on my website. While this makes it easier to implement, it also makes for a somewhat boring blog post—especially since I haven’t even written the process post for the animation yet. (it’s coming, I promise!)

The one aspect I’ll emphasize here is that the more I code these animations, the more I realize that vanilla JavaScript without any libraries is always the way to go. Because I took this approach with the Stripe Capital animation, embedding the animation on the homepage was simply a matter of moving over a single JavaScript file and pointing it at a DOM element. I didn’t need to add any dependencies or transpile any code, and I know that my code is as future-proof as it can be. Years from now, when I’m ready to redesign yet again, I won’t feel burdened by carrying over the Stripe Capital animation the way I do about the CoffeeScript and Angular portions of my app, Cushion.

This site as a whole, however, is a different story. I use Nuxt.js, which is built on Vue.js, which might end up being the Spine.js to React’s Backbone.js. Before too long, we’ll have another cycle with a new pair of frameworks that everyone is excited about. This gives me pause about the underlying stack of this site and whether to strip out any frameworks in favor of either a more future-proof, native web approach or a less exciting, yet battle-tested stack. I’m not going to spend any time on this now, but it’s on my mind. I still think back to when Steve Jobs announced that the iPhone would only support web apps. I can’t help but imagine where the web could be if everyone embraced that and pushed the web forward rather than demanding native apps and ending up writing countless web-to-native compilers.

Article pagination

Now that I’ve reached the point with the blog where I’ve started linking specific articles on Twitter, I realized that each article has become a dead end to anyone landing on it. Once they reach the end of the article, there’s nothing else—not even an obligatory “Made with ❤️ in Brooklyn”. I imagine (and hope) there are folks who stumble upon a single post, then want to read another. Up until now, they would need to go back to the blog and find the next article, which is even more difficult if it’s not on the front page, but that’s another story.

To correct this, I added pagination on the individual article route in the form of next and previous links. At first, I had a single “Continue reading” link to the previous (or older) article, but then I had a chicken or the egg situation—do people read older articles to newer or newer to older? This journal-like format makes me think older to newer, but there’s a good chance someone discovered the most recent article and simply wanted to read more—newer to older.

Before polling the audience, I decided to keep it simple, and cover both directions, by linking to both the next article as well as the previous article. For the markup itself, I originally had “Next” and “Previous”, but with a side-by-side layout, the extra four characters of “Previous” irked me, so I changed it to “Prev”. Then, I realized I could use the ever-handy-but-rarely-used <abbr> tag to still include the full word.

I was almost ready to hit deploy when I realized that using a simple conditional on the “Next” article would make it go away if you’re viewing the most recent article. I started to center the text on the “Previous” text to make the layout work, but then I stopped myself. For instances like this, I think it’s better not to hide something when it doesn’t exist, but rather explain why it’s not there. I knew I couldn’t have a link to nothing, so I replaced the would-be next article with italicized text that says, “No newer article”. Now, if someone keeps clicking on the next or previous article, they don’t hit another empty dead end—they can easily see that there’s no more content, like they’re all caught up.

Context-aware logo

After launching the homepage, surprisingly, one of the most mentioned parts was the “DT” logo changing colors between sections. For the record, this is one of my favorite parts, too, but I was thrilled to see how many people noticed it. After bringing back the logo from the previous design, I felt the need to pin it to the top as a steady anchor as you scroll. This worked out well until I scrolled into sections with a different color scheme than the default.

On the previous design, I did change the logo (and overscroll) color based on the content of the page, but never between sections on a page. I decided to use --accentColor and --knockoutColor CSS variables that I could set on the document element, then implement wherever I wanted them to change, like in the logo SVG as well as the html background for when you scroll past the edge of the page. Adding a simple color transition made any context change subtle enough to feel right and almost unnoticeable until a couple sections down.

As for triggering the color change, I created a Section wrapper component that listens to the scroll. When the given section scrolls halfway past the height of the logo, the section dispatch its color scheme, and the header listens for it. Once I started working on the Carousel animation for the Dropbox section, however, I realized that something was disrupting performance. I enabled paint flashing in the Chrome dev tools, and quickly discovered that changing the html background color—especially transitioning it—probably wasn’t the smartest idea in terms of performance.

Since you can’t see the overscroll area when scrolling through the page, and both the intro and outro sections follow the same default color scheme, it seemed safe to not change the overscroll color at all. This immediately made a noticeable difference for performance, but will remain a to-do on my list when I end up changing the color scheme per page, like in the previous design.

Homepage concept

The concept of building a mashup of my portfolio for the homepage first came to mind when I originally thought of redesigning the previous version from 2016. I found myself with several scroll-based portfolio pieces, which combined to describe my style of work really well—smooth, subtle, and delightful. Initially, I pictured the implementation on a much smaller scale. Each portfolio piece would live either as a decoration off to the side or all in a row. Before too long, real work took over, and I didn’t revisit the idea until years later, but the urge to follow through with it grew stronger as it continued to live in the back of my mind.

In September, I launched my first page at Stripe where I was the Site DRI, or directly responsible individual. Prior to building this page, the homepage concept would’ve fallen flat because I didn’t have much to show for my work at Stripe. I certainly contributed to several pages, like the responsive carousel on Marketplaces or the donut chart on Corporate Card, but I wanted to wait until I had a hero animation under my belt. I wanted the opportunity to instill those previously mentioned qualities into my work at Stripe. Once the Capital page launched, I knew I had what I needed to pursue the portfolio mashup concept.

Homepage concept sketch

Layout sketch for the homepage

I had a general layout in mind, with a brief intro before diving into the animations, but I didn’t have a strong grasp on the right order or amount of content. In my first sketches for the page, I had three-to-four times more copy than I ended up with. Once I found myself spending the majority of my time reworking the copy, I knew it would be better to simply cut it down to a sentence or two per portfolio piece—especially since I had already written entire articles about them.

For the order, I initially thought of starting with my oldest piece, then going chronologically until I reached Stripe. It felt okay, but more random than considered. Then, someone at my studio suggested reversing the order. Since I start by saying I work at Stripe, why not show the work I’ve done at Stripe first? This is yet another instance where I’m in my own head too long before a simple observation makes everything crystal clear.

From there, the layout was smooth sailing—each piece seamlessly fit into place—but I didn’t know how to end the page. Instead of only showing my “portfolio” in the sense of websites I’ve built, I also wanted to include my other work, like my writing and Design Town, the studio space my wife and I started. Still, this didn’t answer the question of how to end the page. I needed some sort of CTA, or call-to-action. I’m accustomed to building CTAs into product pages, but what’s the CTA for an individual who isn’t looking for work?

I decided that my CTAs should lead people to read the rest of the blog, let them follow along on Twitter, and contact me if they need to get in touch. While these were clear cut, I also wanted to embrace the awkwardness of ending the page abruptly. At first, I jokingly wrote “That’s it. That’s the end of the page.” similar to the end credits to the classic film, Ferris Bueller’s Day Off, but then I decided to pull it back a bit. By writing “That’s it for now”, I open up the page to being a continuous, ever growing work-in-progress. As I build more pages, or work on more side projects, I can simply slide them into the page, and it’ll work.


I have a new homepage! I’ve been keeping this one close to the vest because I wanted to flip the switch rather than build it incrementally in the open. Because of this, I didn’t write about it as I built it, which I’m still kicking myself about, but that doesn’t mean I won’t write about it after the fact. I don’t plan to write a single, lengthy post about the page, as opposed to the other landing pages I’ve worked on, but rather spread the process out over a dozen smaller, easier-to-digest posts. Along with making it easier for me to write, this will also let me deep dive into specific parts I want to write about without needing to worry about the article as a whole.

For now, however, I’m going to take a deep breath and simply enjoy it being out there. The concept of building a mashup of my animated websites has literally been years in the making. I still can’t believe it’s live.


In preparation for the big switch from the “2020” subdomain to front-and-center as the root domain, leaving the previous version archived as for eternity, one of the last to-dos on my list included migrating content. In the last version of the site, I decided it’d be best to migrate only the content that I still found relevant to the new version, while still keeping the rest of the content accessible and properly redirected from the root domain. For this version, I continued this spring cleaning tradition by only migrating the most recent post about burnout, the article about my dad, and the process write-ups from my client gigs. This helped me keep the new site fresh, while also avoiding days of manually migrating the content from Jekyll to Contentful.

Interestingly, almost every one of these posts introduced a pre-requisite for me to build. The FiftyThree Pencil post forced me to add support for looping and auto-playing video. The Carousel website post made me handle centering videos that were narrower than the article width. And, the Casper homepage post led me to allow wider images and videos that extend beyond the width of the body copy. Honestly, I love this method of building a website—build what’s necessary when necessary.

For video support, Contentful’s flexibility makes authoring really easy and intuitive. I created a content model that has a reference field for multiple video assets. This lets me include both .mp4 and .ogv files. Then, in the renderer, I loop through the field, and insert source tags for each one. I also provided toggles for autoplaying and looping, which simply append those attributes to the video tag. Autoplaying required an additional playsinline attribute for mobile—otherwise, each video goes fullscreen all at once, which is at first confusing, then hilarious.

As part of the migration, I decided to simplify the feed by housing all the posts in the /blog path, instead of /journal, or my previous (and incredibly embarrassing attempt at being unique) path... /writings. Since the migrated posts are more substantial “articles” than these short-form process posts, I’ll most likely add tags to differentiate them at some point, but I’m not there yet. With this path change, I needed to set up redirects from /writings/* and /journal/* to /blog/$1. I accomplished this by using the @nuxtjs/redirect-module package. For redirecting carried-over content, I simply used a single regular expression, but for content I wasn’t carrying over, I needed to set up individual mappings to the previous “2016” subdomain. This was actually pretty easy in JavaScript by map’ing through an array of slugs:

  redirect: [
    ].map((slug) => ({ from: `^/writings/${slug}`, to: `${slug}` })),

Now, with the redirecting behind me, I simply needed to set the DNS to point to Zeit Now. Surprisingly, this wasn’t as seamless as I expected. For one, hosting a root domain required a DNS provider that supported ANAME or ALIAS, which my AWS Route 53 does not. Even if it did, Zeit mentions that their CDN would most likely be disabled by choosing this approach—not great. The recommended approach is to migrate my DNS to Zeit. This wasn’t ideal, but I really like everything else about Zeit, so I bit my tongue and moved my DNS over.

Before spending an hour manually writing out each DNS setting, I googled and found this handy CLI for exporting AWS Route 53 zones to a file that Zeit could import. I simply needed to run cli53 export > zone.txt, then import it to Zeit with now dns import zone.txt. In between, I did need to adjust AWS’s seemingly proprietary settings for aliases to standard CNAMEs, but besides that, everything moved over successfully, except for one last gotcha.

Despite creating the domain in Zeit, switching the DNS, and successfully showing a correct configuration, the site temporarily showed an error saying there wasn’t a deploy tied to the domain. Zeit did say that there would be one created in a matter of seconds, but that never happened, so I had to manually trigger another deploy. Not a big deal, but not as seamless as I expected.

So, that’s it. The new site is on the root domain, and the previous site has been archived to At this rate, I’ll have to do this all over again in 2024.

Centered layout

I’ve been holding off for a while to make this change, but recent work on a new page, along with the backfilling of previous blog posts, makes it clear that now is the time to switch to a centered layout. While I did love the left-aligned blog, it wasn’t as intuitive—preventing images that might break out of the max width on both sides and forcing folks with large displays to lean left. Maybe there’s a place for a left-aligned layout in the future, but for now, so long. You will be missed.

Contact links

As a half-step towards proper navigation for the site, I added a few links to the header. I realized that the new site had no way of reaching me, in the case that someone landed here without knowing me on Twitter or via email, so I included a “Follow” link that points to my Twitter account and a “Contact” link. For the Twitter link, I added a rel="noopener noreferrer" attribute after Lighthouse complained about it being unsafe. For the mailto: link, I used a JavaScript click handler in an attempt to avoid spam from email-scraping. In-between those links, I added a link to the RSS feed. The feed has been around for a bit, but I made it visible in an effort to encourage more people to get back into subscribing to feeds along with following people on Twitter or Instagram—I’m still nostalgic for the “Slow Web”.


After adding the intro to the site, the plaintext <h1> felt strange—too much text of the same style without much variation in hierarchy. I’ve been planning to replace it with something else, but nothing screamed out to me. Then, I revisited the existing version of my site, which uses a simple square “DT” logo. logo

Honestly, I still really like this little tag—especially how its color continues above the page when you pull it down. Instead of spinning my wheels trying to think of another option, I decided to keep the logo. Redesigning a site doesn’t mean you need to start from scratch with everything. If a part of the previous design still holds up, use it.

When carrying over the logo from the other codebase, I did find a couple ways to improve it. For one, the SVG didn’t have a <title> tag, so the logo wouldn’t be described well for screen readers, or display a tooltip when hovered. I also took this opportunity to make use of CSS variables to assign the color of the logo and background above the fold. Now, when I want to specify a different color, I simply need to set --accentColor on the :root selector. For now, I hardcoded the value as blue to add any sort of accent color to the page. I refrained from picking a really nice color upfront because I want to encourage myself to replace it when I’m ready to consider all of the site’s colors at once.


I’ve been dying to make progress with the site as a whole, rather than only improving upon the blog portion, so I decided to take a simple first step by adding an “intro” to the top of the site. This is my first piece of content outside of the blog stream, which means I needed a new content model in Contentful. Since the content itself could be styled using the tags within the Rich Text editor, I created a Copy content model with only a body Rich Text field and an identifier slug field for referencing.

In my Nuxt view’s asyncData, I now needed to make two requests—one for the article entries and another for the intro. I searched around for to handle this, and found that instead of returning the Promise from the article entries request, I could make the method async, and await Promise.all([...]) with the two requests. Then, I simply return the data I need to use in my template.

Like the Article content model, I also made a Vue component for the Copy content model that renders the Rich Text field. Along with providing a reusable component, this gave me an opportunity to refactor my Article component, which also renders a Rich Text field. Instead of duplicating the code, I simply replaced the Article body with the Copy component.

Now that I took the first step outside the blog, I now have momentum to continue to other parts of the site. I’m not sure what I’ll tackle next, but it feels good to have the intro—even if the copy itself is still in flux.