<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Ryan Geyer’s Blog</title><description>Writing about the joys of frontend development and more</description><link>https://geyer.dev/</link><item><title>I finally found a use for ChatGPT: Workout routines</title><link>https://geyer.dev/blog/chatgpt-fitness</link><guid isPermaLink="true">https://geyer.dev/blog/chatgpt-fitness</guid><description>A blog post by Ryan Geyer</description><pubDate>Mon, 04 Dec 2023 22:56:10 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;ll just come out and say it: &lt;strong&gt;I don&apos;t like ChatGPT very much&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;When it comes to programming, I tend to find that there is rarely any overlap between the tasks which it is actually capable of doing competently, and the things which I actually would like an AI to help me with. However, I have finally found a non-programming use case which has genuinely been life-changing for me: generating workout routines.&lt;/p&gt;
&lt;p&gt;I am very out of shape. I have been for quite a long time, but lately it has started to feel like a real problem which I can&apos;t keep ignoring; I&apos;m only 28 but my joints are starting to ache and physical exertion often feels much harder than it should. I have &lt;a href=&quot;https://powerblock.com/product/pro-100-exp-adjustable-dumbbells/&quot;&gt;a nice set of weights&lt;/a&gt;, but every time I feel motivated to work out, I have always ended up running into a wall: &lt;strong&gt;I have no idea what to do.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If we&apos;re being honest with ourselves, the proper answer to this problem should probably be to join classes or find a personal trainer, but this presents some additional problems for me:
these options cost a non-trivial amount of money, require traveling to another location, can&apos;t be flexibly slotted into my day where I have time, and of course I&apos;ve already spent a good chunk of change on my own equipment at home so I would like to justify that purchase if I can.&lt;/p&gt;
&lt;p&gt;This is where ChatGPT comes in! I have crafted &lt;a href=&quot;https://gist.github.com/Gyanreyer/276d83522b6a05f29b3332f32dac3186&quot;&gt;a prompt which allows me to use ChatGPT as a virtual fitness trainer&lt;/a&gt;. Now, I can just pop into that chat, type &quot;Give me an upper-body workout&quot;, and it will generate a routine for me on demand!&lt;/p&gt;
&lt;p&gt;Now, it&apos;s not perfect: despite really hammering on the fact that I don&apos;t want it to include exercises which require equipment that I don&apos;t have access to, it still suggests exercises requiring an exercise bench with annoying frequency.
Of course, I am not a great prompt engineer, so I&apos;m sure my prompt could be tightened up a lot as well. Despite these imperfections, it always gives me a starting point, and it has generally done a pretty good job at suggesting a fairly complete workout routine. If I can&apos;t do something it suggests, I just ask it to provide an alternative and it usually does better the second time around.&lt;/p&gt;
&lt;p&gt;This has made a huge difference in allowing me to overcome my analysis paralysis and just start working out; my past attempts at getting fit usually sputter out within a couple weeks, but I am now well into my 3rd week of regularly exercising and I am feeling great about it.&lt;/p&gt;
&lt;p&gt;If you can relate to my struggles of wanting to work out more but not knowing how to go about it, I can&apos;t recommend this approach highly enough. Give it a try!&lt;/p&gt;
</content:encoded><language>en-us</language><author>Ryan Geyer</author></item><item><title>My Garden In Review 2025</title><link>https://geyer.dev/blog/garden-in-review-2025</link><guid isPermaLink="true">https://geyer.dev/blog/garden-in-review-2025</guid><description>A blog post by Ryan Geyer</description><pubDate>Tue, 30 Dec 2025 22:25:33 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;style&amp;gt;
figure.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 1rem;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;figcaption {
  grid-column: 1/-1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;[aria-describedby=&quot;already-had-footnote&quot;]::after {
content: &quot;*&quot;;
}&lt;/p&gt;
&lt;p&gt;#already-had-footnote::before {
content: &quot;*&quot;;
}
&amp;lt;/style&amp;gt;&lt;/p&gt;
&lt;p&gt;2025 is over, we&apos;re deep in the winter, and I&apos;m missing my garden. What better time to review what happened this year and reminesce about back when things weren&apos;t gray, cold, and dead?&lt;/p&gt;
&lt;h2&gt;Native Plants&lt;/h2&gt;
&lt;p&gt;I&apos;m very passionate about native plants and insects. In my opinion, landscaping with native plants is one of
the easiest ways for an individual to make a noticeable positive impact on the environment. Getting to see
beneficial insects and birds using the things you planted for them is so incredibly satisfying,
and it makes me feel so much more connected to nature than I ever have before. As an extra bonus, native plants are
adapted to living in your climate on their own, so they require much less maintenance once they&apos;re established!&lt;/p&gt;
&lt;p&gt;&amp;lt;figure class=&quot;two-col&quot;&amp;gt;
&amp;lt;img src=&quot;/assets/blog/garden-in-review-2025/2025-08-11--full-front-garden-photo.jpg&quot; alt=&quot;Two landscaping beds outlined with rocks in front of a house, separated by a strip of grass between them. Each bed has a birdbath and is filled with a variety of plants. The bed closer to the camera has a variety of plants in bloom, including Lanceleaf Coreopsis, Purple Coneflowers, Black-Eyed Susans, Anise Hyssop, and Wild Petunias.&quot;&amp;gt;
&amp;lt;img src=&quot;/assets/blog/garden-in-review-2025/2025-08-11--front-garden-main-section.jpg&quot; alt=&quot;A closer shot of the larger landscaping bed closer to the house. Purple Coneflowers and Black-Eyed Susans are absolutely thriving in this bed, along with a little bit of Butterfly Weed, one of my personal favorites.&quot;&amp;gt;
&amp;lt;figcaption&amp;gt;
Our front yard garden in August. Note that I am painfully aware that there are some non-native plants in here, but the ratio is probably 90% native and rising.
&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;This was the second full growing season since I ripped up a bunch of our yard in the fall of 2023 and started the garden in earnest.&lt;/p&gt;
&lt;p&gt;There were some disappointments; a couple things I planted in the previous year didn&apos;t work out or
struggled a lot more than I expected, including some plants that I was really excited about. But failure is part of the
journey, and I&apos;m learning a lot about what plants will work where depending on their needs.&lt;/p&gt;
&lt;p&gt;With that said, I think things are still going great and this was overall a very successful year.&lt;/p&gt;
&lt;h3&gt;What I planted this year&lt;/h3&gt;
&lt;p&gt;I was a little out of control this year.&lt;/p&gt;
&lt;p&gt;My parents got me a gift certificate for the &lt;a href=&quot;https://www.wildtypeplants.com/&quot;&gt;Wildtype plant nursery&lt;/a&gt; near where I grew up so I took a trip out there this spring and loaded up.
It was an awesome experience! If you&apos;re located in Mid-Michigan and looking for native plants, you can&apos;t do better than Wildtype.&lt;/p&gt;
&lt;p&gt;Most of the plants listed here came from Wildtype, but I couldn&apos;t help but keep adding more to my collection from other nuseries here and there as the year went on.
So here is the full list, minus a couple things I know I bought but apparently didn&apos;t keep records of and
I have no idea what they were now. Guess that&apos;ll be a fun surprise next year!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Ceanothus_americanus&quot;&gt;New Jersey Tea&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Fragaria_virginiana&quot;&gt;Wild Strawberry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Viola_sororia&quot;&gt;Common Violet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Schizachyrium_scoparium&quot;&gt;Little Bluestem&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Packera_aurea&quot;&gt;Golden Ragwort&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Verbena_hastata&quot;&gt;Blue Vervain&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Solidago_speciosa&quot;&gt;Showy Goldenrod&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Eutrochium&quot;&gt;Joe Pye Weed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Pycnanthemum&quot;&gt;Mountain Mint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Chelone_glabra&quot;&gt;Turtlehead&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Liatris_scariosa&quot;&gt;Northern Blazing Star&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Liatris_spicata&quot;&gt;Dense Blazing Star&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Liatris_cylindracea&quot;&gt;Dwarf Blazing Star&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Camassia_scilloides&quot;&gt;Wild Hyacinth&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Agastache_foeniculum&quot;&gt;Anise Hyssop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;span aria-describedby=&quot;already-had-footnote&quot;&amp;gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Asclepias_incarnata&quot;&gt;Swamp Milkweed&lt;/a&gt;&amp;lt;/span&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;span aria-describedby=&quot;already-had-footnote&quot;&amp;gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Asclepias_tuberosa&quot;&gt;Butterfly Weed&lt;/a&gt;&amp;lt;/span&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;span aria-describedby=&quot;already-had-footnote&quot;&amp;gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Aquilegia_canadensis&quot;&gt;Wild Columbine&lt;/a&gt;&amp;lt;/span&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;span id=&quot;already-had-footnote&quot;&amp;gt;already had some, but added more&amp;lt;/span&amp;gt;&lt;/p&gt;
&lt;p&gt;We ripped up two invasive Burning Bushes to replace with New Jersey Tea, I&apos;m excited about that upgrade.&lt;/p&gt;
&lt;p&gt;You may also notice that I went on quite a Blazing Star kick, getting 3 different varieties. A house in our neighborhood has an incredible yard full of native plants and every fall I see their Blazing Stars absolutely dripping with Monarch Butterflies and it fills me with envy, so I&apos;m trying to copy them.&lt;/p&gt;
&lt;p&gt;I planted a majority of these plants in the spring, and for the most part they did great!
Some plants can be a little boring in their first year, especially if you planted them in the spring instead of the previous fall, but at least most of these stayed fairly happy and healthy.
I&apos;m excited to see what they do next year.&lt;/p&gt;
&lt;h2&gt;Bugs&lt;/h2&gt;
&lt;p&gt;The best part of native plant gardening is all the cool bugs that they attract! This was a great year for that. Here&apos;s some of my favorite photos I got, all taken on my phone for better or worse. I tried my best to identify
them correctly but may have gotten some wrong because I&apos;m not even close to an expert!&lt;/p&gt;
&lt;p&gt;&amp;lt;figure class=&quot;two-col&quot;&amp;gt;
&amp;lt;img src=&quot;/assets/blog/garden-in-review-2025/2025-08-23--monarch-cat-1.jpg&quot; alt=&quot;A monarch butterfly caterpillar climbing around on swamp milkweed. Monarch caterpillars have black, white, and yellow stripes and have pairs of black tentacles on their head and rear. I say &apos;tentacles&apos; instead of &apos;antennae&apos; because apparently there is a meaningful difference; an antenna is considered to be stiff and rigid while a tentacle is more flexible and soft.&quot;&amp;gt;
&amp;lt;img src=&quot;/assets/blog/garden-in-review-2025/2025-08-25--monarch-cat-2.jpg&quot; alt=&quot;A smaller monarch butterfly caterpillar on the underside of a swamp milkweed leaf, nibbling on the end of it.&quot;&amp;gt;
&amp;lt;figcaption&amp;gt;Monarch Caterpillars&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;figure&amp;gt;
&amp;lt;img src=&quot;/assets/blog/garden-in-review-2025/2025-07-05--brown-belted-bumblebee.jpg&quot; alt=&quot;A bumblebee on a purple coneflower. The bee has a fuzzy yellow mane around its shoulders and back, and a brown-ish patch on its back which is why I think it&apos;s a Brown-Belted Bumblebee. The bee has a big grayish-yellow pollen sac on its hind leg. Some bee species&apos; females have &apos;pollen baskets&apos; on their hind legs which pollen accumulates in as they go from flower to flower. The photo is an extreme close-up which reveals a lot of detail; you can see little yellow specs of pollen on the flower and on the bee&apos;s face.&quot;&amp;gt;
&amp;lt;figcaption&amp;gt;Brown-Belted Bumblebee&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;figure class=&quot;two-col&quot;&amp;gt;
&amp;lt;img src=&quot;/assets/blog/garden-in-review-2025/2025-07-15--black-swallowtail-cat-1.jpg&quot; alt=&quot;A black swallowtail caterpillar climbing up the stem of a carrot. Black swallowtail caterpillars are green with stipes running along their back that are composed of a series of black and yellow dots.&quot;&amp;gt;
&amp;lt;img src=&quot;/assets/blog/garden-in-review-2025/2025-07-20--black-swallowtail-cat-2.jpg&quot; alt=&quot;A close-up shot of a black swallowtail caterpillar on the stem a carrot, about to eat some leaves on the end.&quot;&amp;gt;
&amp;lt;figcaption&amp;gt;Black Swallowtail Caterpillars&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;figure&amp;gt;
&amp;lt;img src=&quot;/assets/blog/garden-in-review-2025/2025-06-09--eight-spotted-forester-moth.jpg&quot; alt=&quot;An Eight-Spotted Forester moth. This is a very cool looking moth. It has a black body with orange tufts on its legs and yellow spots on what I guess I would call its shoulders? Its wings are all black with 2 white spots on each. It&apos;s not visible here but they have an additional set of inner wings which also have 2 spots each, bringing us to the eight spots in the name!&quot;&amp;gt;
&amp;lt;figcaption&amp;gt;Eight-Spotted Forester Moth&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;figure class=&quot;two-col&quot;&amp;gt;
&amp;lt;img src=&quot;/assets/blog/garden-in-review-2025/2025-07-29--monarch-1.jpg&quot; alt=&quot;A Monarch Butterfly eating nectar from Swamp Milkweed flowers. Monarch Butterflies have black bodies with white spots, and their wings are orange with black lines running through them and more black and white spots along the edges. This is not a great shot from a distance because I have found that Monarchs are annoyingly skittish and don&apos;t like me taking photos of them.&quot;&amp;gt;
&amp;lt;img src=&quot;/assets/blog/garden-in-review-2025/2025-07-29--yellow-swallowtail.jpg&quot; alt=&quot;A Yellow Swallowtail Butterfly eating nectar from Swamp Milkweed flowers. Yellow Swallowtails have yellow wings with black lines running through them. This Swallowtail appears to be a female, so it has a line of prominent blue spots along the edges of its lower wings, followed by a line of yellow spots. I believe males&apos; blue spots are significantly less prominent.&quot;&amp;gt;
&amp;lt;figcaption&amp;gt;Monarch and Yellow Swallowtail Butterflies on Swamp Milkweed&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;figure&amp;gt;
&amp;lt;img src=&quot;/assets/blog/garden-in-review-2025/2025-07-25--texas-leafcutter-bee.jpg&quot; alt=&quot;A Texas Leafcutter Bee on a Swamp Milkweed flower. These bees are very cool because they have unique black and white colors instead of the typical black and yellow. As you might be able to guess from the name, leafcutter bees cut little bits off of plant leaves to build their nests with.&quot;&amp;gt;
&amp;lt;figcaption&amp;gt;Texas Leafcutter Bee&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;figure class=&quot;two-col&quot;&amp;gt;
&amp;lt;img src=&quot;/assets/blog/garden-in-review-2025/2025-08-13--bumblebee-on-cutleaf-coneflower-1.jpg&quot; alt=&quot;A side profiel of a bumblebee on a Cutleaf Coneflower. The flower has thin oval-shaped yellow petals.&quot;&amp;gt;
&amp;lt;img src=&quot;/assets/blog/garden-in-review-2025/2025-08-16--bumblebee-on-cutleaf-coneflower-2.jpg&quot; alt=&quot;A close-up shot of a fuzzy little bumblebee on a Cutleaf Coneflower. The bee has gathered two orange pollen sacs, one on each hind leg.&quot;&amp;gt;
&amp;lt;figcaption&amp;gt;Bumblebees on Cutleaf Coneflowers&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;h2&gt;Rain Barrels!&lt;/h2&gt;
&lt;p&gt;We&apos;ve been dealing with annoying drainage issues since we got this house because the previous owners decided to pave so much usable yard space around our garage that when it rains, the water only has a few
limited places to drain which causes the ground in those spots to basically become a completely saturated swamp.&lt;/p&gt;
&lt;p&gt;I learned that our county has a program where you can apply for rebates for taking actions to improve drainage and reduce water usage, including installing rain barrels, so I went for it! It was a great easy process and we were approved for 2 rain barrels. Between getting all of the pieces to connect the barrels to our downspouts and the materials for building a simple wooden stand for one of the barrels, I ended up spending a bit beyond the maximum rebate amount, but we still got a really nice setup for something like 80% off!&lt;/p&gt;
&lt;p&gt;&amp;lt;figure class=&quot;two-col&quot;&amp;gt;
&amp;lt;img
src=&quot;/assets/blog/garden-in-review-2025/2025-05-30--rain-barrel-1.jpg&quot;
alt=&quot;A terra cotta-colored rain barrel on a patio. A flex tube is running from a nearby downspout to the top of the barrel so that rain running off of the roof will be diverted into the barrel.&quot;&lt;/p&gt;
&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;img
src=&quot;/assets/blog/garden-in-review-2025/2025-05-30--rain-barrel-2.jpg&quot;
alt=&quot;A terra cotta-colored rain barrel, raised up on a wooden stand about 3 feet off the ground, on the other side of the house from where the first barrel is. A flex tube is running from a nearby downspout to the top of the barrel so that rain running off of the roof will be diverted into the barrel.&quot;&lt;/p&gt;
&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;figcaption&amp;gt;
The two rain barrels I installed this spring.
&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;Between installing these barrels and planting a bunch of water-loving native plants in one of the particularly swampy corners of our property, our drainage issues seem to have been almost completely resolved.
Along with the drainage improvements, they were able to serve as a very reliable source of free water for my gardens!
It&apos;s pretty astonishing to see how quickly these barrels can fill with even a relatively small amount of rain, and that rain goes much
further than you might think. I barely had to use a garden hose to water anything all summer, which felt like a huge win.&lt;/p&gt;
&lt;h2&gt;Vegetables&lt;/h2&gt;
&lt;p&gt;I focus a lot of my energy on native plants, but I do grow some vegetables too! The last couple years,
my attempts at growing vegetables went fine, but I kept making the mistake of growing things which seem neat, but weren&apos;t practical at all because we don&apos;t actually care to eat them that much.&lt;/p&gt;
&lt;p&gt;So this year, I tried to shift my focus to things I knew we &lt;em&gt;would&lt;/em&gt; use: cucumbers, tomatoes, and carrots.&lt;/p&gt;
&lt;p&gt;I learned that tomatoes are pretty sensitive to cold, so in cold climates like Michigan, if you want to maximize their growing season you have to start them early indoors until it&apos;s consistently warm enough for them to go outside.
I had tomato seeds I harvested last year from tomato plants that my partner&apos;s uncle had given us, so I just planted those in pots in early April and let them grow on my office window sill. I couldn&apos;t believe how many of the seeds germinated, this went better than I could have dreamed.&lt;/p&gt;
&lt;p&gt;&amp;lt;figure class=&quot;two-col&quot;&amp;gt;
&amp;lt;img
src=&quot;/assets/blog/garden-in-review-2025/2025-04-02--tomato-seedling.jpg&quot;
alt=&quot;A close-up of two tiny tomato plant seedlings poking out of the dirt in a small pot on a window sill.&quot;&lt;/p&gt;
&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;img
src=&quot;/assets/blog/garden-in-review-2025/2025-04-26--larger-tomato-seedling.jpg&quot;
alt=&quot;A small pot on a window sill with 4 small tomato plants in it. One of the plants is getting particularly tall, probably about 8-10 inches.&quot;&lt;/p&gt;
&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;figcaption&amp;gt;
Indoor tomato growth progress from week 1 to week 4.
&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;The tomatoes and cucumbers ended up being massive successes. We had a near-constant supply of fresh cucumbers and tomatoes all summer, and it never felt like we couldn&apos;t find enough uses for them.
Lots of great salads, and I became quite fond of a &quot;greek chickpea bowl&quot; type thing which was just
chickpeas, red onions, cucumbers, and tomatoes over rice with tzatziki sauce. Delicious.&lt;/p&gt;
&lt;p&gt;We did have some points where we were starting to drown in cucumbers, but people love cucumbers so it was pretty easy to offload them on friends and family.&lt;/p&gt;
&lt;p&gt;&amp;lt;figure class=&quot;two-col&quot;&amp;gt;
&amp;lt;img
src=&quot;/assets/blog/garden-in-review-2025/2025-08-13--tomatoes.jpg&quot;
alt=&quot;A raised garden bed with a trellis attached with a bunch of cherry tomato plants growing up it. On the other side of these tomatoes is a dense thicket of Jerusalem artichokes. We&apos;ll get to that.&quot;&lt;/p&gt;
&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;img
src=&quot;/assets/blog/garden-in-review-2025/2025-08-13--cucumbers.jpg&quot;
alt=&quot;A raised garden bed with a trellis attached with cucumber vines growing up it. On the other side of these cucumbers is a patch of carrots.&quot;&lt;/p&gt;
&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;figcaption&amp;gt;
The tomatoes and cucumbers at their peak in mid-August.
&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;As for the carrots... We did use some! Nowhere close to all of them though. As it turns out, carrots don&apos;t always grow as nicely as you see them in the store (or I may have messed something up, not going to rule that out); a lot of them came out short and knotty and kinda ugly, but they did still get the job done. The real problem is that we realized that most dishes that we
like to use carrots in are things like soups and stews which don&apos;t quite feel
as appealing in the summer. I think the ideal setup is to harvest the carrots in the fall and store
them in some way that allows them to stay good over the winter, but I&apos;m not sure what that looks like right now.&lt;/p&gt;
&lt;p&gt;&amp;lt;figure&amp;gt;
&amp;lt;img
src=&quot;/assets/blog/garden-in-review-2025/2025-11-04--carrots.jpg&quot;
alt=&quot;A bunch of carrots held up in front of the camera by a hand. The carrots are all varying sizes from about 3-6 inches. The carrots all have a bunch of places where they have knots and forks where they diverged and started growing in two different directions.&quot;&lt;/p&gt;
&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;figcaption&amp;gt;
Some gnarly-looking carrots.
&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;I haven&apos;t mentioned the fourth vegetable I grew in our vegetable garden yet... Jerusalem Artichoke.
I had a dish which featured Jerusalem Artichoke at a restaurant the year prior and became fascinated by it.
It&apos;s a native plant which is very beneficial to pollinators, but it has edible tubers that taste kinda like
a nutty potato and can basically be cooked the same ways a potato can.&lt;/p&gt;
&lt;p&gt;&amp;lt;figure&amp;gt;
&amp;lt;img
src=&quot;/assets/blog/garden-in-review-2025/2025-09-23--common-checkered-skipper.jpg&quot;
alt=&quot;A close-up shot of a Common Checkered Skipper on a Jerusalem Artichoke flower. The flower is bright yellow with medium-length petals that come to a point. These plants are in the sunflower family and can grow up to 10 feet tall. The skipper is a small butterfly with brown and white spotted wings.&quot;&lt;/p&gt;
&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;figcaption&amp;gt;
A Common Checkered Skipper on a Jerusalem Artichoke flower.
&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;Here&apos;s the thing... you&apos;re supposed to wait to harvest them until after the first frost, but things got away from me and I just never harvested them. Now the ground is too frozen for me to dig them up, so I&apos;m going to have to
wait for a day this winter when things thaw out enough that I can run out there and grab some.&lt;/p&gt;
&lt;p&gt;Even if they&apos;re good, I think it was still a mistake to grow this because the ratio of how much space in my garden they took up to the number of times we&apos;ll actually eat them is completely off. It was nice to grow them
for the pollinators, but the raised garden beds are supposed to be the little patch that&apos;s reserved for
what us humans want to eat. I think next year I&apos;m going to try replacing them with Jalapeños.&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;Whew, that was a lot! 2025 wasn&apos;t a great year for much, but I had a great time with my plants at least.
Here&apos;s hoping that next year will be even better!&lt;/p&gt;
</content:encoded><language>en-us</language><author>Ryan Geyer</author></item><item><title>A simple way to programmatically detect support for cascade layers</title><link>https://geyer.dev/blog/cascade-layer-feature-detection</link><guid isPermaLink="true">https://geyer.dev/blog/cascade-layer-feature-detection</guid><description>A blog post by Ryan Geyer</description><pubDate>Thu, 12 Oct 2023 17:39:00 GMT</pubDate><content:encoded>&lt;p&gt;I recently needed to be able to programmatically test if a browser supports the relatively new &lt;code&gt;@layer&lt;/code&gt; Cascade Layers CSS feature (in my opinion, one of the most exciting and underrated recent additions to CSS).&lt;/p&gt;
&lt;p&gt;However, I quickly ran up against a big problem: &lt;code&gt;@support&lt;/code&gt; queries don&apos;t work for at-rules like &lt;code&gt;@layer&lt;/code&gt;! I&apos;d love to be able to do something like what&apos;s suggested &lt;a href=&quot;https://github.com/whatwg/html/issues/7540#issue-1116552451&quot;&gt;in this issue&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const doesSupportCascadeLayers = window.CSS.supports(&quot;at-rule(@layer)&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But currently, no dice. After a lot of searching around, I almost thought I was out of luck, but then I discovered that Cascade Layer support also comes along with the presence of some global classes which we can test for the existence of on the window!&lt;/p&gt;
&lt;p&gt;This works like a charm:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const doesSupportCascadeLayers = &quot;CSSLayerStatementRule&quot; in window;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unfortunately, I don&apos;t think there&apos;s any way to detect &lt;code&gt;@layer&lt;/code&gt; support in pure CSS, but this was thankfully enough to satisfy my needs.&lt;/p&gt;
</content:encoded><language>en-us</language><author>Ryan Geyer</author></item><item><title>How Waymark finds all of the image elements on your website</title><link>https://geyer.dev/blog/extracting-images</link><guid isPermaLink="true">https://geyer.dev/blog/extracting-images</guid><description>A blog post by Ryan Geyer</description><pubDate>Sat, 16 Sep 2023 21:15:58 GMT</pubDate><content:encoded>&lt;p&gt;At &lt;a href=&quot;https://waymark.com&quot;&gt;Waymark&lt;/a&gt;, one of our core features involves generating brand profiles from user-provided website URLs. This process includes importing images from your website to save you time from having to upload content yourself, identifying which image is most likely to be your brand&apos;s logo, writing a summary of your brand which can be used to inform our AI-powered video generation, and more.&lt;/p&gt;
&lt;p&gt;There is a lot of interesting stuff going on here, but in this article I am going to focus on the journey that we have gone on
figuring out a way to get as many quality images from a page as possible. It&apos;s not as easy as you might think!&lt;/p&gt;
&lt;h2&gt;1. The simplest approach&lt;/h2&gt;
&lt;p&gt;Our image extraction works by opening the provided site in Puppeteer, so the
very first approach that I took when writing this image extraction logic was by far the most appealing at first glance;
Puppeteer allows you to intercept network requests made from the page and access the media type of the request,
so it&apos;s possible to get pretty good results by simply intercepting all requests with an &quot;image&quot; media type and adding those URLs to a set, waiting for the page&apos;s network requests to settle, and calling it a day. The code would look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const imageURLSet = new Set&amp;lt;string&amp;gt;();

await page.setRequestInterception(true);
page.on(&apos;request&apos;, (request: HTTPRequest) =&amp;gt; {
  const resourceType = request.resourceType();

  if(resourceType === &apos;image&apos;){
    imageURLSet.add(imageURLSet.url());
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is lovely! It allows the browser to do the work for us and we can just kick back and pick up the images as they roll in.&lt;/p&gt;
&lt;p&gt;But of course, this approach has downsides:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Data URLs and inline SVG elements will not show up as network requests, so if you care about getting those images, you will need to add some extra handling. SVG elements are relatively easy to query for, but data URLs may prove to be a lot harder to track down.&lt;/li&gt;
&lt;li&gt;The image URLs that come in are entirely black-boxed; if you care about getting additional context about the
images such as where they are positioned on the page, this approach will not work for you.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;As it turns out, Waymark needs to gather a lot of context about how image elements are displayed on the page
to inform how we determine which image is most likely to be the brand&apos;s logo, so that second downside
was a big problem.
I initially tried rolling with some awkward solutions where we would reverse engineer an &lt;code&gt;img&lt;/code&gt; element selector matching the image URL, like &lt;code&gt;document.querySelector(`img[src*=&apos;${imageURLPathname}&apos;]`)&lt;/code&gt;.
But what if the image was loaded from an srcset? Or a picture &lt;code&gt;&amp;lt;source&amp;gt;&lt;/code&gt; tag? Or a CSS background image?
The list of edge cases kept growing, and it started to become clear that there were just too many gaps
for us to be able to confidently provide an acceptable experience to as many users as possible.&lt;/p&gt;
&lt;h2&gt;2. BRUTE FORCE&lt;/h2&gt;
&lt;p&gt;So, we need to be able to get the elements for as many images on the page as possible. The simplest approach would be to
just directly query for all &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt; elements on the page and go from there, but there didn&apos;t seem to be a good way
to query for elements with a CSS background image.
As such, my next approach was to just throw brute force at it by traversing down every single node in
the DOM tree and checking if it was an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;, an &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt;, or had a &lt;code&gt;background-image&lt;/code&gt; CSS style. This worked...
but it was slow, inefficient, and simply overkill. Even worse, the code was pretty hard to read/maintain/debug because it had to all run in a huge monolithic script that was excecuted on the page in Puppeteer, where it becomes a lot harder to reliably log or trace errors.&lt;/p&gt;
&lt;p&gt;Overall, this did close a lot of the gaps that the previous approach had left, but it left me wanting.
Eventually, an opportunity came to refactor this code during an initiative to further improve the reliability of our image extraction and logo identification, and I took it.&lt;/p&gt;
&lt;h2&gt;3. Figure out where the images are and go there directly&lt;/h2&gt;
&lt;p&gt;So, in the first approach, we tried just letting the images come to us, but this had some drawbacks by limiting the amount of information we could easily access.&lt;/p&gt;
&lt;p&gt;In the second approach, we went in the complete opposite direction and tried desperately digging through every nook and cranny of a site looking for images.&lt;/p&gt;
&lt;p&gt;But I have finally landed on a third approach which, in my opinion, works as a really nice middle-ground between the two.
Earlier I mentioned that it didn&apos;t seem like there was a good way to query for elements with CSS background images, but that problem
sat with me for a while; at the end of the day, any CSS styles are applied with a selector. Theoretically, couldn&apos;t we figure out how
to parse the page&apos;s stylesheets, extract the selector(s) for any &lt;code&gt;background-image&lt;/code&gt; styles, and directly query for them? Well, yes we can!&lt;/p&gt;
&lt;p&gt;All of the stylesheets loaded on the page, both from inlined &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tags and external linked CSS files,
can be accessed on &lt;code&gt;document.styleSheets&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This will return a list of &lt;code&gt;CSSStyleSheet&lt;/code&gt; instances, each of which should include a &lt;code&gt;cssRules&lt;/code&gt; property that enumerates every
valid style rule parsed from the stylesheet. Unforunately, depending on how the site in question was implemented,
directly accessing a stylesheet&apos;s &lt;code&gt;cssRules&lt;/code&gt; may throw a CORS error if the stylesheet came from a different origin without a &lt;code&gt;crossorigin=&quot;anonymous&quot;&lt;/code&gt; attribute on the &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;
&lt;p&gt;This led me down a ridiculous aside writing a script to fetch each stylesheet as a string, manually search it for every instance of a background image style rule, and then construct the selector for that style rule, taking into account that nested CSS selectors
are now supported in most browsers. I had a blast writing it and got it working well, but it is extremely important to remember
that anytime you write code which could be described as &quot;a fun puzzle&quot;, you need to deeply interrogate if it&apos;s actually a good idea
to put it in production.&lt;/p&gt;
&lt;p&gt;Of course, there were two much simpler solutions to this problem:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We control the Puppeteer browser, we can just disable CORS errors if we want.&lt;/li&gt;
&lt;li&gt;Pretty much every modern browser supports creating constructed stylesheets with the &lt;code&gt;CSSStyleSheet&lt;/code&gt; constructor. This exposes an API to parse a CSS stylesheet string in the exact way that browsers parse all other stylesheets, so you know it should work quickly and reliably.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I tinkered with disabling CORS, but started feeling a little uneasy as I realized that the Chromium flags required to achieve this appear to have shifted over the years; a simple &lt;code&gt;&quot;--disable-web-security&quot;&lt;/code&gt; used to be all you needed, but
more modern answers seem to indicate that you now also need to provide &lt;code&gt;&quot;--disable-features=IsolateOrigins&quot;&lt;/code&gt; and &lt;code&gt;&quot;--disable-site-isolation-trials&quot;&lt;/code&gt; flags as well. I don&apos;t think Chromium considers disabling CORS to be an important feature that people are supposed to be using (and for good reason!) so maybe we shouldn&apos;t bank on that API not changing under our feet.&lt;/p&gt;
&lt;p&gt;So, the constructed stylesheet approach started looking a lot more appealing. And it worked like a dream! Here&apos;s what the code looks like to make this work:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Gather the text contents of all of the stylesheets on the page into a big combined string
const combinedStylesheetString = (
  await Promise.all(
    Array.from(document.styleSheets).map(async (styleSheet) =&amp;gt; {
      try {
        if (styleSheet.href) {
          // External stylesheets have an href which we can fetch
          return await fetch(styleSheet.href).then((response) =&amp;gt; response.text());
        } else {
          // We can directly access the &amp;lt;style&amp;gt; tags for inlined stylesheets via styleSheet.ownerNode
          return styleSheet.ownerNode?.textContent || &apos;&apos;;
        }
      } catch (e) {
        return &apos;&apos;;
      }
    }),
  )
).join(&apos; &apos;);

const parsedStylesheet = new CSSStyleSheet();
await parsedStylesheet.replace(combinedStylesheetString);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now that we have a &lt;code&gt;CSSStyleSheet&lt;/code&gt; instance to work with, we can search it for any background image style rules and
get the selectors for those rules.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const backgroundImages = new Array&amp;lt;{
  selector: string;
  imageURL: string;
}&amp;gt;();

// Regex matches `url(&quot;&amp;lt;url-here&amp;gt;&quot;)` styles so we can extract the URL from a background image style
const cssURLStyleRegex = /url\([&apos;&quot;](.*?)[&apos;&quot;]\)/;

const rules = parsedStylesheet.cssRules;

for (const styleRule of rules) {
  if (!(styleRule instanceof CSSStyleRule)) {
    continue;
  }

  let backgroundImageURL: string | null = null;

  const styleDeclaration = styleRule.style;
  backgroundImageURL = styleDeclaration.backgroundImage?.match(cssURLStyleRegex)?.[1];
  if (!backgroundImageURL) {
    backgroundImageURL = styleDeclaration.background?.match(cssURLStyleRegex)?.[1];
  }

  if (backgroundImageURL) {
    // If we found a background style with an image URL in it, we can get the selector for that style rule
    // on `styleRule.selectorText`
    backgroundImages.push({
      selector: styleRule.selectorText,
      imageURL: backgroundImageURL,
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So now, we have a list of background image URLs and their selectors parsed from the page&apos;s stylesheets.
Now if we want to find all of the elements on the page with background images, all we have to do is query for those selectors!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const backgroundImagesWithElements = backgroundImages.flatMap(({
  selector,
  imageURL,
})=&amp;gt;{
  const elementsMatchingSelector = document.querySelectorAll(selector);
  return Array.from(elementsMatchingSelector).map((element) =&amp;gt; ({
    element,
    imageURL,
  }));
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&apos;m very pleased with this solution. If we&apos;re honest with ourselves, it&apos;s still a little complex, but a major improvement over our
previous attempts at gathering background image elements.&lt;/p&gt;
&lt;p&gt;Now at this point, you can easily query for image tags with &lt;code&gt;document.getElementsByTagName(&quot;img&quot;)&lt;/code&gt; and SVGs with &lt;code&gt;document.getElementsByTagName(&quot;svg&quot;)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I will not argue that any solution is absolutely bullet-proof, but this feels the closest we&apos;ve ever gotten. Of course, I have seen plenty of...
&quot;creatively&quot; written websites while working on this feature to know that there will
always be some website out there which defeats our image extraction.
But that number is shrinking, and we&apos;ll keep working to get it lower and lower if we can.&lt;/p&gt;
</content:encoded><language>en-us</language><author>Ryan Geyer</author></item><item><title>Reflecting on hover-video-player v1.2</title><link>https://geyer.dev/blog/hover-video-player-1_2_4</link><guid isPermaLink="true">https://geyer.dev/blog/hover-video-player-1_2_4</guid><description>A blog post by Ryan Geyer</description><pubDate>Sat, 11 May 2024 18:06:26 GMT</pubDate><content:encoded>&lt;p&gt;Back in January when I released &lt;a href=&quot;https://github.com/Gyanreyer/hover-video-player&quot;&gt;hover-video-player&lt;/a&gt; v1.1,
it felt like it was finally a &quot;finished&quot; project;
the component was meant to be extremely simple, and it seemed like there were no more obvious gaps in basic
functionality that still needed to be added.&lt;/p&gt;
&lt;p&gt;The next week, I got a new issue asking for a feature which was not currently supported. Classic.&lt;/p&gt;
&lt;p&gt;For those unfamiliar, &lt;code&gt;hover-video-player&lt;/code&gt; is a web component which can be used to add videos to your page which
will play on hover, like a YouTube thumbnail preview. I use it on the &lt;a href=&quot;/&quot;&gt;homepage of this very site&lt;/a&gt;.
It&apos;s based on the significantly more popular &lt;a href=&quot;https://github.com/Gyanreyer/react-hover-video-player&quot;&gt;react-hover-video-player&lt;/a&gt;
library which I also maintain, but as I have fallen more and more out of love with React,
I confess that &lt;code&gt;hover-video-player&lt;/code&gt; has become my favorite child.&lt;/p&gt;
&lt;p&gt;The issue I received was a single sentence, a fairly innocent question: &quot;Is there an easy way to keep one video playing at all times, even when none is hovering?&quot;&lt;/p&gt;
&lt;p&gt;This one put me in a bad mood. This project isn&apos;t finished at all. I did not and still don&apos;t have any plans
to build specific behavior into the component which involves syncing with other components&apos; states,
but my existing implementation didn&apos;t expose APIs which would allow you control component state
externally in order to implement that behavior yourself.&lt;/p&gt;
&lt;p&gt;I didn&apos;t feel like I had any good ideas on how to approach the problem,
nor did I feel much motivation to figure it out, so I just let the issue sit unaddressed.
I spent the next 4 months feeling weirdly guilty about ignoring it, and then suddenly I recently had
a burst of motivation to just power through and get this thing done.&lt;/p&gt;
&lt;p&gt;So, here&apos;s what I did!&lt;/p&gt;
&lt;h2&gt;The changes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Adds the ability to place a &lt;code&gt;&amp;lt;hover-video-player&amp;gt;&lt;/code&gt; component in &quot;controlled&quot; mode, meaning it will stop responding to
hover events and allow you to have full programmatic control over when to start/stop video playback
&lt;ul&gt;
&lt;li&gt;This can be turned on by either setting a &lt;code&gt;controlled&lt;/code&gt; HTML attribute on the element or running &lt;code&gt;hoverVideoPlayer.controlled = true&lt;/code&gt; in JavaScript.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Adds a &lt;code&gt;hover()&lt;/code&gt; method to programmatically transition the player into a &quot;hovering&quot; state which will start playing the video&lt;/li&gt;
&lt;li&gt;Adds a &lt;code&gt;blur()&lt;/code&gt; method to programmatically transition the player out of the &quot;hovering&quot; state to pause the video&lt;/li&gt;
&lt;li&gt;The component will now respond to externally-set &lt;code&gt;data-playback-state&lt;/code&gt; attribute values
&lt;ul&gt;
&lt;li&gt;This means you can make the video auto-play if you set an initial &lt;code&gt;data-playback-state=&quot;playing&quot;&lt;/code&gt; attribute!&lt;/li&gt;
&lt;li&gt;You can even start playback with &lt;code&gt;hoverVideoPlayer.dataset.playbackState = &quot;playing&quot;&lt;/code&gt; if &lt;code&gt;hoverVideoPlayer.hover()&lt;/code&gt; isn&apos;t verbose enough.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Makes &lt;code&gt;hoverstart&lt;/code&gt; and &lt;code&gt;hoverend&lt;/code&gt; events cancelable
&lt;ul&gt;
&lt;li&gt;These events can now be intercepted and canceled with &lt;code&gt;event.preventDefault()&lt;/code&gt; if you want to prevent the player state from updating
in response to a hover/blur event.&lt;/li&gt;
&lt;li&gt;The new &quot;controlled&quot; mode is literally just syntactic sugar for calling &lt;code&gt;preventDefault()&lt;/code&gt; for both &lt;code&gt;hoverstart&lt;/code&gt; and &lt;code&gt;hoverend&lt;/code&gt; events every time they are emitted.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These new APIs unlock everything that is needed in order to build the experience that the
original issue that prompted all of this was asking about. I made a
&lt;a href=&quot;https://codesandbox.io/p/sandbox/hover-video-player-example-forked-8rxh7w?file=%2Fsrc%2Fhover-video-player-group.js%3A11%2C34&quot;&gt;proof-of-concept in codepen&lt;/a&gt;
which works pretty well!&lt;/p&gt;
&lt;p&gt;Unfortunately I got a little overly excited and what started as a clean &lt;code&gt;v1.2.0&lt;/code&gt;
release quickly turned into 4 rapid follow-up versions with lots of bug fixes and API
refinements, finally bringing us to &lt;code&gt;v1.2.4&lt;/code&gt;.
This is the last one, I swear.&lt;/p&gt;
&lt;p&gt;Having to make that many follow-up releases for a project is not the best, I&apos;ll openly admit that.
However, there is something extremely powerful about publishing something before it&apos;s done.
Like clockwork, as soon as it goes up, I&apos;ll think of another issue that I didn&apos;t account for
and then I&apos;m back in the code mines fixing it. It&apos;s not a good loop, but you can&apos;t argue it doesn&apos;t get results.
In the future, I&apos;m going to try using a beta channel for releases until I&apos;m 100% confident that
everything is set in stone; maybe that will produce a similar effect without also putting people using the
library in harm&apos;s way.&lt;/p&gt;
&lt;h2&gt;What I learned&lt;/h2&gt;
&lt;p&gt;This library has helped me discover a ton of patterns I love for writing web components. When I first wrote &lt;code&gt;hover-video-player&lt;/code&gt;,
I stumbled into a new pattern which would later become popularized under the term &lt;a href=&quot;https://blog.jim-nielsen.com/2023/html-web-components/&quot;&gt;&quot;HTML Web Components&quot;&lt;/a&gt;; components which
enhance child content instead of being concerned with rendering the content themselves.
It felt like an incredibly refreshing way of designing web components, and it even influenced
how I design React components (I still need to write a blog going into more depth on that).&lt;/p&gt;
&lt;p&gt;My favorite takeaway from this work was that JavaScript&apos;s built-in event interfaces are
really great to work with. I had to think very hard and go through a couple iterations figuring out how I could
expose a reasonable API for disabling a &lt;code&gt;&amp;lt;hover-video-player&amp;gt;&lt;/code&gt; element&apos;s default behavior when a hover/blur event is
fired, and achieving that by making the events cancelable with &lt;code&gt;preventDefault()&lt;/code&gt; just feels so elegant and like it
really goes with the grain of how native HTML elements work. Super refreshing.&lt;/p&gt;
&lt;p&gt;For any who are curious how cancelable events work, this is it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const evt = new Event(&quot;myevent&quot;, {
  cancelable: true,
});
// wasNotCanceled will be `false` if `evt.preventDefault()` was called in any handlers for this event.
// I kinda wish the values were flipped so it would return `true` if the event was canceled since this makes
// variable naming a little more awkward, but oh well.
const wasNotCanceled = eventTarget.dispatchEvent(evt);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I had no idea it was that easy!&lt;/p&gt;
&lt;p&gt;However, I was also reminded that &lt;code&gt;CustomEvent&lt;/code&gt;s in TypeScript come with some extremely frustrating downsides;
there is no good way to register typings for the events which a custom element can emit, and even if you know
for certain that a given event will have a &lt;code&gt;CustomEvent&lt;/code&gt; object instead of a plain &lt;code&gt;Event&lt;/code&gt; object, TypeScript is
going to fight you every step of the way until you&apos;ve either added a bunch of extremely obnoxious unnecessary runtime checks or have just given up and cast everything as &lt;code&gt;any&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For example, TypeScript will complain about this code block:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ts error: Argument of type &apos;(evt: CustomEvent&amp;lt;&quot;paused&quot; | &quot;loading&quot; | &quot;playing&amp;gt;) =&amp;gt; void&apos; is not assignable to parameter of type &apos;EventListenerOrEventListenerObject&apos;.
hoverVideoPlayer.addEventListener(&quot;playbackstatechange&quot;, (evt: CustomEvent&amp;lt;&quot;paused&quot; | &quot;loading&quot; | &quot;playing&quot;&amp;gt;) =&amp;gt; {});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is what you have to do to satisfy it while still having &lt;code&gt;evt.detail&lt;/code&gt; typed as expected:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hoverVideoPlayer.addEventListener(&quot;playbackstatechange&quot;, (evt) =&amp;gt; {
  if (!(evt instanceof CustomEvent)) {
    return;
  }

  // evt.detail is now typed as any. Not very typesafe. The only way to narrow the type from
  // here is like this:
  const newPlaybackState: unknown = evt.detail;
  if(newPlaybackState !== &quot;paused&quot; &amp;amp;&amp;amp; newPlaybackState !== &quot;loading&quot; &amp;amp;&amp;amp; newPlaybackState !== &quot;playing&quot;) {
    return;
  }

  // newPlaybackState can now be treated as a union of the 3 valid playback states
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As web components continue to gain steam, this feels like an issue that TypeScript
needs to address sooner than later. I&apos;d be very interested to see if there are any proposals
out there on how to make this developer experience better.&lt;/p&gt;
</content:encoded><language>en-us</language><author>Ryan Geyer</author></item><item><title>Micro-optimizing boolean-to-int conversions for... fun?</title><link>https://geyer.dev/blog/micro-optimizing-boolean-to-int</link><guid isPermaLink="true">https://geyer.dev/blog/micro-optimizing-boolean-to-int</guid><description>A blog post by Ryan Geyer</description><pubDate>Mon, 17 Mar 2025 21:29:28 GMT</pubDate><content:encoded>&lt;p&gt;Who doesn&apos;t love a little micro-optimization? I love messing around with in my side projects to push the performance as far as I can. I&apos;m currently working on an RSS feed reader which uses &lt;code&gt;IndexedDB&lt;/code&gt;, and as it turns out IDB doesn&apos;t allow boolean values to be indexed.&lt;/p&gt;
&lt;p&gt;This meant that I had to start representing a boolean value as the numbers &lt;code&gt;0&lt;/code&gt; or &lt;code&gt;1&lt;/code&gt;, and this raised an important question: what is the absolutely blazing-est fast-est-est way to coerce a boolean value into an integer in JavaScript?&lt;/p&gt;
&lt;h2&gt;The setup&lt;/h2&gt;
&lt;p&gt;First, let&apos;s all say it together: micro-benchmarks are bad and rarely a good measure of anything. I tried my best to get something meaningful out of this, bu all results here should be taken with a grain of salt.&lt;/p&gt;
&lt;p&gt;I used https://jsbench.me to set up my tests and ran them in every browser. First, I have a setup script that executes before every run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Make an array of a random size between 128-256
const bools = new Array(128 + Math.floor(Math.random() * 128));
// Randomly fill the array with boolean values
for (let i = 0; i &amp;lt; bools.length; ++i) {
  bools[i] = Math.random() &amp;lt; 0.5;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I had each test case loop over the &lt;code&gt;bools&lt;/code&gt; array and use a different method to coerce each value into an integer:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Ternary
for (const bool of bools) {
  const num = bool ? 1 : 0;
}

// Casting with Number constructor
for (const bool of bools) {
  const num = Number(bool);
}

// Bitwise operator
for (const bool of bools) {
  const num = bool &amp;amp; 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Going into this, my hypothesis was that the ternary operator would perform the worst since it
involves branching, then casting would be a little faster, and the bitwise operation would be a little faster still.&lt;/p&gt;
&lt;h2&gt;The results&lt;/h2&gt;
&lt;p&gt;We will use the ternary operator as our baseline, as that&apos;s what I would consider the most &quot;readable&quot;
approach which I would actually use in a shared codebase where I&apos;m worried about maintainability.
I ran these tests in 3 different browsers and was pretty surprised by the results.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Browser&lt;/th&gt;
&lt;th&gt;Ternary&lt;/th&gt;
&lt;th&gt;Casting&lt;/th&gt;
&lt;th&gt;Bitwise&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Chrome&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;584K ops/s&lt;/td&gt;
&lt;td&gt;752K ops/s (+29%)&lt;/td&gt;
&lt;td&gt;1M ops/s (+78%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Firefox&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.8M ops/s&lt;/td&gt;
&lt;td&gt;439K ops/s (-75%)&lt;/td&gt;
&lt;td&gt;1.8M ops/s (-)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Safari&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3.2M ops/s&lt;/td&gt;
&lt;td&gt;3M ops/s   (-)&lt;/td&gt;
&lt;td&gt;3.1M ops/s (-)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Say what you will about Safari but boy is it fast. Not only did all 3 results fall within
the margin of error of each other, they also all ran over 3-5 times faster than Chrome!&lt;/p&gt;
&lt;p&gt;Firefox didn&apos;t do too bad either, with the ternary and bitwise cases both running very fast and
staying within the margin of error of each other. However, casting with the &lt;code&gt;Number&lt;/code&gt; constructor
did very poorly in Firefox, producing the slowest results out of any of the tests run.&lt;/p&gt;
&lt;p&gt;And so that leaves us with Chrome. These results come the closest to what my expectations had been, but even
then I was surprised by how big the performance gap was between each approach. I certainly did not expect
bitwise operations to outperform ternary operators by nearly 2x!&lt;/p&gt;
&lt;h2&gt;Negation&lt;/h2&gt;
&lt;p&gt;Now I was curious, does this look any different in cases where we want to negate the value so that
&lt;code&gt;true&lt;/code&gt; becomes &lt;code&gt;0&lt;/code&gt; and &lt;code&gt;false&lt;/code&gt; becomes &lt;code&gt;1&lt;/code&gt;? I wrote some new test cases to see:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Ternary
for (const bool of bools) {
  const num = bool ? 0 : 1;
}

// Casting with Number constructor + NOT
for (const bool of bools) {
  const num = Number(!bool);
}

// Casting with Number constructor + subtraction
for (const bool of bools) {
  const num = 1 - Number(bool);
}

// Bitwise operator
for (const bool of bools) {
  const num = bool ^ 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Browser&lt;/th&gt;
&lt;th&gt;Ternary&lt;/th&gt;
&lt;th&gt;Casting + NOT&lt;/th&gt;
&lt;th&gt;Casting + Subtraction&lt;/th&gt;
&lt;th&gt;Bitwise&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Chrome&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;586K ops/s&lt;/td&gt;
&lt;td&gt;455K ops/s (-22%)&lt;/td&gt;
&lt;td&gt;679K ops/s (+16%)&lt;/td&gt;
&lt;td&gt;1.1M ops/s (+87%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Firefox&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.7M ops/s&lt;/td&gt;
&lt;td&gt;430K ops/s (-75%)&lt;/td&gt;
&lt;td&gt;428K ops/s (-75%)&lt;/td&gt;
&lt;td&gt;1.7M ops/s (-)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Safari&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.9M ops/s&lt;/td&gt;
&lt;td&gt;3.1M ops/s (-)&lt;/td&gt;
&lt;td&gt;3M ops/s (-)&lt;/td&gt;
&lt;td&gt;3.1M ops/s (-)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The results look pretty similar to last time, but I was interested to see that Chrome performs
significantly worse when combining a &lt;code&gt;Number&lt;/code&gt; cast with a NOT operator. I&apos;m not sure why that would
be, but that difference has been very consistent across multiple runs of these benchmark tests.&lt;/p&gt;
&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;So, what can be taken away from this? Overall, the results are a little inconsistent between each
browser engine&apos;s implementation, but we can clearly see that the bitwise operator is always either
the outright winner or tied for first place in every browser.&lt;/p&gt;
&lt;p&gt;However, that&apos;s obviously by far the least readable of all of the solutions. Even if a bitwise operation
is nearly 2 times faster in Chrome than the more readable ternary operator, we need to think at scale here; the percentage looks impressive, but the actual difference is probably somewhere around 1-2 nanoseconds. There are much higher impact things you could be focusing on instead.&lt;/p&gt;
&lt;p&gt;If you somehow find yourself solving a problem where you have thousands of boolean values which all need to be converted to integers in bulk, switching to a bitwise operator &lt;em&gt;might&lt;/em&gt; be the right move for you. Otherwise, I wouldn&apos;t consider it to be very practical.&lt;/p&gt;
&lt;p&gt;That being said, I&apos;m going to keep using it in my side projects and you can&apos;t stop me.&lt;/p&gt;
</content:encoded><language>en-us</language><author>Ryan Geyer</author></item><item><title>Streaming promises in the order they resolve</title><link>https://geyer.dev/blog/unordered-promise-iterator</link><guid isPermaLink="true">https://geyer.dev/blog/unordered-promise-iterator</guid><description>A blog post by Ryan Geyer</description><pubDate>Fri, 29 Mar 2024 19:05:53 GMT</pubDate><content:encoded>&lt;p&gt;I have recently been obsessed with the idea of HTML streaming, in particular out-of-order streaming after reading &lt;a href=&quot;https://lamplightdev.com/blog/2024/01/10/streaming-html-out-of-order-without-javascript/&quot;&gt;this article on how out-of-order streaming can be done with declarative shadow DOM&lt;/a&gt;. I&apos;ve been working on an experimental templating language/framework/monstrosity in my free time called &lt;a href=&quot;https://github.com/Gyanreyer/tempeh&quot;&gt;Tempeh&lt;/a&gt;. I would consider the project&apos;s current state to be extremely rough and not ready for public consumption, so click that link at your own risk.&lt;/p&gt;
&lt;p&gt;However, I wanted to share a cool thing I put together while exploring out-of-order HTML streaming possibilities.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#the-implementation&quot;&gt;Click here to skip right to the code&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Context&lt;/h2&gt;
&lt;p&gt;So, let&apos;s say I have 3 pieces of lazy content on a page. I have an &lt;code&gt;html&lt;/code&gt; template string function which will unwrap promises and stream the html string contents in order as they resolve. An unfortunate limitation of this implementation is that it&apos;s order-dependent;
the async lazy content at the end of the template literal can only be streamed as fast as the first promise resolves.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return html`
  &amp;lt;div la-z-id=&quot;vCz0LY_0&quot;&amp;gt;
    Loading...
  &amp;lt;/div&amp;gt;
  &amp;lt;div la-z-id=&quot;vCz0LY_1&quot;&amp;gt;
    Loading...
  &amp;lt;/div&amp;gt;
  &amp;lt;div la-z-id=&quot;vCz0LY_2&quot;&amp;gt;
    Loading...
  &amp;lt;/div&amp;gt;
  ${waitFor(500).then(()=&amp;gt;html`
    &amp;lt;la-z id=&quot;vCz0LY_0&quot; hidden&amp;gt;
      I&apos;m first, but I&apos;m the slowest!
    &amp;lt;/la-z&amp;gt;
  `)}
  ${waitFor(300).then(()=&amp;gt;html`
    &amp;lt;la-z id=&quot;vCz0LY_1&quot; hidden&amp;gt;
      I&apos;m second, and I&apos;m in the middle.
    &amp;lt;/la-z&amp;gt;
  `)}
  ${waitFor(100).then(()=&amp;gt;html`
    &amp;lt;la-z id=&quot;vCz0LY_2&quot; hidden&amp;gt;
      I&apos;m third, but I&apos;m the fastest!
    &amp;lt;/la-z&amp;gt;
  `)}
`.arrayBufferStream();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, we can use streaming to accomplish a cool effect where the static placeholder content can load in immediately, and then we stream in the lazy content as it&apos;s ready. Each piece of lazy content
is wrapped in a &lt;code&gt;&amp;lt;la-z&amp;gt;&lt;/code&gt; web component which, when it mounts on the page, will find its corresponding placeholder element and swap in the final lazy content.&lt;/p&gt;
&lt;p&gt;In the scenario above, the first piece of lazy content is the slowest, so it will act as a bottleneck and prevent the other pieces of content from streaming sooner even if their promises have already resolved first. I want a way to stream the resolved values from an array of promises as they resolve, in the order that they resolve! Unfortunately there are not built-in friendly ways to accomplish this;
the main methods available for dealing with a group of promises are either &lt;code&gt;Promise.all&lt;/code&gt;/&lt;code&gt;Promise.allSettled&lt;/code&gt;, which
force you to wait for all promises to resolve together and preserves their order, or &lt;code&gt;Promise.race&lt;/code&gt;/&lt;code&gt;Promise.any&lt;/code&gt;, which can get the value of the first promise in the group which resolves, but nothing aside from that.&lt;/p&gt;
&lt;p&gt;So, it&apos;s time to build something ourselves! My &lt;code&gt;html&lt;/code&gt; template string function can also unwrap iterators, so I decided that should probably be the move.&lt;/p&gt;
&lt;h2&gt;The implementation&lt;/h2&gt;
&lt;p&gt;Alright, here&apos;s the code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Takes an array of promises and yields the resolved values in the order they resolve.
 * @template TResolveValue
 * @param {Promise&amp;lt;TResolveValue&amp;gt;[]} promises
 */
export async function* unorderedPromiseIterator(promises) {
  const resultStream = new ReadableStream({
    async pull(controller) {
      /** @param {TResolveValue} value */
      const onResolve = (value) =&amp;gt; {
        controller.enqueue(value);
      };

      /** @param {unknown} err */
      const onError = (err) =&amp;gt; {
        controller.enqueue(
          new Error(&quot;unorderedPromiseIterator: An unexpected error occurred.&quot;, {
            cause: err,
          })
        );
      };

      for (const promise of promises) {
        promise.then(onResolve, onError);
      }

      await Promise.allSettled(promises);
      controller.close();
    },
  });

  const reader = resultStream.getReader();

  try {
    /** @type {Awaited&amp;lt;ReturnType&amp;lt;typeof reader.read&amp;gt;&amp;gt;} */
    let readResult;
    while (!(readResult = await reader.read()).done) {
      yield readResult.value;
    }
  } finally {
    reader.releaseLock();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Excuse the JSDoc annotations, I&apos;m experimenting with being able to use TypeScript without a build step. Although it&apos;s a touch clunky, I like it overall!&lt;/p&gt;
&lt;p&gt;So obviously there is some real deep cut neo-JavaScript stuff in here. The &lt;code&gt;ReadableStream&lt;/code&gt; API and generator functions aren&apos;t something that you see super frequently day to day, although they&apos;ve been around long enough to use comfortably both in Node and the browser. But both are extremely powerful and very fun to work with!&lt;/p&gt;
&lt;p&gt;As it turns out, building something like this is pretty hard because generator functions only allow you to &lt;code&gt;yield&lt;/code&gt; within the body of the generator function; this means that yielding from a &lt;code&gt;promise.then&lt;/code&gt; callback is a no-go, and any obvious implementation ends up falling back into the
same trap of having to wait for each promise to resolve in order.&lt;/p&gt;
&lt;p&gt;On my first attempt at this, I went through a couple other iterations where I was tracking the index of each unresolved promise, waiting for any of them to resolve with &lt;code&gt;Promise.race&lt;/code&gt;, removing that resolved promise from the array, adjusting all of the tracked indices, and then
repeating that process until all promises where settled. The code was pretty gnarly and much more inefficient than I think something like this should be, so I kept searching for something better. I finally realized the &lt;code&gt;ReadableStream&lt;/code&gt; API was exactly what I was looking for.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ReadableStream&lt;/code&gt;s provide us with a way to set up a custom stream which we can fully control the underlying implementation of. This makes it easy for us to simply pass a method to each promise&apos;s &lt;code&gt;.then&lt;/code&gt; callback which enqueues the resolved value, and then we can close the stream one all promises have settled. Now we can yield each value in a generator-friendly way as they roll in from the stream&apos;s reader.&lt;/p&gt;
&lt;p&gt;Pretty cool! Here&apos;s how it looks in action:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const promises = [
  waitFor(300).then(()=&amp;gt;&quot;first (middle)&quot;),
  waitFor(500).then(()=&amp;gt;&quot;second (slowest)&quot;),
  waitFor(100).then(()=&amp;gt;&quot;third (fastest)&quot;),
];

for await (const value of unorderedPromiseIterator(promises)) {
  console.log(value);
}

// console log:
// &quot;third (fastest)&quot;
// &quot;first (middle)&quot;
// &quot;second (slowest)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This can then be applied to the html template string example I mentioned above like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return html`
  &amp;lt;div la-z-id=&quot;vCz0LY_0&quot;&amp;gt;
    Loading...
  &amp;lt;/div&amp;gt;
  &amp;lt;div la-z-id=&quot;vCz0LY_1&quot;&amp;gt;
    Loading...
  &amp;lt;/div&amp;gt;
  &amp;lt;div la-z-id=&quot;vCz0LY_2&quot;&amp;gt;
    Loading...
  &amp;lt;/div&amp;gt;
  ${unorderedPromiseIterator([
    waitFor(500).then(()=&amp;gt;html`
      &amp;lt;la-z id=&quot;vCz0LY_0&quot; hidden&amp;gt;
        I&apos;m first, but I&apos;m the slowest!
      &amp;lt;/la-z&amp;gt;
    `),
    waitFor(300).then(()=&amp;gt;html`
      &amp;lt;la-z id=&quot;vCz0LY_1&quot; hidden&amp;gt;
        I&apos;m second, and I&apos;m in the middle.
      &amp;lt;/la-z&amp;gt;
    `),
    waitFor(100).then(()=&amp;gt;html`
      &amp;lt;la-z id=&quot;vCz0LY_2&quot; hidden&amp;gt;
        I&apos;m third, but I&apos;m the fastest!
      &amp;lt;/la-z&amp;gt;
    `),
  ])}
`.arrayBufferStream();
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><language>en-us</language><author>Ryan Geyer</author></item><item><title>The dumb reason why flag emojis aren&apos;t working on your site in Chrome on Windows</title><link>https://geyer.dev/blog/windows-flag-emojis</link><guid isPermaLink="true">https://geyer.dev/blog/windows-flag-emojis</guid><description>A blog post by Ryan Geyer</description><pubDate>Wed, 29 Jan 2025 22:10:35 GMT</pubDate><content:encoded>&lt;p&gt;So, you thought you&apos;d be fancy and use cute little flag emojis in your UI as extra decoration to go along with country/language options.
It&apos;s perfect; it provides an extra visual cue to help users quickly find the country they&apos;re looking for, and it just looks nice.&lt;/p&gt;
&lt;p&gt;You can even dynamically derive country flag emojis from region codes with a simple code snippet, super cool!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// The offset to shift the ASCII code of each character in an ISO country code string by
// to derive the equivalent flag emoji for that code.
const EMOJI_CHARACTER_OFFSET = 127397;

const getEmojiForCountryCode = (countryCode: string) =&amp;gt;
  String.fromCodePoint(
    ...countryCode
      .toUpperCase()
      .split(&apos;&apos;)
      .map((char) =&amp;gt; char.charCodeAt(0) + EMOJI_CHARACTER_OFFSET),
  );

// &quot;en-US&quot;
const currentLanguageCode = navigator.language;
// &quot;US&quot;
const currentCountryCode = currentLanguageCode.split(&quot;-&quot;)[1];
// &quot;🇺🇸&quot;
getEmojiForCountryCode(currentCountryCode);
// &quot;🇫🇷&quot;
getEmojiForCountryCode(&quot;FR&quot;);
// &quot;🇸🇪&quot;
getEmojiForCountryCode(&quot;SE&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Life is good.&lt;/p&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;h2&gt;A bug report comes in.&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Some customers are saying the flag emojis are showing up as weird misaligned letters? I can&apos;t recreate it on my machine.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Huh?&lt;/p&gt;
&lt;p&gt;Maybe they&apos;re using an ancient machine or extremely outdated browser which doesn&apos;t support rendering emojis?&lt;/p&gt;
&lt;p&gt;No. After gathering more information, we find they&apos;re using the latest version of Chrome on a Windows machine.&lt;/p&gt;
&lt;p&gt;What on earth is happening? Out of desperation I asked ChatGPT. It was useless as usual.&lt;/p&gt;
&lt;p&gt;After doing more digging than I feel like I should have needed to, I found my answer: it appears that due to concerns about the fact that
acknowledging the existence of certain countries can be perceived as a nominally political stance,
Microsoft has opted to just avoid the issue altogether by not including country flag emojis in Windows&apos; system font.&lt;/p&gt;
&lt;p&gt;Problem solved! Can you imagine if, &lt;em&gt;*gasp*&lt;/em&gt;, your computer could render a Taiwanese 🇹🇼 or Palestinian 🇵🇸 flag? The horror!&lt;/p&gt;
&lt;p&gt;This seems like a Big Choice, yet there is very little official documentation acknowledging it, mainly just confused people like me posting
on StackOverflow trying to figure out what&apos;s going on.&lt;/p&gt;
&lt;p&gt;So, what does Windows do with flag emojis then? It just renders the letters of the country&apos;s ISO code, usually in an extremely ugly and poorly-aligned manner.&lt;/p&gt;
&lt;p&gt;&amp;lt;figure&amp;gt;
&amp;lt;img src=&quot;/assets/blog/windows-flag-emojis/gb-flag-emojis.png&quot; alt=&quot;A graphic showing how the British flag emoji is displayed on different platforms. It is rendered as an icon of the British flag as expected on Android, macOS/iOS, and Firefox on Windows, but is rendered as just the letters &apos;GB&apos; on Windows.&quot;&amp;gt;
&amp;lt;figcaption&amp;gt;Source: &amp;lt;cite&amp;gt;&amp;lt;a href=&quot;https://fullystacked.net/using-emoji-on-the-web/&quot;&amp;gt;Using emoji on the web - Fully Stacked&amp;lt;/a&amp;gt;&amp;lt;/cite&amp;gt;&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;You may have noted from the image above that Firefox on Windows &lt;em&gt;doesn&apos;t&lt;/em&gt; have this issue. That&apos;s because Mozilla stepped up by shipping their
own custom flag emojis with Firefox to fill the gap that Windows left.&lt;/p&gt;
&lt;p&gt;Why wouldn&apos;t Chrome do the same thing?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;... We currently have no plan to ship our own emoji font alongside the browser, hence marking as WontFix, works as intended. Alternatively, the website that you&apos;re visiting may consider providing an emoji font that has coverage for these flags.
&amp;lt;cite&amp;gt;-- &lt;a href=&quot;https://issues.chromium.org/issues/40766658&quot;&gt;Google Employee, May 21, 2021&lt;/a&gt;&amp;lt;/cite&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Oh. Okay.&lt;/p&gt;
&lt;p&gt;I&apos;m of two minds about Google&apos;s position here: on the one hand, I respect stubbornly refusing to cover for another corporation&apos;s BS which shouldn&apos;t be your problem.&lt;/p&gt;
&lt;p&gt;However, obviously Firefox was able to do it without too much trouble, and Chrome on Windows represents a massive userbase who is forced to live with the
fallout from this decision.&lt;/p&gt;
&lt;p&gt;This means a worse experience for developers who have to account for this garbage, and a worse experience for users because
rendering flag emojis consistently on all devices now requires JavaScript to detect flag emoji support and/or downloading image/font assets which they shouldn&apos;t need in the first place.&lt;/p&gt;
&lt;h2&gt;What you can do about it&lt;/h2&gt;
&lt;p&gt;If you absolutely need flag emojis, your two options are:&lt;/p&gt;
&lt;h3&gt;Replace live emojis with hard-coded image/SVG icons&lt;/h3&gt;
&lt;p&gt;Using hard-coded images is probably appropriate in cases where your needs are pretty simple, but I feel like it would become a huge pain if you&apos;re doing
something more dynamic where you won&apos;t know exactly which flag icons you&apos;ll need until runtime. In this case, I could imagine that using a big SVG spritesheet
might work decently well, but I can&apos;t find any good existing open source examples of that approach. Adding that to my list of open source projects I could do but probably won&apos;t get around to.&lt;/p&gt;
&lt;h3&gt;Polyfill the flag emojis with a custom font&lt;/h3&gt;
&lt;p&gt;Adding a custom polyfill font is interesting. The &lt;a href=&quot;https://github.com/mozilla/twemoji-colr&quot;&gt;Twemoji font&lt;/a&gt; is popular for this (it&apos;s what Mozilla uses in Firefox),
and you can use the &lt;code&gt;unicode-range&lt;/code&gt; CSS property on the font face declaration to only use the subset of the font which covers the flag emojis
(although you&apos;ll still be on the hook for downloading the entire 1.4MB font file).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@font-face {
  font-family: Twemoji;
  unicode-range: U+1F1E6-1F1FF, U+1F3F4, U+E0062-E0063, U+E0065, U+E0067, U+E006C, U+E006E, U+E0073-E0074, U+E0077, U+E007F;
  src: url(&quot;/fonts/twemoji.ttf&quot;) format(&quot;ttf&quot;);
}

:root {
  font-family: Twemoji, &quot;My Main Font&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I haven&apos;t used it, but &lt;a href=&quot;https://github.com/talkjs/country-flag-emoji-polyfill&quot;&gt;this Country Flag Emoji Polyfill library is taking a pretty nicely balanced approach to the problem&lt;/a&gt;.
It uses JavaScript to detect whether the browser supports country flag emojis, and will only load in the polyfill font if necessary. It also uses a custom subset of the
Twemoji font with only the flag emoji characters, significantly reducing the font download size. I wish their code wasn&apos;t so reliant on hard-coded CDN urls, but at a minimum
it seems like a great starting point if you need something like it.&lt;/p&gt;
&lt;p&gt;On that note...&lt;/p&gt;
&lt;h3&gt;How do you programmatically detect country flag emoji support?&lt;/h3&gt;
&lt;p&gt;It&apos;s dumber than you think: you just draw an emoji on a canvas and use brute force to guess at whether it was rendered correctly.&lt;/p&gt;
&lt;p&gt;My approach was to draw an American flag emoji and then look at the pixels to see if any of them contained a non-grayscale color.
This works because the Windows fallback emoji is just black/white text, so as long as we test against a flag which isn&apos;t supposed to be grayscale,
we should be able to tell pretty easily whether it was rendered correctly or not.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const getDoesBrowserSupportFlagEmojis = (): boolean =&amp;gt; {
  const canvas = document.createElement(&apos;canvas&apos;);
  canvas.height = 1;
  canvas.width = 1;
  const ctx = canvas.getContext(&apos;2d&apos;);
  if (!ctx) {
    return false;
  }
  ctx.font = `${canvas.height}px sans-serif`;
  const flagEmoji = &apos;🇺🇸&apos;;
  ctx.fillText(flagEmoji, 0, canvas.height);
  // Read the canvas pixels and search for any non-grayscale pixels. We used an american flag
  // emoji so there should be some red or blue in there if the browser rendered the flag emoji correctly.
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
  for (let i = 0; i &amp;lt; imageData.length; i += 4) {
    if (imageData[i + 3] === 0) {
      // Skip transparent pixels
      continue;
    }
    if (imageData[i] !== imageData[i + 1] || imageData[i] !== imageData[i + 2]) {
      // A pixel is grayscale if all three color channels are the same. If any pixel is not grayscale, that means the browser
      // rendered the flag emoji image instead of the fallback text, so we can return true.
      return true;
    }
  }

  return false;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the project I was working on where we encountered this issue, we decided to not bother with fallbacks and just hide the emoji altogether if we
detect that the browser doesn&apos;t support it. It&apos;s disappointing but it&apos;s fine.&lt;/p&gt;
&lt;p&gt;I think the biggest lesson here is that you should never make flag emojis an integral part of your site&apos;s design, or you will suffer. In an ideal world,
this would be fixed someday, but it&apos;s been years with no movement so I&apos;m not holding my breath. Thanks, Microsoft! 🇹🇼&lt;/p&gt;
</content:encoded><language>en-us</language><author>Ryan Geyer</author></item><item><title>What&apos;s new in hover-video-player v1.3.0</title><link>https://geyer.dev/blog/hover-video-player-1_3_0</link><guid isPermaLink="true">https://geyer.dev/blog/hover-video-player-1_3_0</guid><description>A blog post by Ryan Geyer</description><pubDate>Thu, 18 Jul 2024 15:43:28 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/Gyanreyer/hover-video-player&quot;&gt;hover-video-player&lt;/a&gt; v1.3.0 is out! This is an exciting one
as it finally resolves one of the most common requests I get on both this project and &lt;a href=&quot;https://github.com/Gyanreyer/react-hover-video-player&quot;&gt;react-hover-video-player&lt;/a&gt;: &quot;how do I use a YouTube video/Vimeo video/m3u8 stream?&quot;&lt;/p&gt;
&lt;p&gt;The answer has long been, &quot;sorry, you can&apos;t.&quot; The &lt;code&gt;hover-video-player&lt;/code&gt; component is built entirely on native
&lt;code&gt;HTMLVideoElement&lt;/code&gt; APIs, and I am not going to do custom work just to accomodate some other service&apos;s proprietary API (or in the case of m3u8, add an entire extra library just to support one media type).&lt;/p&gt;
&lt;p&gt;Then I stumbled upon Mux&apos;s excellent &lt;a href=&quot;https://github.com/muxinc/media-elements/&quot;&gt;media-elements&lt;/a&gt;, a collection of
custom element libraries which provide &lt;code&gt;HTMLMediaElement&lt;/code&gt;-compatible APIs for tons of different media sources,
including basically every one I&apos;ve ever been asked about. This set off a light bulb: in theory,
if these custom elements implement all of the media element APIs which &lt;code&gt;hover-video-player&lt;/code&gt; relies on,
they could probably be used in place of a &lt;code&gt;video&lt;/code&gt; element.&lt;/p&gt;
&lt;p&gt;This required a small amount of additional work to support due to a handful of things relying on the assumption
that we&apos;re always working with a &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt;, but it wasn&apos;t too bad. And it works!&lt;/p&gt;
&lt;p&gt;I will note that this feature isn&apos;t flawless; these custom elements do their best, but they usually don&apos;t perfectly
mimick all of an &lt;code&gt;HTMLVideoElement&lt;/code&gt;&apos;s behavior, and some of &lt;code&gt;hover-video-player&lt;/code&gt;&apos;s APIs will most likely
have weird interactions as a result. That&apos;s a price I&apos;m willing to pay though; just supporting these alternative
media sources at all is a big win which should hopefully help give people more options.&lt;/p&gt;
&lt;p&gt;Here&apos;s an example of how it works:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script
  type=&quot;module&quot;
  src=&quot;https://cdn.jsdelivr.net/npm/youtube-video-element@1&quot;
&amp;gt;&amp;lt;/script&amp;gt;

&amp;lt;hover-video-player&amp;gt;
  &amp;lt;youtube-video
    src=&quot;https://www.youtube.com/watch?v=aqz-KE-bpKQ&quot;
    playsinline
    muted
  &amp;gt;&amp;lt;/youtube-video&amp;gt;
&amp;lt;/hover-video-player&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As mentioned earlier, this feature gets requested heavily for my &lt;code&gt;react-hover-video-player&lt;/code&gt; library as well,
but React is very unfriendly when it comes to composing/controlling child elements like this. I think it could be done,
but certainly not very elegantly. Since React has finally started coming around on web component support,
I think I will plan to just direct people toward &lt;code&gt;hover-video-player&lt;/code&gt; for now, but I&apos;ll keep thinking about it.&lt;/p&gt;
&lt;p&gt;Happy hovering!&lt;/p&gt;
</content:encoded><language>en-us</language><author>Ryan Geyer</author></item><item><title>Using (almost) pure CSS to make fancy scroll-driven image sequence animations</title><link>https://geyer.dev/blog/css-image-sequence-animations</link><guid isPermaLink="true">https://geyer.dev/blog/css-image-sequence-animations</guid><description>A blog post by Ryan Geyer</description><pubDate>Thu, 05 Dec 2024 22:39:46 GMT</pubDate><content:encoded>&lt;p&gt;We&apos;ve all seen it. Some new Apple product gets announced and you go to the product page to check it out. As you scroll, you get blasted in the face by a flashy highly-complex scroll-driven animation.
Apple isn&apos;t the only company that uses this effect, but they really love it in particular. I was able to find one on their site in under a minute on the &lt;a href=&quot;https://www.apple.com/airpods-pro/&quot;&gt;AirPods Pro 2 page&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&amp;lt;figure&amp;gt;
&amp;lt;video src=&quot;/assets/blog/css-image-sequence-animations/airpods-product-page-example.mp4&quot; muted autoplay loop&amp;gt;&amp;lt;/video&amp;gt;
&amp;lt;figcaption&amp;gt;
A screen recording showing an animation of a pair of AirPods twisting and flying out towards the screen as you scroll.
&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;If you&apos;re a web dev like me, you&apos;ve probably wondered how that effect works at least once.
Maybe you&apos;ve looked it up and were slightly underwhelmed like I was to discover that it&apos;s all pretty much smoke and mirrors; these effects are usually just a pre-baked video split into individual images for each frame, and then JavaScript checks your scroll position to determine which frame to show, usually by rendering to a canvas.&lt;/p&gt;
&lt;p&gt;The most disappointing part of this is the performance implications: anyone who knows a thing or two about video compression will be able to tell you that the file size difference between a nice 5-second mp4 file and dozens of individual PNGs is eye-popping to say the least.&lt;/p&gt;
&lt;p&gt;My partner &lt;a href=&quot;https://jesskeoshian.com&quot;&gt;Jess&lt;/a&gt; recently had me make some updates to their site including, you guessed it, adding a scroll-driven image sequence animation to the homepage.
I was excited to finally have a chance to take a crack at this effect, and I think it turned out great!&lt;/p&gt;
&lt;p&gt;&amp;lt;figure&amp;gt;
&amp;lt;video src=&quot;/assets/blog/css-image-sequence-animations/jess-cb-phone-img-seq-demo.mp4&quot; muted autoplay loop&amp;gt;&amp;lt;/video&amp;gt;
&amp;lt;figcaption&amp;gt;
A screen recording of my partner Jess&apos; updated website, showing an animation of a phone with the Crate and Barrel app on it twisting as you scroll.
&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;I said the magic behind how these image sequence animations work seemed a little underwhelming,
but actually trying to implement it myself was an interesting challenge
and I learned a ton as I continued to refine it and improve its performance.
After a lot of experimenting, I have ended up landing on an implementation which I haven&apos;t really seen anyone else
try before that is faster, more effecient, and can be implemented in almost pure CSS.
I&apos;m pretty excited to share it!&lt;/p&gt;
&lt;h2&gt;Image sequences, the Apple way&lt;/h2&gt;
&lt;p&gt;First, let&apos;s talk about how most sites implement this effect. Pretty much every implementation I&apos;ve seen, including the Airpods one shown above, take a fairly brute force approach:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Preload every single individual frame image file you can up front, showing some loading state or placeholder in the meantime.&lt;/li&gt;
&lt;li&gt;Once the images are loaded, start watching for scroll events. As the scroll position changes, calculate
which frame of the animation should be shown for that position and render that image to a canvas.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This definitely works! But in my opinion, it&apos;s flawed.&lt;/p&gt;
&lt;h3&gt;Loading 65 individual image files at the same time is not great for performance.&lt;/h3&gt;
&lt;p&gt;I was shocked to discover that the Airpods animation is comprised of 65 PNGs weighing in at a total of 15.2MB.
That&apos;s so much data! If they converted the frames to a more efficient format like WebP, that alone could bring
the total weight down to 1.7MB, a massive nearly 90% reduction. However, there are still additional issues with this approach to consider.&lt;/p&gt;
&lt;p&gt;On top of download size, if you&apos;re serving the image assets over HTTP/1, browsers will be limited to a maximum number of
requests they can run in parallel, meaning you are all but guaranteed to run into a bottleneck that will cause the frames to take much longer to download and may
start impacting how other parts of the site load as well.&lt;/p&gt;
&lt;p&gt;Apple serves their images over HTTP/2 so that is less of a concern, but I have seen this mistake happen in the wild.
I once saw a site where even on a fiber internet connection, you would still get stuck with a loading progress bar
where you had to wait for maybe 30+ seconds before being able to interact with the site at all, just so they could have a fancy scroll-driven image sequence animation.
There is no world where that is worth the wait, no matter how cool the animation is, never ever ever. Completely unacceptable.&lt;/p&gt;
&lt;p&gt;Finally, my last point of contention is that all of this relies on quite a bit of JavaScript to manage loading the images, tracking your scroll position and calculating the current frame to display,
and rendering that frame to a canvas. JavaScript comes with added potential points of failure and is much more expensive per byte than HTML or CSS due to
added overhead required for parsing and execution. JavaScript can&apos;t always be avoided, but I&apos;d love to avoid relying on it as much as possible!&lt;/p&gt;
&lt;p&gt;I did observe that Apple uses one optimization technique worth noting: if you apply network throttling in your dev tools, the page still takes way too long to load in my opinion,
but you&apos;ll be able to see that they try to prioritize loading frames at major checkpoints in the animation first so they can start serving a degraded version of the animation as soon as possible. For instance, you can see that they prioritize loading the first frame, then the last frame, then the frame at 50%, then at 25% and 75%, and so on, slowly increasing the resolution so that you can start playing an extremely choppy and kinda ugly but still functional version of the animation sooner while you wait for the rest of the frames to come in.&lt;/p&gt;
&lt;h2&gt;What if we used spritesheets?&lt;/h2&gt;
&lt;p&gt;I studied game development in college where spritesheets are a very common concept, but for those unfamiliar,
a spritesheet is an asset optimization technique commonly used in game development for 2D games where you
pack a bunch of images into a single big image file that can be loaded all at once.&lt;/p&gt;
&lt;p&gt;&amp;lt;figure&amp;gt;
&amp;lt;img
src=&quot;/assets/blog/css-image-sequence-animations/airpods-spritesheet.webp&quot;
alt=&quot;The frames from the Airpods Pro animation shown above packed into a spritesheet.&quot;
class=&quot;shadow-2&quot;
/&amp;gt;
&amp;lt;figcaption&amp;gt;The frames from the Airpods Pro animation shown above packed into a spritesheet. Note that this asset has been scaled down so its file size is not representative.&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;Spritesheets have a few notable benefits for our purposes.&lt;/p&gt;
&lt;p&gt;First, when you pack multiple images into one, the final image will usually be able to be compressed a little smaller than the total
combined size of the individual files.
In the case of the Airpods Pro animation, converting each frame to WebP resulted in a total
combined size of 1.7MB, but packing all of those into a WebP spritesheet image brings the size down further to 1.5MB.
That&apos;s a relatively modest 10% improvement, but still significant especially at scale.&lt;/p&gt;
&lt;p&gt;Second, we&apos;re able to load all of the frames for the animation in a single network request, so concerns about HTTP/1 request bottlenecks
are significantly lessened. Being a single image also means you could even add a preload &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tag
to the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; to get things loading as soon as possible.&lt;/p&gt;
&lt;h3&gt;Playing through a spritesheet with CSS&lt;/h3&gt;
&lt;p&gt;Using a spritesheet means we can use some tricks to play through the animation with almost pure CSS.
This only requires two elements: an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag with the spritesheet loaded, and a wrapper element with &lt;code&gt;overflow: clip&lt;/code&gt; which will serve as a window
into the current frame we want to display from the spritesheet animation. At that point, all we have to do is shift the &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;&apos;s position around
so the current desired frame is visible in the parent&apos;s window, and we have a little animation going!&lt;/p&gt;
&lt;p&gt;Here&apos;s the HTML and CSS for my implementation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- It&apos;s important that an aspect-ratio is defined on the img-sequence element which matches the
      exact aspect ratio of one cell in the spritesheet grid. --&amp;gt;
&amp;lt;img-sequence style=&quot;--sprite-count: 65; --column-count: 11; --row-count: 6; aspect-ratio: 1440/810&quot;&amp;gt;
  &amp;lt;img src=&quot;/img/my-spritesheet.webp&quot; alt=&quot;&quot; fetchpriority=&quot;high&quot; class=&quot;spritesheet&quot;&amp;gt;
&amp;lt;/img-sequence&amp;gt;
&amp;lt;style&amp;gt;
  img-sequence {
    /* The wrapper img-sequence tag will serve as a window into a single frame of the spritesheet,
        so we need the child &amp;lt;img&amp;gt; tag to be able to be absolutely positioned relative to this tag
        and to clip any overflow. */
    position: relative;
    overflow: clip;
    display: block;
  }

  img-sequence .spritesheet {
    display: block;

    position: absolute;
    top: 0;
    left: 0;
    /* Size the spritesheet so a single cell fits in the dimensions of the parent element */
    width: calc(100% * var(--column-count));
    height: calc(100% * var(--row-count));

    /* Progress in the animation is driven by a --progress CSS variable
       which should be a number from 0-1.
       We will use that to calculate the index of the current cell in the spritesheet grid
       which best matches that percentage of the way into the animation. */
    --cell: round(
      clamp(0, var(--progress), 1) * (var(--sprite-count) - 1) + 1,
      1
    );
    /* Now we can derive the row and column numbers for the current cell in the spritesheet grid. */
    --row: round(up, calc(var(--cell) / var(--column-count)), 1);
    --column: calc(var(--cell) - (var(--row) - 1) * var(--column-count));

    /* Translate the image to the appropriate position to display the current row and column inside the
        bounds of the parent img-sequence element.
        Using translate3d for hardware acceleration. */
    transform: translate3d(
      calc(-100% * (var(--column, 1) - 1) / var(--column-count, 1)),
      calc((var(--row, 1) - 1) * -100% / var(--row-count, 1)),
      0
    );
  }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All we need to do at this point is set a &lt;code&gt;--progress&lt;/code&gt; variable on the &lt;code&gt;&amp;lt;img-sequence&amp;gt;&lt;/code&gt; element and it will shift the image around to show the correct frame
which is that percent of the way into the animation.
I am currently using some JavaScript to calculate the scroll percentage which drives this progress value, so that is where my (almost) caveat in the title comes in.&lt;/p&gt;
&lt;p&gt;Here&apos;s a rough example of how you would make the animation play with JavaScript:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;window.addEventListener(&quot;scroll&quot;, () =&amp;gt; {
  // Play through the animation from start to finish as the
  // user scrolls down by half of the window&apos;s height
  const progress = window.scrollY / (window.innerHeight / 2);
  imageSequence.style.setProperty(&quot;--progress&quot;, String(progress));
}, {
  // Don&apos;t forget to make your scroll listeners passive!
  passive: true
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, in the near future it should theoretically be possible to make this work with pure CSS using &lt;code&gt;@property&lt;/code&gt; and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timeline/scroll&quot;&gt;scroll timelines&lt;/a&gt;!
There is &lt;a href=&quot;https://github.com/flackr/scroll-timeline&quot;&gt;a polyfill for scroll timelines&lt;/a&gt; which I attempted to use, but I was fighting with
too many major inconsistencies in behavior between the real version running in Chrome and the polyfilled version running in Firefox and Safari. I&apos;m going to wait for things to get
a little more baked before I try that again.&lt;/p&gt;
&lt;p&gt;Regardless, here&apos;s a potential vision for how the image sequence could be played with a scroll timeline:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Register --progress variable&apos;s type as a number so
   the browser knows how to transition the value */
@property --progress {
  syntax: &quot;&amp;lt;number&amp;gt;&quot;;
  inherits: true;
  initial-value: 0;
}

/* The scroll timeline will play through this
   animation to transition progress from 0 to 1 */
@keyframes imgSequenceProgress {
  from {
    --progress: 0;
  }

  to {
    --progress: 1;
  }
}

img-sequence {
  animation-name: imgSequenceProgress;
  animation-duration: 1ms;
  animation-direction: alternate;
  /* Play through the animation based on the
     scroll position of the element */
  animation-timeline: scroll(block nearest);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Isn&apos;t that awesome? CSS is awesome.&lt;/p&gt;
&lt;p&gt;I will always relish an opportunity to keep my reliance on JavaScript to a minimum.
I expect this approach will also provide some significant low-level performance benefits in terms of both memory usage and CPU usage compared to rendering to a canvas,
but I haven&apos;t spent enough time profiling to be able to confidently speak to that.&lt;/p&gt;
&lt;h4&gt;Downsides&lt;/h4&gt;
&lt;p&gt;Like everything, this approach still has tradeoffs worth acknowledging.&lt;/p&gt;
&lt;p&gt;First, it relies heavily on the &lt;code&gt;round()&lt;/code&gt; CSS function, which has &lt;a href=&quot;https://caniuse.com/mdn-css_types_round&quot;&gt;okay but not great browser support&lt;/a&gt; as of writing this. However, it is in all major modern browsers, and
the progressive enhancement story isn&apos;t terrible;
if a browser doesn&apos;t support &lt;code&gt;round()&lt;/code&gt;, the animation will just be stuck on the first frame as if it were a static image.&lt;/p&gt;
&lt;p&gt;The bigger flaw is that this simple implementation does not provide a good
experience while you wait for the spritesheet image to load, and a 1.5MB
image is definitely going to take some time to load for a lot of people. The one upside of using individual frames is that you can load and display the first frame a lot quicker while you wait for the rest to come in, but with a spritesheet, it&apos;s all or nothing.&lt;/p&gt;
&lt;p&gt;I ended up writing a simple &lt;code&gt;img-sequence&lt;/code&gt; web component which is initially hidden and fades in once the spritesheet image finishes loading
in an attempt to smooth things out a little bit, but it still leaves me
wanting. I could probably adjust the component further to display a small placeholder frame while we wait for the larger spritesheet to finish loading, but it hasn&apos;t felt necessary yet thankfully.&lt;/p&gt;
&lt;p&gt;Overall, even with these downsides, I am still extremely pleased with how well this works and would highly recommend it to others. CSS is becoming
extremely powerful, and it feels like this is only just beginning!&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Addenda&lt;/h2&gt;
&lt;p&gt;Some additional content which I couldn&apos;t find room for in the main article, but still wanted to touch on.&lt;/p&gt;
&lt;h3&gt;A1: Spritesheet asset prep tips and tools&lt;/h3&gt;
&lt;p&gt;I&apos;ve learned a lot in the process of building and continuing to refine
this project, so I wanted to share some of the tips I&apos;ve come up with and helpful scripts I&apos;ve written along the way.&lt;/p&gt;
&lt;h4&gt;You may not need as many frames as you think&lt;/h4&gt;
&lt;p&gt;This was a game-changing realization for me. Jess&apos; animation was initially
delivered to me as 127 frames; they had just exported a 4.2-second-long clip at 30fps, which seems like a natural thing to do. However, it&apos;s
important to remember that when an animation is scroll-driven, it&apos;s not
going to play the same way.&lt;/p&gt;
&lt;p&gt;I&apos;m going to drop a classic programmer &lt;strong&gt;&quot;it depends™&quot;&lt;/strong&gt; here, because every animation is probably going to need a slightly different treatment.&lt;/p&gt;
&lt;h5&gt;How small/subtle are the movements in your animation?&lt;/h5&gt;
&lt;p&gt;The less movement there is between each individual frame, the more frames you can probably afford to cut down.&lt;/p&gt;
&lt;h5&gt;Consider your pixel:frame ratio.&lt;/h5&gt;
&lt;p&gt;For Jess&apos; site, we wanted the animation to play over the course of scrolling a very small distance; I ended up measuring it to be from 0 to approximately 140 pixels down the page.
Nobody can scroll less than one pixel at a time, and I would argue almost nobody is going to painstakingly scroll down your page one single pixel at a time. In my opinion,
if you have more than one frame for every 2 pixels of scrollable area, you have too many frames.&lt;/p&gt;
&lt;p&gt;I found that cutting the frame count down by half from 127 to 64 still felt just as smooth as before, but with
obviously massive improvements in file size.&lt;/p&gt;
&lt;p&gt;I probably could have gone even further; the AirPods product page I&apos;ve been referencing
is way more aggressive than I would ever dare, as they&apos;re stretching 65 frames over a whopping 1200 pixels for a ~18.5 pixel:frame ratio.
When I pay attention, especially when scrolling slowly, I can see the jitter, but it&apos;s not as bad as I would have thought.&lt;/p&gt;
&lt;h4&gt;Trimming white space around frames&lt;/h4&gt;
&lt;p&gt;Every frame image that I received had some extra transparent padding around it. WebP is good at compressing these blank pixels so that they had basically no effect on file size, but it did make it difficult for me to position the image sequence exactly how I wanted on the page.&lt;/p&gt;
&lt;p&gt;I ended up writing a script which takes the path to a directory of frame images and
uses &lt;a href=&quot;https://sharp.pixelplumbing.com/&quot;&gt;&lt;code&gt;sharp&lt;/code&gt;&lt;/a&gt; to loop over every image, determine the closest dimensions
you can crop in to without cutting off content on any frame, and then applies that crop.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/Gyanreyer/jess/blob/main/scripts/crop-img-sequence-files.mjs&quot;&gt;Image cropping script source&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Building the spritesheet&lt;/h4&gt;
&lt;p&gt;I also wrote a script which takes the path to a directory of frame images and uses &lt;a href=&quot;https://sharp.pixelplumbing.com/&quot;&gt;&lt;code&gt;sharp&lt;/code&gt;&lt;/a&gt;
to resize and pack them into a single WebP spritesheet image file. It also outputs a helpful metadata file containing all of the relevant
information which is needed to make the animation work.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/Gyanreyer/jess/blob/main/scripts/make-spritesheet.mjs&quot;&gt;Spritesheet generation script source&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;My &lt;code&gt;&amp;lt;scroll-progress-region&amp;gt;&lt;/code&gt; web component&lt;/h4&gt;
&lt;p&gt;I glossed over how I&apos;m tracking scroll position. I created a &lt;code&gt;&amp;lt;scroll-progress-region&amp;gt;&lt;/code&gt; web component which tracks its scroll position
on the page and provides a &lt;code&gt;--scroll-pct&lt;/code&gt; CSS variable which its children can use. This is awesome because I&apos;m then able to
stay in CSS land to translate that &lt;code&gt;--scroll-pct&lt;/code&gt; into my image sequence&apos;s &lt;code&gt;--progress&lt;/code&gt; variable.&lt;/p&gt;
&lt;p&gt;I spent a decent amount of time toying around and doing math to figure out the exact formula I wanted for that translation, this
is where it ended up:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--progress: calc((var(--scroll-pct) * -1.05) + 1.05);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/Gyanreyer/jess/blob/main/src/_components/shared/scroll-progress-region.js&quot;&gt;&lt;code&gt;&amp;lt;scroll-progress-region&amp;gt;&lt;/code&gt; component source&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;My &lt;code&gt;&amp;lt;img-sequence&amp;gt;&lt;/code&gt; WebC component&lt;/h4&gt;
&lt;p&gt;Jess&apos; site is written using 11ty and WebC, so I wrote an &lt;code&gt;img-sequence&lt;/code&gt; WebC component which takes the path to a spritesheet file,
loads everything in, and writes the HTML for the image sequence exactly how I need it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;img-sequence @spritesheet=&quot;src/assets/path/to/spritesheet.webp&quot;&amp;gt;&amp;lt;/img-sequence&amp;gt;

&amp;lt;!-- output --&amp;gt;
&amp;lt;img-sequence style=&quot;--spritesheet-url: url(&apos;/img/jTOLRJLwjR-16128.webp&apos;); --sprite-count: 64; --column-count: 63; --row-count: 2; aspect-ratio: 256/510&quot;&amp;gt;
  &amp;lt;img
    src=&quot;/img/jTOLRJLwjR-16128.webp&quot;
    alt=&quot;&quot;
    fetchpriority=&quot;high&quot;
    class=&quot;spritesheet&quot;
  &amp;gt;
&amp;lt;/img-sequence&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WebC is pretty niche, but this may still serve as a good example that you can transfer to whatever your site is built with!&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/Gyanreyer/jess/blob/main/src/_components/shared/img-sequence.webc&quot;&gt;WebC &lt;code&gt;&amp;lt;img-sequence&amp;gt;&lt;/code&gt; component source&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;A2: So... why can&apos;t you use video files again?&lt;/h3&gt;
&lt;p&gt;I glossed over this question earlier, but I wanted to spend a little more
time talking through why image sequences are the de-facto way to
implement these animations for any curious enough. It definitely wasn&apos;t
immediately obvious to me at first!&lt;/p&gt;
&lt;p&gt;All of this image stuff seems like a lot of effort, and if serving an MP4 file would be so much more efficient than a bunch of individual image frames, why not do that?&lt;/p&gt;
&lt;p&gt;From my research, it seems the answer is that you technically &lt;em&gt;can&lt;/em&gt; use video. The simplest implementation of this
would be something like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;window.addEventListener(&quot;scroll&quot;, () =&amp;gt; {
  // Sets the video&apos;s time to a percent of the duration relative
  // to the percent of the window height that the user has scrolled
  video.currentTime = video.duration * window.scrollY / window.innerHeight
}, { passive: true })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will work... okay. However, there are some tradeoffs to this approach worth considering:&lt;/p&gt;
&lt;h4&gt;1. Transparency&lt;/h4&gt;
&lt;p&gt;Let&apos;s get the big one out of the way first: the most common and well-supported video format, h.264/mp4, doesn&apos;t support transparency. That&apos;s a pretty common requirement for these types of effects, so that alone may be a showstopper.&lt;/p&gt;
&lt;p&gt;More modern formats like WebM and the h.265 codec do support transparency, and browser support for those formats appears to be pretty good at this point, but
&lt;a href=&quot;https://css-tricks.com/overlaying-video-with-transparency-while-wrangling-cross-browser-support/&quot;&gt;this article covering how to wrangle all of the browsers into supporting video transparency&lt;/a&gt; does not make me feel good.&lt;/p&gt;
&lt;h4&gt;2. Rapidly scrubbing back and forth through a video is not the best way to use it&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;DISCLAIMER&lt;/strong&gt;: I am not a video encoding expert and will probably get specifics wrong on this point. But I &lt;em&gt;think&lt;/em&gt; I&apos;m mostly right here.&lt;/p&gt;
&lt;p&gt;This is an over-simplification, but video codecs like h.264 use techniques to save space by only
encoding data describing the changes between each frame instead of having to encode the full image of every individual frame.
This is great because it means you can skip encoding a lot of redundant information,
and it&apos;s still extremely fast to render during forward playback because advancing to the next frame is as simple as applying that
frame&apos;s changes to the current frame that you already have loaded.&lt;/p&gt;
&lt;p&gt;However, now let&apos;s imagine that you&apos;re not performing forward playback. If you drop into a random point in the video, you now have to go back and look at the previous frames
leading up to the current one to construct what the current frame should look like. That&apos;s a lot more work!&lt;/p&gt;
&lt;p&gt;This means that although rapidly scrubbing through a video&apos;s timeline may work, the user&apos;s device is going to
be putting in a ton more work decoding frames from scratch up to 60 times per second as the user scrolls.&lt;/p&gt;
&lt;p&gt;I haven&apos;t done enough profiling to confidently say that this is a significant issue, but it&apos;s something that I do think is worth considering.&lt;/p&gt;
</content:encoded><language>en-us</language><author>Ryan Geyer</author></item><item><title>Statically-Typed Scoped Data Attributes with Vanilla Extract and React</title><link>https://geyer.dev/blog/statically-typed-data-attributes</link><guid isPermaLink="true">https://geyer.dev/blog/statically-typed-data-attributes</guid><description>A blog post by Ryan Geyer</description><pubDate>Sat, 05 Aug 2023 05:17:36 GMT</pubDate><content:encoded>&lt;p&gt;Over the last 2 years or so, Waymark has been undergoing an effort to convert our frontend codebase to TypeScript. We have a LOT of code and a fairly small team, so it&apos;s been a gradual process, but
we&apos;ve already seen huge benefits for developer experience.&lt;/p&gt;
&lt;p&gt;In the process, we have also begun shifting our styles from runtime CSS-in-JS to using &lt;a href=&quot;https://vanilla-extract.style/&quot;&gt;Vanilla Extract&lt;/a&gt;, a very cool library which
allows you to write type-safe and scoped CSS in TypeScript that is then converted to static CSS files at build time. Because the styles are written in TypeScript, this gives us flexibility to dynamically generate CSS, the biggest benefit of runtime CSS-in-JS,
but without the performance hit at runtime!&lt;/p&gt;
&lt;p&gt;In many places in our codebase, we like to represent state in our HTML using data attributes. Here&apos;s what an example might look like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// MyComponent.tsx

type Status = &quot;init&quot; | &quot;loading&quot; | &quot;success&quot; | &quot;error&quot;;

export default function MyComponent(){
  const [status, setStatus] = useState&amp;lt;Status&amp;gt;(&quot;init&quot;);

  /* Imagine there&apos;s some code here which does something and updates `status` */

  return (
    &amp;lt;div data-status={status}&amp;gt;
      {status}
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/* MyComponent.css */
[data-status=&quot;init&quot;], [data-status=&quot;loading&quot;] {
  color: gray;
}

[data-status=&quot;success&quot;] {
  color: green;
}

[data-status=&quot;error&quot;] {
  color: red;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This has a few benefits:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It&apos;s a great way to expose internal component state details for unit tests!&lt;/li&gt;
&lt;li&gt;Data attributes can make it easier to see what&apos;s going on in your HTML at a glance when debugging, and it&apos;s also very easy to just manually edit the attribute&apos;s value in your dev tools if you need to manually see what a different state looks like.&lt;/li&gt;
&lt;li&gt;Representing different possible state values on a single attribute tends to feel cleaner than having to manage multiple unique class names for each possible state value and making sure only one is applied at a time.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;However, this approach can come with some downsides:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;There are certain data attribute names which you will probably want to reach for a lot, and they can very easily collide if they aren&apos;t scoped properly. For instance, the &lt;code&gt;data-status&lt;/code&gt; attribute used in the example above may end up being a very common attribute name
that you want to use in multiple different components. This will probably mostly be fine, but the more components that use that attribute name, the more you are opening yourself up to the possibility that one element with a &lt;code&gt;data-status&lt;/code&gt; attribute may end up getting wrapped by another element with a &lt;code&gt;data-status&lt;/code&gt; attribute
and then you may start experiencing some strange unexpected interactions.&lt;/li&gt;
&lt;li&gt;By default, attribute names and values are not type-safe. If you misspell an attribute name or value in your HTML or CSS, or if something gets renamed, there&apos;s very little protection to prevent that bug from sneaking by unnoticed! Of course, this is just a typical risk you are usually taking on when working with HTML and CSS, but we&apos;re writing everything in TypeScript so it feels like we should be able to do better!&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So, this leads me to the neat little &lt;code&gt;makeDataAttribute&lt;/code&gt; util function that I created to solve both of these problems. It allows us to make data attributes both type-safe AND scoped to the components that they&apos;re used in!&lt;/p&gt;
&lt;h2&gt;How it works&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;makeDataAttribute&lt;/code&gt; function takes the attribute name as a param and a generic type representing acceptable values for the attribute. It returns a callback which we can then use to apply the data attribute in our styles and components.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const dataStatus = makeDataAtribute&amp;lt;&quot;init&quot; | &quot;loading&quot; | &quot;success&quot; | &quot;error&quot;&amp;gt;(&quot;data-status&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The returned callback takes an attribute value and returns an object with a scoped version of the attribute name as a key and the provided attribute value as the value.
The returned object can be spread in JSX to apply the attribute to an element.&lt;/p&gt;
&lt;p&gt;Note that the attribute name will get a small unique hash appended to the end to make sure it doesn&apos;t collide with other attribute names.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// { data-status-asf12: &apos;init&apos; }
console.log({ ...dataStatus(&quot;init&quot;) });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This object also has a custom &lt;code&gt;toString()&lt;/code&gt; method defined on its prototype so that when the object
is used as a string, it will be formatted as a valid CSS selector for the attribute + value combo.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// [data-status-asf12=&quot;loading&quot;]
console.log(`${dataStatus(&quot;loading&quot;)}`);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&apos;s what it all looks like in action:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// MyComponent.css.ts

import { style } from &apos;@vanilla-extract/css&apos;;
import { makeDataAttribute } from &apos;@libs/shared-ui-styles&apos;;

import type { Status } from &apos;./MyComponent&apos;;

export const dataStatus = makeDataAttribute&amp;lt;Status&amp;gt;(&quot;data-status&quot;);

export const myComponent = style({
  selectors: {
    [`&amp;amp;:is(${dataStatus(&quot;init&quot;)}, ${dataStatus(&quot;loading&quot;)})`]: {
      color: &quot;gray&quot;,
    },
    [`&amp;amp;${dataStatus(&quot;success&quot;)}`]: {
      color: &quot;green&quot;,
    },
    [`&amp;amp;${dataStatus(&quot;error&quot;)}`]: {
      color: &quot;red&quot;,
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// MyComponent.tsx

import { myComponent, dataStatus } from &apos;./MyComponent.css&apos;;

export type Status = &quot;init&quot; | &quot;loading&quot; | &quot;success&quot; | &quot;error&quot;;

export default function MyComponent() {
  const [status, setStatus] = useState&amp;lt;Status&amp;gt;(&quot;init&quot;);

  /* Insert code that does something to update the status here */

  return (
    &amp;lt;div className={myComponent} {...dataStatus(status)}&amp;gt;
      {status}
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here&apos;s what the HTML and CSS output from this will look like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div className=&quot;qsr7lu14&quot; data-status-asf12=&quot;init&quot;&amp;gt;
  init
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;.qsr7lu14:is([data-status-asf12=&quot;init&quot;], [data-status-asf12=&quot;loading&quot;]) {
  color: gray;
}

.qsr7lu14[data-status-asf12=&quot;success&quot;] {
  color: green;
}

.qsr7lu14[data-status-asf12=&quot;error&quot;] {
  color: red;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I will admit that this API is not perfect; it doesn&apos;t feel perfectly intuitive to me without requiring some explanation,
and the fact that there is a meaningful difference between whether you are using the attribute
as a string for a selector or as an object to spread on an element in JSX doesn&apos;t feel amazing.
I certainly wouldn&apos;t reach for something like this in a smaller project, but in a big complex app like Waymark,
I think the benefits from this approach do still outweigh the downsides.&lt;/p&gt;
&lt;h2&gt;The code&lt;/h2&gt;
&lt;p&gt;Now that I&apos;ve showcased what it is and how it works, let&apos;s dig into the code!&lt;/p&gt;
&lt;p&gt;Note: if you have ideas for how to improve this further let me know. Even better, if want to publish this as an open source library, be my guest!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { addFunctionSerializer } from &apos;@vanilla-extract/css/functionSerializer&apos;;

export function makeDataAttribute&amp;lt;TAttrValue extends string | number | boolean = never&amp;gt;(
  name: string,
) {
  // Add a random 5-digit string to the end of the attribute name to avoid collisions
  const scopedName = `${name}-${Math.random().toString(36).substring(2, 7)}`;

  const dataAttributeValueGetter = _dataAttribute&amp;lt;TAttrValue&amp;gt;(scopedName);

  // Use Vanilla Extract&apos;s magical addFunctionSerializer so we can guarantee that
  // the scoped data attribute value getter callback we&apos;re creating here
  // is able to be exported from a Vanilla Extract .css.ts file
  // in a way that can be used at runtime
  addFunctionSerializer(dataAttributeValueGetter, {
    importName: &apos;_dataAttribute&apos;,
    importPath: &apos;absolute-import-path/to-this-file&apos;,
    args: [[scopedName]],
  });

  return dataAttributeValueGetter;
}

export function _dataAttribute&amp;lt;TAttrValue extends string | number | boolean&amp;gt;(
  attributeName: string,
) {
  const getDataAttributeValue = (attributeValue: TAttrValue): {
    [key: string]: `${TAttrValue}`;
  } =&amp;gt; Object.create(Object.prototype, {
    [attributeName]: {
      value: `${attributeValue}`,
      enumerable: true,
    },
    // Custom toString method to return a string representing the data attribute as a CSS selector
    toString: {
      value: () =&amp;gt; `[${attributeName}=&quot;${attributeValue}&quot;]`,
    },
  });

  // Add an `attributeName` property to the function so we can access it at runtime;
  // this will mainly be useful for debugging/testing purposes
  Object.defineProperty(getDataAttributeValue, &apos;attributeName&apos;, {
    value: attributeName,
  });

  return getDataAttributeValue as typeof getDataAttributeValue &amp;amp; {
    attributeName: string;
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Okay, there is a lot packed in here! Let&apos;s break it down.&lt;/p&gt;
&lt;p&gt;First, we should discuss what exactly Vanilla Extract&apos;s &lt;code&gt;addFunctionSerializer&lt;/code&gt; method is, as that is what makes all of this possible.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://vanilla-extract.style/documentation/api/add-function-serializer/&quot;&gt;&lt;code&gt;addFunctionSerializer&lt;/code&gt;&lt;/a&gt; is a special utility function which is intended only for extremely advanced use cases which usually only matter to library authors.
That&apos;s how you know you&apos;re doing something fun!&lt;/p&gt;
&lt;p&gt;Because Vanilla Extract is compiled down to CSS at build time, it will normally strip away any functions defined in your &lt;code&gt;.css.ts&lt;/code&gt; files. This means that you can&apos;t normally export a function from a &lt;code&gt;.css.ts&lt;/code&gt; file and import it into a different file that would use it at runtime, because Vanilla Extract doesn&apos;t know how to serialize that function. However, you may be able to see where this is going... &lt;code&gt;addFunctionSerializer&lt;/code&gt; enables you to help tell Vanilla Extract both that a function should be serialized and exactly how to serialize it so that it &lt;em&gt;can&lt;/em&gt; be used at runtime!&lt;/p&gt;
&lt;p&gt;So, to wrap our heads around what&apos;s going on here, we need to understand that the contents of the &lt;code&gt;makeDataAttribute&lt;/code&gt; method in this file will only ever be run at Vanilla Extract&apos;s build-time when generating CSS. So, we need to define and export a second &lt;code&gt;_dataAttribute&lt;/code&gt; function which represents the code that we want to be able to be executed at both build-time AND runtime, and then use &lt;code&gt;addFunctionSerializer&lt;/code&gt; to point Vanilla Extract in the right direction on how to serialize the callback returned by &lt;code&gt;makeDataAttribute&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Let&apos;s focus on &lt;code&gt;_dataAttribute&lt;/code&gt; first.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export function _dataAttribute&amp;lt;TAttrValue extends string | number | boolean&amp;gt;(
  attributeName: string,
) {
  const getDataAttributeValue = (attributeValue: TAttrValue): {
    [key: string]: `${TAttrValue}`;
  } =&amp;gt; Object.create(Object.prototype, {
    [attributeName]: {
      value: `${attributeValue}`,
      enumerable: true,
    },
    // Custom toString method to return a string representing the data attribute as a CSS selector
    toString: {
      value: () =&amp;gt; `[${attributeName}=&quot;${attributeValue}&quot;]`,
    },
  });

  // Add an `attributeName` property to the function so we can access it at runtime;
  // this will mainly be useful for debugging/testing purposes
  Object.defineProperty(getDataAttributeValue, &apos;attributeName&apos;, {
    value: attributeName,
  });

  return getDataAttributeValue as typeof getDataAttributeValue &amp;amp; {
    attributeName: string;
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, the main thing this function does is create a &lt;code&gt;getDataAttributeValue&lt;/code&gt; callback which takes an attribute value and returns an object
that we can use to apply the attribute in our style and component code.&lt;/p&gt;
&lt;p&gt;You don&apos;t see &lt;code&gt;Object.create()&lt;/code&gt; a whole lot these days, but I opted for that as it provides a good way to be able to control exactly which properties
on the object should be enumerable or not. This is important because we want to be able to spread the object on a JSX element&apos;s props without including unwanted
junk like our custom &lt;code&gt;toString()&lt;/code&gt; method.&lt;/p&gt;
&lt;p&gt;Alright, now let&apos;s tackle &lt;code&gt;makeDataAttribute&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const scopedName = `${name}-${Math.random().toString(36).substring(2, 7)}`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, we are simply taking the provided data attribute name and appending a random 5-character string to the end to make it unique. This is just using the classic quick and dirty trick of converting a random number to a base-36 string, as this tends to produce pretty decent short unique ID strings.&lt;/p&gt;
&lt;p&gt;Now for the weird part.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const dataAttributeValueGetter = _dataAttribute&amp;lt;TAttrValue&amp;gt;(scopedName);

// Use Vanilla Extract&apos;s magical addFunctionSerializer so we can guarantee that
// the scoped data attribute value getter callback we&apos;re creating here
// is able to be exported from a Vanilla Extract .css.ts file
// in a way that can be used at runtime
addFunctionSerializer(dataAttributeValueGetter, {
  importName: &apos;_dataAttribute&apos;,
  importPath: &apos;absolute-import-path/to-this-file&apos;,
  args: [[scopedName]],
});

return dataAttributeValueGetter;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, ultimately we want &lt;code&gt;makeDataAttribute&lt;/code&gt; to return the callback which &lt;code&gt;_dataAttribute&lt;/code&gt; creates. In order to make sure
this returned callback works at runtime, we&apos;ll then pass it to &lt;code&gt;addFunctionSerializer&lt;/code&gt; along with the name and path where Vanilla Extract can import the
function that created it, as well as any arguments that said function was called with. Given this information, Vanilla Extract should be able to
serialize the callback so we can use it wherever we want!&lt;/p&gt;
&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;Alright, so I&apos;ll admit it: this was a lot of complexity and effort for a relatively small win, and even then the final result still has tradeoffs.&lt;/p&gt;
&lt;p&gt;That being said, I think this new util has made a postive impact in allowing us to write styles with more confidence, and
this was a great exercise for helping build my mental model of how Vanilla Extract works, which has been very helpful as we have continued
integrating it into our codebase.&lt;/p&gt;
&lt;p&gt;And of course, most importantly of all, this was a ton of fun to build! I loved taking on the challenge of trying to design an API
for how type-safe data attributes could even work, and even though I&apos;m not 100% confident that this is the absolute perfect solution,
I&apos;m still pretty happy with how it came out.&lt;/p&gt;
</content:encoded><language>en-us</language><author>Ryan Geyer</author></item></channel></rss>