Adam Argyle

Adam Argyle

By
Download on the App Store

Adam Argyle episodes

  • The Making of gradient.style

    Found these pics in a drawer and thought it'd be fun to share the journey of creating a CSS spec compliant gradient generator in 2023.

    gradient.style
    A CSS5 HDR color enabling gradient tool

    Can even visualize effects using shorthands and DX tricks.

    Gradients and Their Lines
    #

    Here's a sketch of a linear gradient line. The top left image shows a triple color stop gradient, where the 2nd gradient stop uses the double position syntax to spread over more of the line than the other colors.

    I'll be calling the colors in a gradient a color stop 🤓

    There's also a depiction of transition hints and an inline color adjuster concept. I hadn't quite figured out how the color picker would work yet.

    This next concept shows the sketched linear gradient line concept being tested with angles and various syntax.

    Putting the concept to the test essentially! Testing how transition hints and double stop positions might work in practice.

    Coding was the only way to fully test the solution, but still fun to see how far I could get on paper.

    Here's some conic and radial gradient line concepts.

    Stops & Hints
    #

    Here are some concepts and depictions of the colors (or stops as I'll start referring to them as) on a gradient line.

    Some more concepts of a color picker overlay too.

    Stops can have 2 positions on the line, a start and end. Most people don't know this! I chose to visualize it by splitting the cirle into halves, each half representing a stop position.

    Any 2 stops also have a transition hint between them, which is a value that indicates how the stop should transition to the next stop.

    Here's more sketches of colors and hints. I have no idea what that math calculation means anymore.

    These are more sketches testing out various gradients and their estimated visualizations.

    GUI Explorations
    #

    The UI needs to show a layers list view (which I still plan to enable more layers!), a gradient canvas preview, a color stops list view and the CSS output for copying.

    I think a ✓ meant I liked the overall thumbnail idea.

    How it turned out
    #

    Here's a quick video of a basic, responsive, 2 stop gradient line with a custom transition hint; shown on each linear, radial and conic overlay.

    Sketches lined up pretty good!

    Well, I'm not done with it yet! I do want to add layers and background-image sizing. I think it'd make it the perfect tool for finely crafted gradients. Stay tuned!

    0 min
  • Devtools Fm 151

    Ep #151
    DevTools FM

    Join Andrew, Justin and I for a chat about some of the developer tools I've worked on.

    Listen · Watch · Subscribe

    0 min
  • CascadiaJS 2025

    You might be asking yourself, "a CSS talk at a JS conf?". Emphasis on CSS, at JS. WTF?

    While that's a decently common thought, it's also decently incriminating!

    People be installing React packages to do a single line CSS feature.Scott Tolinski - X

    That might be you.

    Watch me at CascadiaJS to not be that dork.

    0 min
  • VisBug was built into a browser!

    The browser is Orion, built by Kagi.

    On first try it felt like it was taking inspiration from Arc. It's even got the hotkey for copying URLs like Arc (which I use all the time) and the sidebar tabs option. But, unlike Arc, which ships with Zaps and Chrome Extension support, Orion ships with a certain Chrome Extension pre-installed: VisBug!

    I always wanted a browser to ship with tools for designers! This is rad!

    Here's a preview of what it's like in Orion to use the "Design Tools":

    Thanks for the tip Kilian!

    0 min
  • LLM chat prototype

    I wanted to take a quick stab at my own LLM chat interface, see how far I could get and how many little details I could squeeze in.

    View source or just try it

    How about that cool new Codepen 2.0 editor?

    I kept the demo minimal, no libraries, no frameworks, no build tools; just brick by brick, line by line, articulated details I wanted. Start with some core primitives, then start solving UX problems until the experience is good. There's still a lot more to do! The basic light/dark theme turned out pretty cool tho.

    I used a real streaming LLM API, then used View Transitions and Scroll Driven Animations where applicable. I also wanted to learn ReadableStreams and have my own expression of how the UI could work.

    The morph
    #

    On submit, the textarea is given a view transition name with a unique incrementing index, then view transition is called and the same name is given to the new message element that's appended into the chat container.

    This creates the morph effect since the old state is the textarea and the new state is the message bubble.

    The shine
    #

    The border shine effect animation draws the eye from the placeholder text to the submit button, while creating a nice sleek design aesthetic.

    I used my own technique for it that was wayyy smaller than similar techniques done in popular libraries. The border radius makes the effect tricky to implement, but I've got a handy dandy notebook entry that's got all this laid out for me.

    form {
    border: 2px solid #0000;
    border-radius: 20px;
    background:
    linear-gradient(var(--bg) 0 0) padding-box,
    linear-gradient(130deg,
    var(--bg) 45%,
    color-mix(in srgb, Canvas, CanvasText 15%),
    var(--bg) 55%
    ) border-box;
    background-size: 300%;
    animation: shine 6s infinite ease-in-out;
    }

    The trick is specifying which box on the node to paint into, like padding-box or border-box. While drawing a solid color over the middle / padding-box. Then as the background is animated, the 130deg shine gradient is shown only over the border-box.

    There's also a neat little trick in there with color stop positions.

    The cursor
    #

    I thought the blinking cursor was cool. It's a throwback to the 80s and also just feels right since the machine is typing back to you.

    It's probably been done other places, but of all the little loader thingies I made, this one was the simplest and most effective. High five to iteration.

    Plenty more
    #

    There's over 100 little decisions in the code that total up to this experience, and I could make 100 more! But… this was only meant to be a short sprint and not something I'm maintaining or looking to make 1000% fantastic.

    Thought it was pretty cool for 2 short bursts of work with kids crawling all over me.

    0 min
  • Working Draft Show Revision 672

    Ep #672
    The Working Draft

    With Schepp, Vanessa and me 🙂 Join us for a tour of of what’s new and what’s next in CSS.

    Listen · Watch · Subscribe

    0 min
  • Nintendo Switch Homescreen recreated with CSS and a li'l bit of JS

    Someone across the room could hear the sounds from a device and know that it's coming from a Nintendo Switch.

    Just the audio alone is iconic; but that homescreen layout and scroll experience, :cheffs-kiss:

    Let's remake it with CSS!

    1. light/dark theme + forced colors support
    2. automatic scroll UX upgrades based on user's browser capabilities
    3. keyboard, mouse, touch, allllll good
    4. See the Pen <a href="https://codepen.io/argyleink/embed/preview/ByNyvox"> by Adam Argyle (<a href="https://codepen.io/argyleink">@argyleink</a>)
      on <a href="https://codepen.io">CodePen</a>.


      This is 1 of 3 scroll experiences I taught at CSS Day 2025. What a lovely conference!

      HTML
      #
      Structure
      #

      I've found the following "3 element structure" for a scroller to help prevent headaches and gotchas across browsers. The structure supports you both in styling, interactions, and container queries.

      1. .scroll--root: highest level container for the scroller, may have borders or spacing styles
      2. .scroll--viewport: the element with overflow, containment and scrollbar styles
      3. .scroll--content: wrapper to aid in cross browser padding oddities or other impacts that an overflow container may apply to children
      4. <section class="scroll--root">
        <div id="scroller" class="scroll--viewport">
        <ul class="scroll--content">
        /* your content */
        ul>
        div>
        section>
        CSS
        #
        Essentials
        #

        These are the things you should always put on your scroller:

        1. use the overflow shorthand and set both axes to prevent surprises
        2. overscroll-behavior should always match the scrolling axis
        3. padding and scroll-padding should always match
        4. smooth scroll-behavior if the use is ok with motion
        5. .scroll--viewport {
          overflow: auto hidden;
          overscroll-behavior-x: contain;
          padding-inline: var(--size-8);
          scroll-padding-inline: var(--size-8);
          @media (prefers-reduced-motion: no-preference) {
          scroll-behavior: smooth;
          }
          }

          See the @layer lessons.essentials in the demo for more comments.

          Containerization
          #

          Since a scroller of often extrinsically sized, it won't suffer from the side effect that container queries can cause (where they collapse). This makes scroller and container queries a natural pair.

          1. container shorthand lets me set the name and the type(s) in one spot
          2. container queries for children to use to ask this container questions about the width and height while also providing a context for container query units like cqw
          3. scroll-state for children to ask about which game is snapped
          4. Plus, scroll-state queries on the scroller and on the snap items are meaningful and worth setting up while we're here.

            .scroll--root {
            container: --homescreen / size;
            }
            .scroll--viewport {
            container: --homescreen-scroller / size scroll-state;
            }
            .scroll--content > li {
            /* children will use snapped scroll-state() */
            container-type: scroll-state;
            }

            See the @layer lessons.containerize in the demo for more comments.

            Snapping
            #

            Meaningfully, the Switch scroller snaps the scroll items to the left, in line with the layout grid. We can do this in CSS with just a couple styles.

            Since the scroll-padding already matches the inline padding, the snapping here will match the alignment perfectly.

            .scroll--viewport {
            scroll-snap-type: x mandatory;
            }
            .scroll--content > li {
            scroll-snap-align: start;
            }

            See the @layer lessons.scroll-snap in the demo for more comments.

            Overscroll effect
            #

            It's common for scrollers to offer an effect if you've reached the beginning or end; an overscroll effect. The most common is an elastic effect, like a rubber band is tight and preventing you from going further and when you let go it snaps back. We can do this with CSS scroll snapping.

            Here's a moment where wrapping all your content in a .scroll--content container is valuable. The overscroll amount is the size of an invisible item at the beginning and end of the scroller. The browser will let you scroll to them, but since they won't have scroll-snap-align set and the scroller itself has x mandatory set, the scroller will never rest on these invisible overscroll spacers.

            1. The inline-start spacer will be 25cqi, offering a 25% overscroll vibe
            2. The inline-end spacer will be larger at 90cqi, ensuring the last game can be snapped to the start
            3. .scroll--content {
              display: flex;
              &::before,
              &::after {
              content: "";
              display: block;
              inline-size: 25cqi;
              flex-shrink: 0;
              }
              &::after {
              inline-size: 90cqi;
              }
              }

              See the @layer lessons.overscroll-effect in the demo for more comments.

              Scroll Animation
              #

              This was a personal addition, not part of the original Switch homescreen design.

              It's subtle; fade out the game you just scrolled past. Leave a hint or peek of it, but dim it out. This reduced cognitive load and helps the eye maintain the alignment of the snapped game to the grid.

              Here's the simple keyframes.

              @keyframes leaving-scroller {
              to {
              opacity: .2;
              }
              }

              Here we use the keyframes on a game if the browser supports the feature, and I clamp the keyframes with animation-range to only run as the element exits the scrollport.

              .game {
              @media (prefers-reduced-motion: no-preference) {
              @supports (animation-timeline: view(x)) {
              animation: leaving-scroller linear both;
              animation-timeline: view(x);
              animation-range: exit 50% exit 100%;
              }
              }
              }

              See the @layer lessons.scroll-animation in the demo for more comments.

              Scroll State
              #

              Here we'll use the scroll-state containerization work from an earlier lesson. Two things occur when a game snaps into place:

              1. the pulsing blue focus outline highlights the game and animates forever
              2. the title of the game slides up
              3. By default I chose to always show the game titles, only applying the animation effect if the browser understands scroll-state queries.

                Here's the keyframes for when a game is snapped:

                @keyframes game-focus {
                from {
                outline-color: hsl(200 100% 50%);
                }
                to {
                outline-color: hsl(200 100% 80%);
                }
                }

                The game image uses the keyframes when it's parent .game parent container is snapped on x, and the image title slides down when it's parent game is not snapped.

                .game {
                img {
                @container scroll-state(snapped: x) {
                outline: 5px solid blue;
                outline-offset: 5px;
                animation: game-focus .5s var(--ease-3) alternate infinite;
                }
                }
                figcaption {
                @media (prefers-reduced-motion: no-preference) {
                z-index: -1;
                transition:
                translate .5s var(--ease-spring-2) .2s,
                opacity .5s var(--ease-3) .2s;
                @container not scroll-state(snapped: x) {
                translate: 0 50px;
                opacity: 0;
                }
                }
                }
                }

                See the @layer lessons.scroll-state in the demo for more comments.

                Colorization
                #

                The goal of this section is to match the Switch Homescreen's scroller style with ::-webkit-* scroll features, while also providing a meaningful fallback with scrollbar-color. In addition to integrated colors and sizing, I also add a few interactive moments like when a keyboard is focused inside the scroller, or a mouse is hovering the thumb.

                1. Integrate scrollbar colors into the adaptive light/dark/forced colors themes
                2. Add hover and focus "hints and feedback"
                3. Look at how cool the forced colors styles are. Transparent borders are your friend!

                  Compare that with the light and dark styles:

                  It takes a lot of selectors and styles to do this robust colorization, here's the chunk that does the work.

                  .scroll--viewport {
                  &:is(:focus-visible, :focus-within) {
                  outline-offset: -2px;
                  outline: none;
                  }
                  &::-webkit-scrollbar {
                  height: 10px;
                  }
                  &::-webkit-scrollbar-track {
                  background: #0000;
                  background-clip: padding-box;
                  }
                  &::-webkit-scrollbar-thumb {
                  background: color-mix(in srgb,
                  light-dark(
                  var(--surface-4),
                  var(--surface-2)
                  ),
                  #0000 50%
                  );
                  border-radius: 1e3px;
                  border: 3px solid var(--surface-1);
                  }
                  &:is(:focus-visible, :focus-within)::-webkit-scrollbar-thumb {
                  background: var(--link);
                  }
                  @media (hover) {
                  &::-webkit-scrollbar-thumb:hover {
                  background: light-dark(var(--surface-4), var(--surface-2));
                  }
                  &::-webkit-scrollbar {
                  opacity: .5;
                  }
                  &::-webkit-scrollbar:hover {
                  opacity: 1;
                  }
                  }
                  @supports (-moz-appearance:none) {
                  scrollbar-width: thin;
                  scrollbar-color: var(--surface-4) #0000;
                  transition: scrollbar-color .3s ease;
                  &:is(:focus-visible, :focus-within) {
                  scrollbar-color: var(--link) #0000;
                  }
                  @media (hover) {
                  scrollbar-color:
                  color-mix(in srgb, var(--surface-4), #0000 50%)
                  #0000;
                  &:hover {
                  scrollbar-color: var(--surface-4) #0000;
                  }
                  }
                  }
                  }

                  See the @layer lessons.colorize in the demo for more comments.

                  Content Introduction
                  #

                  Finally, on load, introduce the content from the right, providing the hint that there's directionality in this games list, hinting at scroll.

                  The star of this effect is @starting-style.

                  .scroll--content .game {
                  @media (prefers-reduced-motion: no-preference) {
                  transition: transform 1s var(--ease-spring-2);
                  transition-delay: calc(var(--i) * .05s);
                  @starting-style {
                  transform: translateX(100vw) translateZ(calc(var(--i) * -1px));
                  }
                  }
                  }

                  See the @layer lessons.loading-animation in the demo for more comments.

                  JavaScript
                  #

                  Iconically, the Switch homescreen has both haptic and audibal feedback for your interactions, specifically to the scroller.

                  1. Snap "click" when the scroller rests on a game
                  2. Pop sound as each game passes by the snap area
                  3. Haptic feedback as a game passes over the snap area
                  4. Ability to tap to snap
                  5. Preload
                    #

                    First let's preload the sounds so they can play instantly at first.

                    const sounds = {
                    snap: new Audio('https://assets.codepen.io/2585/snap.mp3'),
                    pop: new Audio('https://assets.codepen.io/2585/pop.m4a'),
                    }
                    Tap to snap
                    #

                    If a user clicks a game, scroll it to the snap position.

                    scroller.onclick = event => {
                    let target = event.target.closest('.game')
                    if (target) {
                    scroller.scrollTo({
                    left: target.offsetLeft,
                    behavior: 'smooth'
                    })
                    }
                    }
                    Vibrate and pop
                    #

                    The lovely scrollSnapChanging event is perfectly fit for this task.

                    scroller.onscrollsnapchanging = event => {
                    navigator?.vibrate(50)
                    sounds.pop.play()
                    }
                    Snap
                    #

                    The complimentary scroll event scrollSnapChange is perfectly fit for reporting scroll has ended and it's rested on a new snap target.

                    scroller.onscrollsnapchange = event => {
                    sounds.snap.play()
                    }
                    Outro
                    #

                    You made it, which means you definitely picked up some new tricks. Go forth and make great scroll experiences!

                    I'll share a link to the talk if/when it comes out, which provides a lot of commentary on these decisions and styles. But also, in case it wasn't clear in the blog post, check out the demo for all the comments, they should help connect all the dots.

                    0 min
                  6. CSS Day 2025 Speaker Pics

                    It's an honor to speak at CSS Day, the biggest CSS conference every year.

                    Hope y'all enjoyed nerding out about making beautiful scroll experiences.

                    0 min

                  About Adam Argyle

                  From the publisher's feed

                  RSS Feed for Adam Argyle: Web design & development tips & tricks: CSS, JS, HTML, Design, & UX.