I keep bookmarking Adam’s GUI Challenges posts/videos and, before I even have a chance to review and link them up, another one is already published! Fortunately, the homepage for them on web.dev is a nice roundup.
For example, a recent one is the split-button component (article / video / demo). It’s one thing to have a design spec, code it up so it works and looks right, and call it a day. But, dare I say that real front-end development is thinking deeper than that. In just this one component Adam looks at:
How the colors are set up and applied. The colors have a subtlety that makes them feel nice together. For example, the darkest colors are very dark versions of the base colors, but not black. It uses Custom Properties to set up a sort of menu of color combinations—but is, most importantly, set up for theming success as well.
The menu uses shadows to impart an appropriate amount of depth. Because multiple themes are supported, the shadows don’t do anything embarrassing like, reverse themselves, i.e. light shadows on dark backgrounds that look silly, or dark-on-dark shadows.
The use of SVG strokes (efficient! flexible!) which unlock CSS design options, like rounding the end caps.
Clicks (active and hover states) should increase contrast.
Everything is tested for keyboard usage. The menus open when tabbed to, and arrow keys move the focus within the menu which is accentuated by visual changes. A screen reader (VoiceOver, in this case) was also used to test things, for example using the ESC key to close the menu and remove focus is functional.
Reduced motion preferences are honored by not doing as much movement while the menu the opens and closes.
Visually, the only thing that opens and closes the menu is :focus-within in CSS. How simple! But aria attributes are still updated in JavaScript to properly express that.
That’s not everything, but that’s a lot, right? (The article has way more detail, including tools for inspecting what’s going on as it’s being built and small helper libraries that were used.) That’s what real front-end development is. Just “a button with a menu” has a ton of surface area to get right and dangerous implications for getting wrong.
If you like the idea of challenges, CodePen challenges you every week to build something along a prompt with ideas and resources. It’s fun because there are a bunch of people doing it with you, allowing you to see how others approached the same idea differently.
Most developers understand that it’s important to use branches in Git. In fact, I’ve written an entire article on branching strategies in Git, explaining Git’s powerful branching model, the different types of branches, and two of the most common branching workflows. To sum it up: having separate containers, i.e. branches, for your work is incredibly helpful and one of the main reasons for using a version control system.
In this article we’re going to look at integrating branches. How can you get new code back into an existing line of development? There are different ways to achieve this. The fifth episode of our “Advanced Git” series discusses integrating changes in Git, namely merging and rebasing.
Before we go into detail, it’s important to understand that both commands — git merge and git rebase — solve the same problem. They integrate changes from one Git branch into another branch; they just do it differently. Let’s start with merges and how they actually work.
To merge one branch into another, you can use the git merge command. Let’s say you want to merge the main branch (branch-A) into a feature branch (branch-B), you can type something like this:
$ git checkout branch-B
$ git merge branch-A
As a result, Git creates a new merge commit in your current working branch (branch-B in this example), connecting the histories of both branches. To pull this off, Git looks for three commits:
The first one is the “common ancestor commit.” If you follow the history of two branches in a project, they always have at least one commit in common. At this point, both branches have the same content. After that, they evolved differently.
The two other interesting commits are the endpoints of each branch, i.e. their current states. Remember that the goal of an integration is to combine the current states of two branches. So their latest revisions are important, of course.
Combining these three commits performs the integration that we’re aiming for.
Admittedly, this is a simplified scenario — one of the two branches (branch-A) hasn’t seen any new commits since it was created, which is very unlikely in most software projects. Its last commit in this example is, therefore, also the common ancestor.
In this case, the integration is dead simple: Git can just add all the new commits from branch-B on top of the common ancestor commit. In Git, this simplest form of integration is called a “fast-forward” merge. Both branches then share the exact same history (and no additional “merge commit” is necessary).
In most cases, however, both branches would move forward with different commits. So let’s take a more realistic example:
To make an integration, Git has to create a new commit that contains all the changes and take care of the differences between the branches — this is what we call a merge commit.
Human commits and merge commits
Normally, a commit is carefully created by a human being. It’s a meaningful unit that only includes related changes, plus a meaningful commit message which provides context and notes.
Now, a merge commit is a bit different: it’s not created by a developer, but automatically by Git. Also, a merge commit doesn’t necessarily contain a “semantic collection of related changes.” Instead, its purpose is simply to connect two (or more) branches and tie the knot.
If you want to understand such an automatic merge operation, you have to take a look at the history of all branches and their respective commit histories.
Integrating with rebases
Before we talk about rebasing, let me make one thing clear: a rebase is not better or worse than a merge, it’s just different. You can live a happy (Git) life just by merging branches and never even think about rebasing. It does help to understand what a rebase does, though, and learn about the pros and cons that come with it. Maybe you’ll reach a point in a project when a rebase could be helpful…
Alright, let’s go! Remember that we just talked about automatic merge commits? Some people are not too keen on these and prefer to go without them. Besides, there are developers who like their project history to look like a straight line — without any indication that it had been split into multiple branches at some point, even after the branches have been integrated. This is basically what happens during a Git rebase.
Rebasing: Step by step
Let’s walk through a rebase operation step-by-step. The scenario is the same as in the previous examples, and this is what the starting point looks like:
We want to integrate the changes from branch-B into branch-A — but by rebasing, not merging. The actual Git command for this is very simple:
$ git checkout branch-A
$ git rebase branch-B
Similar to a git merge command, you tell Git which branch you want to integrate. Let’s take a look behind the scenes…
In this first step, Git will “remove” all commits on branch-A that happened after the common ancestor commit. Don’t worry, it will not throw them away: you can think of those commits as being “parked” or temporarily saved in a safe place.
In the second step, Git applies the new commits from branch-B. At this point, temporarily, both branches actually look exactly the same.
Finally, those “parked” commits (the new commits from branch-A) are included. Since they are positioned on top of the integrated commits from branch-B, they are rebased.
As a result, the project history looks like development happened in a straight line. There is no merge commit that contains all combined changes, and the original commit structure is preserved.
Potential pitfalls of rebasing
One more thing — and this is important to understand about Git rebase — is that it rewrites the commit history. Take another look at our last diagram. Commit C3* has an asterisk. While C3* has the same content as C3, it’s effectively a different commit. Why? Because it has a new parent commit after the rebase. Before the rebase, C1 was the parent. After the rebase, the parent is C4 — which it was rebased into.
A commit has only a handful of important properties, like the author, date, changeset, and its parent commit. Changing any of this information creates a completely new commit, with a new SHA-1 hash ID.
Rewriting history like that is not a problem for commits that haven’t published yet. But you might be in trouble if you’re rewriting commits you’ve already pushed to a remote repository. Maybe someone else has based their work on the original C3 commit, and now it suddenly no longer exists…
To keep you out of trouble, here’s a simple rule for using rebase: Never use rebases on public branches, i.e. on commits which have already been pushed to a remote repository! Instead, use git rebase to clean up your local commit history before integrating it into a shared team branch.
Integration is everything!
At the end of the day, merging and rebasing are both useful Git strategies, depending on what you want to achieve. Merging is kind of non-destructive since a merge doesn’t change existing history. Rebasing, on the other hand, can help clean up your project history by avoiding unnecessary merge commits. Just remember not to do it in a public branch to avoid messing with your fellow developers.
If you want to dive deeper into advanced Git tools, feel free to check out my (free!) “Advanced Git Kit”: it’s a collection of short videos about topics like branching strategies, Interactive Rebase, Reflog, Submodules and much more.
Happy merging and rebasing — and see you soon for the next part in our “Advanced Git” series!
High five to Jeremy on the big release of Responsible JavaScript on A Book Apart. There is a lot of talk about how the proliferation of JavaScript has had a negative impact on the web, but now we have the canonical reference tome.
The book is just chock-full of Jeremey framing some of the biggest arguments discussions about modern web development, dissecting them, and helping us learn from them. I say “modern web development” there on purpose, because JavaScript has gotten to be such a massive part of building websites these days that the two terms are almost synonymous, for better or worse. While the book title is Responsible JavaScript, it might as well be “Responsible Web Development” to make it go hand and hand with Scott’s book (and Mat’s book, if you need a more gentle introduction to JavaScript).
I like how Jermey blends the old and new together. Readers are introduced and shown how techniques as old as the web (like progressive enhancement) are still useful today and perhaps even more useful than they have ever been. But this isn’t a history novel. New technology (like service workers) is handled with equal grace, and modern techniques for performance gains are given the credit they are due (like build tools and code splitting).
As an aside here — have you ever had an inkling to write a tech book? I have both heard and given this advice: Write a blog post first, or maybe a whole bunch of blog posts. That’ll prove you clearly have words to say about this. Plus it will get the energy of your idea into the world. You might get feedback and constructive insights on the idea as it is shared. Then, hopefully, you can turn that blog post into a talk. That’ll really get you thinking deeply about your idea while getting even more comfortable with the idea of sharing it clearly. And, if all goes well, turn all of that into a book!
Let’s see what happened here. Jeremy wrote a couple of blog posts (hey, nice title). They became a talk (hey, nice title). And that turned into a book (hey, nice title).
There are thousands of articles out there about buttons and links on the web; the differences and how to use them properly. Hey, I don’t mind. I wrote my own as well¹.
It’s such a common mistake on the web that it’s always worth repeating:
Is the intention to send someone to another URL? It’s a link in the form of <a href="">.
Is it to trigger some on-page interactivity? It’s a button in the form of <button>.
Any devition from from those and you better smurfing know what you are doing.
If you had a keyboard and your “e” key would only work 90% of the time, it would be infuriating. Reliability and trust in user interfaces is important to allow users to navigate content and application with ease. If you use the right elements, you support users.
Manuel Matuzović has a Button Cheat Sheet that is a lol-inducing ride about why literally everything other than a <button> isn’t as good as a button. Manuel links up Marcy Sutton’s epic The Links vs. Buttons Showdown (video), pitting the two against each other in a mock battle — “We’ll pit two HTML elements against each other in a crusade of better and worse, right and possible wrong. One element is triggered with the space bar, the other with the enter key. Who will win?” I don’t know whether to laugh or cry at how far we have to go to spread this information.
I think our article A Complete Guide to Links and Buttons is a pretty good example of beginner-oriented content, which is my favorite style of content to write and publish! But because there is so much beginner-oriented content on the web, the bar is pretty high if you want to make and impact and get enough SEO for anyone to even ever find it. So, in this case, the idea was to go big and write nearly as much as there is to write about the technical foundation of links and buttons. If you’ve got a knack for this kind of writing, reach out for sure.
A big heaping 19-minute bowl of not-too-hot, not-too-cold baby bear porridge website building from Rich Harris. I’ve certainly overheard more than my fair share of arguments about Single Page Apps (SPAs) vs Multi-Page Apps (MPAs).
Although, I will say it’s only recently that I’ve heard people put an acronym to MPA, and it feels weird. My guess is that most folks actually hold appropriately-nuanced opinions about what site-building architectures are appropriate for the sites they are building. But it’s fun to pit hardline-opinioned caricatures of developers against each other and extract the best points from each side.
The irony is that the way the industry is going, picking SPA or MPA isn’t an all-in choice. You can literally have aspects of both on one site. And while technologies, like SvelteKit and Astro, are helping it along directly, I’m sure there are lots of sites out there already doing it by virtue of being a hodgepodge of technology smushed together to make business happen over long spans of years. (I may or may not be talking about my own experience on CodePen.)
I quite like how some very newfangled stuff is awesome and worth picking up and taking advantage of, while some old stuff has really stood the test of time and is just as useful today as ever.
We’re all familiar with the concept of autocompletion, right? You type something into a search box and it tries to guess what you’re looking for as you type, displaying suggestions, often below the cursor. While we’re used to autocomplete on eCommerce sites that redirect to search or product pages, an underrated usage is when used as a secondary search pattern to augment the typing experience.
In modern web applications, there’s no reason a composition box has to be a dull, plain text area, or limit itself to rich text formatting. Social and productivity apps like Twitter, Slack, Notion, Google Docs, and Asana have popularized the “@mention” pattern, allowing you to reference other users as you type. You can mention another person, a channel, a file, or some other queryable object using triggers, such as the @ or # characters. This opens a panel with suggestions, letting you complete your message with the right reference.
A Twitter-like compose box with a @mention feature on user names.
The text box acts as a search input, and the suggestions panel acts as a typing assistant, allowing users to reference the right resource without leaving their keyboard. When implemented correctly, everything is only a few keystroke away, including when users misspell something.
Along with powering a composition box, this pattern drives consistency in user-generated content. Hashtags are a good example: users are empowered to create semi-structured data in a free-form context, which then helps categorize content without needing to post-process it. Once users have mentioned other people in a document, referenced resources, or added several hashtags, you get a graph of connections across all the resources available on your app, making it a lot simpler to recommend related content and understand how users think.
Beyond letting users find what they want, a great @mention autocomplete feature should be as fluid as possible. The goal is to create a seamless typing experience where the pattern behaves as an assistant that learns as you type and helps you out, but knows when to get out of your way. You can keep typing, ignoring the suggestions and letting them go away, or you can leverage them to complete your message without friction.
On Twitter, which popularized the pattern with mentions and hashtags, the panel closes as soon as the word being typed is no longer considered a token, to avoid bugging users who want out. Twitter user handles don’t support spaces, so it closes the panel immediately after a space.
Twitter dismisses the @mention panel when hitting Space.
Slack works a bit differently, allowing spaces to let you search full names. It uses different heuristics to determine what signals that users want out.
Slack allows spaces to let users search in full names.
When selecting a suggestion, Twitter closes the panel, replaces the token, and adds a space so the user can keep typing seamlessly. This kind of attention to detail might seem insignificant in isolation, but when they add up, they give a sense of fluidity that empowers users to embrace the pattern instead of fighting it.
Twitter adds a space on select to let users keep on typing.
When you start adding mentions in a composition box like this, they become part of your text, but should also retain full interactivity to let users edit them.
On Twitter, for example, you can “focus” a mention by either clicking on it or navigating the text box with the Left and Right arrow keys. Twitter detects it and reopens the panel with the mentions as the search query. It ensures who is notified when the tweet is sent, and allows you to edit the mention in case of mistake.
Twitter lets you inspect and edit mentions.
One way to build such experiences with minimal effort is using the open-source Autocomplete library. Autocomplete is designed to integrate best with Algolia, but works with any source, static or remote. It lets you build multi-source dynamic and accessible autocomplete experiences, and works great for @mention features.
Mixing different types of suggestions
Having a symbol per result type works well when you have a few, distinct types, such as “@” for people, and “#” for hashtags. As soon as you have more types with blurrier limits, chances are good that users are unable to remember them all. Isolating them could make the experience more cumbersome.
Instead, assigning more than a single type per symbol with federated search (multi-source) helps discover all possible types without having to “learn” too many patterns.
On Slack, suggestions are mixed and differentiated with visual cues (such as different icons and badges). Still, the results look similar to how you experience them in the rest of the app. For example, people suggestions show the users’ avatar, display name, and status. This helps users feel more confident about who or what they mention.
On Slack, the “@” symbol searches for people, groups, and apps.
On Notion, suggestions are grouped by type. Unlike a typical search experience which favors per-result relevance, this approach favors consistency: until you refine the query, you always get dates first, then people, then links. This helps users build muscle memory as they use the tool, by setting expectations regarding where things are.
On Notion, the “@” symbol searches for dates, people, and links.
Grouping by type is achievable either by querying multiple sources at once, or using grouping mechanisms such as Autocomplete’s Reshape API, that transform results after retrieving them.
Another cool Notion pattern is the dynamic placeholder, or predictive suggestion, that they inject based on the active suggestion. By default, the placeholder helps users take action by hinting about what they can do. Then, as they browse, the placeholder updates, letting users know what to expect if they select a suggestion.
When users start typing, suggestions update in the panel, but the placeholder also adapts by pre-filling the input.
Users can apply a suggestion with Enter or Tab.
Peeking into Notion’s code, you can see how they leverage CSS Custom Properties to do this: every time you move through suggestions, they set the CSS variable --pseudoAfter--content on the input’s parent element with JavaScript. This CSS variable is then used with the content property on a ::before pseudo-element. Nifty!
When drawing results from multiple sources, it can become harder to control the number of results. That’s because each source might return the number of requested suggestions, or less, when it doesn’t have enough matches. This can result in a “jumpy” UI, where the number of results fluctuate as the user types.
You can mitigate this either with a fixed height panel containing a scrollbar, or by using combine and limit mechanisms like Autocomplete’s Reshape API.
There are always four results. The number of suggestions varies depending on the number of recent searches.
Thinking outside the search results
When you work on search and discovery every day like I do, you start seeing it everywhere. It’s fantastic how creative you can get with the @mention pattern when you go beyond how it is typically implemented.
If you’re using Slack, you might be used to looking for emojis by typing “:” then refining by name. While it doesn’t look like search or mentions, it uses the same exact mechanisms: you open a suggestions panel with a special character, search for the right item, and “apply” it on select.
On Slack, the “:” symbol searches for emojis.
This is even more striking on Notion, where the panel doesn’t look like search results at all.
Notion uses the “:” sign to let users search for emojis.
What’s interesting here is how versatile the pattern can be when changing simple things like item templates and styling results differently. It integrates even better in a composition box and creates a fluid experience that users recognize across all the apps they use.
A Slack-like compose box with a custom emoji autocomplete
Mentions are the most common use case when using dynamic suggestions in a composition box, but you can go a lot further. While mentions help you “complete” a sentence and enhance the typing experience, a composition box can also be a conversational interface between the user and the app.
On Notion, typing the special character “/” gives you access to inserting actions. You still get to refine suggestions by typing further, but instead of filling the input, selecting a result creates a brand new block of a given type.
On Notion, the “/” symbol lets you create a brand new block of a given type.
This pattern, commonly known as “shortcuts” or “slash commands” was popularized in game chats and has become a standard in general purpose chat applications like Slack and Discord.
On Slack, the “/” symbol lets you type shortcuts to trigger custom actions
such as starting a Zoom call, leaving a channel, posting a GIF, etc.
Shortcuts are interesting because it lets users perform common tasks in one place, without having to look for the feature. For example, it’s common to do impromptu Zoom meetings with remote coworkers, and usually requires sending a Zoom link over Slack. But having to open Zoom, copy the link, then paste it into Slack is cumbersome. The “/zoom” shortcut removes those hurdles by centralizing common tasks in a single interface.
While features like slash commands used to be dedicated to power users, they’re now emerging in more and more apps and targeting all kinds of users.
Ultimately, augmenting the typing experience isn’t about unlocking “power features” but about uncovering content, as well as reducing friction and the cognitive load: instead of teaching users about the complexity of your system ahead of time, you’re bringing the right information at the right time, where and when users need it.