<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Building ProseBird: A Solo Dev’s POV]]></title><description><![CDATA[Building ProseBird: A Solo Dev’s POV]]></description><link>https://www.ricardovigliano.com</link><generator>RSS for Node</generator><lastBuildDate>Wed, 15 Apr 2026 16:01:32 GMT</lastBuildDate><atom:link href="https://www.ricardovigliano.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How I Onboard New Users In 5 Simple Steps]]></title><description><![CDATA[https://youtu.be/w9ciZ6jWx0c
 
When faced with the task of building my own onboarding flow for ProseBird I was pretty lost. Despite being onboarded to countless services throughout my life, I had never looked at it from the developer's perspective.
R...]]></description><link>https://www.ricardovigliano.com/how-i-onboard-new-users-in-5-simple-steps</link><guid isPermaLink="true">https://www.ricardovigliano.com/how-i-onboard-new-users-in-5-simple-steps</guid><dc:creator><![CDATA[Ricardo Vigliano]]></dc:creator><pubDate>Wed, 25 Jun 2025 07:36:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1750834073333/db95e481-97e5-4ca6-9632-bbf7a5c6779d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/w9ciZ6jWx0c">https://youtu.be/w9ciZ6jWx0c</a></div>
<p> </p>
<p>When faced with the task of building my own onboarding flow for <a target="_blank" href="https://www.prosebird.com">ProseBird</a> I was pretty lost. Despite being onboarded to countless services throughout my life, I had never looked at it from the developer's perspective.</p>
<p>Rather than going in blind and wasting time on pointless iterations, I needed to first answer a crucial question: what do I want my onboarding to accomplish?</p>
<p>Initially, this seemed straightforward. My app doesn't require complex setup like identity verification or payment processing, I just needed additional information from users beyond the email they used to create their account.</p>
<p>Turns out that onboarding can be much more than simple data collection. A well-designed onboarding experience can attract new users, understand their needs, generate marketing insights, and even create an emotional connection between users and your product.</p>
<p>That's a lot to accomplish, and without a clear strategy it can get pretty overwhelming really fast. So I outlined four core objectives for my onboarding flow:</p>
<ol>
<li><p><strong>Personalize the user experience</strong>: Creating tailored interactions based on user identity, type, and intended use.</p>
</li>
<li><p><strong>Generate product insights</strong>: Understanding user segments, expectations, and behaviors to guide development decisions.</p>
</li>
<li><p><strong>Drive network growth</strong>: Leveraging existing users to expand the platform through collaboration.</p>
</li>
<li><p><strong>Capture marketing intelligence</strong>: Tracking acquisition channels and attribution for strategic optimization.</p>
</li>
</ol>
<h2 id="heading-design-approach">Design Approach</h2>
<p>When researching onboarding patterns, I found two main presentation styles: modal overlays and dedicated pages.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750836164995/2c427fce-082b-446c-b2ae-80f411e9726f.png" alt class="image--center mx-auto" /></p>
<p>Modal-based onboarding presents the flow as an overlay while showing glimpses of the dashboard behind it. This approach suggests the onboarding is just a brief step before reaching the main experience. Dedicated page onboarding, on the other hand, gives the process full attention and encourages users to invest more time in completing it thoughtfully.</p>
<p>Neither approach is inherently better, the differences aren't massive, and most users probably don't consciously notice the distinction. Initially, I chose the modal approach because it felt simpler. However, after adding the steps, controls, copy, header, and images, the modal felt too cramped. This was a good sign to migrate to a dedicated page, allowing the layout to breathe a little.</p>
<p>A design pattern I knew I wanted to implement was the <strong>split view</strong>, commonly used in authentication and onboarding flows. Split views divide the screen into two sections, typically content on one side and a form on the other.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750836297073/d2752077-5a26-4aca-932f-72b7bcaf1133.png" alt class="image--center mx-auto" /></p>
<p>I was drawn to this pattern partly because it's so prevalent (and when done well, looks professional), but I'll admit that fear of incorrectly handling whitespace in a single-column layout also played a part in my decision.</p>
<p>Ultimately, studying good and bad onboarding designs was helpful but I couldn't find complete flows to directly replicate. Most inspiration came from individual design patterns like the split view mentioned above.</p>
<p>This was expected since onboarding flows are inherently unique to each application's specific needs and user base. For that reason, I focused on matching my app's style, following established UX principles, and iterating until it felt cohesive and asthetically pleasing (spoiler: it took a lot of iterating).</p>
<h2 id="heading-progress-indication">Progress Indication</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750836799752/dea3dba8-9d61-47c1-8d6d-dccf6c5dd63e.png" alt class="image--center mx-auto" /></p>
<p>One often overlooked element I implemented was clear progression tracking. Users can see exactly which step they're on and how many remain, displayed as both a progress bar and step counter.</p>
<p>This addresses a fundamental psychological principle: people are more likely to complete tasks when they can visualize progress and see the end point. As a user I agree, and as a developer I think it looks nice. Moving on.</p>
<h2 id="heading-the-five-steps">The Five Steps</h2>
<p>With objectives and design decisions out of the way, I created the actual user journey. Here's each step in order of appearance and the reasoning behind them:</p>
<h3 id="heading-1-basic-information">1. Basic Information</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750835393394/1651485b-1a67-4822-82fe-caf31a70cef3.png" alt class="image--center mx-auto" /></p>
<p>Unique user identifiers in ProseBird are email addresses. Users can invite others to edit and present scripts through email, even if invitees don't have accounts yet. Once invited, it would be awkward to refer to your teammates by their email address.</p>
<p>Rather than implementing traditional usernames or handles, I chose display names. This approach maintains email as the primary identifier while allowing more natural, personalized interactions between users during collaborative sessions.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750836722495/fa4f7077-8822-4fb0-862e-044bd29e46cb.png" alt class="image--center mx-auto" /></p>
<p>This step also prompts users to upload a profile picture. If they choose not to, a dynamically colored fallback image displays their initials based on their display name. This ensures every user has a visual identity during collaboration, whether they upload a custom image or not.</p>
<p>The step remains simple and straightforward, allowing users to quickly progress while establishing their visual presence in the app.</p>
<h3 id="heading-2-user-type">2. User Type</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750835386007/2fd785ef-3798-4a8a-a3e5-5e11e6c79d52.png" alt class="image--center mx-auto" /></p>
<p>This step collects valuable segmentation data to guide product development decisions.</p>
<p>I identified two primary user types:</p>
<ul>
<li><p>Students: From high school through college, needing concise and engaging presentation tools.</p>
</li>
<li><p>Professionals: Including marketing experts, salespeople, professors, and business leaders who require reliable, high-quality presentation solutions.</p>
</li>
</ul>
<p>Understanding the ratio between these segments is perhaps the most fundamental data point for prioritizing product improvements and feature development.</p>
<p>In addition to these two, a third user type called "Personal" was added to account for users who didn't fit the mold of the first two. Examples of Personal users include: hobbyists, content creators, community organizers, and more.</p>
<h3 id="heading-3-intent">3. Intent</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750835376648/ab180c12-1b2e-4d73-b305-44232e6db69b.png" alt class="image--center mx-auto" /></p>
<p>This step focuses on understanding what users expect ProseBird to help them accomplish.</p>
<p>By allowing users to select specific activities they'll use ProseBird for, they're indirectly communicating which features matter most to them. Combined with ongoing user feedback, this data provides powerful insights into how different user segments interact with the product.</p>
<p>An argument can be made that this is the most strategically important step in the onboarding process, as it directly informs product decisions.</p>
<h3 id="heading-4-team-collaboration">4. Team Collaboration</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750835356056/92145598-9ed8-4d0d-b434-c6c1aafc58bd.png" alt class="image--center mx-auto" /></p>
<p>Collaboration is core to ProseBird's value proposition. The ability to seamlessly coordinate multi-speaker presentations in real time is what sets it apart from boring old teleprompters.</p>
<p>With this in mind, users can inform whether they plan to work alone or with a team. For instance, a YouTuber will likely use the app solo, while college students might need to present group projects collaboratively.</p>
<p>The intention behind this step goes beyond gathering user insights. By allowing new users to provide their potential teammates' email addresses, it's effectively driving organic growth. Referenced individuals receive platform invitations, and having their contact information creates opportunities for future engagement regardless of whether they join immediately.</p>
<p>This step also establishes groundwork for a potential referral system, where successful referrers could share revenue from new paid users they bring to the platform.</p>
<h3 id="heading-5-attribution-tracking">5. Attribution Tracking</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750835366267/a22a9269-8b8f-48cb-b748-0616db9a8c7c.png" alt class="image--center mx-auto" /></p>
<p>The fifth and final step asks users how they discovered ProseBird.</p>
<p>This step is standard practice in most SaaS products and serves purely marketing attribution purposes. Understanding which channels drive the highest-quality users helps optimize acquisition spending and strategy.</p>
]]></content:encoded></item><item><title><![CDATA[How I Handle Outside Clicks In My Applications]]></title><description><![CDATA[From dropdown menus to modals and popups, the "click away" pattern isn’t just widely used, it’s expected in most modern user interfaces. Why bother clicking an “x” when you've got the whole screen to dismiss something for you?
Common Approaches
If yo...]]></description><link>https://www.ricardovigliano.com/how-i-handle-outside-clicks-in-my-applications</link><guid isPermaLink="true">https://www.ricardovigliano.com/how-i-handle-outside-clicks-in-my-applications</guid><dc:creator><![CDATA[Ricardo Vigliano]]></dc:creator><pubDate>Fri, 13 Jun 2025 03:09:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1749784122416/98568025-e705-4067-862e-35fd59aa3ac2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>From dropdown menus to modals and popups, the "click away" pattern isn’t just widely used, it’s expected in most modern user interfaces. Why bother clicking an “x” when you've got the whole screen to dismiss something for you?</p>
<h2 id="heading-common-approaches">Common Approaches</h2>
<p>If you’re using vanilla JavaScript and HTML, your solution might look like this:</p>
<pre><code class="lang-javascript">&lt;div id=<span class="hljs-string">"dropdown"</span>&gt;Dropdown Content&lt;/div&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
  <span class="hljs-keyword">const</span> dropdown = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'dropdown'</span>);

  <span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) </span>{
    <span class="hljs-keyword">if</span> (!dropdown.contains(event.target)) {
      <span class="hljs-comment">// Handle outside click</span>
      dropdown.style.display = <span class="hljs-string">'none'</span>;
    }
  });
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>
</code></pre>
<p>In a React environment, you’d likely do something like this:</p>
<pre><code class="lang-javascript">useEffect(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handleClick</span>(<span class="hljs-params">e</span>) </span>{
    <span class="hljs-keyword">if</span> (ref.current &amp;&amp; !ref.current.contains(e.target)) {
      onOutsideClick();
    }
  }

  <span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">"mousedown"</span>, handleClick);
  <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">document</span>.removeEventListener(<span class="hljs-string">"mousedown"</span>, handleClick);
}, []);
</code></pre>
<p>Maybe wrap it in a custom hook:</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useOutsideClick</span>(<span class="hljs-params">ref, callback</span>) </span>{
  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handleClick</span>(<span class="hljs-params">e</span>) </span>{
      <span class="hljs-keyword">if</span> (ref.current &amp;&amp; !ref.current.contains(e.target)) {
        callback();
      }
    }

    <span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">"mousedown"</span>, handleClick);
    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">document</span>.removeEventListener(<span class="hljs-string">"mousedown"</span>, handleClick);
  }, [ref, callback]);
}
</code></pre>
<h2 id="heading-why-i-didnt-use-a-library">Why I Didn’t Use a Library</h2>
<p>Now, you might ask: couldn't I just use something like <code>useClickAway</code> from <code>react-use</code> or <code>useOutsideClick</code> from <code>Headless UI</code>? Absolutely!</p>
<p>While my version is slightly optimized for my needs by accepting multiple exception refs, the true as to why I built it from scratch is... because i felt like it.</p>
<p>I usually favor the DIY route, I knew it wouldn’t take much time, and figured it might even be instructive. Luckily, these assumptions ended up being correct, something that doesn’t happen as often as I would like.</p>
<h2 id="heading-implementation">Implementation</h2>
<h3 id="heading-usage">Usage</h3>
<p>Wraps any content and triggers a callback <code>(onOutsideClick)</code> when the user clicks anywhere outside of it, unless the click happens in an exception zone.</p>
<pre><code class="lang-typescript">&lt;OutsideClickHandler 
    onOutsideClick={<span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Clicked outside!"</span>)}
    exceptionRefs={[exceptionRef]}
&gt;
    {<span class="hljs-comment">/* content */</span>}
&lt;/OutsideClickHandler&gt;
</code></pre>
<h3 id="heading-props">Props</h3>
<p><code>children</code> (<strong>required</strong>): The content you want to protect or track clicks outside of.</p>
<p><code>onOutsideClick</code> (<strong>required</strong>): A function that fires when the user clicks outside the main element (and any exceptions).</p>
<p><code>exceptionRefs</code> (<strong>optional</strong>, default = <code>[]</code>): An array of refs to other elements you want to ignore in the outside click logic (like tooltips, buttons, floating menus).</p>
<p><code>isActive</code> (<strong>optional</strong>, default = <code>true</code>): A boolean to enable or disable the behavior dynamically.</p>
<h3 id="heading-useeffect"><code>useEffect</code></h3>
<ol>
<li><p>Attaches a <code>mousedown</code> listener to the document on mount.</p>
</li>
<li><p>On every click, checks:</p>
<ul>
<li><p>Whether the click is outside the main ref.</p>
</li>
<li><p>And outside all exception refs (<code>.every()</code>).</p>
</li>
</ul>
</li>
<li><p>If both conditions are met, calls <code>onOutsideClick()</code>.</p>
</li>
<li><p>Cleans up the listener on unmount or when <code>isActive</code> changes, avoiding memory leaks or duplicate handlers.</p>
</li>
</ol>
<h3 id="heading-design-advantages">Design Advantages</h3>
<ul>
<li><p><strong>Encapsulated</strong>: Keeps outside-click logic self-contained and clean.</p>
</li>
<li><p><strong>Flexible</strong>: Supports multiple exception zones (unlike most hooks).</p>
</li>
<li><p><strong>Declarative</strong>: Feels like wrapping behavior around content, rather than injecting logic.</p>
</li>
</ul>
<h2 id="heading-demo">Demo</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codesandbox.io/embed/sxp2rp?view=preview">https://codesandbox.io/embed/sxp2rp?view=preview</a></div>
<p> </p>
<h2 id="heading-source-code">Source code</h2>
<p>The full code is available on <a target="_blank" href="https://github.com/rcdvgn/How-I-Handle-Outside-Clicks-In-My-Applications/">GitHub</a>.</p>
]]></content:encoded></item><item><title><![CDATA[5 Principles That Helped Me Build ProseBird]]></title><description><![CDATA[1. “Your idea shouldn't adapt to you”
ProseBird taught me more in a few months than four years of college ever did. The reason is simple: I didn’t build around what I already knew. I built what the idea demanded, and learned the skills as I went.
Tak...]]></description><link>https://www.ricardovigliano.com/5-principles-that-helped-me-build-prosebird</link><guid isPermaLink="true">https://www.ricardovigliano.com/5-principles-that-helped-me-build-prosebird</guid><dc:creator><![CDATA[Ricardo Vigliano]]></dc:creator><pubDate>Thu, 12 Jun 2025 05:28:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1749706374169/07e7e06f-87e0-4322-8f32-0e232f8f27f6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-1-your-idea-shouldnt-adapt-to-you">1. “Your idea shouldn't adapt to you”</h2>
<p>ProseBird taught me more in a few months than four years of college ever did. The reason is simple: I didn’t build around what I already knew. I built what the idea demanded, and learned the skills as I went.</p>
<p>Taking on ProseBird meant being okay with stepping out of my comfort zone and starting from scratch in many ways. When I started, I didn’t know Next.js, Tailwind CSS, TypeScript, Stripe, PostHog, the list goes on. But catering to my project's needs instead of my own meant I had to figure them out, one by one, sometimes even multiple at once.</p>
<p>And those were just tools. Let alone how individual features forced me to dive into entire areas I had never touched. Stuff like string distance algorithms (<a target="_blank" href="https://en.wikipedia.org/wiki/Levenshtein_distance">Levenshtein</a>) for the spoken-word alignment system, or voice-to-text AI and <a target="_blank" href="https://en.wikipedia.org/wiki/Speech_recognition">speech recognition</a>.</p>
<p>Imposing limits on your product just because they exceed your current skills is one of the worst things a technical founder can do. If you want to grow fast, your skill set has to follow the idea, not the other way around.</p>
<hr />
<h2 id="heading-2-dont-run-away-from-hard-problems">2. “Don't run away from hard problems”</h2>
<p>It’s natural to want to avoid work that feels like it’ll drain a disproportionate amount of time and energy. For me, nothing amplifies that feeling more than uncertainty.</p>
<p>To me, spending hours coding isn’t the end of the world. Maybe you hit a few bumps along the way but at least you can see the finish line. On the other hand, if I’m using that same time and energy digging through new documentation and searching for the “ideal” implementation, that’s a mind-numbing game with no end in sight, one that often leads nowhere.</p>
<p>Regardless of the type, putting off problems like these won’t make them disappear, you’ll have to face them sooner or later. In my experience, whether I tackle an issue right away or put it off for a week, the result is almost always the same: it takes just as long to solve. The only difference is that I ended up delaying progress by a full week for no reason.</p>
<p>You probably won’t wake up tomorrow with a breakthrough that drastically reduces time and effort. So you might as well start now and be done by then.</p>
<p>On top of that, starting with the biggest concerns in a project is often the smartest path forward. These are the problems that tend to define the viability of entire sections, or even the whole project. Solving them early gives you a much clearer picture of both feasibility and strategy.</p>
<hr />
<h2 id="heading-3-dont-paste-code-you-dont-understand">3. “Don't paste code you don't understand”</h2>
<p>This one might be controversial since most developers are guilty of it at some point. And with the rise of vibe coding, falling into this trap is easier than ever. But what makes committing to code you don’t quite understand such a bad thing?</p>
<p>Let’s say you’re working on a school project due in four days.</p>
<p><strong>Day 1:</strong><br />You paste a <code>useEffect</code> or <code>setTimeout</code> from StackOverflow or ChatGPT. You don’t really get it, but it works, so you move on.</p>
<p><strong>Day 2:</strong><br />You add a custom hook, then debounce, then a library. Logic overlaps and you patch bugs without knowing what actually fixed them.</p>
<p><strong>Day 3:</strong><br />By this point it’s a black box you’re afraid to touch. You comment stuff out, rewatch tutorials, copy even more code but nothing actually solves the root problem.</p>
<p><strong>Day 4:</strong><br />Deploys break, you stop refactoring, momentum dies. Now you have to pull an all-nighter, rewrite half of your codebase and pray to be done by the time the sun comes up. Yikes.</p>
<p>A full-on snowball effect, all because you didn’t stop to understand the first few lines.</p>
<p>Personally, once I started treating StackOverflow and AI as teachers instead of shortcuts, I never needed them more than once or twice with any given problem before I could solve it confidently on my own.</p>
<hr />
<h2 id="heading-4-hold-the-bar-as-high-as-you-can">4. “Hold the bar as high as you can”</h2>
<p>When I say “bar,” I mean the metaphorical one, the standard you use to decide whether something is “good enough”.</p>
<p>Mine’s been comfortably high for as long as I can remember. Credit goes to my mom, who drilled that into me early on, and I, for either fear of repercussion or plain peer pressure, held onto it through middle school, high school, and college. Not the best of reasons, but hey, I turned out fine.</p>
<p>Jokes aside, this mindset is one of the most OP life traits you can develop, because it works like a muscle. The more you hold yourself to a high standard, the sharper your critical thinking and prioritization skills get. Whether you're judging someone else's work or doing something yourself, you can tell if it's good, bad, or absolute <em>garbaggio</em>, and you'll often be right.</p>
<p>Living by this naturally takes a toll on your well-being. The tradeoffs are mild neuroticism, hair loss and a potentially shorter life expectancy, but it's not like you can't turn it off from time to time.</p>
<p>In fast-paced environments where speed matters, this principle can easily be mistaken for paralyzing perfectionism. That’s not the point. It’s less about shipping perfect work right away, and more about knowing what a high-bar outcome looks like, then work toward it with every iteration.</p>
<hr />
<h2 id="heading-5-when-your-body-talks-you-listen">5. “When your body talks, you listen”</h2>
<p>This last one is simple, but often overlooked, and I couldn’t leave it out.</p>
<p>Before I internalized this as a principle, I lost countless nights trying to brute-force my way through problems to no success, only to revisit the same issue the next morning with a fresh perspective and solve it in a fraction of the time.</p>
<p>The idea is simple: your brain stops working right past a certain threshold of stress or exhaustion. That threshold varies from person to person, but once you hit it, you’re just wasting time pretending otherwise.</p>
<p>In my experience, pushing through those limits is often encouraged, sometimes even romanticized, as part of the hustle that comes with building something on your own. And sure, there’s truth to that. I still try to push myself when it counts.</p>
<p>But the real skill is knowing when to call it a day. Otherwise, it turns into a pointless game with no winners.</p>
]]></content:encoded></item><item><title><![CDATA[How I Built Tooltips That Don’t Break]]></title><description><![CDATA[When building a commercial application, speed is everything. For that reason, developers will always favor native, out-of-the-box solutions over fully custom ones, even if it means using a few quick hacks to meet specific requirements.
The Tooltip Pr...]]></description><link>https://www.ricardovigliano.com/how-i-built-tooltips-that-dont-break</link><guid isPermaLink="true">https://www.ricardovigliano.com/how-i-built-tooltips-that-dont-break</guid><dc:creator><![CDATA[Ricardo Vigliano]]></dc:creator><pubDate>Wed, 11 Jun 2025 11:45:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1749668567789/9c752170-cd04-45c2-bf9e-a5c73c52dda2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When building a commercial application, speed is everything. For that reason, developers will always favor native, out-of-the-box solutions over fully custom ones, even if it means using a few quick hacks to meet specific requirements.</p>
<h2 id="heading-the-tooltip-problem">The Tooltip Problem</h2>
<p>So when I realized I needed tooltips in my app, the first place I looked was HTML’s native <code>title</code> attribute. I had basically zero experience implementing tooltips, but I knew from prior work that for most common UI elements, there are usually three main options:</p>
<ol>
<li><p>Use a native browser feature (like <code>title</code>)</p>
</li>
<li><p>Import a UI library or headless component system (like <code>shadcn/ui</code>)</p>
</li>
<li><p>Build your own from scratch</p>
</li>
</ol>
<h2 id="heading-comparing-the-options">Comparing the Options</h2>
<p>To figure out which option made the most sense for my use case, I first needed to understand their pros and cons. Here's the breakdown.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Feature / Criteria</strong></td><td><strong>Native HTML Tooltip (</strong><code>title</code> <strong>attr)</strong></td><td><strong>Component Library (e.g. shadcn/ui)</strong></td><td><strong>From Scratch (Custom-Built)</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Styling Capabilities</strong></td><td>No styling allowed, appearance is controlled by the browser</td><td>Fully customizable via className or theme overrides</td><td>Total control, but requires manual styling</td></tr>
<tr>
<td><strong>Trigger Control</strong></td><td>Always shows on hover/focus, no configuration</td><td>Configurable (hover, focus, click) depending on the library</td><td>Any trigger supported full control via props or handlers</td></tr>
<tr>
<td><strong>Positioning Control</strong></td><td>Default browser positioning, no adjustments possible</td><td>Often includes built-in logic like flipping or offsetting</td><td>Customizable via positioning libraries or manual logic</td></tr>
<tr>
<td><strong>Touch Device Support</strong></td><td>Unreliable or completely broken on mobile</td><td>Varies, better with libraries that consider mobile UX</td><td>Can be handled properly, but requires testing across devices</td></tr>
<tr>
<td><strong>Rich Content Support</strong></td><td>Plain text only, no HTML, no formatting</td><td>Allows JSX/HTML inside, including links, images, and interactivity</td><td>Full flexibility, render anything you want</td></tr>
<tr>
<td><strong>Ease of Use / Setup</strong></td><td>Just add a <code>title</code> attribute, zero effort</td><td>Moderate setup depending on the framework and docs</td><td>Slower initial setup, but predictable once built</td></tr>
<tr>
<td><strong>Accessibility</strong></td><td>Basic accessibility out of the box (screen reader reads title)</td><td>Typically built with ARIA roles and keyboard support in mind</td><td>You need to handle ARIA, focus, keyboard behavior manually</td></tr>
<tr>
<td><strong>Dependencies / Bundle</strong></td><td>None</td><td>Adds some weight, especially if part of a broader UI library</td><td>No external deps unless you bring in positioning or animation libs</td></tr>
<tr>
<td><strong>Custom Behavior</strong></td><td>Not possible</td><td>Sometimes extendable, depending on API</td><td>Totally customizable, but every feature has to be built intentionally</td></tr>
</tbody>
</table>
</div><p>My goal with tooltips in this application was to make them customizable and reusable. I wanted to easily create and plug-in different tooltip types for different use cases, each with its own styling, positioning, and data, while still behaving predictably and reliably.</p>
<p>What I had in mind looked something like this:</p>
<pre><code class="lang-typescript">&lt;TooltipWrapper
  <span class="hljs-keyword">type</span>=<span class="hljs-string">"moreInfo"</span>
  options={options}
&gt;
  &lt;a href=<span class="hljs-string">"#"</span>&gt;Hover me&lt;/a&gt;
&lt;/TooltipWrapper&gt;
</code></pre>
<p>This is completely outside the realm of what vanilla HTML is capable of, but perfectly doable with most tooltip libraries out there. The limitations start to show if, for instance, I wanted to fetch user data, treat it, and display it as an image and some text, all from within the tooltip component.</p>
<p>You could argue this is an extreme edge case, or that I could just fetch and prepare all the data in the parent component beforehand. And honestly, those are both valid criticisms.</p>
<p>Still, I'd rather implement a complete solution upfront than go through the trouble of dealing with tooltips again later. You have to choose your battles, and for better or worse, this was the choice I went with.</p>
<h2 id="heading-initial-implementation">Initial Implementation</h2>
<p>Now for the trickiest part: the wrapper.</p>
<p>My initial implementation of the tooltip wrapper accepted the following parameters:</p>
<ul>
<li><p><code>children</code> (required): The anchor element or component that the tooltip is attached to.</p>
</li>
<li><p><code>position</code> (optional, default = "top"): Where the tooltip should appear relative to the anchor.</p>
</li>
<li><p><code>className</code> (optional, default = ""): Tailwind utility classes for styling the wrapper element.</p>
</li>
<li><p><code>tooltipType</code> (optional, default = <code>DefaultTooltip</code>): The custom tooltip component used to render the content.</p>
</li>
<li><p><code>data</code> (required): The actual data passed to the tooltip, ranging from raw text to functions and everything in between.</p>
</li>
</ul>
<p>The wrapper itself was styled with <code>position: relative</code> so the tooltip could be positioned inside its bounds. It returned the following structure:</p>
<pre><code class="lang-typescript">&lt;div className=<span class="hljs-string">"relative group"</span> ref={wrapperRef}&gt;
  {children}
  &lt;TooltipComponent
    ref={tooltipRef}
    data={data}
    className={<span class="hljs-string">`fixed z-[9999] opacity-0 group-hover:opacity-100 pointer-events-none <span class="hljs-subst">${className}</span> <span class="hljs-subst">${getTranslateClass()}</span>`</span>}
    style={{ top: tooltipPosition.top, left: tooltipPosition.left }}
  /&gt;
&lt;/div&gt;
</code></pre>
<p>This layout allowed the tooltip’s position to be calculated like this:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> updateTooltipPosition = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">if</span> (wrapperRef.current &amp;&amp; tooltipRef.current) {
    <span class="hljs-keyword">const</span> wrapperRect = wrapperRef.current.getBoundingClientRect();
    <span class="hljs-keyword">const</span> tooltipRect = tooltipRef.current.getBoundingClientRect();

    <span class="hljs-keyword">const</span> positions = {
      top: {
        top: wrapperRect.top - tooltipRect.height,
        left: wrapperRect.left + wrapperRect.width / <span class="hljs-number">2</span> - tooltipRect.width / <span class="hljs-number">2</span>,
      },
      bottom: {
        top: wrapperRect.bottom,
        left: wrapperRect.left + wrapperRect.width / <span class="hljs-number">2</span> - tooltipRect.width / <span class="hljs-number">2</span>,
      },
      left: {
        top: wrapperRect.top + wrapperRect.height / <span class="hljs-number">2</span> - tooltipRect.height / <span class="hljs-number">2</span>,
        left: wrapperRect.left - tooltipRect.width,
      },
      right: {
        top: wrapperRect.top + wrapperRect.height / <span class="hljs-number">2</span> - tooltipRect.height / <span class="hljs-number">2</span>,
        left: wrapperRect.right,
      },
    };

    setTooltipPosition(positions[position]);
  }
};

useEffect(<span class="hljs-function">() =&gt;</span> {
    updateTooltipPosition();
    <span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">"resize"</span>, updateTooltipPosition);
    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> {
      <span class="hljs-built_in">window</span>.removeEventListener(<span class="hljs-string">"resize"</span>, updateTooltipPosition);
    };
}, [position]);
</code></pre>
<p>The wrapper naturally wrapped around the anchor component, meaning it would never be larger than the anchor itself. This setup ensured the tooltip could be absolutely positioned in relation to its parent without weird overflow or layout issues.</p>
<p>One important limitation: this version didn’t support click-based tooltips. Since everything was triggered on hover, a simple <code>group</code> class on the wrapper combined with <code>group-hover</code> was enough to control visibility.</p>
<p>Instead of hard-setting the tooltip’s position with fixed values per direction, I kept it neutrally placed and used a translation based on the desired direction. That way, it could animate smoothly from its origin point:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> getTranslateClass = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> translations = {
    top: <span class="hljs-string">"group-hover:-translate-y-4"</span>,
    bottom: <span class="hljs-string">"group-hover:translate-y-4"</span>,
    left: <span class="hljs-string">"group-hover:-translate-x-4"</span>,
    right: <span class="hljs-string">"group-hover:translate-x-4"</span>,
  };
  <span class="hljs-keyword">return</span> translations[position];
};
</code></pre>
<p>The result was a customizable, modular tooltip that popped in and out of view with smooth transitions on hover.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codesandbox.io/embed/tksf49?view=preview">https://codesandbox.io/embed/tksf49?view=preview</a></div>
<p> </p>
<h2 id="heading-where-it-fell-short">Where It Fell Short</h2>
<p>It didn’t take much testing to realize how deceptively complicated tooltips can be. Here are the two main ways they didn’t behave as expected in this version:</p>
<p><strong>Positioning:</strong><br />The tooltip’s position wasn’t dynamic, it didn’t track layout changes in real time. Once the anchor element moved (due to a resize, animation, or DOM change), the tooltip often stayed stuck in its original spot, floating somewhere random on the screen. That made it completely unreliable. This happened because I was only recalculating its position on <code>window.resize</code>, ignoring internal layout shifts entirely.</p>
<blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749669553999/426d15ee-2c60-4363-bbfb-c1aaff14c626.png" alt /></p>
<p><em>tooltip mispositioned after layout change</em></p>
</blockquote>
<p><strong>Visibility / Display:</strong><br />The tooltip component was always present in the DOM, even when it wasn’t visible. It stayed hidden using CSS (<code>opacity: 0</code> and <code>pointer-events: none</code>), but never actually unmounted. That meant it was still detectable when inspecting the DOM, and sometimes even interfered with hover and click behaviors. This happened because I never tied its presence to a true visibility state, I was only toggling styles, not whether it should exist at all.</p>
<blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749631354710/41575621-3139-4171-b4d3-d4875df496ce.png" alt /></p>
<p><em>tooltip "shadow" still visible in the DOM</em></p>
</blockquote>
<h2 id="heading-switching-to-floating-ui-framer-motion">Switching to Floating UI + Framer Motion</h2>
<p>Confronted with these issues, my options were twofold: I could either stick with the full DIY implementation and fix its problems individually, or look for a more established solution. Taking performance and long-term reliability into account, I ended up going with the second. If I was going to over-engineer this, I might as well do it the right way.</p>
<p>It didn’t take long to find <a target="_blank" href="https://floating-ui.com/">Floating UI</a> (formerly Popper), a general-purpose abstraction for handling floating positioning. That was perfect because unlike fully pre-built tooltip components, Floating UI wouldn’t confine me to rigid designs or assumptions. I’d get the flexibility I wanted, at the cost of writing a bit more code to set up.</p>
<p>I also decided to offload transitions and animation to Framer Motion, since it gives you more control over the animation flow.</p>
<h2 id="heading-final-version-breakdown">Final Version Breakdown</h2>
<p>With the new version in place, here’s a summary of what changed and why:</p>
<ul>
<li><p>No need for <code>ResizeObserver</code>s or <code>useEffect</code> hacks to track anchor position, Floating UI now handles dynamic positioning automatically using <code>autoUpdate</code> and element refs.</p>
</li>
<li><p>Transitions are now handled by Framer Motion, so all Tailwind transition classes were removed.</p>
</li>
<li><p>The tooltip component is now conditionally mounted with <code>AnimatePresence</code>, fixing the old “invisible but still in the DOM” issue.</p>
</li>
<li><p>New wrapper parameters:</p>
<ul>
<li><p><code>delay</code>: optional animation delay (default is <code>0.5</code>)</p>
</li>
<li><p><code>disabled</code>: disables tooltip triggers entirely</p>
</li>
<li><p><code>openOn</code>: defines the trigger action (<code>click</code> or <code>hover</code>)</p>
</li>
</ul>
</li>
<li><p>Tooltips triggered by clicks are now dismissed via an <code>OutsideClickHandler</code>.</p>
</li>
</ul>
<p>Here’s a high-level overview of how the new tooltip system works in practice:</p>
<ol>
<li><p>Anchor elements are wrapped in <code>TooltipWrapper</code> and passed custom parameters based on need.</p>
<pre><code class="lang-typescript"> &lt;TooltipWrapper
   tooltipType={CustomTooltip}
   position=<span class="hljs-string">"bottom"</span>
   openOn=<span class="hljs-string">"click"</span>
   delay={<span class="hljs-number">0.1</span>}
   data={{ text: <span class="hljs-string">"Hi!"</span> }}
 &gt;
   &lt;button&gt;Click me&lt;/button&gt;
 &lt;/TooltipWrapper&gt;
</code></pre>
</li>
<li><p>The wrapper handles trigger detection, positioning, visibility, and state, and displays the desired tooltip component. If none is specified, it falls back to <code>DefaultTooltip</code>.</p>
<pre><code class="lang-typescript"> {open &amp;&amp; (
   &lt;motion.div
     ref={refs.setFloating}
     initial={{ opacity: <span class="hljs-number">0</span>, scale: <span class="hljs-number">0.9</span>, ...pullAway }}
     animate={{ opacity: <span class="hljs-number">1</span>, scale: <span class="hljs-number">1</span>, x: <span class="hljs-number">0</span>, y: <span class="hljs-number">0</span> }}
     exit={{ opacity: <span class="hljs-number">0</span>, scale: <span class="hljs-number">0.9</span>, ...pullAway }}
     transition={{ duration: <span class="hljs-number">0.2</span>, delay, ease: <span class="hljs-string">"easeInOut"</span> }}
     style={{
       position: strategy,
       top: y ?? <span class="hljs-number">0</span>,
       left: x ?? <span class="hljs-number">0</span>,
     }}
     className=<span class="hljs-string">"z-[9999] pointer-events-none"</span>
   &gt;
     {React.createElement(tooltipType, { data })}
   &lt;/motion.div&gt;
 )}
</code></pre>
</li>
<li><p>Tooltip components themselves can be as simple or complex as needed, making them easy to reuse and tailor per use case.</p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">const</span> DefaultTooltip = forwardRef(<span class="hljs-function">(<span class="hljs-params">{ data, className = <span class="hljs-string">""</span>, style }, ref</span>) =&gt;</span> (
   &lt;div ref={ref} className={<span class="hljs-string">`tooltip-default <span class="hljs-subst">${className}</span>`</span>} style={style}&gt;
     {data.text}
   &lt;/div&gt;
 ));
</code></pre>
</li>
</ol>
<p>Although the changes are somewhat drastic under the hood, the result is visually the same. The real difference is in consistency, reliability, and long-term flexibility. This version doesn’t break on layout shifts, doesn’t leave ghost tooltips behind, and provides full control over when, how, and what gets rendered.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codesandbox.io/embed/ny4y97?view=preview">https://codesandbox.io/embed/ny4y97?view=preview</a></div>
<p> </p>
<h2 id="heading-source-code">Source code</h2>
<p>The full code for both tooltip versions is available <a target="_blank" href="https://github.com/rcdvgn/How-I-Built-Tooltips-That-Don-t-Break">here</a>.</p>
]]></content:encoded></item><item><title><![CDATA[The Goal of This Blog]]></title><description><![CDATA[My goal with this blog is to share my honest opinions, technical perspective, and personal growth as a solo developer and founder on the path to building a successful web project.
Posts will range from technical deep-dives directly tied to ProseBird,...]]></description><link>https://www.ricardovigliano.com/the-goal-of-this-blog</link><guid isPermaLink="true">https://www.ricardovigliano.com/the-goal-of-this-blog</guid><dc:creator><![CDATA[Ricardo Vigliano]]></dc:creator><pubDate>Fri, 23 May 2025 14:37:58 GMT</pubDate><content:encoded><![CDATA[<p>My goal with this blog is to share my honest opinions, technical perspective, and personal growth as a solo developer and founder on the path to building a successful web project.</p>
<p>Posts will range from technical deep-dives directly tied to <a target="_blank" href="http://www.prosebird.com">ProseBird</a>, to personal reflections that connect more loosely (or even indirectly) to the project.</p>
]]></content:encoded></item></channel></rss>