Full Width [alt+shift+f] Shortcuts [alt+shift+k]
Sign Up [alt+shift+s] Log In [alt+shift+l]
33
My click-spark web component was a fun, silly project at best. Yet I've seen it's had some love since being shared. So why not publish it as an npm package? No better time than the present, some say. I had done a major refactor before publishing, the most notable was that the spark would now be contained to the custom element's parent node. After announcing the updates in a Mastodon post, I soon received a PR with some quality feedback including a more advantageous way to handle the parent click event using the handleEvent() method. The click-spark click Let's dive into a "before and after" for handling the click event on this component. In both examples, notice that the parent node is being stored in a variable when connectedCallback runs. This ensures that the click event is properly removed from the parent since it's not available by the time disconnectedCallback is invoked as FND's comment explains. In the "before" approach, the event handler is stored in a variable so that it's cleared from the parent node whenever the click-spark element is removed from the DOM. constructor() { this.clickEvent = this.handleClick.bind(this); } connectedCallback() { this._parent = this.parentNode; this._parent.addEventListener("click", this.clickEvent); } disconnectedCallback() { this._parent.removeEventListener("click", this.clickEvent); delete this._parent; } handleClick() { // Run code on click } Switching to handleEvent() removes the need to store the event handler. Passing this into the event listener will run the component's handleEvent() method every time we click. constructor() { // No longer need to store the callback } connectedCallback() { this._parent = this.parentNode; this._parent.addEventListener("click", this); } disconnectedCallback() { this._parent.removeEventListener("click", this); delete this._parent; } handleEvent(e) { // Run code on click } Helpful resources Chris Ferdinandi's article, The handleEvent() method is the absolute...
27th Jul 2024

Stay updated

Get a weekly newsletter with the top 5 articles worth reading every week.

More from Ryan Mulligan

Transition to the Other Side with Container Query Units

Managing the position of an element as it travels across the length of its parent container can be tricky. Assuming they both have dynamic, responsive dimensions, we might rely on JS to check the width and/or height of each element and do some calculations for a proper end result. The classic FLIP technique has proven to be a solid solution in the past. For a modern approach, the View Transition API can also work well here. I now realize there's a much simpler approach using container query units and a dash of CSS wizardry. The demo Select the container query units option if it's not already, then click and hold the container to witness the magic. Try changing the element dimensions or resizing the parent container using its handle on the bottom right. Open CodePen demo Jump down to the solution if you'd like to get right into it. Otherwise, join me on a transformative journey to this final result. Transition exploration The following CSS will transition an element smoothly to the right when its parent container is pressed: .element { transition: transform 200ms ease-out; } .parent:active .element { transform: translateX(100%); } Individual transform properties are also available and well supported in modern browsers. I tend to use them more frequently when writing simple transforms like this. In the demo, we'll find the translate property is being transitioned. Let's update the above code example to something similar: .element { transition: translate 200ms ease-out; } .parent:active .element { translate: 100%; } Keep in mind that there's a pre-defined order for independent transform properties. Stefan's article explains the fundamental differences between transform functions and individual transforms. Not an issue with our current examples, but something to remember when multiple individual transforms are being applied. Check out the demo with x checked and the percentage option selected. When we click and hold the container, the element transitions the width of itself to the right. We can see that this percentage is based on the element's dimensions. While handy, it doesn't achieve our goal of moving the element all the way to the opposite side. Explicit dimensions If we knew the exact dimensions of the parent container, we could declare a calc() function where the element's full percentage is subtracted from the explicit parent size. .parent { width: 300px; } .element { transition: translate 200ms ease-out; } .parent:active .element { translate: calc(300px - 100%); } It works, but it's uncommon to have explicit dimensions declared like that. Our elements need to be flexible and responsive in any context. What can we do instead? Position properties Properties like top and left are available to us. Could we transition the element by doing something like this? .element { position: relative; left: 0; transition: 200ms ease-out; transition-property: translate, left; } .parent:active .element { left: 100%; translate: -100%; } Seems that we can, at least in the context of the demo. However, animating position properties has a negative impact on layout and creates performance issues. The browser works harder to recalculate element positions, repaint pixels, and then composite the result. This inevitably leads to janky or sluggish animations. GPU-accelerated properties such as transform and translate avoid triggering repaints so animations run buttery-smooth and fluid. Fair enough. We'll focus on moving the element using transform properties. It's time to reveal the strongest solution. The solution In the demo's controls, check that container query units is selected. Click and hold the container. Watch as the element smoothly transitions to the opposite side of the parent container. Try changing the element dimensions using the sliders, or resize the parent with the resize handle on its bottom right. It still works! Here's the gist when we only need to transition to the opposite side of the parent horizontally. .parent { container-type: inline-size; } .element { transition: translate 200ms ease-out; } .parent:active .element { translate: calc(100cqi - 100%); } If we want to transition vertically or in both directions, we'll need the size value for our container-type so that containment is applied to the block and inline directions. The example below translates the element along the y-axis. .parent { container-type: size; } .element { transition: translate 200ms ease-out; } .parent:active .element { translate: 0 calc(100cqb - 100%); } By setting a container-type on the parent, the element is able to access the parent size via container query length units: 1cqi is 1% of the inline size. 1cqb is 1% of the block size. Notice that we're using logical properties instead of physical ones. If this isn't familiar territory, I recommend Ahmad's excellent Digging Into CSS Logical Properties article to learn more. 100cqi is the full inline size of the parent container. We can recall from earlier that a transform percentage reflects the element's dimensions. Once we subtract 100% from that container query unit, the element can gracefully transition to its proper position on the opposite side of the container. Take a moment to enjoy the wonder and magic that is modern CSS. Helpful resources Container Queries and Units How to create high-performance CSS animations Order in CSS transformations – transform functions vs individual transforms Digging Into CSS Logical Properties

11th Oct 2025 1 votes
Blog Questions Challenge

Hey there. It has been a minute since my last post. I was semi-recently tagged by Zach Leatherman to (optionally) participate in this year's Blog Questions Challenge. I had planned on doing it then. But life really hit hard as we entered this year and it has not let up. Energy dedicated to my personal webspace has been non-existent. I am tired. Hopefully this post can help shake off some of the rust, bring me back to writing and sharing with you lovely folks. I won't be tagging anyone to do this challenge. However, if you're inspired to write your own after reading mine, I'd love for you to share it with me. Why did you start blogging in the first place? Blogging has always been a part of my web experience. Earliest I can remember is building my band a GeoCities website back in high school. I'd share short passages about new song ideas, how last night's show went, stuff like that. I also briefly had a Xanga blog running. My memory is totally faded on what exactly I wrote in there—I'm not eager to dig up high school feelings either—but fairly certain all of those entries are just lost digital history. Having an "online journal" was such a fresh idea at the time. Sharing felt more natural and real before the social media platforms took over. [blows raspberry] I've completely dated myself and probably sound like "old man yells at cloud" right now. Anyway, I pretty much stopped blogging for a while after high school. I turned my efforts back to pen on paper, keeping journals of lyrics, thoughts, and feelings mostly to myself. My dev-focused blogging that you may be familiar with really only spans the last decade, give or take a couple years. What platform are you using to manage your blog and why? At the moment and the forseeable future, I'm using 11ty. I published a short post about migrating to 11ty back in 2021. I still feel the same sentiments and still admire those same people. And many new community friends as well! Have you blogged on other platforms before? I've definitely used WordPress but I can't remember what the heck I was even blogging about during that time. Then I switched to just writing posts directly in HTML files and FTP'ing them up to some server somewhere. Pretty silly in retrospect, but boy did I feel alive. How do you write your posts? Always via laptop, never on my phone. I manage posts in markdown files, push them up to a GitHub repo and let that automatically redeploy my site on Netlify. Editing content is done in VSCode. I've debated switching to some lightweight CMS, connecting to Notion or Obsidian, but why introduce any more complexity and mess with what works fine for me? When do you feel most inspired to write? Typically I'll write up a post about something new I discovered while on my wild coding escapades, whether at work or in my free time. If I have trouble finding solutions to my particular problem on the world wide webs, I'm even more inclined to post about it. Most of my ideas are pursued on weekends, but I've had some early morning or late night weekday sessions. What I'm trying to say is that anytime is a good time for blogging. It's like pizza when it's on a bagel. Do you publish immediately after writing, or do you let it simmer a bit as a draft? It depends. If I had been writing for a long period of time, I find it best to take a breather before publishing. When I feel ready, I'll post and share with a small group for feedback, find grammatical errors. Then I eventually add it to whatever social channels feel right. Used to be Twitter, but straight up screw that garbage temple. I'll likely post on Bluesky, toot on Mastodon. Other times I'll slap a new post on this site and not share it on any socials. Let the RSS feeds do their magic. What's your favorite post on your blog? I don't know if I have a favorite. Can I love them all equally? Well, besides that CSS Marquee one. Damn that blog post for becoming so popular. Any future plans for your blog? Once things settle down in life, I think I'll be ready for a redesign. I had a blast building the current version inspired by Super Mario Wonder. Until then? More blogging. It won't be super soon, but I do have a few zesty article ideas percolating in this old, tired brain.

25th Mar 2025 73 votes
The Pixel Canvas Shimmer Effect

I recently stumbled on a super cool, well-executed hover effect from the clerk.com website where a bloom of tiny pixels light up, their glow staggering from the center to the edges of its container. With some available free time over this Thanksgiving break, I hacked together my own version of a pixel canvas background shimmer. It quickly evolved into a pixel-canvas Web Component that can be enjoyed in the demo below. The component script and demo code have also been pushed up to a GitHub repo. Open CodePen demo Usage Include the component script and then insert a pixel-canvas custom element inside the container it should fill. <script type="module" src="pixel-canvas.js"></script> <div class="container"> <pixel-canvas></pixel-canvas> <!-- other elements --> </div> The pixel-canvas stretches to the edges of the parent container. When the parent is hovered, glimmering pixel fun ensues. Options The custom element has a few optional attributes available to customize the effect. Check out the CodePen demo's html panel to see how each variation is made. data-colors takes a comma separated list of color values. data-gap sets the amount of space between each pixel. data-speed controls the general duration of the shimmer. This value is slightly randomized on each pixel that, in my opinion, adds a little more character. data-no-focus is a boolean attribute that tells the Web Component to not run its animation whenever sibling elements are focused. The animation runs on sibling focus by default. There's likely more testing and tweaking necessary before I'd consider using this anywhere, but my goal was to run with this inspiration simply for the joy of coding. What a mesmerizing concept. I tip my hat to the creative engineers over at Clerk.

3rd Dec 2024 96 votes
CSS @property and the New Style

The @property at-rule recently gained support across all modern browsers, unlocking the ability to explicitly define a syntax, initial value, and inheritance for CSS custom properties. It seems like forever ago that CSS Houdini and its CSS Properties and Values API were initially introduced. I experimented sparingly over time, reading articles that danced around the concepts, but I had barely scratched the surface of what @property could offer. The ensuing demo explores what's possible in the next generation of CSS. Calls to action Ever seen those sleek, attention-seeking, shiny call-to-action webpage elements? Waves of sites across the web, especially the ones marketing services and software urging for you to "Upgrade your account" or "Sign up today," have discovered the look and latched on. I'm not here to knock it and admittedly think it's kind of fresh. I thought I'd give that style a try myself. Check out the result in the CodePen below. Open CodePen demo There's a ton to unpack in this demo. Let's start with that shine looping around the button. Toggle open the demo's CSS panel to find a collection of @property rules related to those custom properties that need to animate. Here's the one defined for the --gradient-angle: @property --gradient-angle { syntax: "<angle>"; initial-value: 0deg; inherits: false; } The @property rule communicates to the browser that <angle> is the allowed syntax for this custom property and its initial value is 0deg. This enables the browser to smoothly transition from 0deg to 360deg and output a rotating gradient. @keyframes rotate-gradient { to { --gradient-angle: 360deg; } } .rotate-gradient { background: conic-gradient(from var(--gradient-angle), transparent, black); animation: rotate-gradient 10s linear infinite; } I put together a simple gradient spin demo to focus on the handful of lines necessary to render this concept. Open CodePen demo We can achieve the shiny animated border effect by evolving this code a bit. We'll introduce a linear-gradient as the first value of the element's background property and set a background-origin to each value. The origin of the linear-gradient is set to padding-box. This prevents the gradient from spilling into the border area. The conic-gradient origin is set to border-box. This gradient overflows into the space created by the border width. To reveal the rotating conic-gradient, a single-pixel transparent border is added. .border-gradient { background: linear-gradient(black, black) padding-box, conic-gradient(from var(--gradient-angle), transparent 25%, white, transparent 50%) border-box; border: 1px solid transparent; } In the CSS panel of the simple gradient spin demo, uncomment the .border-gradient ruleset to reveal the shiny animated border. Looking pretty slick! For more examples, I've included a bunch of animated gradient border articles in the resources section at the end of the post. Silky smooth hover transitions A few special ingredients help facilitate a buttery smooth gradient transition when the element is hovered. Let's dig into its background values: .shiny-cta { background: linear-gradient(var(--shiny-cta-bg), var(--shiny-cta-bg)) padding-box, conic-gradient( from calc(var(--gradient-angle) - var(--gradient-angle-offset)), transparent, var(--shiny-cta-highlight) var(--gradient-percent), var(--gradient-shine) calc(var(--gradient-percent) * 2), var(--shiny-cta-highlight) calc(var(--gradient-percent) * 3), transparent calc(var(--gradient-percent) * 4) ) border-box; } Each custom property that needs to animate has a syntax declared in its @property definition so that the browser can interpolate between corresponding value changes and transition them seamlessly. The size of the shiny area is determined by the --gradient-percent value. On hover, a higher percentage lengthens the shine. The --gradient-angle-offset value is used to readjust the gradient angle so that the shine doesn't rubber band back and forth on hover. Your browser cannot play the provided video file. Demonstrating the transition behavior without the angle offset value I had to fine-tune the percent and offset values until the shine length and transition felt optically aligned. Finally, the --gradient-shine brightness gets toned down to blend more seamlessly with the adjacent highlight colors. Slow it on down This CSS tip to slow down a rotation on hover truly blew my mind. In the tip's example code, the same rotate animation is declared twice. The second one is reversed and paused, its duration divided in half. When the element is hovered, animation-play-state: running overrides the paused value and slows the rotation to half speed. The mind-blowing part, at least to me, is that the animation speeds back up at the current position when the element is no longer hovered. No snapping back to a start position, no extra wrapper elements necessary. That is one heck of a tip. The call-to-action animations rely on this method to slow them down when the button is hovered. This technique keeps all the rotations and movements in sync as they change speed. Tiny shiny dots Looking even closer, we'll discover pinhole-sized dots shimmering inside the button as the shiny border passes near them. To render this dot pattern, a radial-gradient background is created. .shiny-cta::before { --position: 2px; --space: calc(var(--position) * 2); background: radial-gradient( circle at var(--position) var(--position), white calc(var(--position) / 4), transparent 0 ) padding-box; background-size: var(--space) var(--space); background-repeat: space; } Remember that --gradient-angle custom property? It has returned! But this time, it's being used in a conic-gradient mask that reveals parts of the dot pattern as it rotates. The gradient angle is offset by 45 degrees to align it perfectly with the shiny border rotation. .shiny-cta::before { mask-image: conic-gradient( from calc(var(--gradient-angle) + 45deg), black, transparent 10% 90%, black ); } For one last touch of magic, a gradient containing the highlight color is added to the ::after pseudo element, spinning in unison with the shine area. These highlights flowing through the button add a pleasant, welcoming ambience that was previously missing. Enhancing the hover colors The hover styles looked decent. But they didn't seem totally finished. I felt the desire to enhance. Create more depth. Make it pop, as they say. The button's ::before and ::after pseudo elements were already in use so I wrapped the button text in a span element. A blurred box-shadow containing the highlight color is applied to one of its pseudo elements which is then expanded to fill the button dimensions. On hover, the pseudo element slowly scales up and down, evoking a vibe similar to relaxed breathing. Paired with the spinning highlight color inside the button, the effect finally resonated with me. This intricately designed call-to-action button felt complete. In with the new style Many of the above techniques would have been nearly impossible only a short time ago. Explicitly defining custom properties unlocks a great big world of opportunity. I'm especially eager to see how @property will be utilized in large-scale applications and design systems. Providing Type Definitions for CSS with @property by Stephanie Eckles as well as Adam Argyle's Type safe CSS design systems with @property are just a couple glimpses into a really promising future for publishing our CSS. Helpful resources Animated CSS gradient borders (no JavaScript, no hacks) Creating an animated gradient border with CSS CSS border animations Animating a CSS Gradient Border CSS border ripple effect The Times You Need A Custom @property Instead Of A CSS Variable @property: Next-gen CSS variables now with universal browser support

2nd Sep 2024 38 votes
Center Items in First Row with CSS Grid

Imagine the following section on a website: A collection of elements, like a series of cards with marketing information, are presented in a grid display. The elements are arranged in rows of three. When there are an odd number of elements left over, they will be center-aligned horizontally. There are a few ways to accomplish styling such a layout. Controlling Leftover Grid Items with Pseudo-selectors by Michelle Barker shares a clever CSS Grid solution. But here's a twist: What if the centered odd number of elements should appear in the first row instead of the last? I've included a CodePen demo at the end of this article if you'd like to jump ahead. Otherwise, continue on a journey of style discovery. Grid Stacks Is it a trapezoid grid? Brick grid? Grid pyramid? Pyragrid? For the sake of this article, I ultimately picked a more generic name, calling it Grid Stack. Here's how we'll build a Grid Stack that contains five cards displayed in a three-column grid. .grid-stack { display: grid; grid-template-columns: repeat(6, 1fr); > * { grid-column-end: span 2; } > :first-child { grid-column-start: 2; } } CSS nesting is being used here, which is newly supported across major browsers. Not feeling ready for that yet? We can move the nested rules into their own top-level rulesets. They just need to start with the parent selector name, i.e. .grid-stack > * { } The parent grid-stack container produces a template with six columns. Notice that the grid-template-columns repeat count is double the amount of columns we want visually present in each row. Each child element will then span across two columns instead of one. Finally, the first child element is aligned to the start of the second column. The result is a visually centered top row. Grid lines (enabled in dev tools) help show where each child element is positioned on the grid. Variations So far, the styles we've created only apply when there are five cards positioned across three columns. We may want different variations depending on our designs. Three cards displayed in two columns? Seven cards in four? Let's tweak the above ruleset to utilize a CSS variable for the grid-template-columns repeat count. Recall that this value should be twice the amount of expected columns. .grid-stack { display: grid; grid-template-columns: repeat(calc(var(--columns) * 2), 1fr); > * { grid-column-end: span 2; } > :first-child { grid-column-start: 2; } } The --columns value gets doubled as it passes through the CSS calc() function. Now we're able to define the preferred amount of columns directly on the parent container. <div class="grid-stack" style="--columns: 2"></div> <div class="grid-stack" style="--columns: 3"></div> <div class="grid-stack" style="--columns: 4"></div> Demo Open CodePen demo Bonus! Pyramid stacks In the above demo, you may have discovered some configurations for the Grid Stack that result in a pyramid-style stack. Maybe now we can call it a pyragrid? Still not sure about that one... Anyway, to achieve this layout involves a few extra ingredients. We'll need to adjust the grid-column-start position of the first element in each row. Let's jump right to the last example with the grid-stack-15 selector: .grid-stack-15 { --columns: 5; > :first-child { grid-column-start: 5; } > :nth-child(2) { grid-column-start: 4; } > :nth-child(4) { grid-column-start: 3; } > :nth-child(7) { grid-column-start: 2; } } This calls for a five-column grid visually, so it sets --columns: 5. Recall that this value gets doubled and outputs a template of ten columns. We'll nudge the first item in each row with grid-column-start. The top row element's start position is equal to the --column value. Subsequent rules will decrease this value by 1. It's surely possible to develop a Sass or PostCSS function that could dynamically generate this CSS output but that seemed a bit overkill for the demo. As an added bonus, check out Temani Afif's Stack Overflow answer that styles elements in a pyramid using float and shape-outside. Very cool! Limitations Each layout variation expects a specific odd-number of child elements to be rendered. I have explored ways of automatically adjusting the layout based on the element count but there were too many edge cases to consider. It created more problems than it solved. Additionally, while these layouts work nicely on a wider viewport, it may not fare as well where less space is available. A media or container query ruleset can ensure our content adapts appropriately, but it certainly couldn't be a one-size-fits-all conditional set of styles. From the community Updated on September 5th — Kevin Powell reached out on Mastodon and shared a CodePen Example that uses :has(:nth-child():last-child) to result in this layout. I certainly dig the approach! Keep in mind that the same aforementioned limitations still apply.

19th Aug 2024 55 votes

More in design

The Empty Heading

For many years, I have maintained a text file called “A Rubric for Website Design Critique.” It is relatively short, but used nonetheless; I’ve returned to it, off and on, for most of my career, referring back, adding things, removing things, adjusting. Its purpose is to standardize how I challenge the work I do and the work I am shown, and even a standard needs maintenance. The documented rubric has five components. Of information architecture, it asks, Is the priority apparent? Does it make sense? Is it actionable? Of layout: Do the visual elements support the architecture? Is the page as scannable as a high-fidelity asset as it was a wireframe? Of accessibility: Is there adequate contrast? Has text been hidden in images? Can a screen reader properly navigate? And of visual language, Is there coherence? Is it consistent? I emphasized documented earlier because it was never complete. The fifth component is art direction, and after that heading in my document is nothing. The file just ends. It isn’t like me to leave something unfinished. I don’t like ragged edges, even when I know they’re natural and sometimes essential. And time and again over the years, I’ve had a chance to wonder at this empty space. Why is it there? Why is it difficult to fill? Perhaps I’m just not the person to fill it. Perhaps that’s where my expertise ends. However, looking again at this unfinished document recently, I have come to a different conclusion. The first four sections — Information Architecture, Layout, Accessibility, Visual Language — are inspection routines. Each one asks a question that has an answer, and the answer can — should — be able to be found by someone who is not me. Art direction is not like that. And that’s why every time I attempted to fill out structured guidance I produced a list of things I did not actually believe… and then deleted them. And so, the section remained empty, which is its own kind of answer, and not a very useful one. Here is a better attempt. Order Is the Floor The first four sections are about order. They ask whether a page is arranged so that it can be seen, perceived, and understood. That is the floor, and a great deal of professional work never gets off it. In fact, the majority of my career has been focused on getting interaction design off the floor. On my team we have run a periodic competition called The Tidiest Designer, where each person submits a composition file for inspection. We look for order, consistency, clarity, and utility. We do not do this because order alone makes design good. We do it because order is what allows good design to happen. Many beautiful, client-applauded comps have been chaotic disasters underneath their presentation modes, and not surprisingly, conflict-inducing when actually produced. Order facilitates that the promise of design becomes its function. But deeper than that, when order is our foundation, we can spend more of our critical energy on the responsible rendering of taste. Section five is that rendering. It is the point at which intent stops being organized and starts being expressed. What follows is not a set of criteria, then, but five places to stand while you look, in the order I tend to look, with a test attached to each that someone else can run. Where a test comes back “I don’t know,” that is a finding. Most designers are intuitive in their creation, which is not a bad thing. But without cross-examining, reinforcing, studying, enriching, and systematizing what begins with our intuition, we end up with something that is meaningful to us and arbitrary to everyone else. This — arbitrariness — is the most common condition of professional design work, and it is nearly invisible from the inside. The Key Every good piece of design has at least one detail that unlocks how the whole thing works. Good designers notice it immediately. Everyone else responds to it without knowing they have. It might be a rule, a crop, a single color used once, a piece of type set deliberately against the grid. Whatever it is, the rest of the composition should be clearing a path for it. A designer on my team once brought me a set of ads for a maker of high-end audio equipment, built around the idea of choice. Two arrows ran in parallel and then diverged, one rendered in color veering off to the left, the other in white, passing it before turning right. The white arrow was the key. It overpowered the bolder colored one simply by pushing further into the space, and its arc carried the eye down to the copy and the call to action. Then I noticed that its curve radius quietly echoed the skewed, rotated “o” in the client’s logotype, and that those two arrows were the only shapes in the entire ad other than text. That last part is the lesson. The key was doing three jobs at once, and everything else had gotten out of its way. The Key Test. Name the key in one sentence. Then say what the composition does to protect it. If nothing on the page is deferring to anything else, there is no key, only assembly. If you can name three, there is also no key, because three keys is zero keys. The Structure Underneath Structure does more work than content while convincing its audience of the opposite. This is the oldest secret in graphic design and painters have known it longest. Mondrian said that every true artist has been inspired more by the beauty of lines and colors and the relationships between them than by the concrete subject of the picture. I adore that because it explains why I can find inspiration in a page of text before I have read a single word. A page held up by its photography is not designed. It is dressed. It is also why I stay in wireframe far longer than most people would think reasonable, finalizing layout with grey boxes and grey lines even when the real material is sitting right there. If it is beautiful on the merits of its structure, it will hold almost any image and almost any text. The Structure Tests. The first one is a classic for graphic designers: Squint until the type turns to grey and the images turn to shapes, and see whether the hierarchy still reads. The other takes a bit more work but, I think is better: Put a grey box where the hero image is and a line of Latin where the headline is. If the design dies, the image was doing the design’s job, and the next round of content will expose it. The Point of View This is the one most design work fails, and it fails in hiding, because nothing is obviously, visually wrong. When we constantly reference existing solutions, our work gravitates toward the mean. We solve for expectations rather than needs. We optimize for recognition rather than revelation. The result is competent and anonymous, and it passes every inspection above. Restraint, on the other hand, is the visible evidence that somebody was directing. It shows up as absence, which makes it hard to credit and easy to skip. The Point of View Test. Put your design beside three others in its category and swap the logos or identifying marks. If this doesn’t break or seriously undermine your work — if your work is that interchangeable — then it has no direction. It is conventional in the truest sense. The harder version of this test is a question you really must ask at various stages of your work: What did I deliberately not do? or What is this not doing? If you cannot answer, then nothing was decided. Such a thing will age at exactly the rate of its category. And because it followed the category’s lead, it will always be behind. What It Is Saying Imagery and type say something before anyone reads a word, and what they say is frequently not what the business does. A few years ago I ran an informal study on a client’s homepage to prove a hunch. They sell technology and expertise to wineries, and they wanted to connect the heritage and craft their customers care about to the stability their technology provides. So they leaned hard on old-style typefaces and historical imagery, to make prospects feel at home. It looked really nice, but I was worried that’s all it did. Traffic was being paid for, and not enough was converting. So, I ran a transient attention test. Participants had eight seconds with the homepage, scrolling but not clicking, and then the page was closed and they were asked what stood out and what the page was for. The page said “commerce technology” and “wine brands” in scannable, plain text. And yet, every participant recalled the imagery instead — an ancient Greco-Roman tapestry — and volunteered words like “history” and “archaeology.” Not one person mentioned wine. Not one mentioned technology. The page was well written. But for its viewers, it was about the wrong thing. The Imagery Test. Give someone outside the project eight seconds to view your design. Afterward, ask what the thing they just saw was — what does the company do? what was the page for? Do not accept a paraphrase of the headline. Ask what the pictures told them. The gap between their answer and the actual business is the size of the art direction problem. Durability Good design is evergreen. The reactions I trust are the ones that survive a week, and the ones I distrust tend to arrive fastest. Anything resting on a technique currently in fashion has a short window before a browser, a platform, or simply everyone else’s adoption closes it. Both of the tests here buy the same thing at different scales: distance. A week of it shows you what belongs to this year. An hour of it shows you what belongs to the last hour of your own looking. The Dated Test. Leave the composition open in a tab and come back to it after a week, even if it has already progressed through reviews, as most things will in that time. Then, name what on it is dated to this year, and ask of each whether it is carrying an idea or just carrying a date. A composition can survive one or two decisions that belong to its moment. It does not survive being made of them. The Interval Test. This one goes after a different fragility, one that lives in your read of the work rather than in the work itself. Clutter accumulates precisely because the eye that added it has stopped seeing it. I have always found that coming back to a finished but unshared design after even just a few hours away, sometimes minutes, has resulted in needed editorial moves. What you have been staring at is porous to every other thing held on your screen or in your recent memory, and your working brain is an unwitting cheat. Breaks expose that immediately. Take enough of them and the work stops absorbing its surroundings. Preference and Judgment Taste is that combination of preference, personality, and perceived novelty that lets an observer tell your work from someone else’s. It belongs in the work. It does not belong in the verdict. I have sat in too many reviews where a real critique was offered, understood, and then dissolved by “well, we like it.” That is nice. But who cares if you like it? Does it do what it is supposed to do? Or is it possible that the things you like about it get in the way? The way through is not to suppress the reaction but to keep going after it. Name what you are responding to, then say what it is doing for the work. If it is doing nothing for the work, you have found a preference. If it is doing something, you have found a judgment, and now you have to justify it, which is the only part of design that has ever been hard. To make it somewhat easier, do not defend it. Sell it. Don’t believe the lie that “good design just works” as if it will be self-evident in the eye of the beholder and embraced without question. Nothing could be further from the truth. Good design often requires advocacy. Every rubric wants to become an inspection. In art school you always knew a critique was going nowhere when someone would ummm and ahhh, approach the piece, back away from it, approach it again, and finally ask, “is this, ummm, is this balsa wood?” They just had to say something, and what a thing is made of was the best they could do. The digital equivalent is talking about the canvas, the type foundry, the plugins, or opening the inspector. None of those are relevant to assessing a design’s quality. Sections one through four can be inspected. Section five has to be seen — by you first, and yet, outside of yourself — which takes time and, more importantly, conviction. I do not think that section five will ever be as short as the others, or as portable. It takes longer to run than all four of them combined. For years I read that as a defect in my system. But lately I have started to suspect it is the only part of the rubric that will still be worth anything in a few years, because production is becoming generative and design is not. Which leaves me somewhere I have not settled. The first four sections are the ones a machine can already run. The fifth is the one it cannot, so the fifth is where the work is going. But the fifth is also the one nobody has ever managed to teach quickly. I do not yet know whether that is a problem to solve or a fact to accept. Better yet, maybe it’s a distant horizon to embrace, because it means we have somewhere left to go. P.S. I have left creative direction out of this entirely, which is a cheat. In my own notes it sits above art direction, closer to the conceptual end of the spectrum that runs down through graphic design to the mechanics of a build. That is a different piece, and I suspect a harder one.

2 days ago 1 votes
The least wrong colors, version 2

Four years ago, I wrote “How to pick the least wrong colors.” The gist is: picking a categorical color palette is an optimization problem. There’s no such thing as the right colors. But if you use the right cost function, and the right kind of hill climbing, you can at least get the least wrong ones. Since the original post I’ve been slowly picking away at improvements and new approaches. Now that we’re past the singularity, I’ve put a few coding robots on the job. It’s reassuring that many of my assumptions were good ones! The robots have been able to improve the code, bridging some of the gaps in my own knowledge. Today, I’m publishing an updated version of the algorithm as an npm package, along with a fancy GUI version. While there’s still more to do, I’m proud of how far I’ve been able to take it. What’s new New evaluators More controls The public API and a CLI What’s improved The annealing algorithm Configurable color space and distance metric The results One more thing Acknowledgements What’s new New evaluators Almost as soon as I published the first version, I realized that the cost function lends itself really well to modularity. Beyond my initial evaluation functions, I could design new ones, and provide a framework for anyone to plug in their own. As a recap, my original criteria for good categorical colors, mapped to evaluation functions: Similarity — a way of measuring the similarity of one palette to another, useful for providing art direction and getting brand alignment Energy — the colors should be different from each other so they aren’t liable to be confused from one another Range — the differences between the colors should be consistent so unintended groupings don’t appear Color vision deficiency — simulating the colors under different types of color blindness (red-green, blue-yellow, partial to full tritanopia) Here’s the new evaluators: JND — strongly reject palettes that have two or more colors that are too similar Avoid — the mirror image of the similarity evaluation, push colors away from a user-defined set Contrast — compares colors, keeping them above the WCAG AA color contrast floor. Can be used with a background color to maintain contrast on a chart’s background Saliency — uses color naming study data to prefer colors that are easy to name Name difference — the mirror image of saliency, avoiding colors that share names Each of these evaluators can be weighted, indicating the kinds of tradeoffs and priorities you’d like for your color palette. Additionally, the whole evaluator system is pluggable: you can define your own evaluators and have them drive the optimizer! More controls Colors can now be fixed in place, or pinned to a particular order, making it easier to load in existing palettes and optimize all or just some of the colors. Individual channels of each color can be locked, too, meaning you can keep the saturation or hue of a color fixed while optimizing its lightness. This works in any color space. The public API and a CLI The whole package is now a proper library, with a public API. This means: 1. the whole thing is now distributable through npm, with proper versioning, 2. there’s a CLI, making it much more ergonomic for both humans and agents. The API allows for full configuration of the algorithm, as well as loading in colors to optimize. Output can be in raw color values, CSS properties, or DTCG JSON. There’s also a new reportJndIssues endpoint that allows you to evaluate palettes without optimizing them, which is useful to compare a generated palette to commonly-used ones (like Observable, d3, IBM Carbon, and more). What’s improved The annealing algorithm When I wrote the initial algorithm in 2022, I had just learned about simulated annealing. I’ll be honest: I don’t know much more today than I did then. But with AI-assisted research, I was able to solve some questions I had about the initial implementation. Now, the algorithm picks the correct starting temperature based on some random initial samples. Mutation also happens in a scaled manner, so colors change less towards the end of the optimization schedule. Iterations can be capped to prevent very long runs, and the whole thing is much, much more performant. Configurable color space and distance metric The first version of the algorithm worked in RGB space. Now, it defaults to okhsl, but even this is configurable. Individual channels can be constrained to dial in the palette’s boundaries. Also, you can choose which color distance metric you’d like to use (but the library uses CIEDE2000 by default). This flexibility is powered largely by a move from chroma.js to culori. I’ve learned a ton about color spaces since 2022, so being able to mix and match color spaces with distance metrics has been extremely useful. The results The category-colors library reliably produces better results than other palette-generating tools and industry-standard color palettes. Compared to other palette-generating tools, category-colors has more control. Palettailor, for example, optimizes for pure color difference, without accounting for color vision deficiency. QualPal brings some of the optimization parameters, but doesn’t allow for steering towards or away from arbitrary colors. Scores at 8 colors ΔEMinimum ΔEworst of CVD Name differenceMinimum Uniformitylower is better category-colors 22.6 ±1.6 13.7 ±2.0 0.35 ±0.14 best in column 0.30 ±0.02 best in column QualPal 1.1.0 24.7 21.8 best in column 0.10 0.44 Palettailor 26.6 ±2.4 best in column 4.5 ±1.7 0.34 ±0.16 0.34 ±0.04 Colorgorical 15.8 ±3.3 4.1 ±1.6 0.09 ±0.06 0.42 ±0.03 All numbers are at 8 colors. Rows with ± are mean ± standard deviation over 10 palettes; rows without are deterministic and produce one palette. category-colors and Palettailor are 10 independent runs on the same seeds; Colorgorical’s row is 10 palettes from its authors’ own sampling script at equal criterion weights. QualPal was run with CVD on, matched bounds, and takes no seed. Name difference is Heer & Stone’s 1 − cosine; Colorgorical’s own interface reports a Hellinger distance instead. Shaded cells are the best value in their column. Compared to industry-standard palettes, category-colors can produce more optimal palettes, especially at high cardinality. Scores at 8 colors ΔEMinimum ΔEworst of CVD Name differenceMinimum Uniformitylower is better category-colors 22.6 ±1.6 best in column 13.7 ±2.0 best in column 0.35 ±0.14 0.30 ±0.02 best in column Okabe–Ito 21.3 8.8 0.06 0.34 Observable 10 18.4 0.6 0.40 0.34 Tableau 10 18.1 3.2 0.24 0.32 d3 category10 16.2 1.6 0.84 best in column 0.40 ColorBrewer Set3 13.7 1.9 0.16 0.32 IBM Carbon 12.8 5.0 0.11 0.34 Same run: 8 colors, 10 trials. Reference palettes are deterministic, so they're single values. Shaded cells are the best value in their column. One more thing I’ve built a UI that consumes the package and makes it easy to generate and optimize palettes. This has been the biggest request since I published the initial essay, so it’s the thing I’m excited to share. It’s ridiculously overengineered, but hey, what else are personal projects for? Acknowledgements Many measurements come from published research: Gaurav Sharma, Wencheng Wu and Edul Dalal for CIEDE2000; Gustavo Machado, Manuel Oliveira and Leandro Fernandes for the color vision deficiency simulation; Maureen Stone, Danielle Albers Szafir and Vidya Setlur for the size-dependent just-noticeable-difference result; Jeffrey Heer and Maureen Stone, whose color naming models and the c3 data from the Stanford Visualization Group power both the saliency and name-difference evaluators. Existing palettes: Masataka Okabe and Kei Ito’s Color Universal Design set; Matthew Petroff’s sequences; and Mark Harrower and Cynthia Brewer’s ColorBrewer. Other generators laid a lot of the groundwork: Kecheng Lu and colleagues (Palettailor), Connor Gramazio, David Laidlaw and Karen Schloss (Colorgorical), Johan Larsson (QualPal), and Chin Tseng, Arran Zeyu Wang, Ghulam Jilani Quadri and Danielle Albers Szafir (CatPAW). Andrew McNutt, Maureen Stone and Jeffrey Heer’s color-buddy has also been indispensable. Finally, Dan Burzo’s culori made it easy to make this library colorspace-agnostic.

a week ago 1 votes
Mountains of work

This is part of a new experiment I started in an effort to document the process of making Niche design.

a week ago 1 votes
When the canvas starts acting, who’s really in control?

Weekly curated resources for designers — thinkers and makers.

a week ago 1 votes
Gestalt Principles for Visual UI Design

Users parse a layout before they read its labels. Whitespace, borders, alignment, color, and motion determine what belongs together. When these cues fight the content, users attach the label, price, warning, status, or action to the wrong object. Proximity, similarity, enclosure, and the other Gestalt cues guide the eye, snapping visual chaos into clarity.

2 weeks ago 2 votes
📚 BoredReading

You seem to be enjoying this.

Join free to unlock everything.

Create free account

Already have an account? Sign in