<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Karim Fawaz</title>
    <description>The latest articles on DEV Community by Karim Fawaz (@karimfawaz).</description>
    <link>https://dev.to/karimfawaz</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4108617%2F957d478e-88e2-4286-8bf5-7d88a80a7151.png</url>
      <title>DEV Community: Karim Fawaz</title>
      <link>https://dev.to/karimfawaz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/karimfawaz"/>
    <language>en</language>
    <item>
      <title>Feedback that knows which component it came from</title>
      <dc:creator>Karim Fawaz</dc:creator>
      <pubDate>Thu, 03 Sep 2026 19:33:32 +0000</pubDate>
      <link>https://dev.to/karimfawaz/feedback-that-knows-which-component-it-came-from-1olb</link>
      <guid>https://dev.to/karimfawaz/feedback-that-knows-which-component-it-came-from-1olb</guid>
      <description>&lt;p&gt;Every feedback tool I've used has the same shape. You install a widget, the widget opens a box, the user types something into the box, and the thing that lands in your inbox is a paragraph of text with a page URL stapled to it. Then you spend twenty minutes working out which component they meant.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The new thing is confusing"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which new thing. There are four new things on that page. The feedback isn't really attached to anything useful. At best it's attached to a page, and a page is not the unit you ship. You ship components. Your backlog is organised by component. Your team argues about components. The inbox is still organised by URL, so somebody has to do the translation by hand, and that somebody is usually you on a Friday evening.&lt;/p&gt;

&lt;p&gt;The fix is unglamorous: put the button &lt;em&gt;inside&lt;/em&gt; the component it's about, and give it a name. Provider once at the root, then the widget lives in the same file as the UI it describes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/providers.tsx&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GotchaProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gotcha-feedback&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Providers&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GotchaProvider&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_GOTCHA_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;GotchaProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/checkout-form.tsx&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Gotcha&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gotcha-feedback&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CheckoutForm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;relative&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* checkout fields */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Gotcha&lt;/span&gt; &lt;span class="na"&gt;elementId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"checkout-form"&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"feedback"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foyycv7ldeyh4lrti4w4m.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foyycv7ldeyh4lrti4w4m.gif" alt="A feedback button attached to a pricing card. Clicking it opens a vote prompt in place; the response then appears in the dashboard already tagged with the component id." width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Live on the Gotcha demo. The button is attached to the pricing card, and the response lands in the dashboard tagged &lt;code&gt;example-pricing&lt;/code&gt;, which is that component's id. Same mechanism as the &lt;code&gt;checkout-form&lt;/code&gt; above.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;elementId="checkout-form"&lt;/code&gt; is the entire mechanism. Ratings, text, and bug reports from that button arrive already tagged. You are not parsing a paragraph to guess what they were looking at; you are looking at a count on &lt;code&gt;checkout-form&lt;/code&gt;. The wrapper needs &lt;code&gt;position: relative&lt;/code&gt; because the button positions against its nearest positioned ancestor. Keep the button in the component file so the two move together. Rename the component and the tag comes with it. Delete the component and you don't leave an orphaned id collecting noise about something that no longer exists.&lt;/p&gt;

&lt;p&gt;The mode only changes what the box asks for. The attachment stays the same.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Gotcha&lt;/span&gt; &lt;span class="na"&gt;elementId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"search-results"&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"feedback"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Gotcha&lt;/span&gt; &lt;span class="na"&gt;elementId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"ai-suggestion"&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"vote"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Gotcha&lt;/span&gt; &lt;span class="na"&gt;elementId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"onboarding"&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"nps"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Gotcha&lt;/span&gt;
  &lt;span class="na"&gt;elementId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"pricing-table"&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"poll"&lt;/span&gt;
  &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Too expensive&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Missing a feature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Just browsing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vote is the one I underestimated. Almost nobody writes a paragraph about a component they mildly dislike. A lot of people will still hit a thumb, which is a signal from people who were never going to type.&lt;/p&gt;

&lt;p&gt;A 2-star tells you something is wrong and nothing about why. If you want the reason in the same interaction, while they are still looking at the thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Gotcha&lt;/span&gt;
  &lt;span class="na"&gt;elementId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"export-flow"&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"feedback"&lt;/span&gt;
  &lt;span class="na"&gt;followUp&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;ratingThreshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;promptText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;What made this a 1 or 2?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ratingThreshold&lt;/code&gt; is inclusive, so &lt;code&gt;2&lt;/code&gt; catches ones and twos. &lt;code&gt;onNegativeVote: true&lt;/code&gt; does the same for a thumbs-down. A survey you email on Thursday is a different, smaller set of people giving a worse answer.&lt;/p&gt;

&lt;p&gt;Bug reports can carry their own context: page URL, viewport, browser, language, timezone. Screenshot capture is opt-in and lazy-loaded, so you are not paying ~10KB of canvas code for users who will never file a bug.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Gotcha&lt;/span&gt; &lt;span class="na"&gt;elementId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"app-shell"&lt;/span&gt; &lt;span class="na"&gt;enableBugFlag&lt;/span&gt; &lt;span class="na"&gt;enableScreenshot&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The part I actually care about more than the widget is what happens after. Most tools are where requests go to die. Someone tells you export is broken, you fix it three weeks later, they never hear. Status can move new → planned → building → shipped, and if they left an email, shipping it can send them a note. That is cheap and strangely rare.&lt;/p&gt;

&lt;p&gt;I built this. The SDK is &lt;a href="https://gotcha.cx/?ref=devto" rel="noopener noreferrer"&gt;Gotcha&lt;/a&gt; (&lt;code&gt;gotcha-feedback&lt;/code&gt; on npm). About 29KB gzipped, plus ~10KB only if you turn screenshots on. React is the only peer dependency. It talks to one origin (ours). No Segment, PostHog, Google, Sentry, cookies, fingerprinting, or third-party fonts; the glyph is inlined. Nothing is sent unless someone actually submits. There is no AI in it. The dashboard groups responses by element and shows the busiest one. That is a database query. Free tier is 500 responses a month, one project, 30-day analytics, no card. Pro is $29/month if you outgrow it. The npm package is public and you can read the bundle; the repo is not open source yet. I have not decided that question.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;gotcha-feedback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sign up at &lt;a href="https://gotcha.cx/?ref=devto" rel="noopener noreferrer"&gt;gotcha.cx&lt;/a&gt;, grab a key, wrap the app, drop one &lt;code&gt;&amp;lt;Gotcha /&amp;gt;&lt;/code&gt; on the component you are least sure about. There is a test-response button in the dashboard so you can see the shape of a row before anything is live. That is only the shape. Real tagged feedback shows up when your users hit that component.&lt;/p&gt;

&lt;p&gt;If you try it, tell me what annoyed you in a comment here. You can also use the widget on gotcha.cx; it lives on the page you were looking at, tagged to that section, which is the idea.&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>nextjs</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
