<?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: Image2</title>
    <description>The latest articles on DEV Community by Image2 (@image2).</description>
    <link>https://dev.to/image2</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%2F3948866%2F6ae92a50-396c-4fff-92b9-daf3174888e7.png</url>
      <title>DEV Community: Image2</title>
      <link>https://dev.to/image2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/image2"/>
    <language>en</language>
    <item>
      <title>Behind a Single "Paste" Button: The Tale of Two Completely Different APIs</title>
      <dc:creator>Image2</dc:creator>
      <pubDate>Sun, 05 Jul 2026 18:30:58 +0000</pubDate>
      <link>https://dev.to/image2/behind-a-single-paste-button-the-tale-of-two-completely-different-apis-32oo</link>
      <guid>https://dev.to/image2/behind-a-single-paste-button-the-tale-of-two-completely-different-apis-32oo</guid>
      <description>&lt;p&gt;I used to think that adding a "paste image" feature to an upload component would be a one-liner. But when I tried to support both &lt;code&gt;Ctrl+V&lt;/code&gt; and a clickable "Paste Button," I realized I was actually dealing with &lt;strong&gt;two fundamentally different browser APIs&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Passive Approach: &lt;code&gt;clipboardData&lt;/code&gt; (The &lt;code&gt;paste&lt;/code&gt; Event)
&lt;/h3&gt;

&lt;p&gt;When a user presses &lt;code&gt;Ctrl+V&lt;/code&gt;, the browser fires a &lt;code&gt;paste&lt;/code&gt; event, and the data is packed inside &lt;code&gt;event.clipboardData&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handlePaste&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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;ClipboardEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboardData&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/&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="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAsFile&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;handleFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&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;strong&gt;The Upside:&lt;/strong&gt; It requires &lt;strong&gt;no permissions&lt;/strong&gt; and has &lt;strong&gt;universal browser support&lt;/strong&gt;. Because the user explicitly initiates the paste, the browser trusts the action.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Active Approach: &lt;code&gt;navigator.clipboard.read()&lt;/code&gt; (Button Triggered)
&lt;/h3&gt;

&lt;p&gt;Clicking a button to read the clipboard is a whole different story. The user didn't press &lt;code&gt;Ctrl+V&lt;/code&gt;; instead, your code is actively trying to read their data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;handleFiles&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s2"&gt;`pasted.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&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;strong&gt;The Catch:&lt;/strong&gt; It &lt;strong&gt;prompts the user for permission&lt;/strong&gt; on the first try, and &lt;strong&gt;Firefox has poor support&lt;/strong&gt; for &lt;code&gt;read()&lt;/code&gt; when handling images. These aren't just two ways to write the same feature—they are two completely separate paths.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;clipboardData&lt;/code&gt; (&lt;code&gt;Ctrl+V&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;clipboard.read()&lt;/code&gt; (Button)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Permission Prompt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Browser Compatibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fully supported&lt;/td&gt;
&lt;td&gt;Poor image support in Firefox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Focus Requirement&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Required (see below)&lt;/td&gt;
&lt;td&gt;Not required&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The Sneakiest Trap: The Focus Pitfall
&lt;/h3&gt;

&lt;p&gt;Developers usually bind &lt;code&gt;onPaste&lt;/code&gt; to a specific UI element. However, DOM elements don't receive &lt;code&gt;paste&lt;/code&gt; events by default unless they are focusable and currently focused. So, many people end up writing this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;tabIndex=&lt;/span&gt;&lt;span class="s"&gt;{0}&lt;/span&gt; &lt;span class="na"&gt;onPaste=&lt;/span&gt;&lt;span class="s"&gt;{handlePaste}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks fine on paper, but it's a ticking time bomb: &lt;strong&gt;&lt;code&gt;Ctrl+V&lt;/code&gt; will only work after the user explicitly clicks this div to give it focus&lt;/strong&gt;. In reality, when a user takes a screenshot and switches back to your page, the focus is almost never on the upload box. The paste silently fails, and the user feels cheated by a feature that "doesn't work".&lt;/p&gt;

&lt;p&gt;The correct approach is to listen to the entire &lt;code&gt;window&lt;/code&gt; so you can catch the paste event from anywhere on the page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;enableWindowPaste&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="c1"&gt;// Prevent multiple upload boxes from fighting over the event&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onPaste&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ClipboardEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* Extract files as shown above */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paste&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onPaste&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paste&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onPaste&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;enableWindowPaste&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We polished all these edge cases into the upload component for &lt;a href="https://image-2.net/" rel="noopener noreferrer"&gt;Image2&lt;/a&gt;. The takeaway is simple: &lt;strong&gt;"Pasting" isn't a single feature; it's a combination of two APIs and a focus model&lt;/strong&gt;. If you want it to be bulletproof, you need to know exactly which path you are taking.&lt;/p&gt;




&lt;p&gt;This post isn't really about the technical nitty-gritty, though. It’s about something I find much more interesting: &lt;strong&gt;What it's actually like to polish a feature alongside an AI.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It all started with a vague request: &lt;em&gt;"We support Ctrl+V upload now, but users don't know it exists. Help me brainstorm some solutions."&lt;/em&gt; What followed wasn't just a "one-shot code generation"—it was a genuine back-and-forth conversation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It asked questions instead of rushing to code.&lt;/strong&gt; When I suggested adding a paste button, it didn't just blindly implement it. Instead, it laid out the fact that buttons and hotkeys use entirely different clipboard APIs. It mapped out the trade-offs regarding permissions, browser compatibility, and focus requirements, and then asked: &lt;em&gt;"Are you okay with the permission prompt?"&lt;/em&gt; It left the executive decision to me rather than making assumptions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It caught the blind spots I didn't even think to ask about.&lt;/strong&gt; While reviewing the code, it spotted the "focus trap"—the fact that &lt;code&gt;Ctrl+V&lt;/code&gt; would fail unless the upload box was focused. This was completely absent from my requirements because I hadn't realized it was an issue. The true value of a great programming partner is &lt;strong&gt;seeing what lies outside your own peripheral vision&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It used multiple-choice options perfectly.&lt;/strong&gt; When we were debating where to place the button, it didn't choose for me. It presented three different layout strategies, complete with text-based mockups, and let me pick. I chose "to the right of the header section," and only then did it write the implementation. It gave guidance when needed and let me steer when appropriate.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It knew how to tie up loose ends.&lt;/strong&gt; Once the core functionality was working, it didn't stop there. It went ahead and added internationalization (i18n), accessible tooltips, and even proactively verified the &lt;code&gt;aria-label&lt;/code&gt; tags for several icon buttons—the exact kind of polish I usually forget about.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After four rounds of iteration, my biggest takeaway is this: the value of an AI partner isn't just "speed." It’s how &lt;strong&gt;it forces a small feature to expose the depth it truly deserves&lt;/strong&gt;. Focus models, API compromises, component communication, i18n, accessibility—we didn't miss a thing. It didn't do the thinking &lt;em&gt;for&lt;/em&gt; me; it helped me think the problem all the way through.&lt;/p&gt;

&lt;p&gt;This heavily refined feature is now live in &lt;a href="https://image-2.net/" rel="noopener noreferrer"&gt;Image2&lt;/a&gt;'s image upload component. If you're curious, feel free to try it out and let me know what you think!&lt;/p&gt;

</description>
      <category>javascript</category>
    </item>
  </channel>
</rss>
