<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Matt Soria&apos;s Journal</title><description>A collection of thoughts, musings, snapshots, and doodles.</description><link>https://mattsoria.com/journal</link><language>en-us</language><item><title>Gradient-Revealed Text: An Alternative Approach to Gsap&apos;s SplitText</title><link>https://mattsoria.com/journal/gradient-revealed-text-an-alternative-approach-to-gsaps-splittext</link><guid isPermaLink="true">https://mattsoria.com/journal/gradient-revealed-text-an-alternative-approach-to-gsaps-splittext</guid><pubDate>Mon, 16 Feb 2026 09:11:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;p&gt;&lt;strong&gt;tl;dr: &lt;/strong&gt;&lt;a href=&quot;#gsap-free-solution&quot;&gt;jump to my CSS/GSAP-free solution&lt;/a&gt;&lt;/p&gt;&lt;/p&gt;&lt;p&gt;I was working on a client project recently that featured a section of text that was supposed to start at a very low-contrast color, and then as the section scrolled into view, the text was supposed to transition, character-by-character, to a high-contrast color. This is something I&apos;ve seen before, and I&apos;m sure you have too. I immediately thought: &quot;GSAP&apos;s SplitText.&quot;&lt;/p&gt;&lt;p&gt;And that&apos;s exactly what I turned to. I put together a component that used &lt;a href=&quot;https://gsap.com/docs/v3/Plugins/ScrollTrigger/&quot;&gt;GSAP&apos;s ScrollTrigger plugin&lt;/a&gt; to handle the triggering of the event when the section was at the right position in the viewport, and &lt;a href=&quot;https://gsap.com/docs/v3/Plugins/SplitText/&quot;&gt;GSAP&apos;s SplitText plugin&lt;/a&gt; to handle the character-by-character color animation. &lt;/p&gt;&lt;p&gt;This all worked well, and was easy to implement, but then I pulled up Deque&apos;s AXE Devtools to test the page for simple accessibility concerns, and it flagged an error on my new component: &lt;a href=&quot;https://dequeuniversity.com/rules/axe/4.11/aria-prohibited-attr?application=AxeChrome&quot;&gt;&lt;strong&gt;Elements must only use permitted ARIA attributes&lt;/strong&gt;&lt;/a&gt;. &lt;/p&gt;&lt;p&gt;
      &lt;span&gt;See the Pen &lt;a href=&quot;https://codepen.io/itsmattsoria/pen/EayGyvG/8bf89d2ea46ac24a14ceb346536ba539&quot;&gt;
  GSAP SplitText A11y Error&lt;/a&gt; by Matt Soria (&lt;a href=&quot;https://codepen.io/itsmattsoria&quot;&gt;@itsmattsoria&lt;/a&gt;)
  on &lt;a href=&quot;https://codepen.io&quot;&gt;CodePen&lt;/a&gt;.&lt;/span&gt;
      &lt;/p&gt;
      &lt;p&gt;If you have &lt;a href=&quot;https://www.deque.com/axe/devtools/extension&quot;&gt;Deque&apos;s AXE Devtools browser extension&lt;/a&gt; installed and you run it on that demo, you&apos;ll see that same error. &lt;/p&gt;&lt;p&gt;I could see what the issue was: I had initiated SplitText on a &lt;code&gt;div&lt;/code&gt;, which is the element SplitText slapped &lt;code&gt;aria-label&lt;/code&gt; onto with the text content of the inner HTML, and a div, without a given role, can&apos;t have &lt;code&gt;aria-label&lt;/code&gt;. I was using a div for a good reason, though: the text I wanted to animate was coming from a rich text field of a CMS, which could contain multiple &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; elements, and some styled &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; elements as well, so a generic container made the most sense. I could futz around with stripping that data of the tags as long as it was all going to be communicated by &lt;code&gt;aria-label&lt;/code&gt; anyway, but whenever I find myself getting into the territory of stripping semantics and structural meaning, it&apos;s a clear red flag begging the question, &quot;Is there not a simpler way?&quot;&lt;/p&gt;&lt;p&gt;GSAP has this very scenario &lt;a href=&quot;https://gsap.com/docs/v3/Plugins/SplitText/#alternate-strategy-for-maximizing-nested-element-accessibility&quot;&gt;covered in their docs,&lt;/a&gt; though, and the proposed solution is to tell GSAP to leave off all of the &lt;code&gt;aria&lt;/code&gt; attributes and let you handle it by duplicating the text markup in question, and visually hiding it, while adding &lt;code&gt;aria-hidden=&quot;true&quot;&lt;/code&gt; to the element to be animated by GSAP:&lt;/p&gt;
&lt;div&gt;
  &lt;p&gt;Animated text...&lt;/p&gt;
  &lt;p&gt;Some &lt;strong&gt;more&lt;/strong&gt; animated text.&lt;/p&gt;
&lt;div&gt;


&lt;div&gt;
  &lt;p&gt;Animated text...&lt;/p&gt;
  &lt;p&gt;Some &lt;strong&gt;more&lt;/strong&gt; animated text.&lt;/p&gt;
&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;My coworker Abigail actually shared &lt;a href=&quot;https://adrianroselli.com/2026/02/you-know-what-just-dont-split-words-into-letters.html&quot;&gt;this post by Adrian Roselli&lt;/a&gt; urging you to just not do it, the day after I encountered this. I was already unsatisfied with the look of the animation (seen in the above CodePen) and this encouraged me to take another look and see if I couldn&apos;t find another approach.&lt;/p&gt;&lt;p&gt;That&apos;s when I decided to try and try it without SplitText and just go straight CSS. For the purposes of the design I was implementing, it actually wasn&apos;t important to be able to animate each letter individually since the letters themselves were meant to stay stationary, and the starting point of the animation was the same text, just at a lighter color.&lt;/p&gt;&lt;p&gt;Here&apos;s what I came up with:&lt;/p&gt;&lt;div id=&quot;gsap-free-solution&quot;&gt;&lt;p&gt;
      &lt;span&gt;See the Pen &lt;a href=&quot;https://codepen.io/itsmattsoria/pen/azZxLBz&quot;&gt;
  GSAP-Free Animated Text Gradient Fill&lt;/a&gt; by Matt Soria (&lt;a href=&quot;https://codepen.io/itsmattsoria&quot;&gt;@itsmattsoria&lt;/a&gt;)
  on &lt;a href=&quot;https://codepen.io&quot;&gt;CodePen&lt;/a&gt;.&lt;/span&gt;
      &lt;/p&gt;
      &lt;/div&gt;&lt;p&gt;I like the smoothness of the gradient spreading across the text over the abruptness of the letter-by-letter color change that happens when using SplitText.&lt;/p&gt;&lt;p&gt;And aesthetics aside, I love this approach because it&apos;s accessible without needing to do anything hacky (it&apos;s just plain markup), and it&apos;s JavaScript-free :sparkles:.&lt;/p&gt;&lt;p&gt;Here&apos;s what the markup looks like:&lt;/p&gt;&lt;div&gt;
  &lt;p&gt;Lorem, ipsum dolor sit amet consectetur adipisicing elit. Minima maiores dicta &lt;strong&gt;vero voluptatem architecto&lt;/strong&gt; quos quo saepe, est error quisquam. Exercitationem quas similique at perferendis voluptate repudiandae consectetur quis itaque!&lt;/p&gt;
  &lt;p&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Excepturi officia eum corporis! Quaerat atque, consequuntur nulla, fuga deleniti obcaecati rem blanditiis reiciendis ipsum facilis dolorum voluptatum id tempore eligendi nemo!&lt;/p&gt;
&lt;/div&gt;&lt;p&gt;That&apos;s it! It&apos;s just a container of semantic text markup with a class on it. &lt;/p&gt;&lt;p&gt;And here&apos;s what the CSS looks like:&lt;/p&gt;:root {
  --color-start: #BBAC9B;
  --color-end: #121110;
}

.animated-text {
  /* Set this as the end/legible color in case a visitor prefers reduced-motion */
  color: var(--color-end);
}

/* Wrap all of the animation styles in check for the visitor&apos;s motion preferences */
@media (prefers-reduced-motion: no-preference) {
  .animated-text {
    /* Scroll-driven Animation Properties */
    view-timeline-name: --animated-text;
    view-timeline-axis: block;
    animation: linear animatedTextBackground forwards;
    animation-timeline: --animated-text;
    animation-range: entry 45% cover 60%;
    /* The background gradient styles */
    color: transparent;
    background-clip: text;
    background-size: 100% 300%;
    background-position: 50% 100%;
    will-change: background-position;
    /* The gradient that creates the effect */
    background-image: linear-gradient(172deg, var(--color-end) 45%, var(--color-start) 55%);
  }
}

/* Animation that moves the background gradient into position */
@keyframes animatedTextBackground {
  from {
    background-position: 50% 90%;
  }
  to {
    background-position: 50% 0%;
  }
}&lt;p&gt;That&apos;s it! The markup is inherently accessible, and on top of that, we can take a &lt;a href=&quot;https://www.tatianamac.com/posts/prefers-reduced-motion&quot;&gt;&lt;i&gt;no-motion-first &lt;/i&gt;approach&lt;/a&gt;, ensuring the text is legible by default, and sprinkling on the animation if it&apos;s (assumed to be) acceptable to the visitor.&lt;/p&gt;&lt;h2&gt;No Hate for GSAP&lt;/h2&gt;&lt;p&gt;I just want to make clear that this post isn&apos;t meant as a dump on GSAP, or to convince you to not use GSAP—I think it&apos;s an incredibly powerful and useful tool, and I find a use for it on nearly every project I take on. It&apos;s just that for this particular design challenge, it seemed like a less-is-more approach was worth considering, and avoided the potential issues that Adrian outlined in his post.&lt;/p&gt;</content:encoded><category>Front-End</category><category>CSS</category><category>Dev</category></item><item><title>Daily Summer Commute</title><link>https://mattsoria.com/journal/daily-summer-commute</link><guid isPermaLink="true">https://mattsoria.com/journal/daily-summer-commute</guid><pubDate>Mon, 16 Jun 2025 07:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I live in an impossibly beautiful place—South Lake Tahoe, California. My daughter&apos;s daycare is on the other side of a state park that butts up against our back yard. So in the summer, I get to ride my mountain bike through the forest and glorious meadow to pick my daughter up, and some days it just feels unreal. I finally felt like I needed to capture a little taste of this commute, so well, here it is:&lt;/p&gt;&lt;div&gt;&lt;/div&gt;</content:encoded><category>Lifestyle</category><category>Cycling</category></item><item><title>TRMNL</title><link>https://mattsoria.com/journal/trmnl</link><guid isPermaLink="true">https://mattsoria.com/journal/trmnl</guid><pubDate>Fri, 16 May 2025 09:02:00 GMT</pubDate><content:encoded>&lt;img src=&quot;personal/journal/trmnl/trmnl-1.jpg&quot; alt=&quot;Photo of the terminal display in black and white.&quot; /&gt;&lt;p&gt;&lt;p&gt;Note: Since writing this, there is &lt;a href=&quot;https://trmnl.com/integrations/notion&quot;&gt;an official Notion Plugin&lt;/a&gt;, but it still may be desirable to create your own if you really want to customize it.&lt;/p&gt;&lt;/p&gt;&lt;p&gt;I picked up this thing recently: &lt;a href=&quot;https://usetrmnl.com/&quot;&gt;https://usetrmnl.com/&lt;/a&gt;&lt;/p&gt;&lt;p&gt;I dug that it was e-ink, having previously been an e-ink dumb phone user, and the documentation on building custom plugins made it seem super easy, so I was intrigued.&lt;/p&gt;&lt;p&gt;The design system is really pleasing, and the simplicity of it is really nice. I’m not a big fan of screens being all over the home. We have one of those frame tvs that appears pretty convincingly to be a framed picture when the tv isn’t in use, and other than our laptops floating away from our desks on occasion, we don’t have any screens that are just on and sitting around.&lt;/p&gt;&lt;p&gt;I liked the idea of a little display to show our shared calendar, some to-do lists, and maybe a few other fun little things, but a full color screen on constant rotation felt like the kind of noise we typically try to avoid in our house. So this seemed great!&lt;/p&gt;&lt;p&gt;And so far, it is! Much like my &lt;a href=&quot;https://www.thelightphone.com/&quot;&gt;Light Phone&lt;/a&gt;, it took a little getting used to its limitations, but after that it becomes pretty enjoyable. In the case of TRMNL it’s main limitation is that it isn’t interactive at all. It’s just a display. There aren’t even any physical buttons to push to rotate through screens—you set intervals for screens in a web-based portal.&lt;/p&gt;&lt;p&gt;The other great thing about it is how easy they made creating custom plugins. In TRMNL land a plugin is the whole thing. It’s what you choose to be displayed on the device. They have &lt;a href=&quot;https://trmnl.com/integrations#explore&quot;&gt;a ton to choose from&lt;/a&gt; in their marketplace, but making your own is pretty dang easy if you’re at least a little bit of a programmer.&lt;/p&gt;&lt;p&gt;I haven’t spent too much time exploring the possibilities, but I did make my own plugin to display a list of to-dos from a database in Notion, and it was super easy. After creating a new private plugin in the TRMNL portal all you really need to do is the following:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Go to your Notion profile and &lt;a href=&quot;https://developers.notion.com/guides/get-started/create-a-notion-integration#create-your-integration-in-notion&quot;&gt;create a new internal integration&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Grab the API secret for that integration&lt;/li&gt;&lt;li&gt;Find the database ID of the Notion databse you want to read and display in your plugin. If you copy the link to your database in Notion it will be the long string in the URL after your username (ex: notion.so/username/&lt;code&gt;XXXXXXXXXXXXXXXXXXXXXXXXXXX&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;In your TRMNL plugin settings add the URL (with the database ID you just grabbed) &lt;code&gt;https://api.notion.com/v1/databases/XXXXXXXXXXXXXXXXXXXXXXXXXXX/query&lt;/code&gt; to the &quot;Polling URL(s)&quot; field&lt;/li&gt;&lt;li&gt;Add &lt;code&gt;authorization=Bearer YOUR_NOTION_INTEGRATION_API_SECRET&lt;/code&gt; to the &quot;Polling Headers&quot; field&lt;/li&gt;&lt;li&gt;Save, and go to &quot;Edit Markup&quot; to edit how you want to display your Notion data&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;I know that last one is pretty vague, but TRMNL has a really nice editor (pictured below) that shows you exactly what variables you have access to from TRMNL, and if you&apos;ve connected to the Notion API correctly, the data it&apos;s getting back from Notion. They also have &lt;a href=&quot;https://trmnl.com/framework/docs&quot;&gt;extensive documentation&lt;/a&gt; on their very nice design system.&lt;/p&gt;&lt;img src=&quot;personal/journal/trmnl/screenshot_2026-02-14_at_02.09.38%402x_2026-02-14-100158_evhn.jpg&quot; alt=&quot;A screenshot of the markup editor in TRML&apos;s web portal.&quot; /&gt;&lt;p&gt;And that&apos;s pretty much it! Even if I don&apos;t get too much more creative than this, it&apos;s already been a really nice thing to have up on the fridge. If I do end up doing anything else interesting with it, I&apos;ll make another post about it and link to it here. &lt;/p&gt;</content:encoded><category>Gadgets</category><category>Technology</category><category>Lifestyle</category></item></channel></rss>