<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/feed.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Luis Steidle</title><description>Posts from luisstd.com</description><link>https://luisstd.com/</link><language>en-us</language><managingEditor>luis@steidle.me (Luis Steidle)</managingEditor><webMaster>luis@steidle.me (Luis Steidle)</webMaster><category>Software Engineering</category><category>Web Development</category><category>Technology</category><category>Programming</category><item><title>Local-first and progressive web apps</title><link>https://luisstd.com/posts/local-first-progressive-web-apps/</link><guid isPermaLink="true">https://luisstd.com/posts/local-first-progressive-web-apps/</guid><description>Reflections on local-first software, progressive web apps, and building a habit tracker with TanstackDB, ElectricSQL, and Postgres.</description><pubDate>Sun, 03 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When building apps or imagining software products I like to use and see in the world, I gravitate towards something close to what &amp;lt;a href=&quot;https://web.dev/learn/pwa/progressive-web-apps/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&amp;gt;progressive web-apps&amp;lt;/a&amp;gt; embody. An app that is mobile-first, but still usable in any web browser and on any device. An app that works offline, that you can &quot;install&quot; without having to search for it on a bespoke app store or marketplace or proprietary platform. Basically the &quot;write once, use anywhere&quot; concept taken to the maximum. Ever since I came across the principles around local-first software, my interest in progressive web-apps was renewed. It made me wonder: can apps just be like this by default, and also be useful for people who don&apos;t necessarily like to think about software or are interested in it at all, who just want stuff to work? Why is downloading a native app from the App Store a mainstream experience that almost every person understands, but &quot;installing a PWA to your homescreen&quot; doesn&apos;t carry that same level of intuitiveness?&lt;/p&gt;
&lt;p&gt;People don&apos;t really care whether their app comes from the Apple App Store, or the Google Play Store or if it&apos;s a progressive web-app (or more generally what technologies something is built with in the first place). But it needs to feel right. I often find, web apps can feel absolutely &quot;right&quot;, and even in popular native apps there is often a large amount of web tech being used in WebViews, without that necessarily being a friction point for the user.&lt;/p&gt;
&lt;p&gt;However, I have never installed someone else&apos;s PWA, except the ones I built myself. I guess that says something. And I ask myself, is this really the answer, does anyone really need this? I guess it&apos;s a stretch to say PWAs are popular, but also how would one really know how much they are used in the wild?&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;I recently set out to become more familiar with the stack around the &quot;local-first&quot; paradigm. Trying to build something that feels like it can really fulfill all those ideals that I have around my understanding of &quot;how apps should be&quot;. It&apos;s a website. It&apos;s built with web technologies. Yet, you can just &quot;install&quot; it on your device. Any device. It works offline. You don&apos;t need internet to use it. Your data stays on your device.&lt;/p&gt;
&lt;p&gt;However: You do have the benefit of cloud magic, you can just keep on working on your laptop after editing something on the train on your phone. Users don&apos;t have to carry the mental load of files that live on their devices nor have to worry about keeping them in sync. It feels like this a very pragmatic compromise between two extremes: &quot;I use files and dedicated software to work on these files, I manage backup and sync myself/with some other service&quot; vs. &quot;I do not know what a file is, my digital life lives in random apps from the app store&quot;&lt;/p&gt;
&lt;p&gt;You get the benefits of a seamless out-of-the-box UX, while still having control and access to your data. You do not have this central dependency on a third-party server which can completely shut off your workflow and access. It seems like the perfect middle ground.&lt;/p&gt;
&lt;p&gt;I have built a very simple app to expose myself to this approach more. A habit tracker. It&apos;s simple enough, I am the kind of person who uses such apps daily (without overdoing it), because I like the structure it gives me. Feeling like this is the perfect use case and being motivated by the thought of replacing a 30€/year SaaS subscription I have to another app I set out.&lt;/p&gt;
&lt;p&gt;The core sync stack I ended up with was the following:&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://tanstack.com/db&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&amp;gt;
TanstackDB
&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://electric-sql.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&amp;gt;
ElectricSQL
&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;https://www.postgresql.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&amp;gt;
Postgres
&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;p&gt;TanstackDB was the piece I was missing to get started, because it made this whole topic of sync engines approachable. The mental model of a reactive client store that I built my browser app against, and that store by itself taking care of talking to the server really resonated with me.&lt;/p&gt;
&lt;p&gt;Hosting ElectricSQL, wiring it up with TanstackDB and Postgres was more straightforward than I expected. This project was also at a real sweet spot for me in terms of using AI to build. I felt quite new and a bit out of the water with all the details of this stack, but had a solid understanding of the overall architecture I wanted to go for. My first eureka moment was opening the app on my laptop and phone side by side and realizing, if I mark the habit done on the laptop as done it is also immediately marked done on my phone.&lt;/p&gt;
&lt;p&gt;However, without AI and the hard work put into these libraries, I realized I would have been stuck a lot longer. The complexity of this setup feels so much higher than the typical API request/server response model and there are so many more things that can break.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;After this exploration I&apos;m still wondering: Could/Should mainstream apps be like this? Just websites you download and &quot;have&quot; on your device. You log in with a passkey, and you&apos;re authenticated. You lose internet connection and you don&apos;t notice. I like the idea. Yet native platforms have a place, because they can offer experiences the web was just never built for.&lt;/p&gt;
&lt;p&gt;Sometimes I feel the idea of (progressive) web apps is just doomed and no one really cares. After all, why would they? What makes a web app better actually, other than some personal ideals of how things should be? Something I am still unsure about. Their principles feel right, yet something seems to not catch people&apos;s interest. Whether that will change, who knows. For now, I will probably keep gravitating towards that.&lt;/p&gt;
</content:encoded><category>local-first</category><category>pwa</category><author>Luis Steidle</author></item><item><title>Implementing dark mode with NextJS and TailwindCSS</title><link>https://luisstd.com/posts/nextjs-dark-mode/</link><guid isPermaLink="true">https://luisstd.com/posts/nextjs-dark-mode/</guid><description>Learn how to implement a flexible dark mode toggle in NextJS with TailwindCSS. Includes light, dark, and system mode options with localStorage persistence.</description><pubDate>Sun, 16 Apr 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In today&apos;s web, providing users with a pleasant browsing experience is essential. One popular way to achieve this is by adding a light/dark mode toggle to your website. In this blog post, I&apos;ll walk you through implementing this functionality using NextJS and TailwindCSS. We&apos;ll create a dropdown menu that allows users to choose between light, dark, and system modes.&lt;/p&gt;
&lt;h2&gt;Desired outcome&lt;/h2&gt;
&lt;p&gt;There are several ways to implement dark mode, but one of the best examples I&apos;ve seen is how it&apos;s done on the TailwindCSS docs. Users can choose between light, dark, and system mode, meaning they either manually select dark or light and have their preference saved to localStorage, or they choose to respect the OS setting.&lt;/p&gt;
&lt;p&gt;I think this approach is the best of both worlds, as users have the flexibility to select a specific mode for each website or tool they use. The desired functionality involves a dropdown with three settings and corresponding logos. When users manually select dark or light mode, a colored logo will indicate the chosen setting. By default, the system mode should be selected.&lt;/p&gt;
&lt;h2&gt;Tools for implementation&lt;/h2&gt;
&lt;p&gt;To implement this functionality, we will use the following tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://tailwindcss.com&quot;&gt;TailwindCSS&lt;/a&gt;: A utility-first CSS framework for rapid UI development&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org&quot;&gt;NextJS&lt;/a&gt;: A React framework for server-rendered applications&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pacocoursey/next-themes&quot;&gt;next-themes&lt;/a&gt;: A plugin by Paco Coursey that provides the &lt;code&gt;useTheme()&lt;/code&gt; hook&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Steps for implementation&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;First make sure all dependencies are installed:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt; pnpm install tailwindcss next next-themes
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Wrap your application with the ThemeProvider component provided by&lt;code&gt;next-themes&lt;/code&gt; in your &lt;code&gt;_app.tsx&lt;/code&gt; file:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;import { ThemeProvider } from &quot;next-themes&quot;;

function App({ Component, pageProps }) {
  return (
    &amp;lt;ThemeProvider&amp;gt;
      &amp;lt;Component {...pageProps} /&amp;gt;
    &amp;lt;/ThemeProvider&amp;gt;
  );
}

export default App;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;The useTheme hook provides the current theme (&lt;code&gt;theme&lt;/code&gt;) and a function to toggle the theme (&lt;code&gt;setTheme&lt;/code&gt;). You can use these values to customize the appearance of your components based on the selected theme.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;import { useTheme } from &quot;next-themes&quot;;

function Component() {
  const { theme, setTheme } = useTheme();

  function toggleTheme() {
    setTheme(theme === &quot;dark&quot; ? &quot;light&quot; : &quot;dark&quot;);
  }

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;button onClick={toggleTheme}&amp;gt;Toggle theme&amp;lt;/button&amp;gt;
      &amp;lt;p&amp;gt;Current theme: {theme}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default Component;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Example for implementation&lt;/h2&gt;
&lt;p&gt;This is a working example that recreates the functionality of the TailwindCSS docs. This example uses Radix UI for the dropdown menu.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import * as DropdownMenu from &quot;@radix-ui/react-dropdown-menu&quot;;
import * as ToggleGroup from &quot;@radix-ui/react-toggle-group&quot;;
import { IconDeviceLaptop, IconMoon, IconSun } from &quot;@tabler/icons-react&quot;;
import { useTheme } from &quot;next-themes&quot;;
import { useEffect, useState } from &quot;react&quot;;

function ThemeDropdown() {
  const [mounted, setMounted] = useState(false);
  const { theme, resolvedTheme, setTheme } = useTheme();

  useEffect(() =&amp;gt; {
    setMounted(true);
  }, []);

  if (!mounted) {
    return null;
  }
  return (
    &amp;lt;&amp;gt;
      &amp;lt;DropdownMenu.Root&amp;gt;
        &amp;lt;DropdownMenu.Trigger&amp;gt;
          {resolvedTheme === &quot;light&quot; &amp;amp;&amp;amp; theme === &quot;light&quot; ? (
            &amp;lt;IconSun size={30} className=&quot;text-blue-400&quot; /&amp;gt;
          ) : resolvedTheme === &quot;light&quot; ? (
            &amp;lt;IconSun size={30} /&amp;gt;
          ) : resolvedTheme === &quot;dark&quot; &amp;amp;&amp;amp; theme === &quot;dark&quot; ? (
            &amp;lt;IconMoon size={30} className=&quot;text-blue-300&quot; /&amp;gt;
          ) : resolvedTheme === &quot;dark&quot; ? (
            &amp;lt;IconMoon size={30} /&amp;gt;
          ) : (
            &amp;lt;IconDeviceLaptop size={30} /&amp;gt;
          )}
        &amp;lt;/DropdownMenu.Trigger&amp;gt;

        &amp;lt;DropdownMenu.Portal&amp;gt;
          &amp;lt;DropdownMenu.Content&amp;gt;
            &amp;lt;DropdownMenu.Label /&amp;gt;
            &amp;lt;DropdownMenu.Item /&amp;gt;

            &amp;lt;ToggleGroup.Root
              type=&quot;single&quot;
              orientation=&quot;vertical&quot;
              value={theme}
              aria-label=&quot;Dark/Light/System Mode Selection&quot;
              className=&quot;flex flex-col items-start w-full gap-1 pt-1 m-2 border-2 rounded-md&quot;
            &amp;gt;
              &amp;lt;ToggleGroup.Item
                value=&quot;light&quot;
                onClick={() =&amp;gt; setTheme(&quot;light&quot;)}
                className={`flex items-center w-full gap-1 px-1 py-1 mx-0 hover:bg-hover-light dark:hover:bg-hover-dark ${
                  theme === &quot;light&quot; ? &quot;font-bold&quot; : &quot;font-normal&quot;
                }`}
              &amp;gt;
                &amp;lt;IconSun size={30} /&amp;gt;
                &amp;lt;span&amp;gt;Light&amp;lt;/span&amp;gt;
              &amp;lt;/ToggleGroup.Item&amp;gt;

              &amp;lt;ToggleGroup.Item
                value=&quot;dark&quot;
                onClick={() =&amp;gt; setTheme(&quot;dark&quot;)}
                className={`flex items-center w-full gap-1 px-1 py-1 mx-0 hover:bg-hover-light dark:hover:bg-hover-dark ${
                  theme === &quot;dark&quot; ? &quot;font-bold&quot; : &quot;font-normal&quot;
                }`}
              &amp;gt;
                &amp;lt;IconMoon size={30} /&amp;gt;
                &amp;lt;span&amp;gt;Dark&amp;lt;/span&amp;gt;
              &amp;lt;/ToggleGroup.Item&amp;gt;

              &amp;lt;ToggleGroup.Item
                value=&quot;system&quot;
                onClick={() =&amp;gt; setTheme(&quot;system&quot;)}
                className={`flex items-center w-full gap-1 px-1 py-1 mx-0 hover:bg-hover-light dark:hover:bg-hover-dark ${
                  theme === &quot;system&quot; ? &quot;font-bold&quot; : &quot;font-normal&quot;
                }`}
              &amp;gt;
                &amp;lt;IconDeviceLaptop size={30} /&amp;gt;
                &amp;lt;span&amp;gt;System&amp;lt;/span&amp;gt;
              &amp;lt;/ToggleGroup.Item&amp;gt;
            &amp;lt;/ToggleGroup.Root&amp;gt;
          &amp;lt;/DropdownMenu.Content&amp;gt;
        &amp;lt;/DropdownMenu.Portal&amp;gt;
      &amp;lt;/DropdownMenu.Root&amp;gt;
    &amp;lt;/&amp;gt;
  );
}

export default ThemeDropdown;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;resolvedTheme&lt;/code&gt; variable is used to check the currently selected theme(light/dark/system). By default, the system mode is selected, and the logo associated with it is displayed. When users manually choose between dark or light mode, the corresponding logo will indicate the selected setting.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In this tutorial, we have explored how to implement a light/dark mode toggle using NextJS and TailwindCSS. By giving users the flexibility to choose between light, dark, and system modes, we can enhance the user experience by catering to their individual preferences.&lt;/p&gt;
</content:encoded><category>nextjs</category><category>tailwindcss</category><category>frontend</category><author>Luis Steidle</author></item><item><title>Introduction to Penpot - an open-source design tool</title><link>https://luisstd.com/posts/intro-to-penpot/</link><guid isPermaLink="true">https://luisstd.com/posts/intro-to-penpot/</guid><description>Discover Penpot, a free and open-source design tool that provides solid features for developers and designers. Cross-platform alternative to proprietary design software.</description><pubDate>Wed, 06 Jul 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Image } from &quot;astro:assets&quot;;
import screenshot01 from &quot;@/assets/screenshot-penpot-01.png&quot;;
import screenshot02 from &quot;@/assets/screenshot-penpot-02.png&quot;;&lt;/p&gt;
&lt;p&gt;In the past year I have been using Penpot quite a bit and I want to give a short introduction to this design tool which is pretty good in my opinion. I think for most developers and designers it provides a really solid set of features, while being completely cross-platform, free and open-source.&lt;/p&gt;
&lt;h2&gt;Alternative to Figma&lt;/h2&gt;
&lt;p&gt;So far Penpot is one of the best alternatives to Figma I have found. It provides most functionality from Figma and has a very clean interface, while also supporting real-time collaboration. One thing which I really like is that you are not only limited to drafts like in the Figma free version. The overall structure within Penpot where you can create as many projects and drafts as you want independent of a team feels intuitive and just makes sense for me. If multiple people want to work on a shared workspace they can create a new &quot;Penpot&quot; (or Team) and then have access to the same projects and drafts there. Custom fonts can be uploaded which I really like and users are not limited to only static designs. It&apos;s also possible to create interactive prototypes.&lt;/p&gt;
&lt;p&gt;&amp;lt;Image
src={screenshot02}
alt=&quot;Penpot user interface&quot;
widths={[240, 540, 720, screenshot02.width]}
sizes={&lt;code&gt;(max-width: 360px) 240px, (max-width: 720px) 540px, (max-width: 1600px) 720px, ${screenshot02.width}px&lt;/code&gt;}
loading=&quot;lazy&quot;
/&amp;gt;&lt;/p&gt;
&lt;h2&gt;Open source, Open standards, Self-hosting&lt;/h2&gt;
&lt;p&gt;Another very positive thing about Penpot is that it&apos;s entirely free, open-source and can be self-hosted. It works with SVG under the hood and promises compatibility with most vector-based tools and open standards. The &lt;a href=&quot;https://github.com/penpot/penpot&quot;&gt;github repository&lt;/a&gt; provides an easy overview on how to get started.&lt;/p&gt;
&lt;h2&gt;Community libraries&lt;/h2&gt;
&lt;p&gt;There is already a good selection of design libraries available. The Penpot design system itself is available as a shared library, as well as popular design frameworks like Ant or Material Design. Several wireframing or whiteboarding kits are available too. Anyone can submit a new library so there is hopefully more to come.&lt;/p&gt;
&lt;p&gt;&amp;lt;Image
src={screenshot01}
alt=&quot;Penpot user interface&quot;
widths={[240, 540, 720, screenshot01.width]}
sizes={&lt;code&gt;(max-width: 360px) 240px, (max-width: 720px) 540px, (max-width: 1600px) 720px, ${screenshot01.width}px&lt;/code&gt;}
loading=&quot;lazy&quot;
/&amp;gt;&lt;/p&gt;
&lt;h2&gt;Still in beta&lt;/h2&gt;
&lt;p&gt;Currently Penpot is still in beta, but the product is definitely usable and there is a promising roadmap ahead. There seems to be steady development happening and the progress is very transparent.&lt;/p&gt;
&lt;p&gt;A changelog can be found at &lt;a href=&quot;https://penpot.app/dev-diaries.html&quot;&gt;https://penpot.app/dev-diaries.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There is also a community forum that launched recently &lt;a href=&quot;https://community.penpot.app&quot;&gt;https://community.penpot.app&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Overall it&apos;s a really solid tool and I&apos;m excited to see how the project will evolve further.&lt;/p&gt;
</content:encoded><category>open-source</category><category>frontend</category><author>Luis Steidle</author></item></channel></rss>