Web Standards
Object-centric vs. Canvas-centric Image Editing
To date, most digital image and video editing tools have been canvas-centric. Advances in artificial intelligence, however, have started a shift toward more object-centric workflows. Here's several examples of this transition.
In canvas-centric image or video editing software, you're adding, removing, or changing pixels (or vectors) that represent objects. Photoshop, used by 90% of the world's creative professionals, is a great example. Its ever increasing set of tools is focused on pixel manipulation. A semantic understanding of the subject matter you're editing isn't really core to Photoshop's interface model.
In an object-centric workflow, on the other hand, every object in an image or video is identified and you can perform editing operations by changing the subject of each. For instance in this example from RunwayML, you can change the character in a video by just dragging one object onto another.
Object-centric workflows allow entities to be be easily selected and manipulated semantically ("make it a sunny day") or through direct manipulation ("move this bench here") as seen in this video from Google Photos' upcoming Magic Editor feature.
Similarly, the Drag Your GAN project enables people to simply drag parts of objects to manipulate them. No need to worry about pixels. That happens behind the scenes.
Object-centric features, of course, can make their way into canvas-centric workflows. Photoshop has been quick to adopt several (below) and it's likely both pixel and object manipulation tools are needed for a large set of image editing tasks.
Despite that, it's interesting to think about how a truly object-centric user interface for editing might work if it was designed from the ground up with today's capabilities: object manipulation as the high-order bit, pixel editing as a secondary level of refinement.
Ask LukeW: Integrated Video Experiences
What happens when AI eats your Web site? The information you've spent years publishing gets reassembled in a new way for people to interact with your content. So far, we've integrated text-based articles and audio files into this experience on Ask LukeW. Today we're adding over 6,000 minutes of video as well.
The Ask LukeW feature is powered by AI models that break down content into related concepts and reassemble it into new answers. When that content is sourced from an article, audio file, and now video file, we cite it and give you the ability to go deeper into the original source file.
In the example above, the information about loading indicators is sourced from two different videos which are linked in the source card to the right. Selecting one of these videos opens an integrated (within the broader conversational UI) video experience that looks like this:
The inline video player starting point is set to where the answer is mostly sourced from. I say "mostly" because a LLM generated answer may pull from multiple points in a video file to create a response, so we start you in the right vicinity of the topic being discussed. Below the video player is an indication of who is speaking that also serves as a scrubber to let you move through the file with context by displaying the current transcript directly underneath.
You can access the full speaker separated transcript and search within it as well. The transcript is generated with an automatic speech recognition AI model and diarized to indicate who is talking at any given time.
For each video file integrated within Ask LukeW, we make use of LLM language operations to:
- summarize the video file
- extract speakers, locations, key topics, and even generate a title from the video file (if we lack the data)
- create follow-on questions to ask based on the content of the video file
- enable people to ask questions using just the content within the video file
- generate answers in response to what people ask
It's like there's AI models everywhere. We even did a bit of machine learning to automate thumbnail selection for the videos. Just generate several thumbnails from each video, train on a few examples, and away you go.
Lastly but not least, we added the transcript search feature to all the audio files in Ask LukeW as well.
For more on how AI continues to eat this Web site, check out these earlier articles or go Ask LukeW about it.
Further Reading- New Ways into Web Content: how AI enables a different way of interacting with a Web site
- Integrated Audio Experiences & Memory: enabling specific content experiences within a conversational UI
- Expanding Conversational User Interfaces: extending chat user interfaces to better support AI capabilities
- Integrated Video Experiences: adding video-specific experiences within conversational UI
Much thanks to Yangguang Li (front end), Thanh Tran (design), and Sam Pullara (back end) in helping these explorations move forward.
Augmented Reality UI Speculation
Augmented reality has the potential to bring digital actions and information to the real world. But what user interface paradigms could help make this a reality? Here's some speculation and pointers to more.
For an augmented reality system to enable digital interactions with the real world, it would likely require the ability to:
- identify objects in the real world
- select objects in the real world
- choose and take actions on selected objects
- get results based on those actions
Adding digital objects in the real world and interacting with them is, of course, also possible but veers into Mixed Reality use cases. So for the sake of simplicity, I'm not focusing on digital object creation and manipulation... think more AR than VR.
Use CasesWhat kinds of use cases could actually "augment" reality? That is, give people abilities they wouldn't otherwise have through the addition of digital information and actions to the physical world. Here's a few examples:
Identifying ObjectsThere's lots of things around us at all times which means the first step in identifying objects is breaking reality down into useful chunks. Meta AI's Segment Anything Model has learned a general notion of what objects are and can generate masks for any object in an image or video. When coupled with a visual captioning model like BLIP each of these object masks can be labeled with a description. So we end up with a list of the objects in our vicinity and what they are.
Selecting ObjectsOnce our field of vision (real time video) is segmented into objects and labeled, how do we actually pick the object we want to act on? One option is to rely on audio input and use it to search the object descriptions mentioned above. Imagine the following but with speech instead of text-based searching.
Another option is to allow people to simply point at the object they want to take action on. To do so, we need to either track their hands, eyes, or provide them with a controller. The later is what the LITHO team (I was an advisor) was working on. LITHO is a finger-worn controller that allows intuitive and precise gestures through a touch surface on the underside, a custom haptic feedback system and an array of motion-tracking sensors. Basically it allows you to point at objects in the real world, select, and even manipulate them.
We can also detect and use eye gaze and/or hand gestures for selection and manipulation. For end users this usually means remembering discrete gestures for specific interactions instead of relying on physical controls (buttons, etc.). As you can see in the video below, the technology to detect even complicated gestures in real time has gotten quite good.
In all cases, we need to let people know their selection took effect. Several ways that could be done: pointers, outlines, masks, and more.
Taking ActionNow we've got the right object identified and selected... time to take action. While I'm sure there's been many "an app store for augmented reality" discussions, maybe it's time to boil things down to actions instead of full-blown apps. Most Web, mobile, desktop, etc. applications have loads of features, many unused. Instead of a store filled with these bloated containers, an augmented reality system could instead focus on the more atomic concept of actions: translate, buy, share.
Since we've identified and labeled objects, we can probably guess what actions are most likely for each, easing the burden on users to find and pick relevant options. Different entities could provide these actions too. Think: translate with Google, buy with Amazon, and so on. People could set their preferred provider as a default for specific actions if they choose too: "always solve with Wolfram Alpha".
With all these actions available, how do you pick? Once again we can lean on speech, hand and/or eye gestures, or a minimal wearable controller. And again I kind of like the explicit intention you get from a hardware controller: press the button to talk, scroll the surface to browse, and so on -especially when conveniently located for one-thumb interactions.
Getting ResultsSo what happens when we take action on an object in the real world? We expect results, but how? Personally, I don't love the idea of plopping graphical user interface (GUI) elements into the real world. It feels like dragging along the past as we move into the future.
Direct object manipulation, however, has its limits so we're probably not done with menus and buttons just yet (sadly).
To go way back, the original Google Glass had a pretty cool feature: a bone transducer that amplified audio output so only you can hear it. Take action on an object and get audio responses whispered into your ear (pretty cool).
What will actually work best? We'll see...
Video: Redesigning a Mobile E-commerce Website
Most mobile e-commerce sites can be improved. But how? This six minute clip from my Mind the Gap presentation provides a step-by-step walkthrough of how to make a standard mobile e-commerce Web site more effective and why with supporting data and design tips.
TranscriptTo illustrate, let's look at an example in e-commerce. I ended up on this one because it made a top 10 e-commerce designs list somewhere. When I followed the link though, I only found two things: an app install banner and a full screen email newsletter promo. Not a great start.
So, I did what most people do and dismissed the pop-up, revealing a promotional banner, an icon-only navigation system, and a feature carousel. Encouraged by how my dismissal of the free shipping interstitial began to reveal more useful content, I tried removing the two promos at the top, and something really interesting happened. I got to a list of categories. Which doesn't seem all that compelling until you consider the impact of this UI.
In a few tests, Growth Rock compared standard e-commerce feature carousel-based layouts with ones that included a few top-level categories, making it clear what kind of products are available on the site. The result was a 5% increase in completed orders. Note the metric we're tracking here. Not clicks on the links, but actual impact on meaningful things, like completed orders.
There was also evidence they ran a similar experiment in another vertical, in this case for an ice cream retailer. Listing their categories up front led to a similar jump in category page views, and in this case, a 29% increase in completed orders.
Another example comes from Google's mobile optimization efforts, where they saw a similar outcome. Edgars is a large fashion retailer in South Africa. They removed the animated banners, introduced some high-level categories near the top of their screen, and saw an increase in revenue per visitor of about 13%. So it seems like getting the categories on the site to be more visible is a good idea, especially if we are tracking impactful metrics like sales.
But there's more we can do here to help people get the value of this product and close that third gap. So next we'll tackle the icon-based navigation system. It's worth mentioning that even the icons we take most for granted, like the search icon, are not as universal as we'd like to believe. So let's clarify the search function a little bit.
Instead of using just icons for a critical function like search, we're going to be more explicit in our product UI and close the gap between what something is and why it exists with a search bar. This also gives us a chance to call out popular items and again, reinforce what the site has to offer. I specifically call search out as critical because exposing it by default can also help with conversions.
In this case, boosting the number of searches as the conversion rate for users who search is usually higher than for users who don't interact with it, probably because they have specific intent. So now we have a pulled out search area, category links exposed, and well, how else can we make it easier for people to get to the value of this product?
It turns out if we drop the featured image, which probably doesn't drive that much in the way of core metrics, we can show some of the actual products this site sells. Imagine that, showing popular or trending products on an e-commerce site. But let's not just show two.
Let's center this module to get more content on the screen and make the images run off the side a bit so people can scroll for more. Right where the thumb is for easy one-handed scrolling. This puts the ability to browse top products in a comfortable to reach zone on large screen sizes. Should make all our one-handed millennial users super happy. Because they'll scroll.
Pinterest found that even removing core features like the number of pins and likes in onboarding increased the number of photos they could show people at any given time, which increased the likelihood they'd find content they like and thereby become an active Pinterest user. Same principle applies here.
Overall, I think we've made progress on getting people to experience the value of this site a bit more directly. We could do even better maybe by putting the products up top and categories next. The goal is to get people from the state of, huh, I think I want to get out of here, to I get it, looks like my kind of thing.
But you may say, Luke, what about that free shipping promo? They were making a really big deal out of that, so it must be important, right? Indeed, the top reason for abandoning a shopping cart after browsing is shipping costs, taxes, etc. So free shipping is a winner and people should know about it. I'm not against that. I just contend that there's probably a better time and place for it.
Perhaps on the product page or the actual checkout experience when total cost is on most people's minds. The tricky but very valuable thing that makes mobile design hard is finding the right time and place for things. It usually isn't right away on the home page with everything.
We can do this all day, but I'll add just one more consideration to this redesign. It's quite possible when someone looks at this design, they could say, but what about the brand? Now, I hope it comes through in the fonts, colors, and especially the products. What people mean when they say that is something more like this, some aspirational imagery that reflects the personality of the company, serves as a hook for people to dive in, like this edgy Mad Max style look.
And I agree, our site design is looking a little too plain. So, we can add in some brand imagery to bring back some soul. But even with this addition, I'd argue we still retain a lot of the functional benefits we've been adding, or rather emphasizing by removing other things. Just be mindful that the reasons we're adding the brand imagery are tied to customer needs and not just the agenda of some department, like brand marketing.
Else you'll end up back at the product experience that mashes up the agenda of multiple teams. Which is increasingly the norm out there. Now, I focused a lot on free people, but they're certainly not alone, looking at a number of other e-commerce sites.
Expanding Conversational User Interfaces
As artificial intelligence models extend what's possible with human computer interactions, our user interfaces need to adapt to support them. Currently many large language model (LLM) interactions are served up within "chat UI" patterns. So here's some ways these conversational interfaces could expand their utility.
To start, what is a chat UI? Well we all spend hours using them in our favorite messaging apps so most people are familiar with the pattern. But here we go... each participant's contribution to a conversation is enclosed in some visual element (a bubble, a box). Contributions are listed in the order they are made (chronologically bottom up or top down). And the size of each contribution determines the amount of vertical space it takes up on screen.
To illustrate this, here's a very simple chat interface with two participants, newest contributions at the bottom, ability to reply below. Pretty familiar right? This, of course, is a big advantage: we all use these kinds of interfaces daily so we can jump right in and start interacting.
Collapse & ExpandWhen each person's contribution to a conversation is relatively short (like when we are one-thumb typing on our phones), this pattern can work well. But what happens when we're asking relatively short questions and getting long-form detailed answers instead? -a pretty common situation when interacting with large language model (LLM) powered tools like Ask LukeW.
In this case, we might want to visually group question and answer pairs (I ask the system, it responds) and even condense earlier question and answer pairs to make them more easily scannable. This allows the current question and answer to clearly take priority in the interface. You can see the difference between this approach and the earlier, more common chat UI pattern below.
Here's how this design shows up on Ask LukeW. The previous question and answer pairs are collapsed and therefore the same size, making it easier to focus on the content within and pick out relevant messages from the list when needed.
If you want to expand the content of an earlier question and answer pair, just tap on it to see its contents and the other messages collapse automatically. (Maybe there's scenarios where having multiple messages open is useful too?)
Integrated ActionsWhen chatting on our phones and computers, we're not just sending text back and forth but also images, money, games, and more. These mostly show up as another message in a conversation but can they also be embedded within messages as well.
As large language model (LLM) powered tools increase their abilities, their responses to our questions can likewise include more than just text: an image, a video, a chart, or even an application. To account for this on Ask LukeW we added a consistent way to present these objects within answers and allow people to select each to open or interact with it.
Here's how this looks for any articles or audio files that come back as sources for generated replies. Selecting the audio or article object expands it. This expanded view has content-specific tools like a player, scrubber, and transcript for audio files and the ability to ask questions only within this file.
After you're done listening to the audio, reading the article, interrogating it, etc, it will collapse just like all the prior question and answer pairs with one exception. Object experiences get a different visual representation in the conversation list. When an answer contains an object, there's a visual indication on the right in the collapsed view. When it is an object experience (a player, reader, app), a more prominent visual indication slows up on the left. Once again this allows you to easily scan for previously seen or used information within the full conversation thread.
Expanding, collapsing, object experiences... seems like there's a lot going on here. But fundamentally these are just a couple modifications to conversational interface patterns we use all the time. So the adjustment when using them in tools like Ask LukeW is small.
Whether or not all these patterns apply to other tools powered by large language models... we'll see soon enough. Because as far as I can tell, every company is trying to add AI to their products right now. In doing so, they may encounter similar constraints to those that led us to these user interface patterns.
Ask LukeW: Integrated Audio Experiences & Memory
We've added a few new updates to the Ask LukeW feature on this site powered by large language models (AI) including an integrated audio experience, conversation memory, and more. Here's the details:
Over the years, I've had the pleasure of appearing on many podcasts and panels about UI and digital product design. When possible, I published links to these conversations on this site: adding up to over 70 hours of audio. Don't worry. I haven't listened to it all. And now... you don't have to either.
There's information in these conversations that doesn't exist in other formats (articles, PDFs, etc.), so we spent some time adding them to the embedding index of content powering Ask LukeW. Casual conversations are often not as information-dense as topically-focused written articles, so several adjustments were needed to pull relevant bits from these long audio files into useful responses.
In the example above, the information about my studies at the University of Illinois comes from a interview I did on the UX Intern podcast. Pretty sure I wasn't this concise in the original podcast which is linked to in the source card to the right if you want to check.
For each article sourced within Ask LukeW, we make use of large language model (LLM) operations to:
- summarize the article
- extract key topics from the article
- create follow-on questions to ask within each article
- enable people to ask questions using the content within the article
- generate answers in response to what people ask
The integrated (within the broader conversational UI) article experience looks like this:
Similarly for each audio file presented within Ask LukeW, we make use of LLM language operations to:
- summarize the audio file
- extract speakers, locations, key topics, and even generate a title from the audio file (if we lack the data)
- create follow-on questions to ask based on the content of the audio file
- enable people to ask questions using the content within the audio file
- generate answers in response to what people ask
The integrated audio experience looks like this:
The inline player starting point is set to where the answer is mostly sourced from. I say "mostly" because a LLM generated answer may pull from multiple points in an audio file to create a response, so we start you in the right vicinity of the topic being discussed. Below the audio player is an indication of who is speaking that also serves as a scrubber to let you move through the file with context. A full, speaker separated transcript follows that's generated by using an automatic speech recognition AI model.
This integrated audio experience follows our general approach of providing content-specific tools within a conversational interface. For both articles and audio files selecting the source card to the right of a generated answer takes you into that deeper experience.
Last but not least, we also added some conversational memory to Ask LukeW. Now most references to previous questions will be understood so you can more naturally continue a conversation.
As before, much thanks to Yangguang Li (front end), Thanh Tran (design), and Sam Pullara (back end) in helping this exploration move forward.
Video: Get People to Your Core Value ASAP
Designers that copy how most mobile apps onboard new users will end up with intro screens, tutorials, and more in their app's first time experience. This two and a half minute clip from my Mind the Gap presentation argues that instead, designers should focus on getting users to their app's core value as soon as possible but not sooner.
TranscriptOnce again... a splash screen, a few permission dialogues, and a tutorial, which is often justified by saying, everybody's doing it. But what does that mean?
Those of you that have worked at a software design company know it's pretty common to kick things off with what's known as a competitive analysis. That is, you look at what other sites or apps are doing for a specific feature, you print them out, put them on the walls, and compare what you see.
In the case of scooter sharing companies, we can look at the onboarding experiences of Jump, Spin, Ofo, Bird, Lime, and we see across most of them that there's an intro tour explaining the service to people. So the result of this competitive analysis is that intro tours are probably a good idea because everybody else has one, right?
But if you actually take the time to test some of these things, like the music service Vevo did, they looked at how people were using their intro tour through user testing and analytics. They found most people were just skipping through the tutorial without reading any of the copy.
So if they're skipping this, what would happen if they just got rid of the tour? Turns out in a 28-day experiment with over 160,000 participants, the total number of people who got into the app increased. Completing the tutorial didn't affect engagement or retention metrics, and more people actually completed sign-up.
You can see similar principles at work in the evolution of several Google products as well. Google Photos, for instance, used to have an intro tour, an animated tour, and an introduction to its Android app.
Following a series of tests, the team ended up with a much reduced experience. Away went the spinning logo, the get started screen, the animated tour, all which were sources of drop-off.
All that was left was a redesigned version of the turn-on auto backup screen, which was overlaid on top of people's photo galleries. This step was critical to getting the value out of Google Photos. It's a service that backs up and makes your photos re-findable easily. Little in the app works without this step, so the team made it the first and only focus of onboarding.
It's a great illustration of the principle of getting people to product value as fast as possible, but not faster.
That is, ask the user for the minimum amount of information you need to get them the most valuable experience. In the case of Google Photos, that's turning on auto backup.
Scaling Conversational Interfaces
With the explosive popularity of Large Language Models, many companies are implementing conversational (chat) user interfaces for a wide range of use cases leading to more UI design explorations like... conversation scaling.
Scaling conversational interfaces adjust the visual weight (through size, color, font, etc.) of previous messages so the current active message gets proper focus. Previous messages are more quickly and easily scanned since they are reduced in size and visual prominence. Here's two recent examples of this pattern.
The Pi (personal AI) user interface resizes messages when someone scrolls through their conversation. This emphasizes the current message/interaction (since it’s bigger) and provides quick access to the rest of the conversation (since it’s smaller).
The Ask LukeW user interface reduces the size of previous messages (using the question and first two lines of each answer) to provide quick access to the rest of the conversation. If someone wants to read a previous message, they can simply click/tap to bring it into focus.
Since the Ask LukeW feature is for product design professionals vs. casual users, it's possible people want to return to information in prior messages more than on Pi. This is also why there's a pin message feature on Ask LukeW to save useful answers.
Video: Why Design Guidelines Aren't Enough
Many companies make use of design systems or common component libraries to make it easier for teams to develop consistent design solutions. This two minute clip from my Mind the Gap presentation looks at why pulling a few off-the-shelf design components from a library is not the same thing as creating a good user experience.
TranscriptIn an effort to scale the impact of design teams, many companies are investing in design systems or common components to make it easy for teams to apply similar solutions. And it's common for me to hear refrains like, well, the design is like that because I was just following the guidelines.
But pulling a few off-the-shelf design components from a library is not the same thing as creating a good user experience.
For example, Jet Radar, a flight search engine, makes use of material design guidelines in their product. They've used material design input fields in the design of their form and material design floating action buttons for their primary call to action. But you don't have to be a UX expert to see that the end result is not particularly great.
Labels and input text is duplicated. What looks like inputs are actually hint text. What looks like hint text is actually labels. Comments are scattered across the page. And the primary action, frankly, just looks like a bug.
Jet Radar's most recent design is much more approachable to people, though I could quibble with some of what they do. The point is, simply applying a style guide or design components doesn't ensure your product design works well. In fact, it could have the opposite effect.
Now in fairness, material design actually has updated both of the guidelines I showed earlier to try and cover cases like this. Always be learning. But the point still stands.
There's more to making a holistic user experience than applying guidelines to mockups. And while design systems have great aims, they can quickly become another reason for applying a specific solution for the sake of consistency.
As we just saw, just because something's consistent doesn't necessarily mean it's good.
Evaluating User Interfaces with the Squint Test
Over twenty years ago (ouch), I outlined how and why visual hierarchy should be used to make the purpose and function of user interfaces clear. Since then, I'm pretty sure most of the feedback I've given on UI design has included visual hierarchy questions. Why?
Each and every screen in a Web site or app needs to make its purpose clear to a user: what can they do and how can they do it? Without visual hierarchy, this becomes very difficult. Image a screen where every bit of content and every action looks the same. It would be impossible to tell the difference between things and thereby very difficult to use.
Of course, the situation is more subtle in most UI designs. Designers make buttons look like buttons and headlines look like headlines. But the overall balance between all these elements often lacks intention. That is, the visual hierarchy doesn't reflect the purpose and function as emphatically as it should.
To illustrate with an example, here's a sequence of design iterations for the PatientsLikeMe home page from 2006. Yes, I'm dating myself again but this design aesthetic probably requires some caveating.
The iteration above doesn't have enough visual hierarchy. Everything is at a similar level of visual weight so it all kind of blends together. This happens because our eyes end up bouncing between all the visually equal elements. They aren't being intentionally guided through a narrative, a prioritization, or a hierarchy.
In the revision above, things are more visually organized. We're using visual elements like background colors, repeating shapes and fonts styles to outline the major sections of the page. This allows our eyes to take in the higher level sections first, then dig into details when they're relevant.
Here we've added even more visual contrast between the elements on the page. This once again helps guide our eyes. We're using the visual priority of the page elements to help walk people through what this site is for and how they can get started using it.
So how can you tell when there's the right amount of visual hierarchy in a UI design? Just squint. This will blur the design just enough to quickly identify if the important elements stand out. I call this the squint test.
But which elements are the important ones? I love this question because it turns visual hierarchy discussions into strategic ones. To know the answer, you need to understand what product you are making, for whom, and why. With this knowledge, what's important is clear and a good product design is just a mater of reflecting it through visual hierarchy.
Video: Why Don't A/B Tests Add Up?
Most companies use A/B tests to help them make product decision decisions. This three minute clip from my Mind the Gap presentation looks at why the cumulative results of these kinds of tests often don't add up to more significant impact.
TranscriptIn this sign-up flow, we can see there's a promo to try their subscription service. However, looking at the design, there doesn't seem to be any way not to take them up on their offer. Tapping "try it free" goes to two paid plan options.
But it turns out if you tap the little arrow in the upper left, you get taken to a map where you can unlock a bike and ride without the subscription plan. Not very clear in design.
I have no insider information, but I suspect this was a pretty well performing A-B test. Lots of people hit that try it free button.
You've probably heard a lot of people talk about the importance of A-B testing and the impact they can have on conversion. But once again we need to think about what are we measuring.
The classic A-B testing example is changing the color of a button and seeing results. In this example, 9% more clicks. When test results come back showing one item outperformed the other for a specific metric, it's pretty natural to want to implement that. So we make a product design choice because the data made us do it.
Isn't this how we improve user experiences by testing and seeing how user behavior improves? Yes, but it matters how you define and measure improves. Many companies have results that look like the button color example. In isolation, they show great short-term gains. But when you look at the long-term impact, the numbers tell a different story.
Multiple successful A-B tests you'd think would give you cumulative results much larger than what most companies end up seeing.
One of the most common reasons behind this is that we're not using tests with enough contrast. Looking at the impact of a button color change is a pretty low contrast comparison. A more significant contrast would be to change the action altogether, to do something like promoting a native payment solution by default on specific platforms.
The reason the button change is a low contrast change is it doesn't really impact what happens after someone clicks on it. They still go into the same checkout flow, the same forms.
The payment method change is higher contrast because it can completely alter the buying flow. In this case, shifting it from a multi-step form-based process to a single double tap with biometric authentication. So one way of making good use of testing is to try bigger, bolder ideas, ones that have higher risk-reward ratios.
The other way of using testing is basic good hygiene in product launches, using experiments to check outcomes when making changes, adding new features, and even fixing bugs. This gives you a way to measurably vet any updates and avoid causing problems by monitoring and being able to turn off new changes.
Personal Computation Mediums & AI
In his AI Speaker Series presentation at Sutter Hill Ventures, Geoffrey Litt discussed his work on personal computation mediums and the impact of generative AI on them. Here's my notes from his talk:
- Alan Kay realized everyone would have their own computers and said: "We now want to edit our tools as we have previously edited our documents." But how can we actually make this vision real?
- When it comes to computers and software, we were promised a bicycle for the mind. We got aircraft carriers instead. Bike are personal, modifiable, light. Aircraft carriers are industrial, heavy, and manufactured.
- So how do we make software that is more personal and dynamic? how can our software be more like a bicycle? and how can AI help influence this?
- When someone sends you a spreadsheet, you don't just get the numbers, you also get little programs you can modify. There's editable bits of computation within.
- Some additional examples of this idea: Webstrates allows you to edit a document in different editors and even edit the editors. Inkbase turns sketches into computational elements. Wildcard images every website was built with a spreadsheet that you can edit.
- Dynamic software is software that is modifiable by the user at run time. Not just what the developer intended.
- Potluck is an experiment in applying this idea to a note taking app for things like recipes: How do people save recipes? Usually in a notes app or dedicated recipe apps.
- Text-based notes are flexible, but static. They're more like a bicycle.
- Specialized apps are dynamic but rigid. Usually they are fixed to a domain, have a fixed feature set, and use rigid schemas to get data into right the right formats. These aircraft carrier apps are too much for what most people need.
- Potluck uses a concept called gradual enrichment to try and give you best of both worlds. It uses extensible searches that allow you to decide what data you want to pull out that has meaning out of that text so that the computer can understand it.
- Potluck then basically pulls that data into a spreadsheet so you can write little spreadsheet formulas and compute with it.
- The last step is dynamic annotations where the app takes whatever you've computed in that spreadsheet and it's put it back in the text file.
- Potluck highlights what elements can be turned into computational elements. Numbers become sliders to edit quantities, times become timers for cooking, etc.
- This illustrates how to think about designing tools that are end user modifiable.
- These kinds of tools can be reused across different domains, support multiple tools per medium, and allow people to make use of personal micro-syntaxes (when people decide on formats that make sense to them) because text is both an input and output in the UI.
- People who are not programmers however, really struggle to get going with tools like Potluck. Programmers tend to do pretty well because they are familiar with this kind of thinking. They do it every day.
- Can we use large language models to plug that gap a bit?
- This doesn't mean adding chatbots to the tool. Chatbots are often not the best UI. For example, to clip a video, a set of sliders with visual feedback is much better than using text to do a similar action through text. GUIs and direct manipulation have their advantages.
- A bicycle is a tool that amplifies what you can do and provides you fine control. A chauffeur is more like an assistant that automates a task for you but to do so, you have to give up some control.
- When you want tool feel, it's very important not to get a assistant feel.
- But with fixed applications, you can get things done faster/better but are limited by what the developer decided you can do when building the app. So if you need a new feature, you have to go to the developer. That's a long and costly loop.
- What if you could ship the feature you need to modify your app as you need it?
- In all computational media, you ultimately need to have code that is written to perform functions. Someone needs to write the capabilities. An LLM can help you write these computational elements.
- You could have written the formula but the UI is helping you to get things done. After working with it for a while, maybe you don't need it anymore cause you learned how to do it.
- How do you know what the AI did? You can get written explanation from AI on how it implemented the feature.
- This way of thinking about collaborating with AI really empowers the user, puts them in the driver's seat. They can add capabilities and also learn how.
- Intent specification is iterative. Assume it won't be right on the first try. Iteration enables a higher ceiling, which is basic agile development.
- Of course, there's more to figure out like: how do you design the iteration loop? what is the back and forth process to improve results driven by feedback?
- But the possibilities of combining personal computation mediums with AI are exciting.
Video: You Are What You Measure
Spending the time to get the right metrics affects so many things down the line. This one and a half minute clip from my Mind the Gap presentation shows why and outlines a way to align on the right metrics to track.
TranscriptLet's say we decide to measure app downloads. Well, we start with a small promo, and then we test out another one. Oh, conversions on it are better. Well, we better keep both of them.
Then we add another promo, and installs went up again. So why not drop in more Ooh, and things get even better when we make them bigger. Pretty soon, you've become what you measure, a giant app install ad.
So please, spend the time working through what metrics to measure and why. Real quick, how to choose metrics.
First, we need to decide what change we actually want to see happen in the world. Next, we have to figure out how could we possibly measure that change. For each of these possibilities, write down what you think is going to happen if you start measuring it. What behaviors will you change? What actions will you take?
Next, rank that list by where you see the clearest impact. Then start actually tracking data for your top few, and see if it actually delivers the outcomes you thought it would. When you find a few that actually work for you, make sure to regularly and visibly track
An AI Icon Standard for Apps?
As more existing Web and native applications add AI-powered features, many have made use of the sparkle emoji icon to represent these additions. But how long will this consistency last?
From Google's Bard to Microsoft's Office and beyond, the sparkle emoji and it's derivatives have come to represent generative AI features in apps: "tap this button for AI-powered images, text, and more."
While it feels like there's an emerging pattern here, should designers hop on the sparkle train when adding the inevitable AI features to the apps they work on?
Perhaps the now defunct Save icon pattern offers a clue. Most apps aligned on a floppy disc representation to allow people to save their work. But not only did floppy discs phase out of existence, so did the need to explicitly save files with the widespread adoption of auto-saving features. R.I.P. save icon.
And as AI permeates more apps, how long will its capabilities retain "magical" properties vs. just becoming normal expected bits of functionality? When iterating on the design of ask.lukew.com, we explored using the sparkle icon to enable people to generate suggested questions.
After realizing how useful suggested questions were for exploring the contents of a site, and thereby getting the most value out of it, we shifted to generating them by default. No explicit user action required. We also changed their visual style and layout to fit more seamlessly in people's gaze path so they wouldn't be missed. Bye-bye sparkle icon.
After all... obvious always wins.
Passkeys: What the Heck and Why?
These things called passkeys sure are making the rounds these days. They were a main attraction at W3C TPAC 2022, gained support in Safari 16, are finding their way into macOS and iOS, and are slated to be the future for password managers like 1Password. They are already supported in Android, and will soon find their way into Chrome OS and Windows in future releases.
Geeky OS security enhancements don’t exactly make big headlines in the front-end community, but it stands to reason that passkeys are going to be a “thing”. And considering how passwords and password apps affect the user experience of things like authentication and form processing, we might want to at least wrap our minds around them, so we know what’s coming.
That’s the point of this article. I’ve been studying and experimenting with passkeys — and the WebAuthn API they are built on top of — for some time now. Let me share what I’ve learned.
Table of contents- Terminology
- What are passkeys?
- How do passkeys replace passwords?
- More about cryptography
- How do we access passkeys?
- The difference between passkeys and WebAuthn
- The process… in a nutshell
- The meat and potatoes
- Some downsides
- Where are things going?
- Resources
Here’s the obligatory section of the terminology you’re going to want to know as we dig in. Like most tech, passkeys are wrought with esoteric verbiage and acronyms that are often roadblocks to understanding. I’ll try to de-mystify several for you here.
- Relying Party: the server you will be authenticating against. We’ll use “server” to imply the Relying Party in this article.
- Client: in our case, the web browser or operating system.
- Authenticator: Software and/or hardware devices that allow generation and storage for public key pairs.
- FIDO: An open standards body that also creates specifications around FIDO credentials.
- WebAuthn: The underlying protocol for passkeys, Also known as a FIDO2 credential or single-device FIDO credentials.
- Passkeys: WebAuthn, but with cloud syncing (also called multi-device FIDO credentials, discoverable credentials, or resident credentials).
- Public Key Cryptography: A generated key pair that includes a private and public key. Depending on the algorithm, it should either be used for signing and verification or encrypting and decrypting. This is also known as asymmetric cryptography.
- RSA: An acronym of the creators’ names, Rivest Shamir and Adel. RSA is an older, but still useful, family of public key cryptography based on factoring primes.
- Elliptic Curve Cryptography (ECC): A newer family of cryptography based on elliptic curves.
- ES256: An elliptic curve public key that uses an ECDSA signing algorithm (PDF) with SHA256 for hashing.
- RS256: Like ES256, but it uses RSA with RSASSA-PKCS1-v1.5 and SHA256.
Before we can talk specifically about passkeys, we need to talk about another protocol called WebAuthn (also known as FIDO2). Passkeys are a specification that is built on top of WebAuthn. WebAuthn allows for public key cryptography to replace passwords. We use some sort of security device, such as a hardware key or Trusted Platform Module (TPM), to create private and public keys.
The public key is for anyone to use. The private key, however, cannot be removed from the device that generated it. This was one of the issues with WebAuthn; if you lose the device, you lose access.
Passkeys solves this by providing a cloud sync of your credentials. In other words, what you generate on your computer can now also be used on your phone (though confusingly, there are single-device credentials too).
Currently, at the time of writing, only iOS, macOS, and Android provide full support for cloud-synced passkeys, and even then, they are limited by the browser being used. Google and Apple provide an interface for syncing via their Google Password Manager and Apple iCloud Keychain services, respectively.
How do passkeys replace passwords?In public key cryptography, you can perform what is known as signing. Signing takes a piece of data and then runs it through a signing algorithm with the private key, where it can then be verified with the public key.
Anyone can generate a public key pair, and it’s not attributable to any person since any person could have generated it in the first place. What makes it useful is that only data signed with the private key can be verified with the public key. That’s the portion that replaces a password — a server stores the public key, and we sign in by verifying that we have the other half (e.g. private key), by signing a random challenge.
As an added benefit, since we’re storing the user’s public keys within a database, there is no longer concern with password breaches affecting millions of users. This reduces phishing, breaches, and a slew of other security issues that our password-dependent world currently faces. If a database is breached, all that’s stored in the user’s public keys, making it virtually useless to an attacker.
No more forgotten emails and their associated passwords, either! The browser will remember which credentials you used for which website — all you need to do is make a couple of clicks, and you’re logged in. You can provide a secondary means of verification to use the passkey, such as biometrics or a pin, but those are still much faster than the passwords of yesteryear.
More about cryptographyPublic key cryptography involves having a private and a public key (known as a key pair). The keys are generated together and have separate uses. For example, the private key is intended to be kept secret, and the public key is intended for whomever you want to exchange messages with.
When it comes to encrypting and decrypting a message, the recipient’s public key is used to encrypt a message so that only the recipient’s private key can decrypt the message. In security parlance, this is known as “providing confidentiality”. However, this doesn’t provide proof that the sender is who they say they are, as anyone can potentially use a public key to send someone an encrypted message.
There are cases where we need to verify that a message did indeed come from its sender. In these cases, we use signing and signature verification to ensure that the sender is who they say they are (also known as authenticity). In public key (also called asymmetric) cryptography, this is generally done by signing the hash of a message, so that only the public key can correctly verify it. The hash and the sender’s private key produce a signature after running it through an algorithm, and then anyone can verify the message came from the sender with the sender’s public key.
How do we access passkeys?To access passkeys, we first need to generate and store them somewhere. Some of this functionality can be provided with an authenticator. An authenticator is any hardware or software-backed device that provides the ability for cryptographic key generation. Think of those one-time passwords you get from Google Authenticator, 1Password, or LastPass, among others.
For example, a software authenticator can use the Trusted Platform Module (TPM) or secure enclave of a device to create credentials. The credentials can be then stored remotely and synced across devices e.g. passkeys. A hardware authenticator would be something like a YubiKey, which can generate and store keys on the device itself.
To access the authenticator, the browser needs to have access to hardware, and for that, we need an interface. The interface we use here is the Client to Authenticator Protocol (CTAP). It allows access to different authenticators over different mechanisms. For example, we can access an authenticator over NFC, USB, and Bluetooth by utilizing CTAP.
One of the more interesting ways to use passkeys is by connecting your phone over Bluetooth to another device that might not support passkeys. When the devices are paired over Bluetooth, I can log into the browser on my computer using my phone as an intermediary!
The difference between passkeys and WebAuthnPasskeys and WebAuthn keys differ in several ways. First, passkeys are considered multi-device credentials and can be synced across devices. By contrast, WebAuthn keys are single-device credentials — a fancy way of saying you’re bound to one device for verification.
Second, to authenticate to a server, WebAuthn keys need to provide the user handle for login, after which an allowCredentials list is returned to the client from the server, which informs what credentials can be used to log in. Passkeys skip this step and use the server’s domain name to show which keys are already bound to that site. You’re able to select the passkey that is associated with that server, as it’s already known by your system.
Otherwise, the keys are cryptographically the same; they only differ in how they’re stored and what information they use to start the login process.
The process… in a nutshellThe process for generating a WebAuthn or a passkey is very similar: get a challenge from the server and then use the navigator.credentials.create web API to generate a public key pair. Then, send the challenge and the public key back to the server to be stored.
Upon receiving the public key and challenge, the server validates the challenge and the session from which it was created. If that checks out, the public key is stored, as well as any other relevant information like the user identifier or attestation data, in the database.
The user has one more step — retrieve another challenge from the server and use the navigator.credentials.get API to sign the challenge. We send back the signed challenge to the server, and the server verifies the challenge, then logs us in if the signature passes.
There is, of course, quite a bit more to each step. But that is generally how we’d log into a website using WebAuthn or passkeys.
The meat and potatoesPasskeys are used in two distinct phases: the attestation and assertion phases.
The attestation phase can also be thought of as the registration phase. You’d sign up with an email and password for a new website, however, in this case, we’d be using our passkey.
The assertion phase is similar to how you’d log in to a website after signing up.
Attestation View full sizeThe navigator.credentials.create API is the focus of our attestation phase. We’re registered as a new user in the system and need to generate a new public key pair. However, we need to specify what kind of key pair we want to generate. That means we need to provide options to navigator.credentials.create.
// The `challenge` is random and has to come from the server const publicKey: PublicKeyCredentialCreationOptions = { challenge: safeEncode(challenge), rp: { id: window.location.host, name: document.title, }, user: { id: new TextEncoder().encode(crypto.randomUUID()), // Why not make it random? name: 'Your username', displayName: 'Display name in browser', }, pubKeyCredParams: [ { type: 'public-key', alg: -7, // ES256 }, { type: 'public-key', alg: -256, // RS256 }, ], authenticatorSelection: { userVerification: 'preferred', // Do you want to use biometrics or a pin? residentKey: 'required', // Create a resident key e.g. passkey }, attestation: 'indirect', // indirect, direct, or none timeout: 60_000, }; const pubKeyCredential: PublicKeyCredential = await navigator.credentials.create({ publicKey }); const { id // the key id a.k.a. kid } = pubKeyCredential; const pubKey = pubKeyCredential.response.getPublicKey(); const { clientDataJSON, attestationObject } = pubKeyCredential.response; const { type, challenge, origin } = JSON.parse(new TextDecoder().decode(clientDataJSON)); // Send data off to the server for registrationWe’ll get PublicKeyCredential which contains an AuthenticatorAttestationResponse that comes back after creation. The credential has the generated key pair’s ID.
The response provides a couple of bits of useful information. First, we have our public key in this response, and we need to send that to the server to be stored. Second, we also get back the clientDataJSON property which we can decode, and from there, get back the type, challenge, and origin of the passkey.
For attestation, we want to validate the type, challenge, and origin on the server, as well as store the public key with its identifier, e.g. kid. We can also optionally store the attestationObject if we wish. Another useful property to store is the COSE algorithm, which is defined above in our PublicKeyCredentialCreationOptions with alg: -7 or alg: -256, in order to easily verify any signed challenges in the assertion phase.
Assertion View full sizeThe navigator.credentials.get API will be the focus of the assertion phase. Conceptually, this would be where the user logs in to the web application after signing up.
// The `challenge` is random and has to come from the server const publicKey: PublicKeyCredentialRequestOptions = { challenge: new TextEncoder().encode(challenge), rpId: window.location.host, timeout: 60_000, }; const publicKeyCredential: PublicKeyCredential = await navigator.credentials.get({ publicKey, mediation: 'optional', }); const { id // the key id, aka kid } = pubKeyCredential; const { clientDataJSON, attestationObject, signature, userHandle } = pubKeyCredential.response; const { type, challenge, origin } = JSON.parse(new TextDecoder().decode(clientDataJSON)); // Send data off to the server for verificationWe’ll again get a PublicKeyCredential with an AuthenticatorAssertionResponse this time. The credential again includes the key identifier.
We also get the type, challenge, and origin from the clientDataJSON again. The signature is now included in the response, as well as the authenticatorData. We’ll need those and the clientDataJSON to verify that this was signed with the private key.
The authenticatorData includes some properties that are worth tracking First is the SHA256 hash of the origin you’re using, located within the first 32 bytes, which is useful for verifying that request comes from the same origin server. Second is the signCount, which is from byte 33 to 37. This is generated from the authenticator and should be compared to its previous value to ensure that nothing fishy is going on with the key. The value should always 0 when it’s a multi-device passkey and should be randomly larger than the previous signCount when it’s a single-device passkey.
Once you’ve asserted your login, you should be logged in — congratulations! Passkeys is a pretty great protocol, but it does come with some caveats.
Some downsidesThere’s a lot of upside to Passkeys, however, there are some issues with it at the time of this writing. For one thing, passkeys is somewhat still early support-wise, with only single-device credentials allowed on Windows and very little support for Linux systems. Passkeys.dev provides a nice table that’s sort of like the Caniuse of this protocol.
Also, Google’s and Apple’s passkeys platforms do not communicate with each other. If you want to get your credentials from your Android phone over to your iPhone… well, you’re out of luck for now. That’s not to say there is no interoperability! You can log in to your computer by using your phone as an authenticator. But it would be much cleaner just to have it built into the operating system and synced without it being locked at the vendor level.
Where are things going?What does the passkeys protocol of the future look like? It looks pretty good! Once it gains support from more operating systems, there should be an uptake in usage, and you’ll start seeing it used more and more in the wild. Some password managers are even going to support them first-hand.
Passkeys are by no means only supported on the web. Android and iOS will both support native passkeys as first-class citizens. We’re still in the early days of all this, but expect to see it mentioned more and more.
After all, we eliminate the need for passwords, and by doing so, make the world safer for it!
ResourcesHere are some more resources if you want to learn more about Passkeys. There’s also a repository and demo I put together for this article.
- Live Demo (no actual information is collected by the form)
- Demo GitHub Repository
- YubiKey Documentation
- Passkeys.dev
- Passkeys.io
- Webauthn.io
Passkeys: What the Heck and Why? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Ask LukeW: New Ways into Web Content
Large language (AI) models allow us to rethink how to build software and design user interfaces. To that end, we made use of these new capabilities to create a different way of interacting with this site at: ask.lukew.com
Though quiet recently, this site built up a decent amount of content over the past 27 years. Specifically, there's nearly 2,000 text articles, 375 presentations, 60 videos, and 3 books worth of explorations and explanations about all forms of digital product design from early Web sites to Mobile apps to Augmented Reality experiences.
Anyone interested in these materials, has essentially two options: search or browse. Searching (primarily through Google) gets people to a specific article, presentation, or video when they have a sense of what they're looking for. Browsing (on this site or other sites with links to this one) helps people discover things they might not have been explicitly looking for.
But with over half a million written words, three and a half thousand minutes of video, and thousands of images it's hard to know what's available, to connect related content, and ultimately get the most value out of this site.
Enter large-scale AI models for language (LLMs). By making use of these models to perform a variety of language operations, we can re-index the content on this site by concepts using embeddings, and generate new ways to interact with it.
We make use of large-language models to:
- summarize articles
- extract key concepts from articles
- create follow-on questions to ask with specific articles
- make exploratory questions to expose people to new content
- generate answers in response to what people ask
This combination of language operations adds up to a very different new way to experience the content on lukew.com
Ask LukeW starts off with a series of suggested questions that change each time someone loads the page. This not only helps with the "what should I ask?" problem of empty text fields but is also a compelling way to explore what the site has to offer. Of course, someone can start with their own specific question. But in testing, many folks gravitate to the suggestions first, which helps expose people to more of the breadth and depth of available content.
After someone selects a question or types their own question, we generate an answer using the corpus of information on lukew.com. These results tend to be more opinionated than what a large language model operating solely on a much bigger set of content (like the Web) provides, even with prompt engineering to direct it toward specific kinds of answers (i.e. UI design-focused).
The content we use to answer someone's question can come from one or more articles so we give provide visual sources to make this clear. In the current build, we're citing Web pages but PDFs and videos are next. It's also worth noting that we follow-up each answer with additional suggested questions to once again give people a better sense of what they can ask next. No dead ends.
If someone wants to go deeper into any of the sourced materials, they can select the card and get an article-specific experience. Here we make use of LLM language operations to create a summary, extract related topics and provide suggested questions that the article can answer. People can ask questions of just this document (as indicated by the green article "chip" in the question bar) or go back to site-wide questions by tapping the close (x) icon.
As the number of answers builds up, we collapse each one automatically, so people can focus on the current question they've asked. This also makes it easier to scroll through a long conversation and pick out answers from short summaries consisting of the question and the first two lines of its answer.
People can also pin individual question and answer pairs to save them for later and come back to previous conversations in addition to making new ones using the menu bar on the left.
While there's a number of features in the Ask LukeW interface, it's mostly a beta. We don't save state from question to question so the kind of ongoing dialog people may expect from ChatGPT isn't there yet, pinned answers and saved conversations are only done locally (cookie-based) and as mentioned before, PDFs and videos aren't yet part of the index.
Despite that, it's been interesting to explore how an existing body of content can gain new life using large-language model technology. I've been regularly surprised and interested by questions like:
- How can progressive enhancement be used in software development?
- What are the central mental traits that people unconsciously display through the products they buy?
- What are the design considerations for touch-based apps for kids?
- What is small multiples and how can it help people make sense of large amounts of information quickly and easily?
- What is the debate around the utility of usability testing in design?
And I wrote all this content! Since that happened across a quarter century, maybe it's not that surprising that I don't remember it all. Anyhow... hope you also enjoy trying out ask.lukew.com and feel free to send any ideas or comments over.
AcknowledgmentsBig thanks to Yangguang Li (front end), Thanh Tran (design), and Sam Pullara (back end) in helping pull together this exploration.
Making Calendars With Accessibility and Internationalization in Mind
Doing a quick search here on CSS-Tricks shows just how many different ways there are to approach calendars. Some show how CSS Grid can create the layout efficiently. Some attempt to bring actual data into the mix. Some rely on a framework to help with state management.
There are many considerations when building a calendar component — far more than what is covered in the articles I linked up. If you think about it, calendars are fraught with nuance, from handling timezones and date formats to localization and even making sure dates flow from one month to the next… and that’s before we even get into accessibility and additional layout considerations depending on where the calendar is displayed and whatnot.
Many developers fear the Date() object and stick with older libraries like moment.js. But while there are many “gotchas” when it comes to dates and formatting, JavaScript has a lot of cool APIs and stuff to help out!
I don’t want to re-create the wheel here, but I will show you how we can get a dang good calendar with vanilla JavaScript. We’ll look into accessibility, using semantic markup and screenreader-friendly <time> -tags — as well as internationalization and formatting, using the Intl.Locale, Intl.DateTimeFormat and Intl.NumberFormat-APIs.
In other words, we’re making a calendar… only without the extra dependencies you might typically see used in a tutorial like this, and with some of the nuances you might not typically see. And, in the process, I hope you’ll gain a new appreciation for newer things that JavaScript can do while getting an idea of the sorts of things that cross my mind when I’m putting something like this together.
First off, namingWhat should we call our calendar component? In my native language, it would be called “kalender element”, so let’s use that and shorten that to “Kal-El” — also known as Superman’s name on the planet Krypton.
Let’s create a function to get things going:
function kalEl(settings = {}) { ... }This method will render a single month. Later we’ll call this method from [...Array(12).keys()] to render an entire year.
Initial data and internationalizationOne of the common things a typical online calendar does is highlight the current date. So let’s create a reference for that:
const today = new Date();Next, we’ll create a “configuration object” that we’ll merge with the optional settings object of the primary method:
const config = Object.assign( { locale: (document.documentElement.getAttribute('lang') || 'en-US'), today: { day: today.getDate(), month: today.getMonth(), year: today.getFullYear() } }, settings );We check, if the root element (<html>) contains a lang-attribute with locale info; otherwise, we’ll fallback to using en-US. This is the first step toward internationalizing the calendar.
We also need to determine which month to initially display when the calendar is rendered. That’s why we extended the config object with the primary date. This way, if no date is provided in the settings object, we’ll use the today reference instead:
const date = config.date ? new Date(config.date) : today;We need a little more info to properly format the calendar based on locale. For example, we might not know whether the first day of the week is Sunday or Monday, depending on the locale. If we have the info, great! But if not, we’ll update it using the Intl.Locale API. The API has a weekInfo object that returns a firstDay property that gives us exactly what we’re looking for without any hassle. We can also get which days of the week are assigned to the weekend:
if (!config.info) config.info = new Intl.Locale(config.locale).weekInfo || { firstDay: 7, weekend: [6, 7] };Again, we create fallbacks. The “first day” of the week for en-US is Sunday, so it defaults to a value of 7. This is a little confusing, as the getDay method in JavaScript returns the days as [0-6], where 0 is Sunday… don’t ask me why. The weekends are Saturday and Sunday, hence [6, 7].
Before we had the Intl.Locale API and its weekInfo method, it was pretty hard to create an international calendar without many **objects and arrays with information about each locale or region. Nowadays, it’s easy-peasy. If we pass in en-GB, the method returns:
// en-GB { firstDay: 1, weekend: [6, 7], minimalDays: 4 }In a country like Brunei (ms-BN), the weekend is Friday and Sunday:
// ms-BN { firstDay: 7, weekend: [5, 7], minimalDays: 1 }You might wonder what that minimalDays property is. That’s the fewest days required in the first week of a month to be counted as a full week. In some regions, it might be just one day. For others, it might be a full seven days.
Next, we’ll create a render method within our kalEl-method:
const render = (date, locale) => { ... }We still need some more data to work with before we render anything:
const month = date.getMonth(); const year = date.getFullYear(); const numOfDays = new Date(year, month + 1, 0).getDate(); const renderToday = (year === config.today.year) && (month === config.today.month);The last one is a Boolean that checks whether today exists in the month we’re about to render.
Semantic markupWe’re going to get deeper in rendering in just a moment. But first, I want to make sure that the details we set up have semantic HTML tags associated with them. Setting that up right out of the box gives us accessibility benefits from the start.
Calendar wrapperFirst, we have the non-semantic wrapper: <kal-el>. That’s fine because there isn’t a semantic <calendar> tag or anything like that. If we weren’t making a custom element, <article> might be the most appropriate element since the calendar could stand on its own page.
Month namesThe <time> element is going to be a big one for us because it helps translate dates into a format that screenreaders and search engines can parse more accurately and consistently. For example, here’s how we can convey “January 2023” in our markup:
<time datetime="2023-01">January <i>2023</i></time> Day namesThe row above the calendar’s dates containing the names of the days of the week can be tricky. It’s ideal if we can write out the full names for each day — e.g. Sunday, Monday, Tuesday, etc. — but that can take up a lot of space. So, let’s abbreviate the names for now inside of an <ol> where each day is a <li>:
<ol> <li><abbr title="Sunday">Sun</abbr></li> <li><abbr title="Monday">Mon</abbr></li> <!-- etc. --> </ol>We could get tricky with CSS to get the best of both worlds. For example, if we modified the markup a bit like this:
<ol> <li> <abbr title="S">Sunday</abbr> </li> </ol>…we get the full names by default. We can then “hide” the full name when space runs out and display the title attribute instead:
@media all and (max-width: 800px) { li abbr::after { content: attr(title); } }But, we’re not going that way because the Intl.DateTimeFormat API can help here as well. We’ll get to that in the next section when we cover rendering.
Day numbersEach date in the calendar grid gets a number. Each number is a list item (<li>) in an ordered list (<ol>), and the inline <time> tag wraps the actual number.
<li> <time datetime="2023-01-01">1</time> </li>And while I’m not planning to do any styling just yet, I know I will want some way to style the date numbers. That’s possible as-is, but I also want to be able to style weekday numbers differently than weekend numbers if I need to. So, I’m going to include data-* attributes specifically for that: data-weekend and data-today.
Week numbersThere are 52 weeks in a year, sometimes 53. While it’s not super common, it can be nice to display the number for a given week in the calendar for additional context. I like having it now, even if I don’t wind up not using it. But we’ll totally use it in this tutorial.
We’ll use a data-weeknumber attribute as a styling hook and include it in the markup for each date that is the week’s first date.
<li data-day="7" data-weeknumber="1" data-weekend=""> <time datetime="2023-01-08">8</time> </li> RenderingLet’s get the calendar on a page! We already know that <kal-el> is the name of our custom element. First thing we need to configure it is to set the firstDay property on it, so the calendar knows whether Sunday or some other day is the first day of the week.
<kal-el data-firstday="${ config.info.firstDay }">We’ll be using template literals to render the markup. To format the dates for an international audience, we’ll use the Intl.DateTimeFormat API, again using the locale we specified earlier.
The month and yearWhen we call the month, we can set whether we want to use the long name (e.g. February) or the short name (e.g. Feb.). Let’s use the long name since it’s the title above the calendar:
<time datetime="${year}-${(pad(month))}"> ${new Intl.DateTimeFormat( locale, { month:'long'}).format(date)} <i>${year}</i> </time> Weekday namesFor weekdays displayed above the grid of dates, we need both the long (e.g. “Sunday”) and short (abbreviated, ie. “Sun”) names. This way, we can use the “short” name when the calendar is short on space:
Intl.DateTimeFormat([locale], { weekday: 'long' }) Intl.DateTimeFormat([locale], { weekday: 'short' })Let’s make a small helper method that makes it a little easier to call each one:
const weekdays = (firstDay, locale) => { const date = new Date(0); const arr = [...Array(7).keys()].map(i => { date.setDate(5 + i) return { long: new Intl.DateTimeFormat([locale], { weekday: 'long'}).format(date), short: new Intl.DateTimeFormat([locale], { weekday: 'short'}).format(date) } }) for (let i = 0; i < 8 - firstDay; i++) arr.splice(0, 0, arr.pop()); return arr; }Here’s how we invoke that in the template:
<ol> ${weekdays(config.info.firstDay,locale).map(name => ` <li> <abbr title="${name.long}">${name.short}</abbr> </li>`).join('') } </ol> Day numbersAnd finally, the days, wrapped in an <ol> element:
${[...Array(numOfDays).keys()].map(i => { const cur = new Date(year, month, i + 1); let day = cur.getDay(); if (day === 0) day = 7; const today = renderToday && (config.today.day === i + 1) ? ' data-today':''; return ` <li data-day="${day}"${today}${i === 0 || day === config.info.firstDay ? ` data-weeknumber="${new Intl.NumberFormat(locale).format(getWeek(cur))}"`:''}${config.info.weekend.includes(day) ? ` data-weekend`:''}> <time datetime="${year}-${(pad(month))}-${pad(i)}" tabindex="0"> ${new Intl.NumberFormat(locale).format(i + 1)} </time> </li>` }).join('')}Let’s break that down:
- We create a “dummy” array, based on the “number of days” variable, which we’ll use to iterate.
- We create a day variable for the current day in the iteration.
- We fix the discrepancy between the Intl.Locale API and getDay().
- If the day is equal to today, we add a data-* attribute.
- Finally, we return the <li> element as a string with merged data.
- tabindex="0" makes the element focusable, when using keyboard navigation, after any positive tabindex values (Note: you should never add positive tabindex-values)
To “pad” the numbers in the datetime attribute, we use a little helper method:
const pad = (val) => (val + 1).toString().padStart(2, '0'); Week numberAgain, the “week number” is where a week falls in a 52-week calendar. We use a little helper method for that as well:
function getWeek(cur) { const date = new Date(cur.getTime()); date.setHours(0, 0, 0, 0); date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); const week = new Date(date.getFullYear(), 0, 4); return 1 + Math.round(((date.getTime() - week.getTime()) / 86400000 - 3 + (week.getDay() + 6) % 7) / 7); }I didn’t write this getWeek-method. It’s a cleaned up version of this script.
And that’s it! Thanks to the Intl.Locale, Intl.DateTimeFormat and Intl.NumberFormat APIs, we can now simply change the lang-attribute of the <html> element to change the context of the calendar based on the current region:
de-DE fa-IR zh-Hans-CN-u-nu-hanidec Styling the calendarYou might recall how all the days are just one <ol> with list items. To style these into a readable calendar, we dive into the wonderful world of CSS Grid. In fact, we can repurpose the same grid from a starter calendar template right here on CSS-Tricks, but updated a smidge with the :is() relational pseudo to optimize the code.
Notice that I’m defining configurable CSS variables along the way (and prefixing them with ---kalel- to avoid conflicts).
kal-el :is(ol, ul) { display: grid; font-size: var(--kalel-fz, small); grid-row-gap: var(--kalel-row-gap, .33em); grid-template-columns: var(--kalel-gtc, repeat(7, 1fr)); list-style: none; margin: unset; padding: unset; position: relative; }Let’s draw borders around the date numbers to help separate them visually:
kal-el :is(ol, ul) li { border-color: var(--kalel-li-bdc, hsl(0, 0%, 80%)); border-style: var(--kalel-li-bds, solid); border-width: var(--kalel-li-bdw, 0 0 1px 0); grid-column: var(--kalel-li-gc, initial); text-align: var(--kalel-li-tal, end); }The seven-column grid works fine when the first day of the month is also the first day of the week for the selected locale). But that’s the exception rather than the rule. Most times, we’ll need to shift the first day of the month to a different weekday.
Remember all the extra data-* attributes we defined when writing our markup? We can hook into those to update which grid column (--kalel-li-gc) the first date number of the month is placed on:
[data-firstday="1"] [data-day="3"]:first-child { --kalel-li-gc: 1 / 4; }In this case, we’re spanning from the first grid column to the fourth grid column — which will automatically “push” the next item (Day 2) to the fifth grid column, and so forth.
Let’s add a little style to the “current” date, so it stands out. These are just my styles. You can totally do what you’d like here.
[data-today] { --kalel-day-bdrs: 50%; --kalel-day-bg: hsl(0, 86%, 40%); --kalel-day-hover-bgc: hsl(0, 86%, 70%); --kalel-day-c: #fff; }I like the idea of styling the date numbers for weekends differently than weekdays. I’m going to use a reddish color to style those. Note that we can reach for the :not() pseudo-class to select them while leaving the current date alone:
[data-weekend]:not([data-today]) { --kalel-day-c: var(--kalel-weekend-c, hsl(0, 86%, 46%)); }Oh, and let’s not forget the week numbers that go before the first date number of each week. We used a data-weeknumber attribute in the markup for that, but the numbers won’t actually display unless we reveal them with CSS, which we can do on the ::before pseudo-element:
[data-weeknumber]::before { display: var(--kalel-weeknumber-d, inline-block); content: attr(data-weeknumber); position: absolute; inset-inline-start: 0; /* additional styles */ }We’re technically done at this point! We can render a calendar grid that shows the dates for the current month, complete with considerations for localizing the data by locale, and ensuring that the calendar uses proper semantics. And all we used was vanilla JavaScript and CSS!
But let’s take this one more step…
Rendering an entire yearMaybe you need to display a full year of dates! So, rather than render the current month, you might want to display all of the month grids for the current year.
Well, the nice thing about the approach we’re using is that we can call the render method as many times as we want and merely change the integer that identifies the month on each instance. Let’s call it 12 times based on the current year.
as simple as calling the render-method 12 times, and just change the integer for month — i:
[...Array(12).keys()].map(i => render( new Date(date.getFullYear(), i, date.getDate()), config.locale, date.getMonth() ) ).join('')It’s probably a good idea to create a new parent wrapper for the rendered year. Each calendar grid is a <kal-el> element. Let’s call the new parent wrapper <jor-el>, where Jor-El is the name of Kal-El’s father.
<jor-el id="app" data-year="true"> <kal-el data-firstday="7"> <!-- etc. --> </kal-el> <!-- other months --> </jor-el>We can use <jor-el> to create a grid for our grids. So meta!
jor-el { background: var(--jorel-bg, none); display: var(--jorel-d, grid); gap: var(--jorel-gap, 2.5rem); grid-template-columns: var(--jorel-gtc, repeat(auto-fill, minmax(320px, 1fr))); padding: var(--jorel-p, 0); } Final demo CodePen Embed Fallback Bonus: Confetti CalendarI read an excellent book called Making and Breaking the Grid the other day and stumbled on this beautiful “New Year’s poster”:
Source: Making and Breaking the Grid (2nd Edition) by Timothy SamaraI figured we could do something similar without changing anything in the HTML or JavaScript. I’ve taken the liberty to include full names for months, and numbers instead of day names, to make it more readable. Enjoy!
CodePen Embed FallbackMaking Calendars With Accessibility and Internationalization in Mind originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
5 Mistakes I Made When Starting My First React Project
You know what it’s like to pick up a new language or framework. Sometimes there’s great documentation to help you find your way through it. But even the best documentation doesn’t cover absolutely everything. And when you work with something that’s new, you’re bound to find a problem that doesn’t have a written solution.
That’s how it was for me the first time I created a React project — and React is one of those frameworks with remarkable documentation, especially now with the beta docs. But I still struggled my way through. It’s been quite a while since that project, but the lessons I gained from it are still fresh in my mind. And even though there are a lot of React “how-to” tutorials in out there, I thought I’d share what I wish I knew when I first used it.
So, that’s what this article is — a list of the early mistakes I made. I hope they help make learning React a lot smoother for you.
Using create-react-app to start a projectTL;DR Use Vite or Parcel.
Create React App (CRA) is a tool that helps you set up a new React project. It creates a development environment with the best configuration options for most React projects. This means you don’t have to spend time configuring anything yourself.
As a beginner, this seemed like a great way to start my work! No configuration! Just start coding!
CRA uses two popular packages to achieve this, webpack and Babel. webpack is a web bundler that optimizes all of the assets in your project, such as JavaScript, CSS, and images. Babel is a tool that allows you to use newer JavaScript features, even if some browsers don’t support them.
Both are good, but there are newer tools that can do the job better, specifically Vite and Speedy Web Compiler (SWC).
These new and improved alternatives are faster and easier to configure than webpack and Babel. This makes it easier to adjust the configuration which is difficult to do in create-react-app without ejecting.
To use them both when setting up a new React project you have to make sure you have Node version 12 or higher installed, then run the following command.
npm create viteYou’ll be asked to pick a name for your project. Once you do that, select React from the list of frameworks. After that, you can select either Javascript + SWC or Typescript + SWC
Then you’ll have to change directory cd into your project and run the following command;
npm i && npm run devThis should run a development server for your site with the URL localhost:5173
And it’s as simple as that.
Article on Jan 11, 2022 Adding Vite to Your Existing Web App learning react Adam Rackis Article on Jan 18, 2022 Making a Site Work Offline Using the VitePWA Plugin learning react Adam Rackis Article on Jan 12, 2022 Parcel CSS: A New CSS Parser, Transformer, and Minifier learning react Chris Coyier Article on Apr 25, 2019 Using Parcel as a Bundler for React Applications learning react Kingsley Silas Using defaultProps for default valuesTL;DR Use default function parameters instead.
Data can be passed to React components through something called props. These are added to a component just like attributes in an HTML element and can be used in a component’s definition by taking the relevant values from the prop object passed in as an argument.
// App.jsx export default function App() { return <Card title="Hello" description="world" /> } // Card.jsx function Card(props) { return ( <div> <h1>{props.title}</h1> <p>{props.description}</p> </div> ); } export default Card;If a default value is ever required for a prop, the defaultProp property can be used:
// Card.jsx function Card(props) { // ... } Card.defaultProps = { title: 'Default title', description: 'Desc', }; export default Card;With modern JavaScript, it is possible to destructure the props object and assign a default value to it all in the function argument.
// Card.jsx function Card({title = "Default title", description= "Desc"}) { return ( <div> <h1>{title}</h1> <p>{description}</p> </div> ) } export default Card;This is more favorable as the code that can be read by modern browsers without the need for extra transformation.
Unfortunately, defaultProps do require some transformation to be read by the browser since JSX (JavaScript XML) isn’t supported out of the box. This could potentially affect the performance of an application that is using a lot of defaultProps.
Article on Oct 23, 2019 Demonstrating Reusable React Components in a Form learning react Kingsley Silas Article on Jun 7, 2017 I Learned How to be Productive in React in a Week and You Can, Too learning react Sarah Drasner Article on Aug 31, 2018 Props and PropTypes in React learning react Kingsley Silas Don’t use propTypesTL;DR Use TypeScript.
In React, the propTypes property can be used to check if a component is being passed the correct data type for its props. They allow you to specify the type of data that should be used for each prop such as a string, number, object, etc. They also allow you to specify if a prop is required or not.
This way, if a component is passed the wrong data type or if a required prop is not being provided, then React will throw an error.
// Card.jsx import { PropTypes } from "prop-types"; function Card(props) { // ... } Card.propTypes = { title: PropTypes.string.isRequired, description: PropTypes.string, }; export default Card;TypeScript provides a level of type safety in data that’s being passed to components. So, sure, propTypes were a good idea back when I was starting. However, now that TypeScript has become the go-to solution for type safety, I would highly recommend using it over anything else.
// Card.tsx interface CardProps { title: string, description?: string, } export default function Card(props: CardProps) { // ... }TypeScript is a programming language that builds on top of JavaScript by adding static type-checking. TypeScript provides a more powerful type system, that can catch more potential bugs and improves the development experience.
Article on Aug 31, 2018 Props and PropTypes in React learning react Kingsley Silas Article on Mar 27, 2018 Putting Things in Context With React learning react Neal Fennimore Article on Nov 16, 2018 An Overview of Render Props in React learning react Kingsley Silas Using class componentsTL;DR: Write components as functions
Class components in React are created using JavaScript classes. They have a more object-oriented structure and as well as a few additional features, like the ability to use the this keyword and lifecycle methods.
// Card.jsx class Card extends React.Component { render() { return ( <div> <h1>{this.props.title}</h1> <p>{this.props.description}</p> </div> ) } } export default Card;I prefer writing components with classes over functions, but JavaScript classes are more difficult for beginners to understand and this can get very confusing. Instead, I’d recommend writing components as functions:
// Card.jsx function Card(props) { return ( <div> <h1>{props.title}</h1> <p>{props.description}</p> </div> ) } export default Card;Function components are simply JavaScript functions that return JSX. They are much easier to read, and do not have additional features like the this keyword and lifecycle methods which make them more performant than class components.
Function components also have the advantage of using hooks. React Hooks allow you to use state and other React features without writing a class component, making your code more readable, maintainable and reusable.
Article on Jul 6, 2019 Getting to Know the useReducer React Hook learning react Kingsley Silas Article on May 1, 2020 Intro to React Hooks learning react Kingsley Silas Article on Jul 15, 2022 React Hooks: The Deep Cuts learning react Blessing Ene Anyebe Importing React unnecessarilyTL;DR: There’s no need to do it, unless you need hooks.
Since React 17 was released in 2020, it’s now unnecessary to import React at the top of your file whenever you create a component.
import React from 'react'; // Not needed! export default function Card() {}But we had to do that before React 17 because the JSX transformer (the thing that converts JSX into regular JavaScript) used a method called React.createElement that would only work when importing React. Since then, a new transformer has been release which can transform JSX without the createElement method.
You will still need to import React to use hooks, fragments, and any other functions or components you might need from the library:
import { useState } from 'react'; export default function Card() { const [count, setCount] = useState(0); // ... } Those were my early mistakes!Maybe “mistake” is too harsh a word since some of the better practices came about later. Still, I see plenty of instances where the “old” way of doing something is still being actively used in projects and other tutorials.
To be honest, I probably made way more than five mistakes when getting started. Anytime you reach for a new tool it is going to be more like a learning journey to use it effectively, rather than flipping a switch. But these are the things I still carry with me years later!
If you’ve been using React for a while, what are some of the things you wish you knew before you started? It would be great to get a collection going to help others avoid the same struggles.
5 Mistakes I Made When Starting My First React Project originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Creating a Clock with the New CSS sin() and cos() Trigonometry Functions
CSS trigonometry functions are here! Well, they are if you’re using the latest versions of Firefox and Safari, that is. Having this sort of mathematical power in CSS opens up a whole bunch of possibilities. In this tutorial, I thought we’d dip our toes in the water to get a feel for a couple of the newer functions: sin() and cos().
There are other trigonometry functions in the pipeline — including tan() — so why focus just on sin() and cos()? They happen to be perfect for the idea I have in mind, which is to place text along the edge of a circle. That’s been covered here on CSS-Tricks when Chris shared an approach that uses a Sass mixin. That was six years ago, so let’s give it the bleeding edge treatment.
Here’s what I have in mind. Again, it’s only supported in Firefox and Safari at the moment:
CodePen Embed FallbackSo, it’s not exactly like words forming a circular shape, but we are placing text characters along the circle to form a clock face. Here’s some markup we can use to kick things off:
<div class="clock"> <div class="clock-face"> <time datetime="12:00">12</time> <time datetime="1:00">1</time> <time datetime="2:00">2</time> <time datetime="3:00">3</time> <time datetime="4:00">4</time> <time datetime="5:00">5</time> <time datetime="6:00">6</time> <time datetime="7:00">7</time> <time datetime="8:00">8</time> <time datetime="9:00">9</time> <time datetime="10:00">10</time> <time datetime="11:00">11</time> </div> </div>Next, here are some super basic styles for the .clock-face container. I decided to use the <time> tag with a datetime attribute.
.clock { --_ow: clamp(5rem, 60vw, 40rem); --_w: 88cqi; aspect-ratio: 1; background-color: tomato; border-radius: 50%; container-type: inline; display: grid; height: var(--_ow); place-content: center; position: relative; width var(--_ow); }I decorated things a bit in there, but only to get the basic shape and background color to help us see what we’re doing. Notice how we save the width value in a CSS variable. We’ll use that later. Not much to look at so far:
It looks like some sort of modern art experiment, right? Let’s introduce a new variable, --_r, to store the circle’s radius, which is equal to half of the circle’s width. This way, if the width (--_w) changes, the radius value (--_r) will also update — thanks to another CSS math function, calc():
.clock { --_w: 300px; --_r: calc(var(--_w) / 2); /* rest of styles */ }Now, a bit of math. A circle is 360 degrees. We have 12 labels on our clock, so want to place the numbers every 30 degrees (360 / 12). In math-land, a circle begins at 3 o’clock, so noon is actually minus 90 degrees from that, which is 270 degrees (360 - 90).
Let’s add another variable, --_d, that we can use to set a degree value for each number on the clock face. We’re going to increment the values by 30 degrees to complete our circle:
.clock time:nth-child(1) { --_d: 270deg; } .clock time:nth-child(2) { --_d: 300deg; } .clock time:nth-child(3) { --_d: 330deg; } .clock time:nth-child(4) { --_d: 0deg; } .clock time:nth-child(5) { --_d: 30deg; } .clock time:nth-child(6) { --_d: 60deg; } .clock time:nth-child(7) { --_d: 90deg; } .clock time:nth-child(8) { --_d: 120deg; } .clock time:nth-child(9) { --_d: 150deg; } .clock time:nth-child(10) { --_d: 180deg; } .clock time:nth-child(11) { --_d: 210deg; } .clock time:nth-child(12) { --_d: 240deg; }OK, now’s the time to get our hands dirty with the sin() and cos() functions! What we want to do is use them to get the X and Y coordinates for each number so we can place them properly around the clock face.
The formula for the X coordinate is radius + (radius * cos(degree)). Let’s plug that into our new --_x variable:
--_x: calc(var(--_r) + (var(--_r) * cos(var(--_d))));The formula for the Y coordinate is radius + (radius * sin(degree)). We have what we need to calculate that:
--_y: calc(var(--_r) + (var(--_r) * sin(var(--_d))));There are a few housekeeping things we need to do to set up the numbers, so let’s put some basic styling on them to make sure they are absolutely positioned and placed with our coordinates:
.clock-face time { --_x: calc(var(--_r) + (var(--_r) * cos(var(--_d)))); --_y: calc(var(--_r) + (var(--_r) * sin(var(--_d)))); --_sz: 12cqi; display: grid; height: var(--_sz); left: var(--_x); place-content: center; position: absolute; top: var(--_y); width: var(--_sz); }Notice --_sz, which we’ll use for the width and height of the numbers in a moment. Let’s see what we have so far.
This definitely looks more like a clock! See how the top-left corner of each number is positioned at the correct place around the circle? We need to “shrink” the radius when calculating the positions for each number. We can deduct the size of a number (--_sz) from the size of the circle (--_w), before we calculate the radius:
--_r: calc((var(--_w) - var(--_sz)) / 2);Much better! Let’s change the colors, so it looks more elegant:
We could stop right here! We accomplished the goal of placing text around a circle, right? But what’s a clock without arms to show hours, minutes, and seconds?
Let’s use a single CSS animation for that. First, let’s add three more elements to our markup,
<div class="clock"> <!-- after <time>-tags --> <span class="arm seconds"></span> <span class="arm minutes"></span> <span class="arm hours"></span> <span class="arm center"></span> </div>Then some common markup for all three arms. Again, most of this is just make sure the arms are absolutely positioned and placed accordingly:
.arm { background-color: var(--_abg); border-radius: calc(var(--_aw) * 2); display: block; height: var(--_ah); left: calc((var(--_w) - var(--_aw)) / 2); position: absolute; top: calc((var(--_w) / 2) - var(--_ah)); transform: rotate(0deg); transform-origin: bottom; width: var(--_aw); }We’ll use the same animation for all three arms:
@keyframes turn { to { transform: rotate(1turn); } }The only difference is the time the individual arms take to make a full turn. For the hours arm, it takes 12 hours to make a full turn. The animation-duration property only accepts values in milliseconds and seconds. Let’s stick with seconds, which is 43,200 seconds (60 seconds * 60 minutes * 12 hours).
animation: turn 43200s infinite;It takes 1 hour for the minutes arm to make a full turn. But we want this to be a multi-step animation so the movement between the arms is staggered rather than linear. We’ll need 60 steps, one for each minute:
animation: turn 3600s steps(60, end) infinite;The seconds arm is almost the same as the minutes arm, but the duration is 60 seconds instead of 60 minutes:
animation: turn 60s steps(60, end) infinite;Let’s update the properties we created in the common styles:
.seconds { --_abg: hsl(0, 5%, 40%); --_ah: 145px; --_aw: 2px; animation: turn 60s steps(60, end) infinite; } .minutes { --_abg: #333; --_ah: 145px; --_aw: 6px; animation: turn 3600s steps(60, end) infinite; } .hours { --_abg: #333; --_ah: 110px; --_aw: 6px; animation: turn 43200s linear infinite; }What if we want to start at the current time? We need a little bit of JavaScript:
const time = new Date(); const hour = -3600 * (time.getHours() % 12); const mins = -60 * time.getMinutes(); app.style.setProperty('--_dm', `${mins}s`); app.style.setProperty('--_dh', `${(hour+mins)}s`);I’ve added id="app" to the clockface and set two new custom properties on it that set a negative animation-delay, as Mate Marschalko did when he shared a CSS-only clock. The getHours() method of JavaScipt’s Date object is using the 24-hour format, so we use the remainder operator to convert it into 12-hour format.
In the CSS, we need to add the animation-delay as well:
.minutes { animation-delay: var(--_dm, 0s); /* other styles */ } .hours { animation-delay: var(--_dh, 0s); /* other styles */ }Just one more thing. Using CSS @supports and the properties we’ve already created, we can provide a fallback to browsers that do not supprt sin() and cos(). (Thank you, Temani Afif!):
@supports not (left: calc(1px * cos(45deg))) { time { left: 50% !important; top: 50% !important; transform: translate(-50%,-50%) rotate(var(--_d)) translate(var(--_r)) rotate(calc(-1*var(--_d))) } }And, voilà! Our clock is done! Here’s the final demo one more time. Again, it’s only supported in Firefox and Safari at the moment.
CodePen Embed Fallback What else can we do?Just messing around here, but we can quickly turn our clock into a circular image gallery by replacing the <time> tags with <img> then updating the width (--_w) and radius (--_r) values:
CodePen Embed FallbackLet’s try one more. I mentioned earlier how the clock looked kind of like a modern art experiment. We can lean into that and re-create a pattern I saw on a poster (that I unfortunately didn’t buy) in an art gallery the other day. As I recall, it was called “Moon” and consisted of a bunch of dots forming a circle.
We’ll use an unordered list this time since the circles don’t follow a particular order. We’re not even going to put all the list items in the markup. Instead, let’s inject them with JavaScript and add a few controls we can use to manipulate the final result.
The controls are range inputs (<input type="range">) which we’ll wrap in a <form> and listen for the input event.
<form id="controls"> <fieldset> <label>Number of rings <input type="range" min="2" max="12" value="10" id="rings" /> </label> <label>Dots per ring <input type="range" min="5" max="12" value="7" id="dots" /> </label> <label>Spread <input type="range" min="10" max="40" value="40" id="spread" /> </label> </fieldset> </form>We’ll run this method on “input”, which will create a bunch of <li> elements with the degree (--_d) variable we used earlier applied to each one. We can also repurpose our radius variable (--_r) .
I also want the dots to be different colors. So, let’s randomize (well, not completely randomized) the HSL color value for each list item and store it as a new CSS variable, --_bgc:
const update = () => { let s = ""; for (let i = 1; i <= rings.valueAsNumber; i++) { const r = spread.valueAsNumber * i; const theta = coords(dots.valueAsNumber * i); for (let j = 0; j < theta.length; j++) { s += `<li style="--_d:${theta[j]};--_r:${r}px;--_bgc:hsl(${random( 50, 25 )},${random(90, 50)}%,${random(90, 60)}%)"></li>`; } } app.innerHTML = s; }The random() method picks a value within a defined range of numbers:
const random = (max, min = 0, f = true) => f ? Math.floor(Math.random() * (max - min) + min) : Math.random() * max;And that’s it. We use JavaScript to render the markup, but as soon as it’s rendered, we don’t really need it. The sin() and cos() functions help us position all the dots in the right spots.
CodePen Embed Fallback Final thoughtsPlacing things around a circle is a pretty basic example to demonstrate the powers of trigonometry functions like sin() and cos(). But it’s really cool that we are getting modern CSS features that provide new solutions for old workarounds I’m sure we’ll see way more interesting, complex, and creative use cases, especially as browser support comes to Chrome and Edge.
Creating a Clock with the New CSS sin() and cos() Trigonometry Functions originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Different Ways to Get CSS Gradient Shadows
It’s a question I hear asked quite often: Is it possible to create shadows from gradients instead of solid colors? There is no specific CSS property that does this (believe me, I’ve looked) and any blog post you find about it is basically a lot of CSS tricks to approximate a gradient. We’ll actually cover some of those as we go.
But first… another article about gradient shadows? Really?
Yes, this is yet another post on the topic, but it is different. Together, we’re going to push the limits to get a solution that covers something I haven’t seen anywhere else: transparency. Most of the tricks work if the element has a non-transparent background but what if we have a transparent background? We will explore this case here!
Before we start, let me introduce my gradient shadows generator. All you have to do is to adjust the configuration, and get the code. But follow along because I’m going to help you understand all the logic behind the generated code.
Table of Contents Non-transparent solutionLet’s start with the solution that’ll work for 80% of most cases. The most typical case: you are using an element with a background, and you need to add a gradient shadow to it. No transparency issues to consider there.
The solution is to rely on a pseudo-element where the gradient is defined. You place it behind the actual element and apply a blur filter to it.
.box { position: relative; } .box::before { content: ""; position: absolute; inset: -5px; /* control the spread */ transform: translate(10px, 8px); /* control the offsets */ z-index: -1; /* place the element behind */ background: /* your gradient here */; filter: blur(10px); /* control the blur */ }It looks like a lot of code, and that’s because it is. Here’s how we could have done it with a box-shadow instead if we were using a solid color instead of a gradient.
box-shadow: 10px 8px 10px 5px orange;That should give you a good idea of what the values in the first snippet are doing. We have X and Y offsets, the blur radius, and the spread distance. Note that we need a negative value for the spread distance that comes from the inset property.
Here’s a demo showing the gradient shadow next to a classic box-shadow:
CodePen Embed FallbackIf you look closely you will notice that both shadows are a little different, especially the blur part. It’s not a surprise because I am pretty sure the filter property’s algorithm works differently than the one for box-shadow. That’s not a big deal since the result is, in the end, quite similar.
This solution is good, but still has a few drawbacks related to the z-index: -1 declaration. Yes, there is “stacking context” happening there!
CodePen Embed FallbackI applied a transform to the main element, and boom! The shadow is no longer below the element. This is not a bug but the logical result of a stacking context. Don’t worry, I will not start a boring explanation about stacking context (I already did that in a Stack Overflow thread), but I’ll still show you how to work around it.
The first solution that I recommend is to use a 3D transform:
.box { position: relative; transform-style: preserve-3d; } .box::before { content: ""; position: absolute; inset: -5px; transform: translate3d(10px, 8px, -1px); /* (X, Y, Z) */ background: /* .. */; filter: blur(10px); }Instead of using z-index: -1, we will use a negative translation along the Z-axis. We will put everything inside translate3d(). Don’t forget to use transform-style: preserve-3d on the main element; otherwise, the 3D transform won’t take effect.
CodePen Embed FallbackAs far as I know, there is no side effect to this solution… but maybe you see one. If that’s the case, share it in the comment section, and let’s try to find a fix for it!
If for some reason you are unable to use a 3D transform, the other solution is to rely on two pseudo-elements — ::before and ::after. One creates the gradient shadow, and the other reproduces the main background (and other styles you might need). That way, we can easily control the stacking order of both pseudo-elements.
.box { position: relative; z-index: 0; /* We force a stacking context */ } /* Creates the shadow */ .box::before { content: ""; position: absolute; z-index: -2; inset: -5px; transform: translate(10px, 8px); background: /* .. */; filter: blur(10px); } /* Reproduces the main element styles */ .box::after { content: """; position: absolute; z-index: -1; inset: 0; /* Inherit all the decorations defined on the main element */ background: inherit; border: inherit; box-shadow: inherit; } CodePen Embed FallbackIt’s important to note that we are forcing the main element to create a stacking context by declaring z-index: 0, or any other property that do the same, on it. Also, don’t forget that pseudo-elements consider the padding box of the main element as a reference. So, if the main element has a border, you need to take that into account when defining the pseudo-element styles. You will notice that I am using inset: -2px on ::after to account for the border defined on the main element.
As I said, this solution is probably good enough in a majority of cases where you want a gradient shadow, as long as you don’t need to support transparency. But we are here for the challenge and to push the limits, so even if you don’t need what is coming next, stay with me. You will probably learn new CSS tricks that you can use elsewhere.
Transparent solutionLet’s pick up where we left off on the 3D transform and remove the background from the main element. I will start with a shadow that has both offsets and spread distance equal to 0.
CodePen Embed FallbackThe idea is to find a way to cut or hide everything inside the area of the element (inside the green border) while keeping what is outside. We are going to use clip-path for that. But you might wonder how clip-path can make a cut inside an element.
Indeed, there’s no way to do that, but we can simulate it using a particular polygon pattern:
clip-path: polygon(-100vmax -100vmax,100vmax -100vmax,100vmax 100vmax,-100vmax 100vmax,-100vmax -100vmax,0 0,0 100%,100% 100%,100% 0,0 0) CodePen Embed FallbackTada! We have a gradient shadow that supports transparency. All we did is add a clip-path to the previous code. Here is a figure to illustrate the polygon part.
The blue area is the visible part after applying the clip-path. I am only using the blue color to illustrate the concept, but in reality, we will only see the shadow inside that area. As you can see, we have four points defined with a big value (B). My big value is 100vmax, but it can be any big value you want. The idea is to ensure we have enough space for the shadow. We also have four points that are the corners of the pseudo-element.
The arrows illustrate the path that defines the polygon. We start from (-B, -B) until we reach (0,0). In total, we need 10 points. Not eight points because two points are repeated twice in the path ((-B,-B) and (0,0)).
There’s still one more thing left for us to do, and it’s to account for the spread distance and the offsets. The only reason the demo above works is because it is a particular case where the offsets and spread distance are equal to 0.
Let’s define the spread and see what happens. Remember that we use inset with a negative value to do this:
CodePen Embed FallbackThe pseudo-element is now bigger than the main element, so the clip-path cuts more than we need it to. Remember, we always need to cut the part inside the main element (the area inside the green border of the example). We need to adjust the position of the four points inside of clip-path.
.box { --s: 10px; /* the spread */ position: relative; } .box::before { inset: calc(-1 * var(--s)); clip-path: polygon( -100vmax -100vmax, 100vmax -100vmax, 100vmax 100vmax, -100vmax 100vmax, -100vmax -100vmax, calc(0px + var(--s)) calc(0px + var(--s)), calc(0px + var(--s)) calc(100% - var(--s)), calc(100% - var(--s)) calc(100% - var(--s)), calc(100% - var(--s)) calc(0px + var(--s)), calc(0px + var(--s)) calc(0px + var(--s)) ); }We’ve defined a CSS variable, --s, for the spread distance and updated the polygon points. I didn’t touch the points where I am using the big value. I only update the points that define the corners of the pseudo-element. I increase all the zero values by --s and decrease the 100% values by --s.
CodePen Embed FallbackIt’s the same logic with the offsets. When we translate the pseudo-element, the shadow is out of alignment, and we need to rectify the polygon again and move the points in the opposite direction.
.box { --s: 10px; /* the spread */ --x: 10px; /* X offset */ --y: 8px; /* Y offset */ position: relative; } .box::before { inset: calc(-1 * var(--s)); transform: translate3d(var(--x), var(--y), -1px); clip-path: polygon( -100vmax -100vmax, 100vmax -100vmax, 100vmax 100vmax, -100vmax 100vmax, -100vmax -100vmax, calc(0px + var(--s) - var(--x)) calc(0px + var(--s) - var(--y)), calc(0px + var(--s) - var(--x)) calc(100% - var(--s) - var(--y)), calc(100% - var(--s) - var(--x)) calc(100% - var(--s) - var(--y)), calc(100% - var(--s) - var(--x)) calc(0px + var(--s) - var(--y)), calc(0px + var(--s) - var(--x)) calc(0px + var(--s) - var(--y)) ); }There are two more variables for the offsets: --x and --y. We use them inside of transform and we also update the clip-path values. We still don’t touch the polygon points with big values, but we offset all the others — we reduce --x from the X coordinates, and --y from the Y coordinates.
Now all we have to do is to update a few variables to control the gradient shadow. And while we are at it, let’s also make the blur radius a variable as well:
CodePen Embed FallbackDo we still need the 3D transform trick?
It all depends on the border. Don’t forget that the reference for a pseudo-element is the padding box, so if you apply a border to your main element, you will have an overlap. You either keep the 3D transform trick or update the inset value to account for the border.
Here is the previous demo with an updated inset value in place of the 3D transform:
CodePen Embed FallbackI‘d say this is a more suitable way to go because the spread distance will be more accurate, as it starts from the border-box instead of the padding-box. But you will need to adjust the inset value according to the main element’s border. Sometimes, the border of the element is unknown and you have to use the previous solution.
With the earlier non-transparent solution, it’s possible you will face a stacking context issue. And with the transparent solution, it’s possible you face a border issue instead. Now you have options and ways to work around those issues. The 3D transform trick is my favorite solution because it fixes all the issues (The online generator will consider it as well)
Adding a border radiusIf you try adding border-radius to the element when using the non-transparent solution we started with, it is a fairly trivial task. All you need to do is to inherit the same value from the main element, and you are done.
CodePen Embed FallbackEven if you don’t have a border radius, it’s a good idea to define border-radius: inherit. That accounts for any potential border-radius you might want to add later or a border radius that comes from somewhere else.
It’s a different story when dealing with the transparent solution. Unfortunately, it means finding another solution because clip-path cannot deal with curvatures. That means we won’t be able to cut the area inside the main element.
We will introduce the mask property to the mix.
This part was very tedious, and I struggled to find a general solution that doesn’t rely on magic numbers. I ended up with a very complex solution that uses only one pseudo-element, but the code was a lump of spaghetti that covers only a few particular cases. I don’t think it is worth exploring that route.
I decided to insert an extra element for the sake of simpler code. Here’s the markup:
<div class="box"> <sh></sh> </div>I am using a custom element, <sh>, to avoid any potential conflict with external CSS. I could have used a <div>, but since it’s a common element, it can easily be targeted by another CSS rule coming from somewhere else that can break our code.
The first step is to position the <sh> element and purposely create an overflow:
.box { --r: 50px; position: relative; border-radius: var(--r); } .box sh { position: absolute; inset: -150px; border: 150px solid #0000; border-radius: calc(150px + var(--r)); }The code may look a bit strange, but we’ll get to the logic behind it as we go. Next, we create the gradient shadow using a pseudo-element of <sh>.
.box { --r: 50px; position: relative; border-radius: var(--r); transform-style: preserve-3d; } .box sh { position: absolute; inset: -150px; border: 150px solid #0000; border-radius: calc(150px + var(--r)); transform: translateZ(-1px) } .box sh::before { content: ""; position: absolute; inset: -5px; border-radius: var(--r); background: /* Your gradient */; filter: blur(10px); transform: translate(10px,8px); }As you can see, the pseudo-element uses the same code as all the previous examples. The only difference is the 3D transform defined on the <sh> element instead of the pseudo-element. For the moment, we have a gradient shadow without the transparency feature:
CodePen Embed FallbackNote that the area of the <sh> element is defined with the black outline. Why I am doing this? Because that way, I am able to apply a mask on it to hide the part inside the green area and keep the overflowing part where we need to see the shadow.
I know it’s a bit tricky, but unlike clip-path, the mask property doesn’t account for the area outside an element to show and hide things. That’s why I was obligated to introduce the extra element — to simulate the “outside” area.
Also, note that I am using a combination of border and inset to define that area. This allows me to keep the padding-box of that extra element the same as the main element so that the pseudo-element won’t need additional calculations.
Another useful thing we get from using an extra element is that the element is fixed, and only the pseudo-element is moving (using translate). This will allow me to easily define the mask, which is the last step of this trick.
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); mask-composite: exclude; CodePen Embed FallbackIt’s done! We have our gradient shadow, and it supports border-radius! You probably expected a complex mask value with oodles of gradients, but no! We only need two simple gradients and a mask-composite to complete the magic.
Let’s isolate the <sh> element to understand what is happening there:
.box sh { position: absolute; inset: -150px; border: 150px solid red; background: lightblue; border-radius: calc(150px + var(--r)); }Here’s what we get:
CodePen Embed FallbackNote how the inner radius matches the main element’s border-radius. I have defined a big border (150px) and a border-radius equal to the big border plus the main element’s radius. On the outside, I have a radius equal to 150px + R. On the inside, I have 150px + R - 150px = R.
We must hide the inner (blue) part and make sure the border (red) part is still visible. To do that, I’ve defined two mask layers —One that covers only the content-box area and another that covers the border-box area (the default value). Then I excluded one from another to reveal the border.
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); mask-composite: exclude; CodePen Embed FallbackI used the same technique to create a border that supports gradients and border-radius. Ana Tudor has also a good article about masking composite that I invite you to read.
Are there any drawbacks to this method?
Yes, this definitely not perfect. The first issue you may face is related to using a border on the main element. This may create a small misalignment in the radii if you don’t account for it. We have this issue in our example, but perhaps you can hardly notice it.
The fix is relatively easy: Add the border’s width for the <sh> element’s inset.
.box { --r: 50px; border-radius: var(--r); border: 2px solid; } .box sh { position: absolute; inset: -152px; /* 150px + 2px */ border: 150px solid #0000; border-radius: calc(150px + var(--r)); }Another drawback is the big value we’re using for the border (150px in the example). This value should be big enough to contain the shadow but not too big to avoid overflow and scrollbar issues. Luckily, the online generator will calculate the optimal value considering all the parameters.
The last drawback I am aware of is when you’re working with a complex border-radius. For example, if you want a different radius applied to each corner, you must define a variable for each side. It’s not really a drawback, I suppose, but it can make your code a bit tougher to maintain.
.box { --r-top: 10px; --r-right: 40px; --r-bottom: 30px; --r-left: 20px; border-radius: var(--r-top) var(--r-right) var(--r-bottom) var(--r-left); } .box sh { border-radius: calc(150px + var(--r-top)) calc(150px + var(--r-right)) calc(150px + var(--r-bottom)) calc(150px + var(--r-left)); } .box sh:before { border-radius: var(--r-top) var(--r-right) var(--r-bottom) var(--r-left); } CodePen Embed FallbackThe online generator only considers a uniform radius for the sake of simplicity, but you now know how to modify the code if you want to consider a complex radius configuration.
Wrapping upWe’ve reached the end! The magic behind gradient shadows is no longer a mystery. I tried to cover all the possibilities and any possible issues you might face. If I missed something or you discover any issue, please feel free to report it in the comment section, and I’ll check it out.
Again, a lot of this is likely overkill considering that the de facto solution will cover most of your use cases. Nevertheless, it’s good to know the “why” and “how” behind the trick, and how to overcome its limitations. Plus, we got good exercise playing with CSS clipping and masking.
And, of course, you have the online generator you can reach for anytime you want to avoid the hassle.
Different Ways to Get CSS Gradient Shadows originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
