<?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: Muhammad Hanzala</title>
    <description>The latest articles on DEV Community by Muhammad Hanzala (@muhammad_hanzala_4d01c319).</description>
    <link>https://dev.to/muhammad_hanzala_4d01c319</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%2F1994358%2F7e1b0b1b-29ae-4519-86a2-7e30c71d9561.jpg</url>
      <title>DEV Community: Muhammad Hanzala</title>
      <link>https://dev.to/muhammad_hanzala_4d01c319</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/muhammad_hanzala_4d01c319"/>
    <language>en</language>
    <item>
      <title>How to add file uploads to shadcn/ui in Next.js (direct to S3 or R2)</title>
      <dc:creator>Muhammad Hanzala</dc:creator>
      <pubDate>Tue, 13 Oct 2026 15:45:03 +0000</pubDate>
      <link>https://dev.to/muhammad_hanzala_4d01c319/how-to-add-file-uploads-to-shadcnui-in-nextjs-direct-to-s3-or-r2-72d</link>
      <guid>https://dev.to/muhammad_hanzala_4d01c319/how-to-add-file-uploads-to-shadcnui-in-nextjs-direct-to-s3-or-r2-72d</guid>
      <description>&lt;p&gt;File uploads look simple until you ship them: progress bars, retries when the network drops, files too big for one request, keeping bytes off your server, and a UI that matches the rest of your shadcn app.&lt;/p&gt;

&lt;p&gt;This guide uses UploadCN, an open-source upload kit for shadcn/ui, to add production-ready uploads to a Next.js App Router project in a few minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Add the upload component
&lt;/h2&gt;

&lt;p&gt;In a project set up with the shadcn CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx shadcn@latest add @uploadcn/file-upload @uploadcn/upload-config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI copies the components into &lt;code&gt;components/&lt;/code&gt;, in your project's shadcn style and icon library, and installs the small &lt;code&gt;@uploadcn/core&lt;/code&gt; and &lt;code&gt;@uploadcn/react&lt;/code&gt; packages that handle the upload logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Add the upload route
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx shadcn@latest add @uploadcn/upload-route
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates &lt;code&gt;app/api/upload/route.ts&lt;/code&gt;. It doesn't receive files: it signs each upload, so the browser sends bytes straight to your bucket. Add your credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;S3_BUCKET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-uploads
&lt;span class="nv"&gt;S3_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-east-1
&lt;span class="nv"&gt;S3_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...
&lt;span class="nv"&gt;S3_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Cloudflare R2, use the R2 variables instead (see the docs). Allow &lt;code&gt;PUT&lt;/code&gt; from your origin in the bucket's CORS settings and expose the &lt;code&gt;ETag&lt;/code&gt; header.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Configure it once
&lt;/h2&gt;

&lt;p&gt;Wrap your app so every upload component knows where files go:&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/layout.tsx&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;UploadConfig&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="s2"&gt;@/components/upload-config&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&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="nt"&gt;html&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"en"&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;body&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;UploadConfig&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;UploadConfig&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;body&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;html&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;h2&gt;
  
  
  4. Use it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FileUpload&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="s2"&gt;@/components/file-upload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Page&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FileUpload&lt;/span&gt; &lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"image/*"&lt;/span&gt; &lt;span class="na"&gt;maxSize&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;maxFiles&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;5&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;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get drag and drop, a file list with previews and progress, retry and remove, validation, and uploads that resume when the connection comes back. Big files upload in parallel chunks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Other storage: Cloudinary, local disk, or any API through &lt;code&gt;createAdapter&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Virus scanning in the route (ClamAV, VirusTotal) and OCR&lt;/li&gt;
&lt;li&gt;58 blocks: avatar crop, gallery, document checklist, chat attachments, CSV import and 30 animated uploaders&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Docs and live demos: &lt;a href="https://uploadcn.dev" rel="noopener noreferrer"&gt;https://uploadcn.dev&lt;/a&gt;&lt;br&gt;
Repo (MIT): &lt;a href="https://github.com/uploadcn/uploadcn" rel="noopener noreferrer"&gt;https://github.com/uploadcn/uploadcn&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you try it, I'd love to hear what you'd improve.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
