<?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" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ shadcn - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ shadcn - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Wed, 09 Sep 2026 16:53:51 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/shadcn/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Create a Scalable KYC Onboarding Flow in React with Shadcn UI ]]>
                </title>
                <description>
                    <![CDATA[ Every B2B SaaS product with a compliance requirement (like banking, lending, payroll, or crypto) hits the same wall early on: before you can let a business use your platform, you need to verify who th ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-create-a-kyc-onboarding-flow-with-shadcn-ui/</link>
                <guid isPermaLink="false">6a7e08ac157d6ad1bb83d9cf</guid>
                
                    <category>
                        <![CDATA[ shadcn ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Vaibhav Gupta ]]>
                </dc:creator>
                <pubDate>Thu, 13 Aug 2026 18:10:52 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/c86a7b7f-9199-499c-8f61-7a1fda09f519.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Every B2B SaaS product with a compliance requirement (like banking, lending, payroll, or crypto) hits the same wall early on: before you can let a business use your platform, you need to verify who they are.</p>
<p>That means collecting a business type, pulling in registration documents, and showing the user where their verification stands, all without making onboarding feel like a customs form.</p>
<p>This article breaks down a working three step KYC (Know Your Customer) flow built with Shadcn UI: a stepper for progress, a radio group for account type, a file upload zone for documents, and an alert for verification status. You'll see the actual component code, not a simplified stand-in, along with the reasoning behind each decision.</p>
<p>You can try the finished flow at <a href="http://onboarding-kyc-flow.vercel.app"><strong>onboarding-kyc-flow.vercel.app</strong></a>. Click through it once before reading on, as it makes the code below easier to follow. And it also comes in dark and light mode.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-youre-building">What You're Building</a></p>
</li>
<li><p><a href="#heading-project-structure">Project Structure</a></p>
</li>
<li><p><a href="#heading-radix-ui-vs-base-ui-which-primitives-this-flow-uses">Radix UI vs Base UI: Which Primitives this Flow Uses</a></p>
</li>
<li><p><a href="#heading-scaffolding-the-flow-with-v0-and-an-mcp-server">Scaffolding the Flow with v0 and an MCP Server</a></p>
</li>
<li><p><a href="#heading-step-1-account-type-with-a-radio-group">Step 1: Account Type with a Radio Group</a></p>
</li>
<li><p><a href="#heading-step-2-document-upload-with-drag-and-drop">Step 2: Document Upload with Drag and Drop</a></p>
</li>
<li><p><a href="#heading-step-3-verification-status-with-an-alert">Step 3: Verification Status with an Alert</a></p>
</li>
<li><p><a href="#heading-adding-a-stepper-to-the-flow">Adding a Stepper to the Flow</a></p>
</li>
<li><p><a href="#heading-small-details-that-make-it-feel-finished">Small Details that Make it Feel Finished</a></p>
</li>
<li><p><a href="#heading-accessibility-notes">Accessibility notes</a></p>
</li>
<li><p><a href="#heading-key-concepts-recap">Key Concepts Recap</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-resources">Resources</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before working through this flow, you should be comfortable with React function components and hooks, specifically <code>useState</code>, <code>useRef</code>, and <code>useEffect</code>.</p>
<p>You should have:</p>
<ul>
<li><p>A Next.js project with the App Router and shadcn/ui already initialized, since this article doesn't cover that initial setup.</p>
</li>
<li><p>A v0 account is optional. You can also use Bolt or Lovable, which support the same shadcn MCP prompt feature.</p>
</li>
</ul>
<h2 id="heading-what-youre-building">What You're Building</h2>
<p>The flow has three steps:</p>
<ol>
<li><p><strong>Account type:</strong> The user picks Startup, Enterprise, or Government. This decision drives the rest of the experience. It's shown back to the user as a confirmation line, and would typically decide which workspace defaults get applied.</p>
</li>
<li><p><strong>Document upload:</strong> The user drags in a business registration document, a tax return, or a company registry export, in PDF or CSV format.</p>
</li>
<li><p><strong>Verification status:</strong> The user sees a live status: checking in progress, then either verified or an issue that needs attention.</p>
</li>
</ol>
<h2 id="heading-project-structure">Project Structure</h2>
<p>The project is a standard Next.js app with <a href="https://shadcnspace.com/"><strong>shadcn/ui</strong></a> already initialized. Here's the top-level layout:</p>
<pre><code class="language-javascript">onboarding-kyc-flow/
├── .vercel/
├── app/
├── components/
├── lib/
├── public/
├── .env.development.local
├── .gitignore
├── components.json
├── next-env.d.ts
├── next.config.mjs
├── package.json
├── pnpm-lock.yaml
├── postcss.config.mjs
├── tsconfig.json
└── tsconfig.tsbuildinfo
</code></pre>
<p><code>components.json</code> is the file the shadcn CLI reads to know where your components live and which style and primitives you're using. <code>components/</code> holds the shared UI pieces (Alert, Badge, Button, Card, Progress, RadioGroup, Separator) that the flow is built from. <code>lib/utils.ts</code> provides the <code>cn</code> helper used throughout the flow to combine conditional class names. <code>app/</code> holds the page itself, shown in full below.</p>
<h2 id="heading-radix-ui-vs-base-ui-which-primitives-this-flow-uses">Radix UI vs Base UI: Which Primitives this Flow Uses</h2>
<p><a href="https://shadcnspace.com/components"><strong>Shadcn components</strong></a> aren't tied to one underlying primitive library. Most of the ecosystem defaults to Radix UI, but Base UI has become a solid alternative, and it's what this flow is built on.</p>
<p>The underlying primitive library can affect how a component behaves and how you work with it in your project. If you're pulling components from a set like Shadcn UI, check which primitive library it targets before mixing components from different sources.</p>
<p>Mixing Radix-based and Base UI-based components generally works, but it means using two different unstyled primitive libraries in the same project. You can <a href="https://shadcnspace.com/blog/radix-ui-vs-base-ui"><strong>compare Radix UI and Base UI here</strong></a>.</p>
<h2 id="heading-scaffolding-the-flow-with-v0-and-an-mcp-server">Scaffolding the Flow with v0 and an MCP Server</h2>
<p>An MCP (Model Context Protocol) server exposes a component library to an AI coding assistant as a set of callable tools. Instead of the assistant guessing at component names and props from training data, it queries the server for the real, current API.</p>
<p>This matters here specifically, since there are now several shadcn-style component sets with similar names and different props.</p>
<p>The Shadcn Components library publishes an MCP server for its free set, connected to v0 by following its <a href="https://shadcnspace.com/docs/getting-started/mcp-server-docs"><strong>getting started guide</strong></a>. The video below covers the connection step by step. The same generated output can also be copied into Lovable or Bolt through their copy prompt feature, so the workflow isn't locked to one AI builder.</p>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/ymTlzbkvvPk" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>

<p>The prompt used to scaffold this flow looked like this:</p>
<blockquote>
<p>Create an Enterprise SaaS Onboarding &amp; KYC Flow. Use free components of the shadcn space MCP server: shadcn alert, shadcn radio group, shadcn stepper, shadcn file upload. Only use free components, not pro ones, and list which free component was used for each part.</p>
<p>Step 1: Account Type (stepper) - radio group for Startup, Enterprise, or Government</p>
<p>Step 2: Upload Documents (stepper) - file upload for a business registration document</p>
<p>Step 3: Verification (stepper) - alert showing verification status</p>
</blockquote>
<p>This produces a working first draft fast. What follows is the result after cleaning that draft up: real state management, real validation, and states that a generated draft tends to skip.</p>
<h2 id="heading-step-1-account-type-with-a-radio-group">Step 1: Account Type with a Radio Group</h2>
<p>Account type is the first decision in the flow because it's the one most likely to affect what comes after it. Asking it early keeps the rest of the flow feeling relevant to the choice the user just made.</p>
<pre><code class="language-javascript">const tiers: { id: Tier; name: string; description: string; tag: string }[] = [
  { id: 'startup', name: 'Startup', description: 'For teams building and scaling fast', tag: 'Up to 25 seats' },
  { id: 'enterprise', name: 'Enterprise', description: 'For established teams with advanced needs', tag: 'Unlimited seats' },
  { id: 'government', name: 'Government', description: 'For public sector and regulated teams', tag: 'FedRAMP-ready' },
]
</code></pre>
<pre><code class="language-javascript">&lt;RadioGroup value={tier} onValueChange={(value) =&gt; setTier(value as Tier)} className="grid gap-3"&gt;
  &lt;fieldset className="contents"&gt;
    &lt;legend className="sr-only"&gt;Account type&lt;/legend&gt;
    {tiers.map((item) =&gt; (
      &lt;label
        key={item.id}
        htmlFor={item.id}
        className={cn(
          'flex cursor-pointer items-start gap-4 rounded-xl border p-4 transition-colors hover:border-primary/50',
          tier === item.id &amp;&amp; 'border-primary bg-primary/5'
        )}
      &gt;
        &lt;RadioGroupItem value={item.id} id={item.id} className="mt-0.5" /&gt;
        &lt;span className="flex flex-1 flex-col gap-1"&gt;
          &lt;span className="flex flex-wrap items-center gap-2 text-sm font-semibold"&gt;
            {item.name}
            {item.id === 'enterprise' &amp;&amp; &lt;Badge variant="secondary"&gt;Recommended&lt;/Badge&gt;}
          &lt;/span&gt;
          &lt;span className="text-sm text-muted-foreground"&gt;{item.description}&lt;/span&gt;
          &lt;span className="mt-1 font-mono text-[11px] uppercase tracking-wide text-muted-foreground"&gt;{item.tag}&lt;/span&gt;
        &lt;/span&gt;
      &lt;/label&gt;
    ))}
  &lt;/fieldset&gt;
&lt;/RadioGroup&gt;
</code></pre>
<p>Two things worth noticing here. The tier data lives in a plain array outside the component, so adding a fourth tier later is a one-line change, not a markup change. And the <code>fieldset</code> with a visually hidden (<code>sr-only</code>) legend groups the three options as one related choice for screen readers. Sighted users never see it, since the card title above already states "Choose your account type" visually.</p>
<p>This step uses a <a href="https://shadcnspace.com/components/radio-group"><strong>shadcn radio group</strong></a> rather than a select or checkboxes, since account type is a single, mutually exclusive choice, and a radio group is the only one of the three that makes both the options and the current selection visible at a glance.</p>
<h3 id="heading-live-preview"><strong>Live Preview:</strong></h3>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/5ad2e892-88ae-4e6b-b83d-451ebe13dc74.png" alt="Step 1: Account type with a radio group" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<hr>
<h2 id="heading-step-2-document-upload-with-drag-and-drop">Step 2: Document Upload with Drag and Drop</h2>
<p>The upload zone needs to handle three states cleanly: nothing selected yet, a file selected and ready, and a rejected file with a specific reason why.</p>
<pre><code class="language-javascript">function FileUpload({ file, onFile, onRemove, error }: {
  file: File | null
  onFile: (file: File) =&gt; void
  onRemove: () =&gt; void
  error: string
}) {
  const inputRef = useRef&lt;HTMLInputElement&gt;(null)
  const [dragging, setDragging] = useState(false)

  const accept = (candidate: File) =&gt; {
    if (candidate.type !== 'application/pdf' &amp;&amp; candidate.type !== 'text/csv' &amp;&amp; !candidate.name.toLowerCase().endsWith('.csv')) {
      return 'Upload a PDF or CSV file only.'
    }
    if (candidate.size &gt; 10 * 1024 * 1024) {
      return 'Files must be smaller than 10 MB.'
    }
    onFile(candidate)
    return ''
  }

  return (
    &lt;div className="flex flex-col gap-3"&gt;
      {!file ? (
        &lt;button
          type="button"
          className={cn(
            'group flex min-h-44 flex-col items-center justify-center rounded-xl border border-dashed bg-muted/30 px-6 text-center transition-colors hover:border-primary hover:bg-primary/5',
            dragging &amp;&amp; 'border-primary bg-primary/10'
          )}
          onClick={() =&gt; inputRef.current?.click()}
          onDragOver={(event) =&gt; { event.preventDefault(); setDragging(true) }}
          onDragLeave={() =&gt; setDragging(false)}
          onDrop={(event) =&gt; {
            event.preventDefault()
            setDragging(false)
            const dropped = event.dataTransfer.files[0]
            if (dropped) accept(dropped)
          }}
        &gt;
          &lt;input
            ref={inputRef}
            className="sr-only"
            type="file"
            accept=".pdf,.csv,application/pdf,text/csv"
            onChange={(event) =&gt; {
              const selected = event.target.files?.[0]
              if (selected) accept(selected)
            }}
          /&gt;
          &lt;span className="mb-3 flex size-11 items-center justify-center rounded-lg border bg-background text-primary shadow-sm"&gt;
            &lt;UploadCloud className="size-5" aria-hidden="true" /&gt;
          &lt;/span&gt;
          &lt;span className="text-sm font-semibold"&gt;Drop your business document here&lt;/span&gt;
          &lt;span className="mt-1 text-xs text-muted-foreground"&gt;or click to browse · PDF or CSV · max 10 MB&lt;/span&gt;
        &lt;/button&gt;
      ) : (
        &lt;div className="flex items-center gap-3 rounded-xl border bg-muted/30 p-4"&gt;
          &lt;span className="flex size-10 items-center justify-center rounded-lg bg-primary/10 text-primary"&gt;
            &lt;FileText className="size-5" aria-hidden="true" /&gt;
          &lt;/span&gt;
          &lt;div className="min-w-0 flex-1"&gt;
            &lt;p className="truncate text-sm font-semibold"&gt;{file.name}&lt;/p&gt;
            &lt;p className="text-xs text-muted-foreground"&gt;{(file.size / 1024 / 1024).toFixed(2)} MB · Ready to verify&lt;/p&gt;
          &lt;/div&gt;
          &lt;Badge variant="secondary" className="hidden sm:inline-flex"&gt;Uploaded&lt;/Badge&gt;
          &lt;Button type="button" variant="ghost" size="icon-sm" aria-label="Remove file" onClick={onRemove}&gt;
            &lt;X className="size-4" aria-hidden="true" /&gt;
          &lt;/Button&gt;
        &lt;/div&gt;
      )}
      {error &amp;&amp; (
        &lt;Alert variant="destructive"&gt;
          &lt;AlertCircle className="size-4" aria-hidden="true" /&gt;
          &lt;AlertTitle&gt;Unsupported document&lt;/AlertTitle&gt;
          &lt;AlertDescription&gt;{error}&lt;/AlertDescription&gt;
        &lt;/Alert&gt;
      )}
    &lt;/div&gt;
  )
}
</code></pre>
<p>The <code>accept</code> function is the whole validation layer, and it runs from two different places: the change handler on the hidden file input, and the drop handler on the drag zone.</p>
<p>Both paths call the same function, so a file dragged in gets the same validation checks as a file selected by clicking browse. It ensures that only <strong>PDF or CSV files</strong> are allowed, regardless of how the file is added.</p>
<p>This is where <a href="https://shadcnspace.com/components/file-upload"><strong>shadcn file upload</strong></a> earns its place over a plain <code>&lt;input type="file"&gt;</code>: the drag zone, the selected state, and the rejected state are all handled as one component instead of three separate pieces wired together by hand.</p>
<h3 id="heading-live-preview"><strong>Live Preview:</strong></h3>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/959c806e-9e52-43f9-ad7f-c2855329ac5c.png" alt="Step 2: Document upload with drag and drop" style="display:block;margin:0 auto" width="1919" height="945" loading="lazy">

<h2 id="heading-step-3-verification-status-with-an-alert">Step 3: Verification Status with an Alert</h2>
<p>Verification isn't instant, so the interface needs to say clearly what's happening and what happens next, rather than showing a spinner with no explanation.</p>
<pre><code class="language-javascript">{verified ? (
  &lt;Alert className="border-primary/30 bg-primary/5"&gt;
    &lt;CheckCircle2 className="size-4 text-primary" aria-hidden="true" /&gt;
    &lt;AlertTitle&gt;Verification complete&lt;/AlertTitle&gt;
    &lt;AlertDescription&gt;
      Your {selectedTier.name.toLowerCase()} workspace is ready to configure.
    &lt;/AlertDescription&gt;
  &lt;/Alert&gt;
) : (
  &lt;&gt;
    &lt;Alert&gt;
      &lt;AlertCircle className="size-4" aria-hidden="true" /&gt;
      &lt;AlertTitle&gt;Verification in progress&lt;/AlertTitle&gt;
      &lt;AlertDescription&gt;
        This usually takes a few moments. You can keep this tab open while we finish.
      &lt;/AlertDescription&gt;
    &lt;/Alert&gt;
    &lt;div className="flex flex-col gap-3"&gt;
      &lt;div className="flex items-center justify-between text-sm"&gt;
        &lt;span className="font-medium"&gt;Checking business registry&lt;/span&gt;
        &lt;span className="font-mono text-xs text-muted-foreground"&gt;{checking ? '68%' : '100%'}&lt;/span&gt;
      &lt;/div&gt;
      &lt;Progress value={checking ? 68 : 100} /&gt;
      &lt;div className="flex items-center gap-2 text-xs text-muted-foreground"&gt;
        &lt;Building2 className="size-3.5" aria-hidden="true" /&gt; Matching company details and tax identifiers
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/&gt;
)}
</code></pre>
<p>Pairing the <a href="https://shadcnspace.com/components/alert"><strong>shadcn alert</strong></a> with a progress bar does two jobs at once: the alert states the current status in words, while the progress bar gives a rough sense of how much is left, without promising a specific time. Neither one alone tells the full story, the alert alone feels static, and a progress bar alone doesn't explain what's actually being checked.</p>
<p>Worth adding here, and easy to skip when a demo only shows the success path: a mismatch state, where the tax ID on the document doesn't match the company registry, deserves its own alert with a clear next step: contact support or re-upload a corrected document. It's not shown above, since the flow currently resolves to either checking or verified, but it's the state a production version of this flow would hit the most.</p>
<h3 id="heading-live-preview"><strong>Live Preview:</strong></h3>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/55f02dfe-af23-4d07-aaaa-868e2a7fb834.png" alt="Step 3: Verification status with an alert" style="display:block;margin:0 auto" width="1919" height="946" loading="lazy">

<hr>
<h2 id="heading-adding-a-stepper-to-the-flow">Adding a Stepper to the Flow</h2>
<p>The stepper is the visual anchor of the whole flow. It's the piece that tells the user how much is left before the checking and account-type-selecting are done.</p>
<pre><code class="language-javascript">function Stepper({ current }: { current: Step }) {
  return (
    &lt;nav
      aria-label="Onboarding progress"
      className="grid grid-cols-[minmax(0,1fr)_minmax(2rem,5rem)_minmax(0,1fr)_minmax(2rem,5rem)_minmax(0,1fr)] items-start gap-0"
    &gt;
      {steps.map((step, index) =&gt; (
        &lt;div key={step.number} className="contents"&gt;
          &lt;div className="flex min-w-0 flex-col items-center text-center"&gt;
            &lt;div
              className={cn(
                'flex size-9 items-center justify-center rounded-full border text-sm font-semibold transition-colors',
                current &gt; step.number
                  ? 'border-primary bg-primary text-primary-foreground'
                  : current === step.number
                  ? 'border-primary bg-primary/10 text-primary'
                  : 'border-border bg-background text-muted-foreground'
              )}
              aria-current={current === step.number ? 'step' : undefined}
            &gt;
              {current &gt; step.number ? &lt;Check className="size-4" aria-hidden="true" /&gt; : step.number}
            &lt;/div&gt;
            &lt;div className="mt-2 min-w-0"&gt;
              &lt;p className={cn('truncate text-sm font-semibold', current &gt;= step.number ? 'text-foreground' : 'text-muted-foreground')}&gt;
                {step.label}
              &lt;/p&gt;
              &lt;p className="mt-1 hidden text-xs leading-5 text-muted-foreground sm:block"&gt;{step.caption}&lt;/p&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          {index &lt; steps.length - 1 &amp;&amp; (
            &lt;div className={cn('mt-4 h-px w-full', current &gt; step.number ? 'bg-primary' : 'bg-border')} /&gt;
          )}
        &lt;/div&gt;
      ))}
    &lt;/nav&gt;
  )
}
</code></pre>
<p>The <code>current &gt; step.number</code> check keeps the entire stepper in sync with a single comparison. It determines the circle’s fill color, decides when the step number should be replaced by a checkmark, and controls whether the connecting line to the next step is filled.</p>
<p>This is important because the stepper only needs one piece of state, <code>step</code>, from the parent component. It doesn’t need to know why the user is on step 2, it only needs to know which step is currently active and update its visual state accordingly.</p>
<p>The "continue" logic that actually advances <code>step</code> lives outside the stepper itself:</p>
<pre><code class="language-javascript">const continueStep = () =&gt; {
  if (step === 1) setStep(2)
  else if (step === 2 &amp;&amp; file) {
    setStep(3)
    setChecking(true)
    window.setTimeout(() =&gt; {
      setChecking(false)
      setVerified(true)
    }, 1400)
  }
}
</code></pre>
<p>Keeping this in the page component, not inside the <a href="https://shadcnspace.com/components/stepper"><strong>shadcn stepper</strong></a> itself, is what keeps the stepper reusable. It only renders progress. Whether the user is allowed to move forward, a file is required on step 2, or whether nothing is required on step 1, is a decision for the flow around it to make.</p>
<h2 id="heading-small-details-that-make-it-feel-finished">Small Details that Make it Feel Finished</h2>
<p>A few things in this build are easy to skip but change how the flow feels in practice:</p>
<ul>
<li><p><strong>A dark mode toggle</strong> in the header, wired to a <code>darkMode</code> state that toggles a class on <code>document.documentElement</code>. It's small, but it means the flow doesn't fight a user's system theme preference.</p>
</li>
<li><p><strong>A security note</strong> in the sidebar, stating documents are encrypted and deleted after verification. This is copy, not code, but it answers the question a corporate user is quietest about and most worried by: what happens to the file after I upload it.</p>
</li>
<li><p><strong>A "Selected" confirmation line</strong> under the radio group, restating the chosen tier in plain text. A small detail, but it removes any doubt about what was actually selected before moving on.</p>
</li>
</ul>
<p>If you're looking to wrap a flow like this inside a full application shell, with navigation and a dashboard around it, the <a href="https://shadcnspace.com/admin-dashboard"><strong>Shadcn Dashboard</strong></a> starter uses the same component set. It's a reasonable base to extend from rather than building a shell from scratch.</p>
<h3 id="heading-live-preview">Live Preview:</h3>
<p><a class="embed-card" href="https://onboarding-kyc-flow.vercel.app/">https://onboarding-kyc-flow.vercel.app/</a></p>

<p>This project is open source, and you can easily download the zip and if you like. Please consider giving it a star.</p>
<ul>
<li><a href="https://github.com/vaibhavsudo/onboarding-kyc-flow"><strong>Github Repo</strong></a></li>
</ul>
<h2 id="heading-accessibility-notes">Accessibility notes</h2>
<ul>
<li><p>The stepper's <code>nav</code> element has an <code>aria-label</code>, and the current step carries <code>aria-current="step"</code>, so assistive technology can identify progress without relying on visual position alone.</p>
</li>
<li><p>The radio group sits inside a <code>fieldset</code> with a screen-reader-only <code>legend</code>, grouping the three account types as one decision.</p>
</li>
<li><p>Icons throughout (<code>Check</code>, <code>AlertCircle</code>, <code>UploadCloud</code>, and so on) carry <code>aria-hidden="true"</code>, since they're decorative next to text that already states the same information. This stops screen readers from announcing redundant icon labels.</p>
</li>
<li><p>The remove-file button has an explicit <code>aria-label</code>, since its visible content is an icon only, with no text.</p>
</li>
</ul>
<h2 id="heading-key-concepts-recap">Key Concepts Recap</h2>
<ul>
<li><p>Account type comes first because it's the one decision most likely to affect the rest of the flow, and it's kept in state at the page level, not inside the radio group itself.</p>
</li>
<li><p>File validation runs in a shared function used by both the drag-and-drop path and the click-to-browse path, so both paths apply the same PDF/CSV validation.</p>
</li>
<li><p>Verification status is communicated with both words (the alert) and a rough sense of progress (the progress bar), since either one alone leaves out part of the picture.</p>
</li>
<li><p>The stepper is a pure display component driven by a single <code>step</code> value from its parent. The logic for whether the user can advance lives outside it, not inside it.</p>
</li>
<li><p>Small, non-technical details (like a security note, a confirmation line, or a theme toggle) do as much for how finished a flow feels as any of the four core components.</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>None of the four components in this flow are complicated individually. What makes a KYC flow work is the decisions around them: which choice comes first, where validation actually runs, and how honestly the interface talks to the user while something outside their control is being checked.</p>
<p>Whether the first draft comes from typing every line by hand or from scaffolding it with an MCP server and v0, that's the part worth spending time getting right before it ships.</p>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><a href="https://shadcnspace.com/components"><strong>Shadcn Components</strong></a>, the free component set used in this flow</p>
</li>
<li><p><a href="https://shadcnspace.com/"><strong>ShadcnSpace</strong></a>, the base library these components extend</p>
</li>
<li><p><a href="https://shadcnspace.com/mcp"><strong>MCP server walkthrough</strong></a></p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current"><strong>MDN: ARIA current attribute</strong></a></p>
</li>
<li><p><a href="https://modelcontextprotocol.io/"><strong>Model Context Protocol specification</strong></a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build an Open Source SaaS Landing Page Template with shadcn/ui ]]>
                </title>
                <description>
                    <![CDATA[ Most SaaS landing pages share the same core sections: a hero, social proof, features, pricing, FAQ, and a footer. And most developers end up building these from scratch on every project. That's repeti ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-landing-page-nextjs-shadcn/</link>
                <guid isPermaLink="false">6a70e0650d58f4d80d2eca59</guid>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Open Source ]]>
                    </category>
                
                    <category>
                        <![CDATA[ shadcn ]]>
                    </category>
                
                    <category>
                        <![CDATA[ shadcnui ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ash ]]>
                </dc:creator>
                <pubDate>Mon, 03 Aug 2026 18:39:33 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5fc16e412cae9c5b190b6cdd/33d9aa05-3187-4d07-8aea-bcd83fe13ac0.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Most SaaS landing pages share the same core sections: a hero, social proof, features, pricing, FAQ, and a footer. And most developers end up building these from scratch on every project. That's repetition, not engineering.</p>
<p>So I built and open-sourced a complete SaaS landing page template called <a href="https://www.shadcndeck.com/templates/chatdeck-saas-landing-page">ChatDeck</a>. It runs on Next.js 16, React 19, shadcn/ui with the new <code>base-nova</code> style, Tailwind CSS v4, and TypeScript. The full source is on GitHub under the MIT license. I built and open-sourced this template, and everything here comes from decisions made during that process.</p>
<p>Building it forced me to make real decisions on a stack that moved significantly in the past 12 months. This article is about those decisions: what worked, what didn't, and what I'd do differently if I started today.</p>
<p><strong>Prerequisites:</strong> This article assumes you're comfortable with React and TypeScript. Some familiarity with the Next.js App Router is helpful but not required. Each lesson is explained from first principles.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-the-stack-choices-and-why-they-matter">The Stack Choices and Why They Matter</a></p>
</li>
<li><p><a href="#heading-getting-started">Getting Started</a></p>
</li>
<li><p><a href="#heading-project-structure">Project Structure</a></p>
</li>
<li><p><a href="#heading-lesson-1-shadcnuis-new-base-nova-style-changes-what-accessible-means">Lesson 1: shadcn/ui's Newbase-novaStyle Changes What "Accessible" Means</a></p>
</li>
<li><p><a href="#heading-lesson-2-tailwind-css-v4-requires-a-mental-model-shift">Lesson 2: Tailwind CSS v4 Requires a Mental Model Shift</a></p>
</li>
<li><p><a href="#heading-lesson-3-oklch-colors-make-dark-mode-predictable">Lesson 3: OKLCH Colors Make Dark Mode Predictable</a></p>
</li>
<li><p><a href="#heading-lesson-4-page-architecture-flat-beats-clever">Lesson 4: Page Architecture — Flat Beats Clever</a></p>
</li>
<li><p><a href="#heading-lesson-5-staggered-animations-without-managing-individual-delays">Lesson 5: Staggered Animations Without Managing Individual Delays</a></p>
</li>
<li><p><a href="#heading-lesson-6-css-only-infinite-scroll-no-library-needed">Lesson 6: CSS-Only Infinite Scroll — No Library Needed</a></p>
</li>
<li><p><a href="#heading-lesson-7-css-subgrid-solves-pricing-card-alignment-natively">Lesson 7: CSS Subgrid Solves Pricing Card Alignment Natively</a></p>
</li>
<li><p><a href="#heading-lesson-8-inline-svgs-beat-image-libraries-for-simple-logos">Lesson 8: Inline SVGs Beat Image Libraries for Simple Logos</a></p>
</li>
<li><p><a href="#heading-what-id-do-differently">What I'd Do Differently</a></p>
</li>
<li><p><a href="#heading-summary">Summary</a></p>
</li>
</ul>
<h2 id="heading-the-stack-choices-and-why-they-matter">The Stack Choices and Why They Matter</h2>
<p>Before getting into the code, here's what the template runs on. Each choice was deliberate — none of these are defaults you get from <code>create-next-app</code>.</p>
<table>
<thead>
<tr>
<th>Technology</th>
<th>Version</th>
<th>Why I chose it</th>
</tr>
</thead>
<tbody><tr>
<td>Next.js</td>
<td>^16.0.3</td>
<td>App Router gives you React Server Components out of the box. Static sections like Hero and Features render on the server — no client-side JS needed for content that never changes.</td>
</tr>
<tr>
<td>React</td>
<td>19.2.0</td>
<td>React 19 stabilises the <code>use</code> hook and concurrent features. Staying on the latest version means the template doesn't immediately feel stale.</td>
</tr>
<tr>
<td>shadcn/ui</td>
<td>^4.13.0 (CLI)</td>
<td>Components are copied into your codebase, not installed as a package. You own the code. No version lock-in, no fighting library defaults when you need to customize.</td>
</tr>
<tr>
<td>Base UI (<code>@base-ui/react</code>)</td>
<td>^1.6.0</td>
<td>shadcn/ui's new <code>base-nova</code> style uses Base UI instead of Radix as its headless primitive layer. It has a smaller peer dependency footprint and tighter ARIA integration. More on this in Lesson 1.</td>
</tr>
<tr>
<td>Tailwind CSS</td>
<td>^4</td>
<td>v4 moves theme configuration from a JavaScript config file into CSS directly. Custom animations, color tokens, and radius scales all live in <code>globals.css</code>. More on this in Lesson 2.</td>
</tr>
<tr>
<td>Motion (<code>motion/react</code>)</td>
<td>^12.23.24</td>
<td>The rebranded Framer Motion. Handles entrance animations on the Hero and scroll-triggered animations on the Features section. Chosen over CSS animations because staggered sequences are much simpler to manage.</td>
</tr>
<tr>
<td>TypeScript</td>
<td>^5</td>
<td>Full type safety throughout. Component props, icon maps, pricing plan objects — all typed. Catches errors at build time, not at runtime.</td>
</tr>
<tr>
<td>Lucide React</td>
<td>^0.553.0</td>
<td>Consistent, well-maintained icon set that works cleanly with Tailwind's <code>size-*</code> utilities. No custom SVG wrangling needed for UI icons.</td>
</tr>
</tbody></table>
<p>The most interesting decisions in this list are the ones that reflect how the ecosystem changed in the past year: Base UI replacing Radix inside shadcn/ui, and Tailwind v4's shift to CSS-first configuration. The lessons below walk through each of these in detail, starting with the choices that had the biggest impact on how the code is actually written.</p>
<h2 id="heading-getting-started">Getting Started</h2>
<p>Before diving into the lessons, here's how to get the project running locally. Having it open alongside this article makes the code examples easier to follow.</p>
<pre><code class="language-bash">git clone https://github.com/ShadcnDeck/chatdeck-shadcn-saas-landing-page-template.git
cd chatdeck-shadcn-saas-landing-page-template
pnpm install
pnpm dev
</code></pre>
<p>Open <code>http://localhost:3000</code> and you'll see the full landing page running locally.</p>
<p>All section content lives as plain TypeScript arrays inside each Block component. To change the features, edit the <code>features</code> array in <code>FeatureSection.tsx</code>. To change pricing tiers, edit the <code>plans</code> array in <code>PricingSection.tsx</code>. No CMS, no config files — just TypeScript objects.</p>
<p>To customize colors, update the OKLCH values in <code>app/globals.css</code> under the <code>:root</code> block. Change <code>--primary</code> and every button, link, and accent color updates across the entire template.</p>
<p>Deploy to Vercel with a single <code>vercel</code> command or by pushing to GitHub and connecting the repo. Next.js is detected automatically.</p>
<h2 id="heading-project-structure">Project Structure</h2>
<p>Here's the full directory layout before we go through each part of it:</p>
<pre><code class="language-plaintext">chatdeck/
├── app/
│   ├── globals.css         # Theme tokens + custom animations (Tailwind v4 @theme)
│   ├── layout.tsx          # Root layout — Navbar, Footer, fonts
│   └── page.tsx            # Section imports — 16 lines
├── components/
│   ├── Blocks/             # Page sections (Hero, Features, Pricing, etc.)
│   ├── ui/                 # shadcn/ui components — base-nova style
│   └── navbar.tsx          # Scroll-aware sticky navbar
└── lib/
    └── utils.ts            # cn() helper (clsx + tailwind-merge)
</code></pre>
<p>The key separation is <code>Blocks/</code> vs <code>ui/</code>. The <code>ui/</code> folder holds primitive components — Button, Badge, Accordion — that come from shadcn/ui and rarely change. The <code>Blocks/</code> folder holds page-level sections that are specific to this template and change often. When you're customising, you mostly work in <code>Blocks/</code>. When you upgrade <a href="https://www.shadcndeck.com/blog/shadcn-components">shadcn/ui components</a>, you touch <code>ui/</code>.</p>
<p>The lessons below go through specific files in this structure piece by piece: <code>components.json</code> and <code>ui/accordion.tsx</code> in Lesson 1, <code>app/globals.css</code> in Lessons 2 and 3, <code>app/page.tsx</code> in Lesson 4, and the individual Block components in Lessons 5 through 8.</p>
<h2 id="heading-lesson-1-shadcnuis-new-base-nova-style-changes-what-accessible-means">Lesson 1: shadcn/ui's New <code>base-nova</code> Style Changes What "Accessible" Means</h2>
<p>If you've used shadcn/ui before, you know the default setup uses <strong>Radix UI</strong> primitives, headless components that handle focus management, keyboard navigation, and ARIA attributes. Radix has been the default for years.</p>
<p>But shadcn/ui introduced a new style in 2025 called <code>base-nova</code>, which replaces <a href="https://www.shadcndeck.com/blog/radix-vs-base-ui">Radix with <strong>Base UI</strong></a>, the headless primitive library from MUI.</p>
<p>Based on shadcn's public direction and the components released through 2025, <code>base-nova</code> appears to be the intended default going forward (though shadcn hasn't yet deprecated the Radix style).</p>
<p>In the project's <code>components.json</code>:</p>
<pre><code class="language-json">{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "base-nova",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "css": "app/globals.css",
    "baseColor": "neutral",
    "cssVariables": true
  },
  "iconLibrary": "lucide"
}
</code></pre>
<p>The <code>"style": "base-nova"</code> line means every component the shadcn/ui CLI installs wraps Base UI primitives instead of Radix. To understand what this changes in practice, here's what the same Accordion trigger component looks like in the older Radix-based default style:</p>
<pre><code class="language-tsx">// Radix-based default style (the old way)
import * as AccordionPrimitive from "@radix-ui/react-accordion"

const AccordionTrigger = React.forwardRef&lt;
  React.ElementRef&lt;typeof AccordionPrimitive.Trigger&gt;,
  React.ComponentPropsWithoutRef&lt;typeof AccordionPrimitive.Trigger&gt;
&gt;(({ className, children, ...props }, ref) =&gt; {
  const [isOpen, setIsOpen] = React.useState(false)

  return (
    &lt;AccordionPrimitive.Header className="flex"&gt;
      &lt;AccordionPrimitive.Trigger
        ref={ref}
        className={cn("flex flex-1 items-center justify-between ...", className)}
        onClick={() =&gt; setIsOpen(!isOpen)}
        {...props}
      &gt;
        {children}
        &lt;ChevronDownIcon
          className={cn(
            "h-4 w-4 shrink-0 transition-transform duration-200",
            isOpen ? "hidden" : "block"
          )}
        /&gt;
        &lt;ChevronUpIcon
          className={cn(
            "h-4 w-4 shrink-0 transition-transform duration-200",
            isOpen ? "block" : "hidden"
          )}
        /&gt;
      &lt;/AccordionPrimitive.Trigger&gt;
    &lt;/AccordionPrimitive.Header&gt;
  )
})
</code></pre>
<p>Notice the <code>useState(false)</code> tracking whether the accordion is open, and the <code>onClick</code> handler that toggles it. This means the component has to manually keep its own <code>isOpen</code> state in sync with what Radix internally knows about the open/closed state.</p>
<p>Now here's the same component using the <code>base-nova</code> style with Base UI:</p>
<pre><code class="language-tsx">// components/ui/accordion.tsx — base-nova style (the new way)
import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion"

function AccordionTrigger({ className, children, ...props }: AccordionPrimitive.Trigger.Props) {
  return (
    &lt;AccordionPrimitive.Header className="flex"&gt;
      &lt;AccordionPrimitive.Trigger
        data-slot="accordion-trigger"
        className={cn(
          "group/accordion-trigger relative flex flex-1 items-start ...",
          className
        )}
        {...props}
      &gt;
        {children}
        &lt;ChevronDownIcon
          className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden"
        /&gt;
        &lt;ChevronUpIcon
          className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline"
        /&gt;
      &lt;/AccordionPrimitive.Trigger&gt;
    &lt;/AccordionPrimitive.Header&gt;
  )
}
</code></pre>
<p>No <code>useState</code>. No <code>onClick</code>. No <code>isOpen</code> variable. The chevron visibility is controlled entirely by <code>group-aria-expanded/accordion-trigger:hidden</code> — a Tailwind class that reads the <code>aria-expanded</code> attribute Base UI sets automatically on the trigger element.</p>
<p><strong>The lesson here:</strong> in the Radix version, you have two parallel systems: the component's own <code>isOpen</code> state, and the ARIA attributes that the library manages separately for screen readers. These can drift out of sync — for example, if the accordion closes via keyboard navigation, the ARIA state updates correctly but your <code>isOpen</code> state doesn't unless you wire up the right callbacks. In the Base UI version, there is only one system. ARIA state IS the state. Tailwind reads it directly. There's nothing to keep in sync and nothing that can drift.</p>
<p><strong>Lesson:</strong> use the primitive library's ARIA attributes as your source of truth for visual state. If your headless component library already sets <code>aria-expanded</code>, <code>aria-selected</code>, or <code>aria-checked</code>, Tailwind can respond to those directly with <code>aria-*</code> variant classes — no parallel JavaScript state needed.</p>
<p>So when you install shadcn/ui today, choose <code>base-nova</code> over the default Radix style. You get tighter Base UI integration, a smaller peer dependency footprint, and components that are more aligned with where the ecosystem is moving.</p>
<h2 id="heading-lesson-2-tailwind-css-v4-requires-a-mental-model-shift">Lesson 2: Tailwind CSS v4 Requires a Mental Model Shift</h2>
<p>Tailwind CSS v4 moves primary theme configuration out of the JavaScript config file and into CSS. This sounds small. In practice, it changes how you think about the entire theming system.</p>
<p>In Tailwind v3, you'd extend the theme in <code>tailwind.config.js</code>:</p>
<pre><code class="language-js">// OLD — tailwind.config.js (v3)
module.exports = {
  theme: {
    extend: {
      animation: {
        marquee: "marquee 40s linear infinite",
      },
      keyframes: {
        marquee: {
          from: { transform: "translateX(0)" },
          to: { transform: "translateX(calc(-100% - var(--gap)))" },
        },
      },
    },
  },
}
</code></pre>
<p>In Tailwind v4, that same configuration lives in your CSS file instead:</p>
<pre><code class="language-css">/* app/globals.css — Tailwind v4 */
@import "tailwindcss";

@theme inline {
  --animate-marquee: marquee var(--duration) infinite linear;
  --animate-marquee-vertical: marquee-vertical var(--duration) linear infinite;

  @keyframes marquee {
    from { transform: translateX(0); }
    to   { transform: translateX(calc(-100% - var(--gap))); }
  }

  --radius-2xl: calc(var(--radius) * 1.8);
  --radius-3xl: calc(var(--radius) * 2.2);
  --radius-4xl: calc(var(--radius) * 2.6);
}
</code></pre>
<p>The <code>@theme inline</code> block extends Tailwind's design token system. Define <code>--animate-marquee</code> here and you can use <code>className="animate-marquee"</code> anywhere in your components. Tailwind generates the utility class automatically from the CSS variable.</p>
<p>Custom animations, radius scales, and color tokens all live in CSS now. The benefit is that CSS is where styles belong. The config file was always an indirection layer between "what I want my design system to look like" and "where that actually lives." Tailwind v4 removes the indirection.</p>
<p><strong>The friction:</strong> if you start a Tailwind v4 project with a v3 mental model, you'll spend time looking for theme config in the wrong place. Read the v4 migration guide before you start, not after you're confused.</p>
<p><strong>Lesson:</strong> move your mental model of "theme config" from JavaScript to CSS. In Tailwind v4, if you want a custom animation, a new radius scale, or a color token, define it in <code>@theme inline</code> inside <code>globals.css</code>. That's where it belongs, and that's where every developer on your team will find it.</p>
<h2 id="heading-lesson-3-oklch-colors-make-dark-mode-predictable">Lesson 3: OKLCH Colors Make Dark Mode Predictable</h2>
<p>The template uses OKLCH color values throughout, not hex or HSL:</p>
<pre><code class="language-css">:root {
  --background: oklch(1 0 0);        /* white */
  --foreground: oklch(0.145 0 0);    /* near-black */
  --primary: oklch(0.205 0 0);
  --border: oklch(0.922 0 0);
}

.dark {
  --background: oklch(0.145 0 0);    /* near-black */
  --foreground: oklch(0.985 0 0);    /* near-white */
  --primary: oklch(0.922 0 0);
  --border: oklch(1 0 0 / 10%);      /* white at 10% opacity */
}
</code></pre>
<p>OKLCH is a perceptually uniform color space. When you increase the lightness value in OKLCH, the color actually <em>looks</em> lighter to human eyes, consistently. Hex and HSL don't guarantee this. You can increase the <code>L</code> in HSL and get a color that looks the same or even darker depending on the hue.</p>
<p>For dark mode specifically, this matters because you're inverting a whole color system. With HSL, you'll often end up manually tweaking individual color values until contrast ratios look right. With OKLCH, increasing or decreasing the lightness value gives you predictable results across all your tokens.</p>
<p>The dark mode switch itself is <strong>zero JavaScript.</strong> Adding <code>class="dark"</code> to the <code>&lt;html&gt;</code> element swaps every CSS variable. Tailwind reads the updated variables and re-renders every component. There's no context provider and no <code>useTheme</code> hook needed for the CSS layer — just a class toggle on the root element.</p>
<p><strong>Lesson:</strong> swap your color tokens to OKLCH. When defining dark mode values, adjust the first OKLCH parameter (lightness) and the result will look predictably lighter or darker. With hex or HSL you're often guessing; with OKLCH you're reasoning.</p>
<h2 id="heading-lesson-4-page-architecture-flat-beats-clever">Lesson 4: Page Architecture — Flat Beats Clever</h2>
<p>The main page file is 16 lines:</p>
<pre><code class="language-tsx">// app/page.tsx
import Hero from "@/components/Blocks/Hero";
import { LogoCarousel } from "@/components/Blocks/LogoCarousel";
import { FeatureSection } from "@/components/Blocks/FeatureSection";
import { TeamSection } from "@/components/Blocks/TeamSection";
import { TestimonialSection } from "@/components/Blocks/TestimonialSection";
import { PricingSection } from "@/components/Blocks/PricingSection";
import { FaqSection } from "@/components/Blocks/FaqSection";

export default function Home() {
  return (
    &lt;main className="min-h-screen bg-white dark:bg-black"&gt;
      &lt;div className="mx-auto max-w-7xl px-6 pt-40"&gt;
        &lt;Hero /&gt;
        &lt;LogoCarousel /&gt;
        &lt;FeatureSection /&gt;
        &lt;TeamSection /&gt;
        &lt;TestimonialSection /&gt;
        &lt;PricingSection /&gt;
        &lt;FaqSection /&gt;
      &lt;/div&gt;
    &lt;/main&gt;
  );
}
</code></pre>
<p>No dynamic imports, no lazy-loading config, no context providers wrapping everything. Each section is a completely self-contained component in <code>components/Blocks/</code>. None of them import from each other.</p>
<p>This decision came from watching how developers actually use <a href="https://www.shadcndeck.com/templates">shadcn templates</a>. The first thing anyone does after cloning is delete the sections they don't need and reorder the ones they keep. With flat imports, removing the Team section is one deleted line. Reordering sections is moving one line. Adding a new section is creating a file and adding one import.</p>
<p>The alternative (a sections array, a renderer loop, a config file that controls order) sounds sophisticated. In practice, it adds indirection that makes the template harder to understand and slower to customize. Templates should be obvious, not impressive.</p>
<p><strong>Lesson:</strong> in a template context, the simplest architecture is the correct architecture. The developer cloning your template isn't impressed by abstraction. They want to understand the code fast and change it faster.</p>
<h2 id="heading-lesson-5-staggered-animations-without-managing-individual-delays">Lesson 5: Staggered Animations Without Managing Individual Delays</h2>
<p>The Hero section uses entrance animations where each element fades up sequentially: badge first, then heading, then subheading, then CTA. The naïve approach sets a different <code>delay</code> prop on each element manually. The correct approach uses <code>staggerChildren</code>:</p>
<pre><code class="language-tsx">// components/Blocks/Hero.tsx
"use client"
import { motion, type Variants } from "motion/react"

const containerVariants: Variants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.15,  // each child animates 150ms after the previous
      delayChildren: 0.1,
    },
  },
}

const fadeUpVariants: Variants = {
  hidden: { opacity: 0, y: 20 },
  visible: {
    opacity: 1,
    y: 0,
    transition: { duration: 0.5, ease: "easeOut" },
  },
}

const Hero = () =&gt; (
  &lt;motion.div variants={containerVariants} initial="hidden" animate="visible"&gt;
    &lt;motion.div variants={fadeUpVariants}&gt;
      {/* Badge */}
    &lt;/motion.div&gt;
    &lt;motion.h1 variants={fadeUpVariants}&gt;
      AI Chatbot for Customer Support.
    &lt;/motion.h1&gt;
    &lt;motion.p variants={fadeUpVariants}&gt;
      {/* Subheading */}
    &lt;/motion.p&gt;
    &lt;motion.div variants={fadeUpVariants}&gt;
      {/* CTA */}
    &lt;/motion.div&gt;
  &lt;/motion.div&gt;
)
</code></pre>
<p>The parent defines <code>staggerChildren: 0.15</code>. Every child with <code>variants={fadeUpVariants}</code> automatically inherits a 150ms offset from the previous child. Want to add a new element? Give it <code>variants={fadeUpVariants}</code> and the stagger chain extends automatically. No manually updated delay values.</p>
<p>The Features section uses <strong>scroll-triggered animations</strong> with a different easing:</p>
<pre><code class="language-tsx">// components/Blocks/FeatureSection.tsx
&lt;motion.div
  initial={{ opacity: 0, y: 40 }}
  whileInView={{ opacity: 1, y: 0 }}
  viewport={{ once: true, amount: 0.3 }}
  transition={{
    duration: 0.5,
    delay: index * 0.15,
    ease: [0.22, 1, 0.36, 1],
  }}
&gt;
</code></pre>
<p><code>viewport={{ once: true }}</code> fires the animation once when the element enters the viewport, not on every scroll pass. <code>amount: 0.3</code> starts the animation when 30% of the element is visible, not when the full element is on screen. The cubic bezier <code>[0.22, 1, 0.36, 1]</code> is a fast-out-slow-in curve that feels physical rather than mechanical.</p>
<p><strong>Quick note on the import:</strong> most of the core API is compatible, but <code>motion/react</code> isn't a straight drop-in rename of <code>framer-motion</code>. If you're upgrading an existing project, check the <a href="https://motion.dev/docs/react-upgrade-guide">official migration guide</a> before swapping the import. Layout animations, <code>AnimatePresence</code> behaviour, and some hooks changed.</p>
<p><strong>Lesson:</strong> define animation variants at the parent level and use <code>staggerChildren</code> to orchestrate the sequence. Never set <code>delay</code> manually on individual elements — that creates a brittle list of numbers you have to update every time you add or remove an element. Let the parent handle timing; let children just declare what they animate to.</p>
<h2 id="heading-lesson-6-css-only-infinite-scroll-no-library-needed">Lesson 6: CSS-Only Infinite Scroll — No Library Needed</h2>
<p>The testimonials use a dual-row auto-scrolling marquee. The second row scrolls in reverse. There's no third-party marquee package. It's a small component built entirely on CSS animations defined in Tailwind v4's <code>@theme</code> block.</p>
<pre><code class="language-tsx">// components/ui/marquee.tsx
export function Marquee({
  reverse = false,
  pauseOnHover = false,
  vertical = false,
  children,
  repeat = 4,
  ...props
}) {
  return (
    &lt;div className="group flex gap-(--gap) overflow-hidden [--duration:40s] [--gap:2rem]"&gt;
      {Array(repeat).fill(0).map((_, i) =&gt; (
        &lt;div
          key={i}
          className={cn("flex shrink-0 justify-around gap-(--gap)", {
            "animate-marquee flex-row": !vertical,
            "group-hover:paused": pauseOnHover,
            "[animation-direction:reverse]": reverse,
          })}
        &gt;
          {children}
        &lt;/div&gt;
      ))}
    &lt;/div&gt;
  )
}
</code></pre>
<p>The <code>repeat={4}</code> prop renders the children 4 times side by side. As the CSS animation scrolls the container left, the repetitions create a seamless loop. By the time the first set has scrolled off screen, the second set is already in position.</p>
<p><code>group-hover:paused</code> is Tailwind applying <code>animation-play-state: paused</code> when the parent has <code>group</code> class and is hovered. No <code>onMouseEnter</code>/<code>onMouseLeave</code> handlers or state, just pure CSS.</p>
<p>To customize the scroll speed without touching the component source, you override the CSS variable inline:</p>
<pre><code class="language-tsx">&lt;Marquee pauseOnHover className="[--duration:20s]"&gt;
  {items.map(item =&gt; &lt;Card key={item.id} {...item} /&gt;)}
&lt;/Marquee&gt;
</code></pre>
<p><code>[--duration:20s]</code> is a Tailwind arbitrary property. It sets <code>--duration</code> directly on the element, which the animation reads via <code>var(--duration)</code>. Speed customization without a prop, without touching the component.</p>
<p><strong>Lesson:</strong> before reaching for a third-party animation package, check whether a CSS keyframe animation and a couple of Tailwind utilities can do the same job. A marquee, a fade loop, a pulsing skeleton — all of these are achievable with native CSS. Fewer dependencies means fewer breaking changes when the ecosystem moves.</p>
<h2 id="heading-lesson-7-css-subgrid-solves-pricing-card-alignment-natively">Lesson 7: CSS Subgrid Solves Pricing Card Alignment Natively</h2>
<p>The pricing section has three cards: Free, Pro, and Business. Each card has four rows: plan name, price, CTA button, and features list. The features list height varies between plans. Without CSS subgrid, the rows don't align across cards.</p>
<p>The common workaround is <code>min-height</code> on each row, or JavaScript that measures each card and sets explicit heights. Both approaches are fragile. Subgrid solves it in CSS:</p>
<pre><code class="language-tsx">// components/Blocks/PricingSection.tsx
&lt;div className="grid lg:grid-cols-3"&gt;
  {plans.map((plan) =&gt; (
    &lt;div className="p-8 grid grid-rows-subgrid row-span-4 gap-6"&gt;
      &lt;div&gt;{/* Plan name + description */}&lt;/div&gt;
      &lt;div&gt;{/* Price */}&lt;/div&gt;
      &lt;div&gt;{/* CTA button */}&lt;/div&gt;
      &lt;div&gt;{/* Features list */}&lt;/div&gt;
    &lt;/div&gt;
  ))}
&lt;/div&gt;
</code></pre>
<p><code>grid-rows-subgrid</code> tells each card to participate in the parent grid's row tracks rather than creating its own. Each card spans 4 rows (<code>row-span-4</code>). The plan name row, price row, CTA row, and features row align across all three cards (regardless of content height) because they're all on the same row tracks.</p>
<p>Each card's <code>row-span-4</code> reserves four rows in the parent's implicit grid. Because every card spans the same four shared row tracks, their internal rows align automatically even though the parent never declares explicit row heights.</p>
<p>CSS subgrid has been in all modern browsers since late 2023. There's no reason to reach for a JavaScript layout solution when the platform handles it.</p>
<p><strong>Lesson:</strong> when you have a grid of cards where each card has multiple internal rows that need to align across columns, reach for <code>grid-rows-subgrid</code> before reaching for <code>min-height</code> or JavaScript. Define the number of rows each card spans with <code>row-span-N</code>, and the browser handles the rest.</p>
<h2 id="heading-lesson-8-inline-svgs-beat-image-libraries-for-simple-logos">Lesson 8: Inline SVGs Beat Image Libraries for Simple Logos</h2>
<p>The logo carousel renders 12 brand logos: Shopify, Stripe, GitHub, Google, and others. The first instinct is to use a package like <code>react-icons</code> or <code>simple-icons</code>. I went a different direction: inline SVG paths stored as a plain TypeScript object.</p>
<pre><code class="language-tsx">// components/Blocks/LogoCarousel.tsx
const iconMap = {
  stripe: "M13.976 9.15c-2.172-.806...",
  github: "M12 .297c-6.63 0-12...",
  google: "M12.48 10.92v3.28h7.84...",
  // ...
} as const

const SimpleIcon = ({ iconSlug, size = 24 }: { iconSlug: string; size?: number }) =&gt; {
  const iconPath = iconMap[iconSlug as keyof typeof iconMap]
  return (
    &lt;svg role="img" viewBox="0 0 24 24" className="fill-black dark:fill-white"&gt;
      &lt;path d={iconPath} /&gt;
    &lt;/svg&gt;
  )
}
</code></pre>
<p>The <code>fill-black dark:fill-white</code> class means every logo automatically inverts in dark mode: no separate dark mode logo assets, and no conditional rendering based on theme.</p>
<p>The carousel itself duplicates the logo array to create a seamless loop:</p>
<pre><code class="language-tsx">{/* First pass */}
{techCompanies.map((company, i) =&gt; &lt;LogoCard key={`first-${i}`} {...company} /&gt;)}
{/* Second pass — identical, creates the seamless loop */}
{techCompanies.map((company, i) =&gt; &lt;LogoCard key={`second-${i}`} {...company} /&gt;)}
</code></pre>
<p>The CSS animation (<code>animate-logo-scroll</code>) scrolls the container left. When the first pass disappears off the left edge, the second pass is already in position. The loop is seamless.</p>
<p><strong>The trade-off:</strong> maintaining SVG paths manually is fine for a fixed set of logos. If you need a large dynamic icon set, reach for <code>simple-icons</code> or a proper icon library. For 12 brand logos that rarely change, this approach ships zero extra dependencies.</p>
<p><strong>Lesson:</strong> match your tooling to your actual requirements. A logo carousel with a fixed set of brand logos doesn't need an icon library — it needs a TypeScript object and two Tailwind classes. Installing a package to solve a problem you could solve with 10 lines of code adds maintenance surface for no gain.</p>
<h2 id="heading-what-id-do-differently">What I'd Do Differently</h2>
<p>These are the three decisions I'd change if starting the template today.</p>
<h3 id="heading-1-extract-animation-variants-to-a-shared-file">1. Extract Animation Variants to a Shared File</h3>
<p><code>containerVariants</code> and <code>fadeUpVariants</code> are currently defined locally in both <code>Hero.tsx</code> and <code>FeatureSection.tsx</code>. If you want to change the global animation timing (say, reduce duration from 0.5s to 0.3s) you update two files. A shared <code>lib/animations.ts</code> exporting the standard variants would make global timing changes a one-line edit.</p>
<pre><code class="language-ts">// lib/animations.ts
export const fadeUpVariants: Variants = {
  hidden: { opacity: 0, y: 20 },
  visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: "easeOut" } },
}

export const containerVariants: Variants = {
  hidden: { opacity: 0 },
  visible: { opacity: 1, transition: { staggerChildren: 0.15, delayChildren: 0.1 } },
}
</code></pre>
<h3 id="heading-2-use-subgrid-in-the-features-section-too">2. Use Subgrid in the Features Section Too</h3>
<p>The Features grid uses a border-based visual separation pattern — borders between cells create the grid appearance. It works, but the hover states have an inconsistency: the gradient hover overlay height varies slightly between cells in the same row because content heights differ. Subgrid would lock those row heights across cards the same way it does in the Pricing section.</p>
<h3 id="heading-3-use-nextfont-more-consistently">3. Use <code>next/font</code> More Consistently</h3>
<p>The layout loads both Geist and Inter font families. Inter is used via <code>--font-sans</code>. Geist is loaded but the <code>geistSans.variable</code> and <code>geistMono.variable</code> are applied to <code>&lt;body&gt;</code> as className strings while Inter drives the actual font rendering through the CSS variable. The result is that Geist is loaded but not actually displayed. Cleaning this up could shave tens of kilobytes from the font payload — worth verifying in Lighthouse or the Network tab before deploying.</p>
<h2 id="heading-summary">Summary</h2>
<p>These are the five things from this build worth taking into your next project:</p>
<ol>
<li><p><strong>shadcn/ui's</strong> <code>base-nova</code> <strong>style</strong> runs on Base UI primitives. ARIA state drives visual state — no parallel JavaScript state needed.</p>
</li>
<li><p><strong>Tailwind v4 moves theme config to CSS.</strong> All theme tokens, custom animations, and radius scales live in CSS via <code>@theme inline</code>. This is the right place for them.</p>
</li>
<li><p><strong>OKLCH gives predictable dark mode contrast.</strong> Adjusting lightness in OKLCH actually changes perceived brightness. Hex and HSL don't guarantee this.</p>
</li>
<li><p><code>staggerChildren</code> <strong>in motion/react</strong> eliminates manually managed animation delays. The parent orchestrates while the children just declare their animation variant.</p>
</li>
<li><p><strong>CSS subgrid (</strong><code>grid-rows-subgrid</code><strong>)</strong> aligns card rows across columns natively. No JavaScript measurement, no fixed heights.</p>
</li>
</ol>
<p>The full template is MIT-licensed and available at <a href="https://github.com/ShadcnDeck/chatdeck-shadcn-saas-landing-page-template">github.com/ShadcnDeck/chatdeck-shadcn-saas-landing-page-template</a>. If it's useful, a star helps others find it.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Create a Marketing Landing Page Using shadcn/ui ]]>
                </title>
                <description>
                    <![CDATA[ Most marketing landing pages start with the same problem: you're staring at a blank screen and rebuilding sections you've already created countless times. A hero section, feature grid, testimonials, p ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-create-a-marketing-landing-page-using-shadcn-ui/</link>
                <guid isPermaLink="false">6a6c8095409fd0bcb0afd1c6</guid>
                
                    <category>
                        <![CDATA[ shadcn ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mcp server ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Design ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Vaibhav Gupta ]]>
                </dc:creator>
                <pubDate>Fri, 31 Jul 2026 11:01:41 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5fc16e412cae9c5b190b6cdd/82bb4c21-aebe-48c4-9a66-f013011223f4.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Most marketing landing pages start with the same problem: you're staring at a blank screen and rebuilding sections you've already created countless times.</p>
<p>A hero section, feature grid, testimonials, pricing, FAQ, and footer are common building blocks, yet developers often spend hours recreating them for every new project.</p>
<p>In this guide, you'll learn how to build a modern marketing landing page using Next.js, shadcn/ui, Tailwind CSS, and reusable Shadcn blocks. Instead of building every section from scratch, you'll assemble a production-ready page, customize it to match your brand, and finish with a foundation that's ready for real-world projects.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-are-we-building">What Are We Building?</a></p>
</li>
<li><p><a href="#heading-project-setup-with-base-ui-using-the-shadcn-preset">Project Setup with Base UI Using the Shadcn Preset</a></p>
</li>
<li><p><a href="#heading-two-ways-to-build-your-marketing-landing-page">Two Ways to Build Your Marketing Landing Page</a></p>
</li>
<li><p><a href="#heading-option-1-build-using-the-cli">Option 1: Build Using the CLI</a></p>
</li>
<li><p><a href="#heading-option-2-build-using-the-mcp-server">Option 2: Build Using the MCP Server</a></p>
</li>
<li><p><a href="#heading-how-to-optimize-your-landing-page">How to Optimize Your Landing Page</a></p>
</li>
<li><p><a href="#heading-how-to-expand-your-marketing-website">How to Expand Your Marketing Website</a></p>
</li>
<li><p><a href="#heading-live-preview">Live Preview:</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<p>Before you start, make sure you have:</p>
<ul>
<li><p>Node.js 18 or higher installed</p>
</li>
<li><p>shadcn/ui initialized in your project (<code>npx shadcn@latest init</code>)</p>
</li>
<li><p>Basic knowledge of React and TypeScript</p>
</li>
</ul>
<p>If you haven't initialized shadcn/ui yet, run <code>npx shadcn@latest init</code> in your project root and follow the prompts before continuing.</p>
<h2 id="heading-what-are-we-building"><strong>What Are We Building?</strong></h2>
<p>In this tutorial, we'll build a modern marketing landing page using production-ready blocks from <a href="https://shadcnspace.com/">Shadcn Space</a>. Instead of designing and developing every section from scratch, we'll assemble a complete landing page using reusable shadcn/ui blocks and customize them to fit our brand and product.</p>
<p>You can use the same approach to create landing pages for SaaS products, AI tools, startups, agencies, developer tools, portfolios, and many other types of websites. Since every block is built with React, Tailwind CSS, and shadcn/ui, you have full control over the code and can easily modify the content, layout, and styling.</p>
<h3 id="heading-why-build-with-shadcn-space">Why Build with Shadcn Space?</h3>
<p>Creating a professional marketing website typically involves designing multiple sections that work together to tell your product's story and guide visitors to take action.</p>
<p>But instead of building every section from scratch, you can start with production-ready blocks that are easy to customize.</p>
<p>This lets you build marketing websites faster with reusable blocks (and you can also mix and match blocks to create unique page layouts). It also gives you full ownership of your clean React and Tailwind CSS code. And overall, you save development time without sacrificing flexibility.</p>
<h3 id="heading-sections-well-build">Sections We'll Build</h3>
<p>We'll build our marketing landing page using the following sections:</p>
<ul>
<li><p>Hero section with a compelling headline, call-to-action, and trusted-by logos.</p>
</li>
<li><p>Features section to highlight your product's key capabilities.</p>
</li>
<li><p>Product Showcase &amp; Benefits section to demonstrate your product and communicate its value.</p>
</li>
<li><p>Testimonials section to build credibility with customer feedback.</p>
</li>
<li><p>Pricing section to present your plans clearly.</p>
</li>
<li><p>FAQ section answers common questions and reduces friction.</p>
</li>
<li><p>Call-to-Action section to encourage visitors to get started.</p>
</li>
<li><p>Footer with navigation and important links.</p>
</li>
</ul>
<h3 id="heading-final-page-structure">Final Page Structure</h3>
<p>Our landing page will have this structure:</p>
<pre><code class="language-javascript">&lt;main&gt;
  {/* 1. Hero section + Trusted by / Logo cloud */}
  &lt;AgencyHeroSection /&gt;

  {/* 2. Features section */}
  &lt;Feature01 /&gt;

  {/* 3. Product showcase &amp; Benefits */}
  &lt;AboutAndStats01 /&gt;

  {/* 4. Testimonials */}
  &lt;Testimonials /&gt;

  {/* 5. Pricing section */}
  &lt;Pricing /&gt;

  {/* 6. FAQ section */}
  &lt;Faq /&gt;

  {/* 7. Call-to-action section */}
  &lt;CTA /&gt;

  {/* Footer */}
  &lt;Footer /&gt;
&lt;/main&gt;
</code></pre>
<p>Each section will be installed from the Shadcn Space registry and customized directly inside our project. By the end of this tutorial, you'll have a fully responsive marketing landing page built with Next.js, Tailwind CSS, and shadcn/ui that you can adapt for your own product or business.</p>
<h2 id="heading-project-setup-with-base-ui-using-the-shadcn-preset"><strong>Project Setup with Base UI Using the Shadcn Preset</strong></h2>
<p>Since Shadcn Space blocks are built using Base UI primitives, we'll create our project using the Base UI preset instead of the default Radix setup.</p>
<p>This ensures that our landing page uses the same foundation as the blocks we're going to install.</p>
<h3 id="heading-1-create-the-project-with-base-ui">1. Create the Project with Base UI</h3>
<p>Run the following command:</p>
<pre><code class="language-javascript">pnpm dlx shadcn@latest init --preset b0 --template next
</code></pre>
<p>This command does a few important things:</p>
<ul>
<li><p>Creates a Next.js project</p>
</li>
<li><p>Configures Tailwind CSS</p>
</li>
<li><p>Sets up Base UI as the component foundation</p>
</li>
<li><p>Uses the Nova style preset</p>
</li>
<li><p>Configures Lucide icons</p>
</li>
<li><p>Uses Inter font</p>
</li>
<li><p>Applies neutral theme tokens</p>
</li>
</ul>
<p>You now have a Base UI-powered Next.js project ready for building your landing page.</p>
<h3 id="heading-2-add-the-shadcn-space-registry">2. Add the Shadcn Space Registry</h3>
<p>Open your <code>components.json</code> and add the following registry configuration:</p>
<pre><code class="language-javascript">{
  "registries": {
    "@shadcn-space": {
      "url": "https://shadcnspace.com/r/{name}.json",
    }
  }
}
</code></pre>
<p>This tells the CLI where to fetch components and blocks from the registry.</p>
<p>For more information about how to use it in your project, <a href="https://shadcnspace.com/docs/getting-started/how-to-use-shadcn-cli">check out the docs</a>.</p>
<h2 id="heading-two-ways-to-build-your-marketing-landing-page"><strong>Two Ways to Build Your Marketing Landing Page</strong></h2>
<p>Now that your project is set up with shadcn/ui, it's time to start building the marketing landing page.</p>
<p>You can install production-ready blocks directly into your project in two different ways:</p>
<ul>
<li><p>Using the CLI</p>
</li>
<li><p>Using the MCP Server inside your AI-powered editor</p>
</li>
</ul>
<p>Both approaches install the actual React and Tailwind CSS source code into your project, giving you complete control over customization. The only difference is how you discover and install the blocks.</p>
<h2 id="heading-option-1-build-using-the-cli">Option 1: Build Using the CLI</h2>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/n6dvjVxy02U" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>

<p>The CLI is the fastest way to browse the registry and install individual blocks into your project. It gives you full control over which sections you want to use and how you customize them.</p>
<h3 id="heading-step-1-browse-marketing-blocks">Step 1: Browse Marketing Blocks</h3>
<p>Visit the block registry and explore the available marketing blocks.</p>
<p>Let's review the sections we'll use for this tutorial:</p>
<ul>
<li><p>Hero section with a call-to-action and trusted-by logos</p>
</li>
<li><p>Features section</p>
</li>
<li><p>Product showcase &amp; benefits section</p>
</li>
<li><p>Testimonials section</p>
</li>
<li><p>Pricing section</p>
</li>
<li><p>FAQ section</p>
</li>
<li><p>Call-to-action section</p>
</li>
<li><p>Footer</p>
</li>
</ul>
<p>Choose the blocks that best match the design and style of your website. Since every block is fully customizable, you can easily update the content, colors, spacing, and layout to match your brand.</p>
<p>In the following sections, we'll install each block and customize them.</p>
<h3 id="heading-step-2-install-selected-blocks">Step 2: Install Selected Blocks</h3>
<p>Once you find a block you like, install it using the CLI:</p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/{block-name}
</code></pre>
<p>Each command downloads the block, places it inside <code>components/shadcn-space/blocks</code>, and installs the required dependencies.</p>
<p>Now your folder might look like this:</p>
<pre><code class="language-javascript">components/
  shadcn-space/
    blocks/
      about-us-section-01/
      hero-01/
      features-01/
      pricing-01/
      testimonial-01/
      faq-01/
      cta-01/
      footer-01/
</code></pre>
<p><strong>Note:</strong> I've used the first block from each section in this tutorial. You can choose any other <a href="https://shadcnspace.com/blocks"><strong>shadcn block</strong></a> that suits best according to your needs.</p>
<h3 id="heading-step-3-add-a-hero-section">Step 3: Add a Hero Section</h3>
<p>Every great marketing landing page starts with a strong hero section. It's the first thing visitors see, so it should clearly communicate what your product does, who it's for, and encourage users to take action.</p>
<p>Instead of building the section from scratch, we'll install a production-ready Hero block and customize it to match our landing page.</p>
<h4 id="heading-1-install-the-hero-block">1. Install the Hero Block</h4>
<p>Run the following CLI command to add the Hero block to your project:</p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/hero-01
</code></pre>
<p>The CLI will automatically download the Hero block source code, add the component to your project, and install any required dependencies.</p>
<p>After the installation completes, you should see the following directory structure:</p>
<pre><code class="language-javascript">components/
  shadcn-space/
    blocks/
      hero-01/
        index.tsx
</code></pre>
<p>You can now open the component and customize the heading, description, call-to-action buttons, images, and other content to match your product and branding.</p>
<p><strong>2. Understand the Hero Block Structure</strong></p>
<p>Once the installation is complete, open the Hero block located at:</p>
<pre><code class="language-javascript">components/shadcn-space/blocks/hero-01/index.tsx
</code></pre>
<p>You'll see a component similar to the following:</p>
<pre><code class="language-javascript">import HeroSection from "@/components/shadcn-space/blocks/hero-01/hero";
import type { NavigationSection } from "@/components/shadcn-space/blocks/hero-01/header";
import Header from "@/components/shadcn-space/blocks/hero-01/header";
import BrandSlider, { BrandList } from "@/components/shadcn-space/blocks/hero-01/brand-slider";
import type { AvatarList } from "@/components/shadcn-space/blocks/hero-01/hero";

export default function AgencyHeroSection() {
  const avatarList: AvatarList[] = [...];
  const navigationData: NavigationSection[] = [...];
  const brandList: BrandList[] = [...];

  return (
    &lt;div className="relative"&gt;
      &lt;Header navigationData={navigationData} /&gt;
      &lt;main&gt;
        &lt;HeroSection avatarList={avatarList} /&gt;
        &lt;BrandSlider brandList={brandList} /&gt;
      &lt;/main&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p><strong>What Should You Notice?</strong></p>
<p>Before making any changes, take a moment to understand how the block is organized. Rather than being a single large component, it's composed of smaller, reusable components that work together.</p>
<p>In this example:</p>
<ul>
<li><p>The <code>Header</code> component renders the navigation and receives its menu items through the <code>navigationData</code> array.</p>
</li>
<li><p>The <code>HeroSection</code> component contains the main headline, description, call-to-action buttons, and social proof, while the <code>avatarList</code> provides the data displayed in the hero.</p>
</li>
<li><p>The <code>BrandSlider</code> component displays the company logos using the <code>brandList</code> array.</p>
</li>
</ul>
<p>This separation keeps the code modular and makes each part of the landing page easier to customize or replace independently.</p>
<p><strong>Why This Matters?</strong></p>
<p>Because the block is copied directly into your project, you're working with standard React components instead of a compiled package. Every file is fully editable, allowing you to understand how the section is built and modify it to suit your own requirements.</p>
<p>For example, you can update the navigation links, replace the placeholder content with your own branding, add additional sections, integrate custom functionality, or adjust the styling using Tailwind CSS classes. Since everything lives inside your codebase, you're free to restructure the component however you like without being locked into predefined APIs or abstractions.</p>
<h4 id="heading-3-render-the-hero-section">3. Render the Hero Section</h4>
<p>Now that the Hero block has been installed, it's time to display it on the page.</p>
<p>Import the component into your <code>app/page.tsx</code> file:</p>
<pre><code class="language-javascript">import AgencyHeroSection from "@/components/shadcn-space/blocks/hero-01";

export default function Page() {
  return (
    &lt;AgencyHeroSection /&gt;
  );
}
</code></pre>
<p>Save the file and start your development server if it isn't already running. When you open the application in your browser, you'll see the Hero section rendered as the first part of your marketing landing page.</p>
<p>With the Hero section in place, we've completed the first building block of our landing page. Next, we'll continue by adding the remaining sections to create a complete marketing website.</p>
<h3 id="heading-install-the-remaining-blocks">Install the Remaining Blocks</h3>
<p>Now that you've added and rendered the Hero section, let's install the remaining blocks required for our marketing landing page.</p>
<p>Run the following command to install all the remaining sections at once:</p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/feature-01 @shadcn-space/about-us-section-01 @shadcn-space/testimonial-01 @shadcn-space/pricing-01 @shadcn-space/faq-01 @shadcn-space/cta-01  @shadcn-space/footer-01
</code></pre>
<p><strong>Note:</strong> If you're using Windows Command Prompt or PowerShell, run the command on a single line instead of using <code>\</code> for line continuation.</p>
<p>After the installation is complete, update your <code>app/page.tsx</code> by importing the newly added blocks and rendering them in the following order:</p>
<pre><code class="language-javascript">import AgencyHeroSection from "@/components/shadcn-space/blocks/hero-01";
import AboutAndStats01 from "@/components/shadcn-space/blocks/about-us-01";
import Feature01 from "@/components/shadcn-space/blocks/feature-01";
import Pricing from "@/components/shadcn-space/blocks/pricing-01/pricing";
import Testimonials from "@/components/shadcn-space/blocks/testimonial-01/testimonial";
import Faq from "@/components/shadcn-space/blocks/faq-01/faq";
import CTA from "@/components/shadcn-space/blocks/cta-01/cta";
import Footer from "@/components/shadcn-space/blocks/footer-01/footer";


export const metadata = {
  title: "Acme Agency – Innovative Digital Solutions",
  description:
    "We craft immersive digital experiences for bold brands. Explore our services, pricing, and success stories.",
};


export default function Page() {
  return (
    &lt;main&gt;
      {/* 1. Hero section + Trusted by / logo cloud */}
      &lt;AgencyHeroSection /&gt;


      {/* 2. Features section */}
      &lt;Feature01 /&gt;


      {/* 3. Product showcase &amp; Benefits section (Using About/Stats as a placeholder for these) */}
      &lt;AboutAndStats01 /&gt;


      {/* 4. Testimonials */}
      &lt;Testimonials /&gt;


      {/* 5. Pricing section */}
      &lt;Pricing /&gt;


      {/* 6. FAQ section */}
      &lt;Faq /&gt;


      {/* 7. Call-to-action section */}
      &lt;CTA /&gt;


      {/* Footer */}
      &lt;Footer /&gt;
    &lt;/main&gt;
  );
}
</code></pre>
<p>That's it! Your complete marketing landing page is now assembled. You can start customizing the content, images, colors, and layout of each section to match your product and brand.</p>
<h3 id="heading-customize-the-marketing-landing-page"><strong>Customize the Marketing Landing Page</strong></h3>
<p>At this point, the overall structure of your marketing landing page is complete. The next step is to personalize each section so it reflects your product, brand, and messaging instead of the default placeholder content.</p>
<p>One of the biggest advantages of working with reusable React components is that every section can be customized independently. You can update the content, replace images, adjust layouts, and refine the styling without rebuilding the page from scratch.</p>
<p>The following examples show how the default blocks can be transformed into a polished marketing website.</p>
<p><strong>Customize the Hero Section</strong></p>
<p>The Hero section is the first thing visitors see, so it's the most important place to communicate your product's value. Replace the placeholder headline, supporting text, call-to-action buttons, and trusted brand logos with content that represents your own business.</p>
<p><strong>Before:</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/f29da0e0-0da0-482e-bd29-35f3c3c696db.png" alt="Customize the Hero Section Before Using MCP" style="display:block;margin:0 auto" width="1919" height="850" loading="lazy">

<p><strong>After:</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/1ec0dad0-0dee-448f-aed2-136e310bbc5b.png" alt="Customize the Hero Section After Using MCP" style="display:block;margin:0 auto" width="1906" height="865" loading="lazy">

<p>Notice how the customized version immediately establishes the product's identity through updated messaging, branding, imagery, and call-to-action buttons.</p>
<p><strong>Customize the Features Section</strong></p>
<p>The Features section should explain what your product offers and why it stands out. Replace the sample feature cards with capabilities that highlight your product's most valuable functionality.</p>
<p><strong>Before:</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/b44dfc47-8603-4a1c-847e-b608b84c7723.png" alt="Customize the Features Section Before Using MCP" style="display:block;margin:0 auto" width="1474" height="866" loading="lazy">

<p><strong>After:</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/dbc1d529-88ac-42f2-a5d1-0c0348165944.png" alt="Customize the Features Section After Using MCP" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Updating the feature titles, descriptions, and icons makes the section more relevant to your audience while reinforcing your product's key selling points.</p>
<p><strong>Customize the Pricing Section</strong></p>
<p>Your pricing section should clearly communicate the plans you offer and help visitors choose the option that best fits their needs. Replace the default plans, pricing, feature lists, and button labels with information that matches your business model.</p>
<p><strong>Before:</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/7e053524-b281-4ee4-9afe-f0ee1ad76d48.png" alt="Customize the Pricing Section Before Using MCP" style="display:block;margin:0 auto" width="1631" height="722" loading="lazy">

<p><strong>After:</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/5ae201a8-46a9-4ca4-909c-7e1ecc5c4077.png" alt="Customize the Pricing Section After Using MCP" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>A customized pricing section builds trust by presenting accurate information while making it easier for potential customers to compare plans.</p>
<p><strong>Customize the FAQ Section</strong></p>
<p>The FAQ section is a great opportunity to answer common questions before visitors contact your team. Replace the placeholder questions with answers related to your product, pricing, integrations, support, or onboarding process.</p>
<p><strong>Before:</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/4b161671-61f0-45c0-8266-3f39015958ec.png" alt="Customize the FAQ Section Before Using MCP" style="display:block;margin:0 auto" width="1650" height="883" loading="lazy">

<p><strong>After:</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/8f68f5a8-ad09-4021-843a-58329d0a08ce.png" alt="Customize the FAQ Section After Using MCP" style="display:block;margin:0 auto" width="1638" height="785" loading="lazy">

<p>Tailoring the FAQ to your product helps reduce uncertainty, improves the user experience, and can answer many questions before a customer reaches out.</p>
<p><strong>The Result</strong></p>
<p>With just a few content updates, the default blocks evolve into a professional marketing landing page tailored to your brand. Since every section is built with reusable React components, you can continue refining the design, adjusting layouts, and adding new content as your product grows without changing the overall page structure.</p>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/3759ae7a-d244-487d-8354-d9dd42570bfd.gif" alt="Full Preview of the Landing Page" style="display:block;margin:0 auto" width="1909" height="840" loading="lazy">

<h2 id="heading-option-2-build-using-the-mcp-server"><strong>Option 2: Build Using the MCP Server</strong></h2>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/mMlxAmJlbMI" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>

<p>If you prefer a faster workflow, you can use the MCP Server to generate your landing page directly inside your editor. Instead of manually browsing and installing individual blocks, you simply describe the page you want to build, and the MCP Server assembles an initial version for you.</p>
<p>The MCP Server works with supported editors like Antigravity, VS Code, Cursor, Windsurf, and other MCP-compatible editors.</p>
<h3 id="heading-step-1-install-the-mcp-server">Step 1: Install the MCP Server</h3>
<p><strong>Quick Installation</strong>:</p>
<p>The fastest way to get started. Choose your package manager and run the command corresponding to your client:</p>
<p><strong>For Claude Code</strong>:</p>
<pre><code class="language-javascript">claude mcp add shadcnspace-mcp -- npx -y shadcnspace-mcp@latest
</code></pre>
<p><strong>For Others</strong>:</p>
<pre><code class="language-javascript">npx shadcnspace-cli install &lt;client&gt;
</code></pre>
<p>Replace <code>&lt;client&gt;</code> with <strong>cursor, antigravity, vscode,</strong> or <strong>windsurf</strong>.</p>
<p><strong>Manual Installation For VS Code:</strong></p>
<pre><code class="language-javascript">{
  "servers": {
    "shadcnspace-mcp": {
      "command": "npx",
      "args": ["-y", "shadcnspace-mcp@latest"]
    }
  }
}
</code></pre>
<ul>
<li><p>Open .vscode/mcp.json.</p>
</li>
<li><p>Click Start next to the Shadcn Space MCP server</p>
</li>
</ul>
<p>For a detailed guide, follow the <a href="https://shadcnspace.com/docs/getting-started/mcp-server-docs"><strong>MCP Server documentation</strong></a> to install it for your preferred editor.</p>
<p>Once the installation is complete, restart your editor to enable the MCP connection.</p>
<h3 id="heading-step-2-open-the-ai-chat">Step 2: Open the AI Chat</h3>
<p>Open the AI chat panel inside your editor and describe the landing page you'd like to create.</p>
<p>For example:</p>
<pre><code class="language-javascript">Create a modern marketing landing page for an AI SaaS product.

Use Shadcn Space blocks and include:

- Hero section
- Features section
- Product showcase &amp; benefits
- Testimonials
- Pricing
- FAQ
- Call-to-action
- Footer

Use a clean, modern, and responsive design.
</code></pre>
<p>Feel free to replace the product description with your own and customize as you like.</p>
<h3 id="heading-step-3-generate-the-landing-page">Step 3: Generate the Landing Page</h3>
<p>After receiving your prompt, the MCP Server analyzes your requirements and selects the most suitable blocks for your landing page. It automatically assembles the page using a combination of reusable sections, giving you a working layout in just a few moments.</p>
<p>The generated sections are added directly to your project as standard React components. Since everything is real source code, you can customize every part of the page: update the content, replace images, modify the layout, adjust spacing, or restyle components using Tailwind CSS.</p>
<p>The MCP Server simply speeds up the initial setup, while giving you complete control over the final implementation.</p>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/2247726d-12ab-47d8-bd7a-49b025ea0eb5.gif" alt="How to Generate the Landing Page" style="display:block;margin:0 auto" width="1909" height="913" loading="lazy">

<h2 id="heading-how-to-optimize-your-landing-page">How to Optimize Your Landing Page</h2>
<p>Building a visually appealing landing page is only the first step. Before publishing your website, it's worth spending some time optimizing it for performance, search engines, and user experience.</p>
<p>A fast, responsive landing page not only feels more polished but also helps improve engagement and conversion rates. Fortunately, Next.js provides several built-in features that make these optimizations straightforward.</p>
<h3 id="heading-optimize-images-with-nextjs">Optimize Images with Next.js</h3>
<p>Images are often the largest assets on a marketing website, so optimizing them can significantly improve loading performance. If you're using Next.js, prefer the built-in <code>next/image</code> component instead of the standard HTML image tag. It automatically serves appropriately sized images, supports modern image formats, and reduces layout shifts as the page loads.</p>
<p>Before adding screenshots or illustrations to your project, make sure they're compressed and sized appropriately. Well-optimized images create a smoother browsing experience across desktop and mobile devices while also contributing to better Core Web Vitals.</p>
<h3 id="heading-improve-seo-for-your-landing-page">Improve SEO for Your Landing Page</h3>
<p>A well-designed landing page is only effective if people can discover it. Search engine optimization starts with creating meaningful content that clearly communicates what your product offers. Choose a descriptive page title, write a concise meta description, and organize your content using logical headings.</p>
<p>Your primary keyword should appear naturally throughout the page without forcing it into every paragraph. Focus on writing for your audience first, then structure the content in a way that search engines can easily understand. Combining valuable content with a clear page hierarchy gives your landing page the best chance of ranking for relevant searches.</p>
<h3 id="heading-add-metadata-and-open-graph-images">Add Metadata and Open Graph Images</h3>
<p>When someone shares your landing page on social media or in a messaging application, the preview is generated from your page's metadata. Configuring Open Graph and Twitter metadata allows you to control the title, description, and preview image that appear when your website is shared.</p>
<p>A custom preview image that reflects your branding makes your links look more professional and can encourage more people to click through. Taking a few minutes to configure these settings helps create a more polished experience whenever your content is shared online.</p>
<h3 id="heading-optimize-performance">Optimize Performance</h3>
<p>Performance plays a major role in how visitors perceive your website. A page that loads quickly feels more responsive and encourages users to continue exploring your content. As you customize your landing page, keep unnecessary JavaScript to a minimum, optimize static assets, and avoid loading resources that aren't immediately needed.</p>
<p>Even small improvements, such as reducing image sizes or simplifying animations, can noticeably improve loading speed. Before deploying your project, test the page under different network conditions to ensure it performs well for all visitors.</p>
<h3 id="heading-improve-core-web-vitals">Improve Core Web Vitals</h3>
<p>Core Web Vitals are Google's metrics for measuring real-world user experience. They evaluate how quickly your main content appears, how responsive the page feels during interactions, and whether elements remain stable as the page loads. Monitoring these metrics throughout development helps identify potential issues before they affect users.</p>
<p>Tools such as Lighthouse and PageSpeed Insights provide detailed reports that can help you improve loading performance and responsiveness. A landing page with strong Core Web Vitals not only creates a better experience for visitors but can also contribute to improved search rankings.</p>
<hr>
<h2 id="heading-how-to-expand-your-marketing-website"><strong>How to Expand Your Marketing Website</strong></h2>
<p>A landing page is often just the beginning of a complete marketing website. As your product grows, you'll likely need additional pages that provide more information, improve navigation, and create a better experience for your visitors. Common additions include Blog, Blog Details, Pricing, FAQ, Changelog, Contact, Integration, Error, and About Us pages.</p>
<p>Building these pages with a consistent design system helps maintain a unified look and feel across your entire website while reducing development time. Reusing layouts and components also makes your project easier to maintain as it evolves.</p>
<p>If you're looking to expand your website beyond a single landing page, explore the collection of production-ready <a href="https://shadcnspace.com/pages"><strong>Shadcn website pages</strong></a>, built with React, Next.js, Tailwind CSS, and shadcn/ui.</p>
<h2 id="heading-live-preview"><strong>Live Preview:</strong></h2>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/a189f09d-550d-4c7f-b20a-477f5bc1bfa0.gif" alt="How to Expand Your Marketing Website" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>In this tutorial, we built a complete marketing landing page using Next.js, shadcn/ui, Tailwind CSS, and reusable Shadcn Space blocks. Starting from a fresh project, we assembled a production-ready page by combining sections such as the Hero, Features, Product Showcase, Testimonials, Pricing, FAQ, Call-to-Action, and Footer.</p>
<p>Because every block is added as standard React source code, you're free to customize the content, layout, styling, and functionality to match your own product and branding. Whether you're building a SaaS application, a startup website, an AI product, an agency site, or a developer tool, the same approach can be adapted to your requirements.</p>
<p>A marketing landing page is just the foundation of your online presence. As your product grows, you can continue expanding your website with additional marketing pages while maintaining a consistent design system and development workflow.</p>
<p>I hope this guide has helped you understand how to quickly build a modern, responsive marketing landing page using reusable components. Feel free to experiment with different block combinations, personalize the design, and create a website that best represents your product.</p>
<p><strong>Appreciation</strong>: I wrote this article with the help of Mihir Koshti (Sr. Full Stack Developer) – <a href="https://www.linkedin.com/in/mihir-koshti/">Connect on LinkedIn</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Shadcn Sheet Component in React: Cart and Filter Panel Examples ]]>
                </title>
                <description>
                    <![CDATA[ A Sheet is a panel that slides in from the edge of the screen instead of popping up in the center like a modal. You've likely used one when you've opened a shopping cart on an e-commerce site or tappe ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-shadcn-sheet-component-in-react-cart-and-filter-panel-examples/</link>
                <guid isPermaLink="false">6a54f29d91d656f74f1f4a21</guid>
                
                    <category>
                        <![CDATA[ shadcn ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ components ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Vaibhav Gupta ]]>
                </dc:creator>
                <pubDate>Mon, 13 Jul 2026 14:13:49 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/958cd73a-253a-4a48-b83c-5a5d52420b88.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>A Sheet is a panel that slides in from the edge of the screen instead of popping up in the center like a modal. You've likely used one when you've opened a shopping cart on an e-commerce site or tapped a filter icon and watched options slide in from the side.</p>
<p>Building a Sheet that scrolls correctly with long content, keeps a header and footer fixed in place, and manages form or counter state without bugs takes more than wrapping a <code>&lt;div&gt;</code> in a slide animation.</p>
<p>In this tutorial, you'll build two production-ready Sheet components using shadcn/ui and Base UI primitives via Shadcn Space:</p>
<ol>
<li><p>A <strong>Shopping Cart Sheet</strong> with quantity controls, item removal, and a live subtotal.</p>
</li>
<li><p>A <strong>Filter Panel Sheet</strong> with category checkboxes, a price range slider, star ratings, and an active filter count.</p>
</li>
</ol>
<p>By the end, you'll have both components running in your project, and you'll understand the state and layout decisions behind them well enough to build your own Sheet variants from scratch.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-youll-build">What You'll Build</a></p>
</li>
<li><p><a href="#heading-how-to-set-up-the-cli-registry">How to Set Up the CLI Registry</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-shopping-cart-sheet-sheet-03">How to Build the Shopping Cart Sheet (sheet-03)</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-filter-panel-sheet-sheet-04">How to Build the Filter Panel Sheet (sheet-04)</a></p>
</li>
<li><p><a href="#heading-quick-reference-table">Quick Reference Table</a></p>
</li>
<li><p><a href="#heading-key-concepts-recap">Key Concepts Recap</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-resources">Resources</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you start, make sure you have:</p>
<ul>
<li><p>Node.js 18 or higher installed</p>
</li>
<li><p>shadcn/ui initialized in your project (<code>npx shadcn@latest init</code>)</p>
</li>
<li><p>Basic knowledge of React and TypeScript</p>
</li>
</ul>
<p>If you haven't initialized shadcn/ui yet, run <code>npx shadcn@latest init</code> in your project root and follow the prompts before continuing.</p>
<h2 id="heading-what-youll-build">What You'll Build</h2>
<p>This tutorial uses components installed through the <a href="https://shadcnspace.com/"><strong>Shadcn Space</strong></a> registry, an open-source collection of production-ready components and UI blocks for shadcn/ui.</p>
<p>You can browse the full set in the <a href="https://shadcnspace.com/components/sheet"><strong>Shadcn Sheet component library</strong></a>. Each component supports both Radix UI and Base UI primitives and includes a <strong>Copy Prompt option</strong>, so you can paste the component spec into tools like v0, Lovable, or Bolt if you're prototyping. This tutorial uses the Base UI versions.</p>
<p><strong>Shopping Cart Sheet (sheet-03)</strong></p>
<ul>
<li><p>Slide-in panel from the right with a cart icon trigger and item-count badge</p>
</li>
<li><p>Quantity increment, decrement, and removal per item</p>
</li>
<li><p>Live subtotal and total calculated from cart state</p>
</li>
<li><p>Fixed header and footer with a scrollable item list in between</p>
</li>
</ul>
<p><strong>Filter Panel Sheet (sheet-04)</strong></p>
<ul>
<li><p>Slide-in panel from the left</p>
</li>
<li><p>Multi-select category checkboxes</p>
</li>
<li><p>Price range slider with live min/max display</p>
</li>
<li><p>Single-select star rating filter</p>
</li>
<li><p>Active filter count badge and a "Clear All" action that doesn't close the panel</p>
</li>
</ul>
<h2 id="heading-how-to-set-up-the-cli-registry">How to Set Up the CLI Registry</h2>
<p>Before running any install commands, register the Shadcn Space registry in your <code>components.json</code> file.</p>
<p>Open <code>components.json</code> in your project root and add the <code>registries</code> field:</p>
<pre><code class="language-javascript">{
  "registries": {
    "@shadcn-space": {
      "url": "https://shadcnspace.com/r/{name}.json"
    }
  }
}
</code></pre>
<p>This tells the shadcn CLI where to resolve components prefixed with <code>@shadcn-space/</code>. Without this step, the install commands below will fail.</p>
<p>For a full walkthrough of registry setup, see the <a href="https://shadcnspace.com/docs/getting-started/how-to-use-shadcn-cli"><strong>getting started guide</strong></a>. Shadcn Space also ships an <a href="https://shadcnspace.com/mcp"><strong>MCP server</strong></a>, so you can browse and install components straight from your editor if your workflow uses MCP tooling. If you prefer following along visually with a video, here you go:</p>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/mMlxAmJlbMI" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>

<h2 id="heading-how-to-build-the-shopping-cart-sheet-sheet-03">How to Build the Shopping Cart Sheet (sheet-03)</h2>
<h3 id="heading-what-the-cart-sheet-does">What the Cart Sheet Does</h3>
<p>The cart icon sits in your navbar. Clicking it slides a panel in from the right showing each item's image, name, variant, and price, with controls to change quantity or remove the item entirely. The subtotal updates as you go, and the footer stays pinned at the bottom regardless of how many items are in the list.</p>
<h3 id="heading-how-to-install-the-cart-sheet">How to Install the Cart Sheet</h3>
<p>Run one of the following based on your package manager:</p>
<p><strong>npm</strong>:</p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/sheet-03
</code></pre>
<p><strong>pnpm</strong>:</p>
<pre><code class="language-javascript">pnpm dlx shadcn@latest add @shadcn-space/sheet-03
</code></pre>
<p><strong>Yarn</strong>:</p>
<pre><code class="language-javascript">yarn dlx shadcn@latest add @shadcn-space/sheet-03
</code></pre>
<p><strong>Bun</strong>:</p>
<pre><code class="language-javascript">bunx --bun shadcn@latest add @shadcn-space/sheet-03
</code></pre>
<p>The CLI copies the component into your project at:</p>
<pre><code class="language-javascript">components/
  shadcn-space/
    sheet/
      sheet-03.tsx
</code></pre>
<h3 id="heading-the-component-code">The Component Code</h3>
<pre><code class="language-javascript">"use client";
import { useState } from "react";
import { ShoppingCartIcon, PlusIcon, MinusIcon, Trash2Icon } from "lucide-react";
import {
  Sheet,
  SheetTrigger,
  SheetContent,
  SheetHeader,
  SheetTitle,
  SheetDescription,
  SheetFooter,
  SheetClose,
} from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";

const initialItems = [
  {
    id: 1,
    name: "Apple Watch S9",
    variant: "Midnight / 41mm",
    price: 684.0,
    qty: 1,
    image: "https://images.shadcnspace.com/assets/ecommerce/product-category/product-category-03-1.webp",
  },
  {
    id: 2,
    name: "Beige Jacket",
    variant: "Size M / Beige",
    price: 479.0,
    qty: 1,
    image: "https://images.shadcnspace.com/assets/ecommerce/product-category/product-category-02-2.webp",
  },
  {
    id: 3,
    name: "Glow Serum",
    variant: "30ml / Vitamin C",
    price: 46.0,
    qty: 2,
    image: "https://images.shadcnspace.com/assets/ecommerce/product-category/product-category-03-3.webp",
  },
];

const ShoppingCartDemo = () =&gt; {
  const [items, setItems] = useState(initialItems);

  const updateQty = (id, delta) =&gt; {
    setItems((prev) =&gt;
      prev
        .map((item) =&gt; (item.id === id ? { ...item, qty: item.qty + delta } : item))
        .filter((item) =&gt; item.qty &gt; 0)
    );
  };

  const subtotal = items.reduce((sum, item) =&gt; sum + item.price * item.qty, 0);
  const totalCount = items.reduce((sum, item) =&gt; sum + item.qty, 0);

  return (
    &lt;Sheet&gt;
      &lt;SheetTrigger render={&lt;Button variant="outline" size="icon" className="relative cursor-pointer" /&gt;}&gt;
        &lt;ShoppingCartIcon size={18} /&gt;
        {totalCount &gt; 0 &amp;&amp; (
          &lt;Badge className="absolute -top-2 -right-2 size-5 justify-center rounded-full p-0 text-[10px]"&gt;
            {totalCount}
          &lt;/Badge&gt;
        )}
      &lt;/SheetTrigger&gt;

      &lt;SheetContent side="right" className="flex flex-col p-0 gap-0"&gt;
        &lt;SheetHeader className="px-4 pt-5 pb-4 border-b"&gt;
          &lt;SheetTitle&gt;Your Cart&lt;/SheetTitle&gt;
          &lt;SheetDescription&gt;
            {totalCount &gt; 0
              ? `${totalCount} item${totalCount &gt; 1 ? "s" : ""} in your cart`
              : "Your cart is empty"}
          &lt;/SheetDescription&gt;
        &lt;/SheetHeader&gt;

        &lt;div className="flex-1 overflow-y-auto px-4 py-3 flex flex-col gap-3"&gt;
          {items.map((item) =&gt; (
            &lt;div key={item.id} className="flex items-start gap-3"&gt;
              &lt;div className="size-16 rounded-lg bg-muted shrink-0 overflow-hidden"&gt;
                &lt;img src={item.image} alt={item.name} className="size-full object-cover" /&gt;
              &lt;/div&gt;
              &lt;div className="flex-1 min-w-0"&gt;
                &lt;p className="text-sm font-medium leading-snug truncate"&gt;{item.name}&lt;/p&gt;
                &lt;p className="text-xs text-muted-foreground"&gt;{item.variant}&lt;/p&gt;
                &lt;p className="text-sm font-semibold mt-1"&gt;${item.price.toFixed(2)}&lt;/p&gt;
              &lt;/div&gt;
              &lt;div className="flex flex-col items-end gap-2 shrink-0"&gt;
                &lt;Button
                  variant="ghost"
                  size="icon-sm"
                  className="text-muted-foreground hover:text-destructive hover:bg-destructive/10! cursor-pointer"
                  onClick={() =&gt; updateQty(item.id, -item.qty)}
                &gt;
                  &lt;Trash2Icon size={14} /&gt;
                &lt;/Button&gt;
                &lt;ButtonGroup&gt;
                  &lt;Button variant="outline" size="icon-sm" className="cursor-pointer" onClick={() =&gt; updateQty(item.id, -1)}&gt;
                    &lt;MinusIcon /&gt;
                  &lt;/Button&gt;
                  &lt;ButtonGroupText className="px-2 text-sm min-w-7 justify-center"&gt;{item.qty}&lt;/ButtonGroupText&gt;
                  &lt;Button variant="outline" size="icon-sm" className="cursor-pointer" onClick={() =&gt; updateQty(item.id, 1)}&gt;
                    &lt;PlusIcon /&gt;
                  &lt;/Button&gt;
                &lt;/ButtonGroup&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          ))}
        &lt;/div&gt;

        &lt;SheetFooter className="flex flex-col gap-3 px-4 pt-3 pb-5 border-t"&gt;
          &lt;div className="flex flex-col gap-1.5 w-full"&gt;
            &lt;div className="flex justify-between text-sm text-muted-foreground"&gt;
              &lt;span&gt;Subtotal&lt;/span&gt;
              &lt;span&gt;${subtotal.toFixed(2)}&lt;/span&gt;
            &lt;/div&gt;
            &lt;div className="flex justify-between text-sm text-muted-foreground"&gt;
              &lt;span&gt;Shipping&lt;/span&gt;
              &lt;span className="text-teal-400"&gt;Free&lt;/span&gt;
            &lt;/div&gt;
            &lt;Separator className="my-1" /&gt;
            &lt;div className="flex justify-between text-sm font-semibold"&gt;
              &lt;span&gt;Total&lt;/span&gt;
              &lt;span&gt;${subtotal.toFixed(2)}&lt;/span&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;Button className="w-full cursor-pointer hover:bg-primary/80"&gt;Checkout&lt;/Button&gt;
          &lt;SheetClose render={&lt;Button variant="outline" className="w-full cursor-pointer" /&gt;}&gt;
            Continue Shopping
          &lt;/SheetClose&gt;
        &lt;/SheetFooter&gt;
      &lt;/SheetContent&gt;
    &lt;/Sheet&gt;
  );
};

export default ShoppingCartDemo;
</code></pre>
<p>Let's go through how this works.</p>
<h4 id="heading-1-derived-totals-instead-of-tracked-state">1. Derived totals instead of tracked state</h4>
<p><code>subtotal</code> and <code>totalCount</code> are calculated from <code>items</code> on every render, not stored in their own <code>useState</code>. If you tracked them separately, they'd drift out of sync the first time you updated <code>items</code> and forgot to update the count alongside them.</p>
<h4 id="heading-2-removing-an-item-is-just-zeroing-its-quantity">2. Removing an item is just zeroing its quantity</h4>
<pre><code class="language-javascript">const updateQty = (id, delta) =&gt; {
  setItems((prev) =&gt;
    prev
      .map((item) =&gt; (item.id === id ? { ...item, qty: item.qty + delta } : item))
      .filter((item) =&gt; item.qty &gt; 0)
  );
};
</code></pre>
<p>The trash icon calls <code>updateQty(item.id, -item.qty)</code>, which drops that item's quantity to zero. The <code>.filter()</code> step then removes it from the array. This means you don't need a separate "remove item" function that duplicates logic already living in <code>updateQty</code>.</p>
<h4 id="heading-3-render-prop-instead-of-aschild">3. <code>render</code> prop instead of <code>asChild</code></h4>
<p><code>SheetTrigger</code> and <code>SheetClose</code> use the <code>render</code> prop (<code>render={&lt;Button ... /&gt;}</code>) rather than wrapping children in an <code>asChild</code> pattern. This is the Base UI convention that Shadcn Space components use, and it keeps the button's accessibility behavior (focus states, keyboard handling) entire instead of overriding it with a custom wrapper.</p>
<h4 id="heading-4-fixed-header-and-footer-scrollable-middle">4. Fixed header and footer, scrollable middle</h4>
<pre><code class="language-javascript">&lt;SheetContent className="flex flex-col p-0 gap-0"&gt;
  &lt;SheetHeader className="border-b" /&gt;
  &lt;div className="flex-1 overflow-y-auto"&gt;{/* items */}&lt;/div&gt;
  &lt;SheetFooter className="border-t" /&gt;
&lt;/SheetContent&gt;
</code></pre>
<p><code>flex-1 overflow-y-auto</code> on the middle div is what keeps the header and footer fixed while the item list scrolls independently. Skip this, and a long cart pushes the checkout button off-screen.</p>
<h3 id="heading-live-preview"><strong>Live Preview:</strong></h3>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/dec70851-3b64-43d4-9611-f75165f7c4ca.gif" alt="dec70851-3b64-43d4-9611-f75165f7c4ca" style="display:block;margin:0 auto" width="1152" height="648" loading="lazy">

<h2 id="heading-how-to-build-the-filter-panel-sheet-sheet-04">How to Build the Filter Panel Sheet (sheet-04)</h2>
<h3 id="heading-what-the-filter-sheet-does">What the Filter Sheet Does</h3>
<p>The Filters button slides a panel in from the left with category checkboxes, a price range slider, a star rating filter, and an availability toggle. An active filter count shows on the trigger button itself, and clearing filters resets the state without closing the panel.</p>
<h3 id="heading-how-to-install-the-filter-sheet">How to Install the Filter Sheet</h3>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/sheet-04
</code></pre>
<p>The CLI copies the component into:</p>
<pre><code class="language-javascript">components/
  shadcn-space/
    sheet/
      sheet-04.tsx
</code></pre>
<h3 id="heading-the-component-code">The Component Code</h3>
<pre><code class="language-javascript">"use client";

import { useState } from "react";
import { SlidersHorizontalIcon, StarIcon } from "lucide-react";
import {
  Sheet,
  SheetTrigger,
  SheetContent,
  SheetHeader,
  SheetTitle,
  SheetDescription,
  SheetFooter,
  SheetClose,
} from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Slider } from "@/components/ui/slider";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";

const CATEGORIES = ["Watches", "Clothing", "Beauty", "Electronics", "Home &amp; Living"];
const RATINGS = [4, 3, 2, 1];
const AVAILABILITY = ["In Stock", "On Sale"];

const AdvancedFiltersDemo = () =&gt; {
  const [selectedCategories, setSelectedCategories] = useState([]);
  const [priceRange, setPriceRange] = useState([0, 1000]);
  const [selectedRating, setSelectedRating] = useState(null);
  const [selectedAvailability, setSelectedAvailability] = useState([]);

  const toggleCategory = (cat) =&gt;
    setSelectedCategories((prev) =&gt; (prev.includes(cat) ? prev.filter((c) =&gt; c !== cat) : [...prev, cat]));

  const toggleAvailability = (val) =&gt;
    setSelectedAvailability((prev) =&gt; (prev.includes(val) ? prev.filter((v) =&gt; v !== val) : [...prev, val]));

  const clearAll = () =&gt; {
    setSelectedCategories([]);
    setPriceRange([0, 1000]);
    setSelectedRating(null);
    setSelectedAvailability([]);
  };

  const activeCount =
    selectedCategories.length +
    (priceRange[0] !== 0 || priceRange[1] !== 1000 ? 1 : 0) +
    (selectedRating !== null ? 1 : 0) +
    selectedAvailability.length;

  return (
    &lt;Sheet&gt;
      &lt;SheetTrigger render={&lt;Button variant="outline" className="relative cursor-pointer gap-2" /&gt;}&gt;
        &lt;SlidersHorizontalIcon size={16} /&gt;
        Filters
        {activeCount &gt; 0 &amp;&amp; (
          &lt;Badge className="size-5 justify-center rounded-full p-0 text-[10px]"&gt;{activeCount}&lt;/Badge&gt;
        )}
      &lt;/SheetTrigger&gt;

      &lt;SheetContent side="left" className="flex flex-col p-0 gap-0"&gt;
        &lt;SheetHeader className="px-4 pt-5 pb-4 border-b"&gt;
          &lt;SheetTitle&gt;Filters&lt;/SheetTitle&gt;
          &lt;SheetDescription&gt;Narrow down products by your preferences.&lt;/SheetDescription&gt;
        &lt;/SheetHeader&gt;

        &lt;div className="flex-1 overflow-y-auto px-4 py-4 flex flex-col gap-5"&gt;
          &lt;div className="flex flex-col gap-3"&gt;
            &lt;p className="text-sm font-medium"&gt;Category&lt;/p&gt;
            {CATEGORIES.map((cat) =&gt; (
              &lt;div key={cat} className="flex items-center gap-2.5"&gt;
                &lt;Checkbox id={`cat-${cat}`} checked={selectedCategories.includes(cat)} onCheckedChange={() =&gt; toggleCategory(cat)} /&gt;
                &lt;Label htmlFor={`cat-${cat}`} className="text-sm font-normal cursor-pointer"&gt;{cat}&lt;/Label&gt;
              &lt;/div&gt;
            ))}
          &lt;/div&gt;

          &lt;Separator /&gt;

          &lt;div className="flex flex-col gap-4"&gt;
            &lt;div className="flex items-center justify-between"&gt;
              &lt;p className="text-sm font-medium"&gt;Price Range&lt;/p&gt;
              &lt;span className="text-xs text-muted-foreground"&gt;${priceRange[0]} -- ${priceRange[1]}&lt;/span&gt;
            &lt;/div&gt;
            &lt;Slider value={priceRange} onValueChange={(val) =&gt; setPriceRange(Array.isArray(val) ? [...val] : [val])} min={0} max={1000} step={10} /&gt;
          &lt;/div&gt;

          &lt;Separator /&gt;

          &lt;div className="flex flex-col gap-3"&gt;
            &lt;p className="text-sm font-medium"&gt;Rating&lt;/p&gt;
            {RATINGS.map((rating) =&gt; (
              &lt;div key={rating} className="flex items-center gap-2.5 cursor-pointer" onClick={() =&gt; setSelectedRating(selectedRating === rating ? null : rating)}&gt;
                &lt;Checkbox id={`rating-${rating}`} checked={selectedRating === rating} onCheckedChange={() =&gt; setSelectedRating(selectedRating === rating ? null : rating)} /&gt;
                &lt;Label htmlFor={`rating-${rating}`} className="flex items-center gap-1 text-sm font-normal cursor-pointer"&gt;
                  {Array.from({ length: rating }).map((_, i) =&gt; &lt;StarIcon key={i} size={13} className="fill-amber-400 text-amber-400" /&gt;)}
                  {Array.from({ length: 5 - rating }).map((_, i) =&gt; &lt;StarIcon key={i} size={13} className="text-muted-foreground/40" /&gt;)}
                  &lt;span className="ml-0.5 text-muted-foreground"&gt;&amp; up&lt;/span&gt;
                &lt;/Label&gt;
              &lt;/div&gt;
            ))}
          &lt;/div&gt;

          &lt;Separator /&gt;

          &lt;div className="flex flex-col gap-3"&gt;
            &lt;p className="text-sm font-medium"&gt;Availability&lt;/p&gt;
            {AVAILABILITY.map((val) =&gt; (
              &lt;div key={val} className="flex items-center gap-2.5"&gt;
                &lt;Checkbox id={`avail-${val}`} checked={selectedAvailability.includes(val)} onCheckedChange={() =&gt; toggleAvailability(val)} /&gt;
                &lt;Label htmlFor={`avail-${val}`} className="text-sm font-normal cursor-pointer"&gt;{val}&lt;/Label&gt;
              &lt;/div&gt;
            ))}
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;SheetFooter className="flex flex-row gap-2 px-4 pt-3 pb-5 border-t"&gt;
          &lt;Button variant="outline" className="flex-1 cursor-pointer" onClick={clearAll}&gt;Clear All&lt;/Button&gt;
          &lt;SheetClose render={&lt;Button className="flex-1 cursor-pointer hover:bg-primary/80" /&gt;}&gt;Apply Filters&lt;/SheetClose&gt;
        &lt;/SheetFooter&gt;
      &lt;/SheetContent&gt;
    &lt;/Sheet&gt;
  );
};

export default AdvancedFiltersDemo;
</code></pre>
<p>How this component works:</p>
<h4 id="heading-1-use-a-separate-state-for-each-filter-type">1. Use a separate state for each filter type</h4>
<p>Categories and availability use arrays because users can select multiple options. Rating stores a single value (or <code>null</code>) since only one rating can be selected. Price uses a two-value array (<code>[min, max]</code>) to store the selected range. This keeps each filter's state simple and matches how the UI works.</p>
<h4 id="heading-2-activecount-is-computed-not-tracked">2. <code>activeCount</code> is computed, not tracked</h4>
<p>Same reasoning as the cart badge above:</p>
<pre><code class="language-javascript">const activeCount =
  selectedCategories.length +
  (priceRange[0] !== 0 || priceRange[1] !== 1000 ? 1 : 0) +
  (selectedRating !== null ? 1 : 0) +
  selectedAvailability.length;
</code></pre>
<p>If you tracked this count manually, you'd eventually add a new filter, forget to update the count logic, and end up with a badge that lies to the user.</p>
<h4 id="heading-3-apply-filters-closes-the-panel-clear-all-doesnt">3. "Apply Filters" closes the panel, "Clear All" doesn't</h4>
<p><code>Clear All</code> calls <code>clearAll()</code> directly on a plain <code>Button</code>. <code>Apply Filters</code> is wrapped in <code>SheetClose</code>, so it resets nothing but closes the panel. A user clearing filters usually wants to keep the panel open to pick new ones. Closing on every action would force them to reopen it each time.</p>
<h4 id="heading-4-toggling-a-category-with-filter-and-spread">4. Toggling a category with <code>.filter()</code> and spread</h4>
<pre><code class="language-javascript">const toggleCategory = (cat) =&gt;
  setSelectedCategories((prev) =&gt; (prev.includes(cat) ? prev.filter((c) =&gt; c !== cat) : [...prev, cat]));
</code></pre>
<p>This one line handles both adding and removing a category from the array, based on whether it's already present. It's a pattern worth reusing anywhere you have a multi-select checkbox list backed by an array of strings.</p>
<h3 id="heading-live-preview"><strong>Live Preview:</strong></h3>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/e3850372-c172-4d49-b973-09192e80bc61.gif" alt="e3850372-c172-4d49-b973-09192e80bc61" style="display:block;margin:0 auto" width="1152" height="648" loading="lazy">

<h2 id="heading-quick-reference-table">Quick Reference Table</h2>
<table style="min-width:378px"><colgroup><col style="min-width:25px"><col style="width:80px"><col style="width:273px"></colgroup><tbody><tr><td><p><strong>Sheet</strong></p></td><td><p><strong>Identifier</strong></p></td><td><p><strong>Use Case</strong></p></td></tr><tr><td><p><strong>Shopping Cart Sheet</strong></p></td><td><p>sheet-03</p></td><td><p>Cart drawers, order summaries</p></td></tr><tr><td><p><strong>Filter Panel Sheet</strong></p></td><td><p>sheet-04</p></td><td><p>Product filters, search refinement panels</p></td></tr></tbody></table>

<p>To install either, swap the identifier in the CLI command:</p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/&lt;identifier&gt;
</code></pre>
<hr>
<h2 id="heading-key-concepts-for-using-shadcn-sheet">Key Concepts for Using Shadcn Sheet</h2>
<ul>
<li><p><strong>Use a Sheet for secondary workflows:</strong> It's ideal for filters, shopping carts, settings, navigation menus, and forms that shouldn't interrupt the main page.</p>
</li>
<li><p><strong>Keep the layout consistent:</strong> Use a fixed header, a scrollable content area (<code>flex-1 overflow-y-auto</code>), and a fixed footer so actions remain visible even with long content.</p>
</li>
<li><p><strong>Derive UI values from state:</strong> Counts, totals, and badges (such as active filters or cart items) should be calculated from state instead of being stored separately.</p>
</li>
<li><p><strong>Close the Sheet after completing an action:</strong> Actions like <strong>Apply Filters</strong>, <strong>Save</strong>, or <strong>Checkout</strong> should close the Sheet. Actions like <strong>Clear Filters</strong> or <strong>Reset</strong> should keep it open so users can continue making changes.</p>
</li>
<li><p><strong>Choose the right state structure:</strong> Use arrays for multi-select options (such as categories or availability), a single value (or <code>null</code>) for single-select options (such as ratings), and a two-value array (<code>[min, max]</code>) for range filters like price.</p>
</li>
<li><p><strong>Keep actions easy to reach:</strong> Place primary actions like <strong>Apply</strong>, <strong>Save</strong>, or <strong>Checkout</strong> in the footer so they remain accessible while the content scrolls.</p>
</li>
<li><p><strong>Avoid overloading the Sheet:</strong> If the workflow is long, requires multiple steps, or needs the user's full attention, consider using a dedicated page instead of a Sheet.</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this guide, we built two practical Shadcn Sheet components, a shopping cart and a filter panel, to demonstrate the patterns you'll use most often in React applications. Along the way, we covered layout best practices, state management, derived values, and interaction patterns that make Sheets feel intuitive and reliable.</p>
<p>These examples are more than just demos. They provide a reusable foundation for building settings drawers, mobile navigation, notification panels, and many other side panel experiences. By following these patterns, you can create Shadcn Sheet components that are clean, responsive, and easy to maintain as your application grows.</p>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><a href="https://shadcnspace.com/"><strong>Shadcn UI</strong></a>: Component library and documentation home</p>
</li>
<li><p><a href="https://shadcnspace.com/components/sheet"><strong>Shadcn Sheet Components</strong></a>: All Sheet variants are available in Radix and Base UI</p>
</li>
<li><p><a href="https://shadcnspace.com/components"><strong>Shadcn Components</strong></a>: Full component library with all available categories</p>
</li>
<li><p><a href="https://shadcnspace.com/admin-dashboard"><strong>Shadcn Dashboard</strong></a>: Ready-to-use admin dashboard built on the same system</p>
</li>
<li><p><a href="https://shadcnspace.com/blocks/dashboard-ui/sidebars"><strong>Shadcn Sidebar</strong></a>: If you're looking for a shadcn sidebar to pair with these panels, this is where to start</p>
</li>
<li><p><a href="https://shadcnspace.com/docs/getting-started/how-to-use-shadcn-cli"><strong>How to Use the Shadcn CLI</strong></a>: Getting started guide for the CLI and registry setup</p>
</li>
<li><p><a href="https://shadcnspace.com/mcp"><strong>Shadcn MCP Server</strong></a>: Browse and install components from your editor</p>
</li>
<li><p><a href="https://youtu.be/mMlxAmJlbMI?si=4f37Bg9076ldMllL"><strong>Video Walkthrough: How to Use Shadcn Space with MCP Server</strong></a></p>
</li>
<li><p><a href="https://ui.shadcn.com/docs"><strong>Official shadcn/ui Documentation</strong></a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build Production-Ready Card Components with shadcn/ui ]]>
                </title>
                <description>
                    <![CDATA[ Card components are one of the most common UI patterns in web development. You see them in property listing apps, SaaS analytics dashboards, e-commerce product pages, and admin panels. But building a  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-production-ready-card-components-with-shadcn-ui/</link>
                <guid isPermaLink="false">6a4d22a80e40282fd1e657a8</guid>
                
                    <category>
                        <![CDATA[ shadcn ]]>
                    </category>
                
                    <category>
                        <![CDATA[ UI Design ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Vaibhav Gupta ]]>
                </dc:creator>
                <pubDate>Tue, 07 Jul 2026 16:00:40 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/1efc2e2c-00a5-4891-9845-18de2a38c5f1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Card components are one of the most common UI patterns in web development. You see them in property listing apps, SaaS analytics dashboards, e-commerce product pages, and admin panels.</p>
<p>But building a card that handles hover states cleanly, supports dark mode, stays accessible, and works across screen sizes takes more than wrapping content in a <code>&lt;div&gt;</code>. You need a consistent component structure, a reliable design system, and well-thought-out Tailwind patterns.</p>
<p>In this tutorial, you'll build four types of production-ready card components using shadcn/ui and Base UI primitives via Shadcn Space. Each card targets a specific, real-world UI pattern that developers run into regularly.</p>
<p>By the end, you'll have:</p>
<ol>
<li><p>A Preview Card with a group hover image effect, an overlay arrow icon, and a property details layout</p>
</li>
<li><p>An Analytics Card with typed metric props, conditional badge colors, and a decorative background image</p>
</li>
<li><p>A Statistics Card with a responsive four-column e-commerce stats grid and icon badges</p>
</li>
<li><p>An Ecommerce Product Variant Card with size selection, a wishlist toggle, a bag button, and a ripple animation on the buy button</p>
</li>
</ol>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-why-shadcnui">Why shadcn/ui?</a></p>
</li>
<li><p><a href="#heading-what-is-shadcn-space">What is Shadcn Space?</a></p>
</li>
<li><p><a href="#heading-what-youll-build">What You'll Build</a></p>
</li>
<li><p><a href="#heading-how-to-set-up-the-cli-registry">How to Set Up the CLI Registry</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-preview-card-card-02">How to Build the Preview Card (card-02)</a></p>
</li>
<li><p><a href="#heading-live-preview">Live Preview</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-analytics-card-card-05">How to Build the Analytics Card (card-05)</a></p>
</li>
<li><p><a href="#heading-live-preview">Live Preview</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-statistics-card-card-06">How to Build the Statistics Card (card-06)</a></p>
</li>
<li><p><a href="#heading-live-preview">Live Preview</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-ecommerce-product-variant-card-card-17">How to Build the Ecommerce Product Variant Card (card-17)</a></p>
</li>
<li><p><a href="#heading-live-preview">Live Preview</a></p>
</li>
<li><p><a href="#heading-quick-reference-table">Quick Reference Table</a></p>
</li>
<li><p><a href="#heading-key-concepts-recap">Key Concepts Recap</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-resources">Resources</a></p>
</li>
</ul>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<p>Before you start, make sure you have the following in place:</p>
<ul>
<li><p>Node.js 18 or higher installed</p>
</li>
<li><p>A Next.js or React project set up</p>
</li>
<li><p>shadcn/ui initialized in your project (<code>npx shadcn@latest init</code>)</p>
</li>
<li><p>Tailwind CSS configured</p>
</li>
<li><p>Basic knowledge of React and TypeScript</p>
</li>
</ul>
<p>If you haven't initialized shadcn/ui yet, run <code>npx shadcn@latest init</code> in your project root and follow the prompts before continuing.</p>
<h2 id="heading-why-shadcnui"><strong>Why shadcn/ui?</strong></h2>
<p><a href="https://ui.shadcn.com/"><strong>shadcn/ui</strong></a> is a collection of accessible, open-source React components built on top of Radix UI, Base UI, and styled with Tailwind CSS.</p>
<p>The way it works is different from a traditional component library. Instead of installing a package, you use a CLI to copy the component source files directly into your project. This means you own every line of the code. You can read it, edit it, and the component will never break because of a library update you didn't control.</p>
<p>Some key benefits:</p>
<ul>
<li><p><strong>Accessible by default</strong>: built on Radix UI and Base UI primitives</p>
</li>
<li><p><strong>Fully Tailwind-based</strong>: no external CSS files, no specificity conflicts</p>
</li>
<li><p><strong>Zero lock-in</strong>: components live in your <code>components/</code> folder, not inside <code>node_modules</code></p>
</li>
<li><p><strong>Works everywhere</strong>: Next.js, Vite, Astro, Remix, and other React frameworks</p>
</li>
</ul>
<p>The <code>Card</code>, <code>Badge</code>, <code>Button</code>, and <code>Separator</code> components you'll use in this tutorial all come from the shadcn/ui base install.</p>
<h2 id="heading-what-is-shadcn-space"><strong>What is Shadcn Space?</strong></h2>
<p><a href="https://shadcnspace.com/"><strong>Shadcn Space</strong></a> is an open-source registry of production-ready components and UI blocks built on top of shadcn/ui. It extends the default shadcn/ui component set with additional variants from common patterns to highly appealing layouts.</p>
<p>The key difference from the default shadcn/ui <code>Card</code> component is that Shadcn Space cards are designed for specific layout patterns. You get more structure out of the box.</p>
<p>Each component in Shadcn Space supports both Radix UI and Base UI primitives. You also get the functionality of <strong>Copy Prompt</strong>. This tutorial uses the Base UI versions. You install them the same way as any shadcn/ui component, through a single CLI command, and the source files land in your project.</p>
<p>You can browse the full card collection in the <a href="https://shadcnspace.com/components/card"><strong>Shadcn card component library</strong></a>.</p>
<h2 id="heading-what-youll-build"><strong>What You'll Build</strong></h2>
<p>Here's an overview of the four cards you'll build, along with their specific features:</p>
<p><strong>Preview Card (card-02)</strong></p>
<ul>
<li><p>Large image with hover brightness and scale animation</p>
</li>
<li><p>An arrow icon that appears only on hover</p>
</li>
<li><p>Property title and location</p>
</li>
<li><p>Price badge with a teal color scheme</p>
</li>
<li><p>Amenity row with bed, bath, and area icons</p>
</li>
</ul>
<p><strong>Analytics Card (card-05)</strong></p>
<ul>
<li><p>Typed TypeScript props with a built-in default dataset</p>
</li>
<li><p>Two metric columns separated by a vertical divider</p>
</li>
<li><p>Conditional badge colors based on positive or negative trend</p>
</li>
<li><p>Decorative background image pinned to the bottom-right corner</p>
</li>
</ul>
<p><strong>Statistics Card (card-06)</strong></p>
<ul>
<li><p>Four-column responsive grid that stacks on mobile</p>
</li>
<li><p>Iconify Solar icon set for each metric</p>
</li>
<li><p>Badge with trend direction icon</p>
</li>
<li><p>Border dividers are removed from the last column automatically</p>
</li>
</ul>
<p><strong>Ecommerce Product Variant Card (card-17)</strong></p>
<ul>
<li><p>Product image with 3D drop shadow and hover zoom</p>
</li>
<li><p>Wishlist heart toggle with dark mode support</p>
</li>
<li><p>Size selector with active state highlighting</p>
</li>
<li><p>Bag icon toggle that fills on click</p>
</li>
<li><p>"Buy Now" button with a CSS ripple animation</p>
</li>
<li><p>Dynamic delivery date with ordinal suffix formatting</p>
</li>
</ul>
<h2 id="heading-how-to-set-up-the-cli-registry"><strong>How to Set Up the CLI Registry</strong></h2>
<p>Before you run any install commands, you need to register the Shadcn Space registry in your <code>components.json</code> file.</p>
<p>Open <code>components.json</code> in your project root and add the <code>registries</code> field:</p>
<pre><code class="language-javascript">{
  "registries": {
    "@shadcn-space": {
      "url": "https://shadcnspace.com/r/{name}.json"
    }
  }
}
</code></pre>
<p>This tells the shadcn CLI where to find components prefixed with <code>@shadcn-space/</code>. Without this step, all the install commands in this tutorial will fail.</p>
<p>Your full <code>components.json</code> should look something like this after adding the registry:</p>
<pre><code class="language-javascript">{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "default",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "app/globals.css",
    "baseColor": "neutral",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils"
  },
  "registries": {
    "@shadcn-space": {
      "url": "https://shadcnspace.com/r/{name}.json"
    }
  }
}
</code></pre>
<p>For a full walkthrough of how the CLI works with third-party registries, visit the <a href="https://shadcnspace.com/docs/getting-started/how-to-use-shadcn-cli"><strong>getting started guide</strong></a>. You can also watch the video walkthrough if you prefer to follow along visually.</p>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/n6dvjVxy02U" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>

<h2 id="heading-how-to-build-the-preview-card-card-02"><strong>How to Build the Preview Card (card-02)</strong></h2>
<h3 id="heading-what-the-preview-card-does">What the Preview Card Does</h3>
<p>The Preview Card is designed for property listings, hotel pages, or any content that benefits from a large image with supporting details below it.</p>
<p>When a user hovers the card, the image darkens and scales up. An arrow icon appears in the corner. Below the image, a title, location, price badge, and amenity row are displayed.</p>
<h3 id="heading-how-to-install-the-preview-card">How to Install the Preview Card</h3>
<p>Run one of the following commands based on your package manager:</p>
<p><strong>npm:</strong></p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/card-02
</code></pre>
<p><strong>pnpm:</strong></p>
<pre><code class="language-javascript">pnpm dlx shadcn@latest add @shadcn-space/card-02
</code></pre>
<p><strong>Yarn:</strong></p>
<pre><code class="language-javascript">yarn dlx shadcn@latest add @shadcn-space/card-02
</code></pre>
<p><strong>Bun:</strong></p>
<pre><code class="language-javascript">bunx --bun shadcn@latest add @shadcn-space/card-02
</code></pre>
<p>The CLI copies the component into your project at:</p>
<pre><code class="language-javascript">components/
  shadcn-space/
    card/
      Card-02.tsx
</code></pre>
<h3 id="heading-the-component-code">The Component Code</h3>
<pre><code class="language-javascript">import { Badge } from "@/components/ui/badge";
import { Card } from "@/components/ui/card";
import { ArrowRight, Bath, BedDouble, Expand } from "lucide-react";

const PreviewCard = () =&gt; (
  &lt;Card className="relative gap-0 py-0 rounded-2xl group hover:shadow-3xl duration-300"&gt;
    &lt;div className="overflow-hidden rounded-t-2xl"&gt;
      &lt;a href="#"&gt;
        &lt;div className="w-full h-72"&gt;
          &lt;img
            src="https://images.shadcnspace.com/assets/card/property-cover-1.jpg"
            alt="Serenity Residential Home"
            width={440}
            height={300}
            className="w-full h-full object-cover rounded-t-2xl group-hover:brightness-50 group-hover:scale-125 transition duration-300 delay-75"
          /&gt;
        &lt;/div&gt;
      &lt;/a&gt;

      &lt;div className="absolute top-6 right-6 hidden p-4 bg-white rounded-full group-hover:block"&gt;
        &lt;ArrowRight className="text-card-foreground" /&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;div className="p-6"&gt;
      &lt;div className="flex justify-between gap-5 mb-6"&gt;
        &lt;div&gt;
          &lt;a href="#"&gt;
            &lt;h3 className="text-xl font-medium duration-300 group-hover:text-primary"&gt;
              Serenity Residential Home
            &lt;/h3&gt;
          &lt;/a&gt;
          &lt;p className="text-base font-normal text-muted-foreground"&gt;
            15 S Aurora Ave, Miami
          &lt;/p&gt;
        &lt;/div&gt;

        &lt;Badge className="px-5 py-4 text-base font-normal rounded-full bg-teal-500/10 text-teal-500"&gt;
          $570,000
        &lt;/Badge&gt;
      &lt;/div&gt;

      &lt;div className="flex"&gt;
        &lt;div className="flex flex-col gap-2 xs:pr-4 pr-8 border-e border-border"&gt;
          &lt;BedDouble size={20} /&gt;
          &lt;p className="text-sm sm:text-base"&gt;5 Bedrooms&lt;/p&gt;
        &lt;/div&gt;

        &lt;div className="flex flex-col gap-2 xs:px-4 px-8 border-e border-border"&gt;
          &lt;Bath size={20} /&gt;
          &lt;p className="text-sm sm:text-base"&gt;3 Bathrooms&lt;/p&gt;
        &lt;/div&gt;

        &lt;div className="flex flex-col gap-2 xs:pl-4 pl-8"&gt;
          &lt;Expand size={20} /&gt;
          &lt;p className="text-sm sm:text-base"&gt;
            120m&lt;sup&gt;2&lt;/sup&gt;
          &lt;/p&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/Card&gt;
);

export default PreviewCard;
</code></pre>
<p>Let's now go through how this code works.</p>
<h4 id="heading-1-group-hover-behavior">1. Group hover behavior</h4>
<p>The <code>group</code> class on the outer <code>Card</code> element is the core of this component. Any child element with a <code>group-hover:</code> class will respond when the card is hovered, not just that individual element.</p>
<p>This is how the image darkens (<code>group-hover:brightness-50</code>), scales up (<code>group-hover:scale-125</code>), and the arrow icon appears (<code>group-hover:block</code>).</p>
<h4 id="heading-2-overflow-clipping-on-image-zoom">2. Overflow clipping on image zoom</h4>
<p>Without <code>overflow-hidden</code> on the image wrapper, the <code>scale-125</code> transform would bleed past the card's rounded corners on hover. The wrapper clips the image so it stays inside the card boundary.</p>
<p>Notice that <code>rounded-t-2xl</code> appears on both the wrapper and the image itself to maintain a consistent corner radius during the transition.</p>
<h4 id="heading-3-logical-border-properties-in-the-amenity-row">3. Logical border properties in the amenity row</h4>
<p>The amenity row uses <code>border-e</code> instead of <code>border-r</code>. This is a CSS logical property meaning "border at the inline end." In left-to-right layouts, that's the right side. In right-to-left layouts, it flips automatically. Using logical properties is a good production habit for any component that may need to support multiple locales.</p>
<h3 id="heading-live-preview">Live Preview:</h3>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/21233f5e-2d39-4430-8013-a780bd24419c.gif" alt="21233f5e-2d39-4430-8013-a780bd24419c" style="display:block;margin:0 auto" width="960" height="720" loading="lazy">

<hr>
<h2 id="heading-how-to-build-the-analytics-card-card-05"><strong>How to Build the Analytics Card (card-05)</strong></h2>
<h3 id="heading-what-the-analytics-card-does">What the Analytics Card Does</h3>
<p>The Analytics Card is a compact dashboard widget. It shows two metrics side by side with values and percentage-change badges. A decorative chart image sits in the bottom-right corner.</p>
<p>The component is typed with TypeScript interfaces, making it easy to swap in real data from an API.</p>
<h3 id="heading-how-to-install-the-analytics-card">How to Install the Analytics Card</h3>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/card-05
</code></pre>
<p>The CLI copies the component into:</p>
<pre><code class="language-javascript">components/
  shadcn-space/
    card/
      card-05.tsx
</code></pre>
<h3 id="heading-the-component-code">The Component Code</h3>
<pre><code class="language-javascript">import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";

type DashboardMetric = {
  label: string;
  value: string;
  percentage: string;
  isPositive?: boolean;
};

type MainDashboardData = {
  title: string;
  description: string;
  metrics: DashboardMetric[];
};

type WidgetProps = {
  mainDashboard?: MainDashboardData;
};

const mainDashboardData: MainDashboardData = {
  title: "Analytics Dashboard",
  description: "Check all the statistics",
  metrics: [
    {
      label: "Earnings",
      value: "$27,850",
      percentage: "+18%",
      isPositive: true,
    },
    {
      label: "Expense",
      value: "$18,453",
      percentage: "-5%",
      isPositive: false,
    },
  ],
};

const AnalyticsCard = ({ mainDashboard = mainDashboardData }: WidgetProps) =&gt; {
  return (
    &lt;div className="flex items-center justify-center w-full"&gt;
      &lt;div className="max-w-7xl mx-auto px-4 lg:px-8 xl:px-16 py-10 w-full"&gt;
        &lt;Card className="p-0 ring-0 border rounded-2xl relative h-full max-w-xl w-full mx-auto"&gt;
          &lt;CardContent className="p-0"&gt;
            &lt;div className="ps-6 py-4 flex flex-col gap-9 justify-between"&gt;
              &lt;div&gt;
                &lt;p className="text-lg font-medium text-card-foreground"&gt;
                  {mainDashboard.title}
                &lt;/p&gt;
                &lt;p className="text-xs font-normal text-muted-foreground"&gt;
                  {mainDashboard.description}
                &lt;/p&gt;
              &lt;/div&gt;
              &lt;div className="flex items-center gap-6"&gt;
                {mainDashboard.metrics.map((metric, index) =&gt; (
                  &lt;div key={index} className="flex items-center gap-6"&gt;
                    &lt;div&gt;
                      &lt;p className="text-xs font-normal text-muted-foreground"&gt;
                        {metric.label}
                      &lt;/p&gt;
                      &lt;div className="flex items-center gap-1"&gt;
                        &lt;p className="text-2xl font-medium text-card-foreground"&gt;
                          {metric.value}
                        &lt;/p&gt;
                        &lt;Badge
                          className={cn(
                            "font-normal text-muted-foreground",
                            metric.isPositive
                              ? "bg-teal-400/10"
                              : "bg-red-500/10"
                          )}
                        &gt;
                          {metric.percentage}
                        &lt;/Badge&gt;
                      &lt;/div&gt;
                    &lt;/div&gt;
                    {index &lt; mainDashboard.metrics.length - 1 &amp;&amp; (
                      &lt;Separator orientation="vertical" className="h-12" /&gt;
                    )}
                  &lt;/div&gt;
                ))}
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;img
              src="https://images.shadcnspace.com/assets/backgrounds/stats-01.webp"
              alt="stats chart"
              width={211}
              height={168}
              className="absolute bottom-0 right-0 hidden sm:block"
            /&gt;
          &lt;/CardContent&gt;
        &lt;/Card&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default AnalyticsCard;
</code></pre>
<p>How the Analytics Card works:</p>
<h4 id="heading-1-optional-props-with-a-default-dataset">1. Optional props with a default dataset</h4>
<p>The component accepts an optional <code>mainDashboard</code> prop. If you don't pass anything, it falls back to <code>mainDashboardData</code>, the constant is defined in the same file:</p>
<pre><code class="language-javascript">const AnalyticsCard = ({ mainDashboard = mainDashboardData }: WidgetProps) =&gt; {
</code></pre>
<p>This pattern lets the component work out of the box in demos or Storybook, while still being fully driven by real API data in production. To connect it to live data, you just pass a prop that matches the <code>MainDashboardData</code> shape.</p>
<h4 id="heading-2-conditional-badge-colors-with-cn">2. Conditional badge colors with <code>cn()</code></h4>
<p>The <code>cn()</code> utility (from <code>@/lib/utils</code>) merges Tailwind class names and handles conditional logic cleanly. It also de-duplicates conflicting Tailwind classes automatically, which plain template literals don't do:</p>
<pre><code class="language-javascript">className={cn(
  "font-normal text-muted-foreground",
  metric.isPositive ? "bg-teal-400/10" : "bg-red-500/10"
)}
</code></pre>
<h4 id="heading-3-separators-only-between-metrics-not-after-the-last-one">3. Separators only between metrics, not after the last one</h4>
<p>The <code>Separator</code> component renders only between metrics, never after the last one. The index check handles this:</p>
<pre><code class="language-javascript">{index &lt; mainDashboard.metrics.length - 1 &amp;&amp; (
  &lt;Separator orientation="vertical" className="h-12" /&gt;
)}
</code></pre>
<h4 id="heading-4-absolute-positioned-decorative-image">4. Absolute-positioned decorative image</h4>
<p>The chart image is used <code>absolute bottom-0 right-0</code> to pin it to the card's bottom-right corner. It hides on small screens with <code>hidden sm:block</code> to avoid layout issues on mobile. The parent <code>Card</code> has <code>relative</code> positioning to contain it.</p>
<h3 id="heading-live-preview">Live Preview:</h3>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/de09194f-ff75-49c1-95e5-21d758e499e8.png" alt="de09194f-ff75-49c1-95e5-21d758e499e8" style="display:block;margin:0 auto" width="1266" height="356" loading="lazy">

<hr>
<h2 id="heading-how-to-build-the-statistics-card-card-06"><strong>How to Build the Statistics Card (card-06)</strong></h2>
<h3 id="heading-what-the-statistics-card-does">What the Statistics Card Does</h3>
<p>The Statistics Card displays four e-commerce metrics in a horizontal grid: Orders, Sales, Profit, and Expense. Each column has an icon, a large value, a time period label, and a badge showing the percentage trend.</p>
<p>The layout is fully responsive, collapsing from four columns to two on medium screens and stacking on mobile.</p>
<p>This card uses <code>@iconify/react</code> for icons instead of <code>lucide-react</code>, giving you access to thousands of icon sets using string-based icon names.</p>
<h3 id="heading-how-to-install-the-statistics-card">How to Install the Statistics Card</h3>
<p>First, install <code>@iconify/react</code> if you don't have it:</p>
<pre><code class="language-javascript">npm install @iconify/react
</code></pre>
<p>Then add the card component:</p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/card-06
</code></pre>
<p>The CLI copies the component into:</p>
<pre><code class="language-javascript">components/
  shadcn-space/
    card/
      card-06.tsx
</code></pre>
<h3 id="heading-the-component-code">The Component Code</h3>
<pre><code class="language-javascript">"use client";
import { Icon } from "@iconify/react";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";

const StatisticsCard = () =&gt; {
  const EcommerceActions = [
    {
      title: "Orders",
      subtitle: "5868",
      cardIcon: "solar:bag-4-line-duotone",
      badgeColor: "bg-teal-400/10",
      statusValue: "+18%",
      statusIcon: "solar:course-up-line-duotone",
    },
    {
      title: "Sales",
      subtitle: "$96,850",
      cardIcon: "solar:box-line-duotone",
      badgeColor: "bg-orange-400/10",
      statusValue: "-5%",
      statusIcon: "solar:course-down-line-duotone",
    },
    {
      title: "Profit",
      subtitle: "$82,906",
      cardIcon: "solar:chart-square-line-duotone",
      badgeColor: "bg-teal-400/10",
      statusValue: "+18%",
      statusIcon: "solar:course-up-line-duotone",
    },
    {
      title: "Expense",
      subtitle: "$14,653",
      cardIcon: "solar:star-line-duotone",
      badgeColor: "bg-teal-400/10",
      statusValue: "+18%",
      statusIcon: "solar:course-up-line-duotone",
    },
  ];

  return (
    &lt;div className="max-w-7xl mx-auto px-4 w-full"&gt;
      &lt;Card className="p-0"&gt;
        &lt;CardContent className="flex items-center w-full lg:flex-nowrap flex-wrap px-0"&gt;
          {EcommerceActions.map((item, index) =&gt; (
            &lt;div
              className="lg:w-3/12 md:w-6/12 w-full border-e border-border last:border-e-0"
              key={index}
            &gt;
              &lt;div className="p-6"&gt;
                &lt;div className="flex flex-col gap-1"&gt;
                  &lt;div className="flex justify-between items-start"&gt;
                    &lt;h5 className="text-base font-medium"&gt;{item.title}&lt;/h5&gt;
                    &lt;div className="p-3 rounded-full outline outline-border text-primary"&gt;
                      &lt;Icon icon={item.cardIcon} width={16} height={16} /&gt;
                    &lt;/div&gt;
                  &lt;/div&gt;
                  &lt;div className="flex flex-col gap-1"&gt;
                    &lt;h5 className="text-2xl font-semibold"&gt;{item.subtitle}&lt;/h5&gt;
                    &lt;div className="flex items-center gap-2"&gt;
                      &lt;p className="text-xs text-muted-foreground"&gt;Last 7 days&lt;/p&gt;
                      &lt;Badge className={`${item.badgeColor} text-muted-foreground`}&gt;
                        &lt;div className="flex items-center gap-1"&gt;
                          {item.statusValue}
                          &lt;Icon icon={item.statusIcon} width={14} height={14} /&gt;
                        &lt;/div&gt;
                      &lt;/Badge&gt;
                    &lt;/div&gt;
                  &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          ))}
        &lt;/CardContent&gt;
      &lt;/Card&gt;
    &lt;/div&gt;
  );
};

export default StatisticsCard;
</code></pre>
<p>How the Statistics Card works:</p>
<h4 id="heading-1-data-driven-layout-with-an-array">1. Data-driven layout with an array</h4>
<p>All four metrics live in the <code>EcommerceActions</code> array. Adding or removing a metric only requires updating the array. The JSX stays the same. This is the right approach for any component with a repeating structure: keep data and markup separate.</p>
<h4 id="heading-2-responsive-column-widths">2. Responsive column widths</h4>
<p>Each column uses three width classes to handle every breakpoint:</p>
<ul>
<li><p><code>w-full</code> on mobile (single column, stacked vertically)</p>
</li>
<li><p><code>md:w-6/12</code> on medium screens (two columns)</p>
</li>
<li><p><code>lg:w-3/12</code> on large screens (four equal columns)</p>
</li>
</ul>
<p>The <code>flex-wrap</code> on the <code>CardContent</code> lets columns wrap naturally on smaller screens. <code>lg:flex-nowrap</code> forces them into a single row on large screens.</p>
<h4 id="heading-3-removing-the-last-border-with-last">3. Removing the last border with <code>last:</code></h4>
<p>The <code>last:border-e-0</code> class removes the right border from the final column. Without it, there'd be a stray border on the right edge of the card.</p>
<p>The <code>last:</code> variant is a Tailwind pseudo-class that targets the last child element in a group, which is cleaner than tracking the index manually.</p>
<h4 id="heading-4-why-use-client-is-needed-here">4. Why <code>"use client"</code> is needed here</h4>
<p>The <code>@iconify/react</code> package requires a browser environment. In Next.js with the App Router, any component that imports a client-only package needs the <code>"use client"</code> directive at the top of the file. Without it, the server will throw an error during rendering.</p>
<h3 id="heading-live-preview">Live Preview:</h3>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/dd7600b4-753c-4e67-abff-2d8955a22653.png" alt="dd7600b4-753c-4e67-abff-2d8955a22653" style="display:block;margin:0 auto" width="1272" height="257" loading="lazy">

<hr>
<h2 id="heading-how-to-build-the-ecommerce-product-variant-card-card-17"><strong>How to Build the Ecommerce Product Variant Card (card-17)</strong></h2>
<h3 id="heading-what-the-ecommerce-product-variant-card-does">What the Ecommerce Product Variant Card Does</h3>
<p>This is the most interactive card in this tutorial. It's a product card for a shoe listing with a hover zoom, a wishlist toggle, size buttons, a bag toggle, and a ripple-animation buy button. All interactions are handled with React's <code>useState</code>, so no external state management library is required.</p>
<h3 id="heading-how-to-install-the-product-variant-card">How to Install the Product Variant Card</h3>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/card-17
</code></pre>
<p>The CLI copies the component into:</p>
<pre><code class="language-javascript">components/
  shadcn-space/
    card/
      card-17.tsx
</code></pre>
<h3 id="heading-the-component-code">The Component Code</h3>
<pre><code class="language-javascript">"use client";

import { useState } from "react";
import { Card, CardContent, CardFooter } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Heart, ShoppingBag } from "lucide-react";
import { cn } from "@/lib/utils";

const sizes = ["7", "8", "9", "10"];

const getDeliveryDate = () =&gt; {
  const date = new Date();
  date.setDate(date.getDate() + 3);
  const day = date.getDate();
  const month = [
    "Jan", "Feb", "Mar", "Apr", "May", "Jun",
    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
  ][date.getMonth()];
  const suffix = ["th", "st", "nd", "rd"][
    day % 10 &gt; 3 ? 0 : (day % 100 - day % 10 !== 10 ? day % 10 : 0)
  ];
  return `${day}${suffix} ${month}`;
};

export default function EcommerceProductCard() {
  const [activeSize, setActiveSize] = useState(1);
  const [isWishlisted, setIsWishlisted] = useState(false);
  const [inBag, setInBag] = useState(false);

  return (
    &lt;div className="flex items-center justify-center p-8 w-full bg-background"&gt;
      &lt;Card className="w-80 rounded-2xl overflow-hidden p-0 gap-0 group/card"&gt;

        {/* Image zone */}
        &lt;div className="relative overflow-hidden h-80"&gt;
          &lt;img
            src="https://images.shadcnspace.com/assets/card/running-shoe-3d.png"
            className="object-contain drop-shadow-2xl px-8 py-6 transition-transform duration-500 ease-out group-hover/card:scale-105"
            alt="Nike Air Max Pulse"
          /&gt;

          {/* Discount badge */}
          &lt;span className="absolute top-3 left-3 text-xs tracking-widest font-bold uppercase bg-foreground text-background px-2.5 py-1 rounded-sm select-none"&gt;
            -21%
          &lt;/span&gt;

          {/* Wishlist button */}
          &lt;button
            onClick={() =&gt; setIsWishlisted(!isWishlisted)}
            title="Wishlist"
            className={cn(
              "absolute top-3 right-3 h-8 w-8 rounded-full border shadow-sm flex items-center justify-center transition-all duration-200 hover:scale-110 active:scale-95",
              isWishlisted
                ? "bg-rose-50 border-rose-200 dark:bg-rose-950 dark:border-rose-800"
                : "bg-background"
            )}
          &gt;
            &lt;Heart
              className={cn(
                "w-3.5 h-3.5 transition-colors",
                isWishlisted ? "fill-rose-500 text-rose-500" : "text-muted-foreground"
              )}
            /&gt;
          &lt;/button&gt;
        &lt;/div&gt;

        {/* Info zone */}
        &lt;CardContent className="px-4 pt-4 pb-4 space-y-1.5"&gt;
          &lt;div className="min-w-0"&gt;
            &lt;h3 className="text-base font-bold text-foreground truncate"&gt;Nike&lt;/h3&gt;
            &lt;p className="text-sm text-muted-foreground truncate"&gt;
              Air Max Pulse Running Shoes
            &lt;/p&gt;
          &lt;/div&gt;

          &lt;div className="flex items-center gap-2 pt-1"&gt;
            &lt;span className="text-green-600 dark:text-green-500 font-semibold text-sm"&gt;
              Down 21%
            &lt;/span&gt;
            &lt;span className="text-muted-foreground line-through text-sm"&gt;$150&lt;/span&gt;
            &lt;span className="text-foreground font-bold text-base"&gt;$119&lt;/span&gt;
          &lt;/div&gt;

          &lt;div className="text-xs text-muted-foreground font-medium"&gt;
            Delivery by{" "}
            &lt;span suppressHydrationWarning className="text-foreground font-bold"&gt;
              {getDeliveryDate()}
            &lt;/span&gt;
          &lt;/div&gt;

          {/* Size selector */}
          &lt;div className="flex gap-1.5 pt-2"&gt;
            {sizes.map((s, i) =&gt; (
              &lt;button
                key={s}
                onClick={() =&gt; setActiveSize(i)}
                className={cn(
                  "flex-1 h-7 rounded-lg text-sm font-medium border transition-all duration-150",
                  activeSize === i
                    ? "bg-foreground text-background border-foreground"
                    : "text-muted-foreground hover:border-foreground/50 hover:text-foreground"
                )}
              &gt;
                US {s}
              &lt;/button&gt;
            ))}
          &lt;/div&gt;
        &lt;/CardContent&gt;

        {/* Action zone */}
        &lt;CardFooter className="px-4 pb-6 gap-2 bg-transparent border-t-0"&gt;
          &lt;button
            onClick={() =&gt; setInBag(!inBag)}
            title={inBag ? "Remove from bag" : "Add to bag"}
            className={cn(
              "h-12 w-12 shrink-0 rounded-xl border flex items-center justify-center transition-all duration-200 hover:scale-105 active:scale-95",
              inBag
                ? "bg-foreground text-background border-foreground"
                : "bg-background text-muted-foreground hover:border-foreground/50 hover:text-foreground"
            )}
          &gt;
            &lt;ShoppingBag className="w-5 h-5" /&gt;
          &lt;/button&gt;

          &lt;Button className="relative overflow-hidden group/btn flex-1 h-12 rounded-xl font-semibold text-base cursor-pointer border border-primary transition-all flex items-center justify-center gap-2"&gt;
            &lt;span className="absolute left-1/2 -translate-x-1/2 top-full -translate-y-1/2 w-8 h-8 bg-white dark:bg-gray-950 rounded-full scale-0 transition-transform duration-700 ease-in-out group-hover/btn:scale-[20]" /&gt;
            &lt;span className="relative z-10 transition-colors duration-500 group-hover/btn:text-gray-950 dark:group-hover/btn:text-white"&gt;
              Buy Now
            &lt;/span&gt;
          &lt;/Button&gt;
        &lt;/CardFooter&gt;

      &lt;/Card&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>How the Ecommerce Product Variant Card works:</p>
<h4 id="heading-1-named-group-hover-scopes">1. Named group hover scopes</h4>
<p>This card uses two independent hover group scopes: <code>group/card</code> on the outer card and <code>group/btn</code> on the Buy Now button. Tailwind's named group feature uses the <code>/name</code> suffix to keep them separate:</p>
<pre><code class="language-javascript">// Card-level hover: zooms the product image
&lt;Card className="... group/card"&gt;
  &lt;img className="... group-hover/card:scale-105" /&gt;

// Button-level hover: triggers the ripple animation
  &lt;Button className="... group/btn"&gt;
    &lt;span className="... group-hover/btn:scale-[20]" /&gt;
</code></pre>
<p>Without named groups, hovering the button would also trigger the card's hover styles. The <code>/card</code> and <code>/btn</code> suffixes prevent this.</p>
<h4 id="heading-2-css-ripple-animation-on-the-buy-now-button">2. CSS ripple animation on the Buy Now button</h4>
<p>The ripple effect uses a pure CSS scale animation. A white circle (<code>w-8 h-8 rounded-full</code>) starts at <code>scale-0</code> and transitions to <code>scale-[20]</code> when the button is hovered. The <code>overflow-hidden</code> on the <code>Button</code> clips it to the button's boundary. The <code>z-10</code> on the label keeps the text visible above the expanding circle.</p>
<h4 id="heading-3-ordinal-suffix-logic-for-the-delivery-date">3. Ordinal suffix logic for the delivery date</h4>
<p>The <code>getDeliveryDate()</code> function calculates a date three days from now and attaches the correct ordinal suffix (st, nd, rd, th):</p>
<pre><code class="language-javascript">const suffix = ["th", "st", "nd", "rd"][
  day % 10 &gt; 3 ? 0 : (day % 100 - day % 10 !== 10 ? day % 10 : 0)
];
</code></pre>
<p>The logic handles the edge cases for 11th, 12th, and 13th, which always use "th" regardless of their last digit. This is a common gotcha in ordinal formatting.</p>
<h4 id="heading-4-suppresshydrationwarning-on-the-delivery-date-span">4. <code>suppressHydrationWarning</code> on the delivery date span</h4>
<p>The delivery date is calculated at render time using <code>new Date()</code>. The server calculates it at request time, and the client recalculates it at hydration time.</p>
<p>If there's a timezone difference, React throws a hydration mismatch warning. <code>suppressHydrationWarning</code> silences this warning for that specific node without affecting the rest of the tree.</p>
<h3 id="heading-live-preview">Live Preview:</h3>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/f81584b2-a2ba-4233-a628-5daca018a1d9.gif" alt="f81584b2-a2ba-4233-a628-5daca018a1d9" style="display:block;margin:0 auto" width="960" height="720" loading="lazy">

<h2 id="heading-quick-reference-table"><strong>Quick Reference Table</strong></h2>
<table>
<thead>
<tr>
<th>Card</th>
<th>Identifier</th>
<th>Use Case</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Preview Card</strong></td>
<td><code>card-02</code></td>
<td>Property listings, hotel cards, product previews</td>
</tr>
<tr>
<td><strong>Analytics Card</strong></td>
<td><code>card-05</code></td>
<td>Dashboard widgets with metric data</td>
</tr>
<tr>
<td><strong>Statistics Card</strong></td>
<td><code>card-06</code></td>
<td>E-commerce stats grids</td>
</tr>
<tr>
<td><strong>Product Variant Card</strong></td>
<td><code>card-17</code></td>
<td>Product pages with size selection and cart</td>
</tr>
</tbody></table>
<p>To install any of these, replace the identifier in the CLI command:</p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/&lt;identifier&gt;
</code></pre>
<h2 id="heading-key-concepts-recap"><strong>Key Concepts Recap</strong></h2>
<p>Here's a summary of the key Tailwind, React, and TypeScript patterns used across the four cards in this tutorial.</p>
<h3 id="heading-tailwind-group-hover">Tailwind Group Hover</h3>
<p>The <code>group</code> class on a parent element lets any child respond to the parent's hover state using <code>group-hover:</code> classes.</p>
<p>For nested hover scopes, use named groups like <code>group/card</code> and <code>group/btn</code> with <code>group-hover/card:</code> and <code>group-hover/btn:</code>. This prevents hover styles from bleeding across component boundaries.</p>
<h3 id="heading-the-cn-utility">The <code>cn()</code> Utility</h3>
<p><code>cn()</code> from <code>@/lib/utils</code> merges Tailwind class strings, handles conditional class logic, and de-duplicates conflicting Tailwind utilities. Use it instead of template literals whenever you have conditional classes.</p>
<h3 id="heading-last-tailwind-variant"><code>last:</code> Tailwind Variant</h3>
<p>The <code>last:</code> pseudo-class variant targets the last child element in a group. In the Statistics Card, <code>last:border-e-0</code> remove the trailing border from the final column without any index tracking in JavaScript.</p>
<h3 id="heading-css-logical-properties">CSS Logical Properties</h3>
<p><code>border-e</code> means "border at the inline end," which is the right side in LTR layouts and the left side in RTL layouts. Using logical properties like <code>border-e</code>, <code>ps-</code>, and <code>pe-</code> instead of <code>border-r</code>, <code>pl-</code>, and <code>pr-</code> makes your components locale-aware by default.</p>
<h3 id="heading-typescript-optional-props-with-defaults">TypeScript Optional Props with Defaults</h3>
<p>Assigning a default value directly in the function signature, like <code>({ mainDashboard = mainDashboardData }: WidgetProps)</code>, is a clean pattern for components that need sensible fallback data while still being configurable. It works for demos, Storybook, and production use with real API data.</p>
<h3 id="heading-use-client-in-nextjs-app-router"><code>"use client"</code> in Next.js App Router</h3>
<p>Any component that uses <code>useState</code>, browser APIs, or client-only packages like <code>@iconify/react</code> needs the <code>"use client"</code> directive at the top of the file. Without it, the Next.js App Router will try to render the component on the server and throw an error.</p>
<h3 id="heading-suppresshydrationwarning"><code>suppressHydrationWarning</code></h3>
<p>When a value rendered on the server (for example, the current date or time) differs from the value rendered on the client due to timezone differences, React throws a hydration mismatch warning. Adding <code>suppressHydrationWarning</code> to the specific element silences the warning without affecting the rest of the component tree.</p>
<h3 id="heading-css-ripple-animation-pattern">CSS Ripple Animation Pattern</h3>
<p>A CSS ripple effect can be built without JavaScript by using a <code>scale-0</code> to <code>scale-[N]</code> transition on a <code>rounded-full</code> element placed inside an <code>overflow-hidden</code> container. On hover, the circle expands and gets clipped by the container boundary. The label text sits above it with <code>relative z-10</code>.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>In this tutorial, you built four production-ready card components using shadcn/ui and Base UI primitives:</p>
<ol>
<li><p><strong>Preview Card</strong>: group hover image animation, overflow clipping, and a logical border amenity row</p>
</li>
<li><p><strong>Analytics Card</strong>: typed props with default data, conditional badge colors with <code>cn()</code>, and an absolutely-positioned decorative image</p>
</li>
<li><p><strong>Statistics Card</strong>: data-driven repeating layout, responsive flex columns, and automatic last-border removal with <code>last:</code></p>
</li>
<li><p><strong>Ecommerce Product Variant Card</strong>: named hover groups, a CSS ripple button, ordinal date formatting, and hydration warning suppression</p>
</li>
</ol>
<p>Each card is installed with one CLI command and lives in your project's source tree. You own the code and can modify anything to fit your design system.</p>
<p>The patterns covered here, from named group hover scopes to typed props with defaults to logical CSS properties, apply well beyond card components. You'll find them useful across most UI components you build with shadcn/ui and Tailwind CSS.</p>
<h2 id="heading-resources"><strong>Resources</strong></h2>
<ul>
<li><p><a href="https://shadcnspace.com/"><strong>Shadcn UI</strong></a>: Component library and documentation home</p>
</li>
<li><p><a href="https://shadcnspace.com/components/card"><strong>Shadcn Card Component Collection</strong></a>: All card variants available in Radix and Base UI</p>
</li>
<li><p><a href="https://shadcnspace.com/docs/getting-started/how-to-use-shadcn-cli"><strong>How to Use the Shadcn CLI</strong></a>: Getting started guide for the CLI and registry setup</p>
</li>
<li><p><a href="https://shadcnspace.com/cli"><strong>Shadcn CLI Reference</strong></a>: Full CLI command reference</p>
</li>
<li><p><a href="https://shadcnspace.com/docs/getting-started/component"><strong>Component Getting Started Guide</strong></a>: How to install and use individual components</p>
</li>
<li><p><a href="https://shadcnspace.com/components"><strong>Shadcn Components</strong></a>: Full component library with all available categories</p>
</li>
<li><p><a href="https://shadcnspace.com/blocks/dashboard-ui"><strong>Shadcn Dashboard UI Blocks</strong></a>: Ready-to-use dashboard layout blocks built from the same system</p>
</li>
<li><p><a href="https://ui.shadcn.com/docs"><strong>Official Shadcn/ui Documentation</strong></a></p>
</li>
<li><p><a href="https://shadcnspace.com/figma"><strong>Shadcn Figma Kit</strong></a>: Figma UI kit that mirrors the Shadcn Space component system</p>
</li>
<li><p><a href="https://youtu.be/n6dvjVxy02U?si=EXfClzSyI8D97VaI"><strong>Video Walkthrough: How to Use Shadcn Space with CLI</strong></a>: YouTube tutorial for CLI setup and component installation</p>
</li>
<li><p><a href="https://www.npmjs.com/package/@iconify/react"><strong>@iconify/react on npm</strong></a>: Icon library used in the Statistics Card</p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build an Animated Badge Component with shadcn/ui ]]>
                </title>
                <description>
                    <![CDATA[ Badges are everywhere in modern web apps. You see them on notification counters, status labels, and feature tags. Most of them are static, though. They sit there doing nothing, blending into the page. ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-an-animated-badge-component-with-shadcn-ui/</link>
                <guid isPermaLink="false">6a3c0d40b101451dd3ba52e8</guid>
                
                    <category>
                        <![CDATA[ shadcn ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Vaibhav Gupta ]]>
                </dc:creator>
                <pubDate>Wed, 24 Jun 2026 17:00:48 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/90ffff22-4ea2-47c2-8b8c-011e8e566301.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Badges are everywhere in modern web apps. You see them on notification counters, status labels, and feature tags.</p>
<p>Most of them are static, though. They sit there doing nothing, blending into the page. But a well-animated badge can tell the user something happened without them having to read a single word.</p>
<p>In this tutorial, you'll build an animated “success” badge using shadcn/ui, Tailwind CSS, and Framer Motion. The badge will have a glowing top light, an animated check icon that bounces into view, and letters that drop in one at a time with a stagger effect.</p>
<p>The component comes from the <a href="https://shadcnspace.com/components/badge"><strong>Shadcn Space badge collection</strong></a> and uses the Base UI primitive version of Badge. You'll install it with a single CLI command, then walk through every piece of code.</p>
<p>By the end, you'll build an animated "Success" badge by:</p>
<ol>
<li><p>Installing the <code>badge-07</code> component from Shadcn Space using the Shadcn CLI</p>
</li>
<li><p>Using <code>motion.create()</code> to wrap the shadcn/ui <code>Badge</code> into an animatable component</p>
</li>
<li><p>Adding layered radial-gradient glow effects as absolutely positioned spans</p>
</li>
<li><p>Animating the check icon with a scale and rotate entrance</p>
</li>
<li><p>Animating each letter of the label individually using staggered <code>variants</code></p>
</li>
</ol>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-youll-build">What You'll Build</a></p>
</li>
<li><p><a href="#heading-how-to-install-the-component">How to Install the Component</a></p>
</li>
<li><p><a href="#heading-component-structure">Component Structure</a></p>
</li>
<li><p><a href="#heading-step-1-set-up-the-imports">Step 1: Set Up the Imports</a></p>
</li>
<li><p><a href="#heading-step-2-define-letter-animation-variants">Step 2: Define Letter Animation Variants</a></p>
</li>
<li><p><a href="#heading-step-3-wrap-the-badge-with-motion">Step 3: Wrap the Badge with Motion</a></p>
</li>
<li><p><a href="#heading-step-4-build-the-glow-layers">Step 4: Build the Glow Layers</a></p>
</li>
<li><p><a href="#heading-step-5-animate-the-icon">Step 5: Animate the Icon</a></p>
</li>
<li><p><a href="#heading-step-6-animate-each-letter">Step 6: Animate Each Letter</a></p>
</li>
<li><p><a href="#heading-how-to-use-it-in-your-app">How to Use It in Your App</a></p>
</li>
<li><p><a href="#heading-how-to-customize-the-component">How to Customize the Component</a></p>
</li>
<li><p><a href="#heading-live-preview">Live Preview</a></p>
</li>
<li><p><a href="#heading-key-concepts-recap">Key Concepts Recap</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-resources">Resources</a></p>
</li>
</ul>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<p>You'll need:</p>
<ul>
<li><p>A Next.js project with shadcn/ui initialized</p>
</li>
<li><p>Tailwind CSS set up</p>
</li>
<li><p><code>motion</code> installed: <code>npm install motion</code></p>
</li>
<li><p><code>lucide-react</code> installed: <code>npm install lucide-react</code></p>
</li>
<li><p>Basic TypeScript and React knowledge</p>
</li>
</ul>
<h2 id="heading-what-youll-build"><strong>What You'll Build</strong></h2>
<p>In this tutorial, we'll build a self-contained animated badge with three moving parts:</p>
<pre><code class="language-plaintext">├── MotionBadge (outline, rounded-full, teal border)
│   ├── Glow layers  → 3 radial gradient spans above the top border
│   ├── CheckCircle  → scale + rotate entrance, easeOutBack
│   └── Letter spans → staggered drop-in, easeOutCubic
</code></pre>
<p>After installation, the component file lands here:</p>
<pre><code class="language-plaintext">components/
└── shadcn-space/
    └── badge/
        └── badge-07.tsx
</code></pre>
<h2 id="heading-how-to-install-the-component"><strong>How to Install the Component</strong></h2>
<p><a href="https://shadcnspace.com/"><strong>Shadcn UI</strong></a> provides a registry of production-ready components. You pull them into your project with the Shadcn CLI, just like you'd add any standard shadcn/ui component.</p>
<p>Before running any command, check the <a href="https://shadcnspace.com/docs/getting-started/how-to-use-shadcn-cli"><strong>Getting Started guide</strong></a> or the <a href="https://shadcnspace.com/cli"><strong>CLI page</strong></a> for setup details.</p>
<p>You can also follow along with this video walkthrough:</p>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/n6dvjVxy02U" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>

<p>Run the command for your package manager:</p>
<p><strong>pnpm</strong></p>
<pre><code class="language-javascript">pnpm dlx shadcn@latest add @shadcn-space/badge-07
</code></pre>
<p><strong>npm</strong></p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/badge-07
</code></pre>
<p><strong>Yarn</strong></p>
<pre><code class="language-javascript">yarn dlx shadcn@latest add @shadcn-space/badge-07
</code></pre>
<p><strong>Bun</strong></p>
<pre><code class="language-javascript">bunx --bun shadcn@latest add @shadcn-space/badge-07
</code></pre>
<p><strong>Note:</strong> <code>badge-07</code> uses the <strong>Base UI</strong> primitive version of Badge. Both Radix and Base UI versions are available in the registry. This tutorial covers the Base UI version.</p>
<h2 id="heading-component-structure"><strong>Component Structure</strong></h2>
<p>Here's the complete component. Read through it once, then each step below breaks down a specific part.</p>
<pre><code class="language-javascript">'use client'
import { motion, type Variants } from "motion/react";
import { CheckCircle } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";

const LETTER_VARIANTS: Variants = {
  hidden: { y: -14, opacity: 0 },
  visible: (i: number) =&gt; ({
    y: 0,
    opacity: 1,
    transition: {
      delay: i * 0.038,
      duration: 0.35,
      ease: [0.215, 0.61, 0.355, 1],
    },
  }),
};

const MotionBadge = motion.create(Badge);

const SuccessBadgeDemo = () =&gt; {
  const label = "Success";

  return (
    &lt;MotionBadge
      variant="outline"
      className={cn(
        "relative h-auto cursor-default overflow-visible rounded-full",
        "gap-2 px-3 py-2",
        "bg-background backdrop-blur-md",
        "text-foreground text-sm font-medium leading-none",
        "border-teal-400/25",
      )}
    &gt;
      {/* Top glow */}
      &lt;motion.span
        aria-hidden
        animate={{ opacity: 0.55 }}
        transition={{ duration: 0.45 }}
        className="pointer-events-none absolute -top-2 left-[10%] right-[10%] h-4 blur bg-[radial-gradient(ellipse_80%_100%_at_50%_100%,rgba(45,212,191,0.95)_0%,transparent_70%)]"
      /&gt;
      &lt;motion.span
        aria-hidden
        animate={{ opacity: 0.75 }}
        transition={{ duration: 0.45 }}
        className="pointer-events-none absolute -top-1 left-[22%] right-[22%] h-2 blur-sm bg-[radial-gradient(ellipse_70%_100%_at_50%_100%,rgba(45,212,191,0.85)_0%,transparent_70%)]"
      /&gt;
      &lt;motion.span
        aria-hidden
        animate={{ opacity: 0.9 }}
        transition={{ duration: 0.45 }}
        className="pointer-events-none absolute top-0 left-[28%] right-[28%] h-px bg-[radial-gradient(ellipse_40%_50%_at_50%_50%,rgba(45,212,191,0.95)_0%,transparent_100%)]"
      /&gt;

      {/* Icon */}
      &lt;motion.span
        initial={{ scale: 0.35, opacity: 0, rotate: -25 }}
        animate={{ scale: 1, opacity: 1, rotate: 0 }}
        transition={{ duration: 0.32, ease: [0.175, 0.885, 0.32, 1.275] }}
        className="flex h-4 w-4 shrink-0 items-center justify-center"
      &gt;
        &lt;CheckCircle size={16} strokeWidth={2} className="text-teal-400" /&gt;
      &lt;/motion.span&gt;

      {/* Animated label */}
      &lt;span className="inline-flex overflow-hidden leading-none"&gt;
        {label.split("").map((char, i) =&gt; (
          &lt;motion.span
            key={i}
            custom={i}
            variants={LETTER_VARIANTS}
            initial="hidden"
            animate="visible"
            className="inline-block whitespace-pre"
          &gt;
            {char}
          &lt;/motion.span&gt;
        ))}
      &lt;/span&gt;
    &lt;/MotionBadge&gt;
  );
};

export default SuccessBadgeDemo;
</code></pre>
<p>Now let's break it down piece by piece.</p>
<h2 id="heading-step-1-set-up-the-imports"><strong>Step 1: Set Up the Imports</strong></h2>
<pre><code class="language-javascript">'use client'
import { motion, type Variants } from "motion/react";
import { CheckCircle } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
</code></pre>
<p><code>'use client'</code> marks this as a Client Component in Next.js App Router. Motion animations run in the browser, not on the server, so this directive is required.</p>
<p><code>motion/react</code> is the import path for Motion v11 and above. If your project uses an older version, the import is <code>framer-motion</code>. The <code>Variants</code> type is a TypeScript helper for typing named animation state objects.</p>
<p><code>cn()</code> is the class name utility that ships with every shadcn/ui project. It merges Tailwind classes and handles conditional logic cleanly.</p>
<h2 id="heading-step-2-define-letter-animation-variants"><strong>Step 2: Define Letter Animation Variants</strong></h2>
<pre><code class="language-javascript">const LETTER_VARIANTS: Variants = {
  hidden: { y: -14, opacity: 0 },
  visible: (i: number) =&gt; ({
    y: 0,
    opacity: 1,
    transition: {
      delay: i * 0.038,
      duration: 0.35,
      ease: [0.215, 0.61, 0.355, 1],
    },
  }),
};
</code></pre>
<p>Each letter starts 14px above its final position and is fully transparent. When the component mounts, it moves to <code>y: 0</code> at full opacity.</p>
<p>The <code>delay: i * 0.038</code> formula is the stagger. Letter 0 has no delay, letter 1 waits 38ms, letter 2 waits 76ms, and so on. This makes the letters appear to cascade in from left to right.</p>
<p>The <code>ease</code> value <code>[0.215, 0.61, 0.355, 1]</code> is <code>easeOutCubic</code>. It starts fast and decelerates at the end, giving each letter a natural landing rather than a hard stop.</p>
<p>The <code>visible</code> function accepts a <code>custom</code> value. When you pass <code>custom={i}</code> on the <code>motion.span</code>, Motion calls this function with that index. Each letter calculates its own delay independently.</p>
<p><strong>Accessibility tip:</strong> To respect users with reduced motion preferences, import <code>useReducedMotion</code> from <code>motion/react</code> and skip the stagger when it returns <code>true</code>.</p>
<h2 id="heading-step-3-wrap-the-badge-with-motion"><strong>Step 3: Wrap the Badge with Motion</strong></h2>
<pre><code class="language-javascript">const MotionBadge = motion.create(Badge);
</code></pre>
<p>The <code>Badge</code> Component from shadcn/ui is a standard React component. You can't apply Motion props like <code>animate</code> or <code>initial</code> to it directly.</p>
<p><code>motion.create()</code> wraps any React component and returns a new version that accepts all Motion animation props. The result, <code>MotionBadge</code>, behaves exactly like <code>Badge</code> But it's now fully animatable.</p>
<p>Use this pattern any time you want to animate a custom or third-party library component with Motion.</p>
<h2 id="heading-step-4-build-the-glow-layers"><strong>Step 4: Build the Glow Layers</strong></h2>
<pre><code class="language-javascript">&lt;motion.span
  aria-hidden
  animate={{ opacity: 0.55 }}
  transition={{ duration: 0.45 }}
  className="pointer-events-none absolute -top-2 left-[10%] right-[10%] h-4 blur bg-[radial-gradient(ellipse_80%_100%_at_50%_100%,rgba(45,212,191,0.95)_0%,transparent_70%)]"
/&gt;
&lt;motion.span
  aria-hidden
  animate={{ opacity: 0.75 }}
  transition={{ duration: 0.45 }}
  className="pointer-events-none absolute -top-1 left-[22%] right-[22%] h-2 blur-sm bg-[radial-gradient(ellipse_70%_100%_at_50%_100%,rgba(45,212,191,0.85)_0%,transparent_70%)]"
/&gt;
&lt;motion.span
  aria-hidden
  animate={{ opacity: 0.9 }}
  transition={{ duration: 0.45 }}
  className="pointer-events-none absolute top-0 left-[28%] right-[28%] h-px bg-[radial-gradient(ellipse_40%_50%_at_50%_50%,rgba(45,212,191,0.95)_0%,transparent_100%)]"
/&gt;
</code></pre>
<p>Three spans stack on top of each other above the badge border. Each is narrower and more opaque than the one behind it:</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Position</th>
<th>Width</th>
<th>Blur</th>
<th>Final Opacity</th>
</tr>
</thead>
<tbody><tr>
<td>Outer</td>
<td><code>-top-2</code></td>
<td>80%</td>
<td><code>blur</code></td>
<td>0.55</td>
</tr>
<tr>
<td>Middle</td>
<td><code>-top-1</code></td>
<td>56%</td>
<td><code>blur-sm</code></td>
<td>0.75</td>
</tr>
<tr>
<td>Inner line</td>
<td><code>top-0</code></td>
<td>44%</td>
<td>none</td>
<td>0.90</td>
</tr>
</tbody></table>
<p>The innermost layer is only 1px tall (<code>h-px</code>) with no blur. This gives the glow a crisp, bright edge right at the badge border. The two outer layers create the soft falloff around it.</p>
<p>All three carry <code>aria-hidden</code> because they're purely decorative. Screen readers skip them. The <code>overflow-visible</code> class on <code>MotionBadge</code> is what allows these spans to render outside the component's boundary without clipping.</p>
<h2 id="heading-step-5-animate-the-icon"><strong>Step 5: Animate the Icon</strong></h2>
<pre><code class="language-javascript">&lt;motion.span
  initial={{ scale: 0.35, opacity: 0, rotate: -25 }}
  animate={{ scale: 1, opacity: 1, rotate: 0 }}
  transition={{ duration: 0.32, ease: [0.175, 0.885, 0.32, 1.275] }}
  className="flex h-4 w-4 shrink-0 items-center justify-center"
&gt;
  &lt;CheckCircle size={16} strokeWidth={2} className="text-teal-400" /&gt;
&lt;/motion.span&gt;
</code></pre>
<p>The icon starts at 35% scale, invisible, and rotated 25 degrees counter-clockwise. It animates to full size and zero rotation on mount.</p>
<p>The <code>ease</code> value <code>[0.175, 0.885, 0.32, 1.275]</code> is <code>easeOutBack</code>. Unlike <code>easeOutCubic</code>, this curve overshoots its target slightly before snapping back. The icon appears to spring into place. It is a subtle effect, but it makes the icon feel physical.</p>
<p><code>shrink-0</code> on the wrapper prevents the icon from compressing inside the flex container.</p>
<h2 id="heading-step-6-animate-each-letter"><strong>Step 6: Animate Each Letter</strong></h2>
<pre><code class="language-javascript">&lt;span className="inline-flex overflow-hidden leading-none"&gt;
  {label.split("").map((char, i) =&gt; (
    &lt;motion.span
      key={i}
      custom={i}
      variants={LETTER_VARIANTS}
      initial="hidden"
      animate="visible"
      className="inline-block whitespace-pre"
    &gt;
      {char}
    &lt;/motion.span&gt;
  ))}
&lt;/span&gt;
</code></pre>
<p><code>label.split("")</code> turns <code>"Success"</code> into <code>["S", "u", "c", "c", "e", "s", "s"]</code>. Each character gets its own <code>motion.span</code>.</p>
<p><code>variants={LETTER_VARIANTS}</code> connects each span to the animation states from Step 2. <code>custom={i}</code> passes the character's index into the <code>visible</code> resolver so each letter knows its own delay.</p>
<p>Two Tailwind classes matter here:</p>
<ul>
<li><p><code>overflow-hidden</code> on the wrapper clips, each letter as it slides in from above. Without it, letters would be visible outside the badge before they land.</p>
</li>
<li><p><code>inline-block</code> on each <code>motion.span</code> is required for <code>translateY</code> to work. CSS transforms do not apply to inline elements by default.</p>
</li>
</ul>
<h2 id="heading-how-to-use-it-in-your-app"><strong>How to Use It in Your App</strong></h2>
<p>Import and render <code>SuccessBadgeDemo</code> anywhere in your project:</p>
<pre><code class="language-javascript">// app/page.tsx
import SuccessBadgeDemo from "@/components/shadcn-space/badge/badge-07";

export default function Page() {
  return (
    &lt;div className="flex items-center justify-center min-h-screen"&gt;
      &lt;SuccessBadgeDemo /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>The component is self-contained. It carries its own animation state, theme tokens, and glow layers. No props are required.</p>
<h2 id="heading-how-to-customize-the-component"><strong>How to Customize the Component</strong></h2>
<p>You can change the label by replacing <code>"Success"</code> it with any string. The letter animation applies automatically since it splits whatever string you pass.</p>
<p>To build a complete blue "Verified" variant, you just need to change three things: the border color class, the glow gradient color values, and the icon. Here's the full updated component:</p>
<pre><code class="language-javascript">'use client'
import { motion, type Variants } from "motion/react";
import { ShieldCheck } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";

const LETTER_VARIANTS: Variants = {
  hidden: { y: -14, opacity: 0 },
  visible: (i: number) =&gt; ({
    y: 0,
    opacity: 1,
    transition: {
      delay: i * 0.038,
      duration: 0.35,
      ease: [0.215, 0.61, 0.355, 1],
    },
  }),
};

const MotionBadge = motion.create(Badge);

const VerifiedBadgeDemo = () =&gt; {
  const label = "Verified";

  return (
    &lt;MotionBadge
      variant="outline"
      className={cn(
        "relative h-auto cursor-default overflow-visible rounded-full",
        "gap-2 px-3 py-2",
        "bg-background backdrop-blur-md",
        "text-foreground text-sm font-medium leading-none",
        "border-blue-400/25",
      )}
    &gt;
      &lt;motion.span aria-hidden animate={{ opacity: 0.55 }} transition={{ duration: 0.45 }}
        className="pointer-events-none absolute -top-2 left-[10%] right-[10%] h-4 blur bg-[radial-gradient(ellipse_80%_100%_at_50%_100%,rgba(96,165,250,0.95)_0%,transparent_70%)]"
      /&gt;
      &lt;motion.span aria-hidden animate={{ opacity: 0.75 }} transition={{ duration: 0.45 }}
        className="pointer-events-none absolute -top-1 left-[22%] right-[22%] h-2 blur-sm bg-[radial-gradient(ellipse_70%_100%_at_50%_100%,rgba(96,165,250,0.85)_0%,transparent_70%)]"
      /&gt;
      &lt;motion.span aria-hidden animate={{ opacity: 0.9 }} transition={{ duration: 0.45 }}
        className="pointer-events-none absolute top-0 left-[28%] right-[28%] h-px bg-[radial-gradient(ellipse_40%_50%_at_50%_50%,rgba(96,165,250,0.95)_0%,transparent_100%)]"
      /&gt;

      &lt;motion.span
        initial={{ scale: 0.35, opacity: 0, rotate: -25 }}
        animate={{ scale: 1, opacity: 1, rotate: 0 }}
        transition={{ duration: 0.32, ease: [0.175, 0.885, 0.32, 1.275] }}
        className="flex h-4 w-4 shrink-0 items-center justify-center"
      &gt;
        &lt;ShieldCheck size={16} strokeWidth={2} className="text-blue-400" /&gt;
      &lt;/motion.span&gt;

      &lt;span className="inline-flex overflow-hidden leading-none"&gt;
        {label.split("").map((char, i) =&gt; (
          &lt;motion.span key={i} custom={i} variants={LETTER_VARIANTS}
            initial="hidden" animate="visible" className="inline-block whitespace-pre"
          &gt;
            {char}
          &lt;/motion.span&gt;
        ))}
      &lt;/span&gt;
    &lt;/MotionBadge&gt;
  );
};

export default VerifiedBadgeDemo;
</code></pre>
<p>The only changes from the original: <code>border-blue-400/25</code> on the badge, <code>rgba(96, 165, 250, ...)</code> in the glow gradients (<code>blue-400</code> in Tailwind), <code>ShieldCheck</code> for the icon, and <code>text-blue-400</code> on the icon class.</p>
<p>To adjust stagger speed, just change the delay multiplier in <code>LETTER_VARIANTS</code>:</p>
<pre><code class="language-javascript">delay: i * 0.06, // slower stagger
delay: i * 0.02, // faster stagger
</code></pre>
<p>You can also explore the <a href="https://shadcnspace.com/blocks"><strong>Shadcn Blocks</strong></a> collection to see how animated badges fit into full dashboard and card layouts.</p>
<hr>
<h2 id="heading-live-preview"><strong>Live Preview</strong></h2>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/08db3820-9f72-4ddb-a507-e33cdcda5fb8.gif" alt="08db3820-9f72-4ddb-a507-e33cdcda5fb8" style="display:block;margin:0 auto" width="1152" height="648" loading="lazy">

<h2 id="heading-key-concepts-recap"><strong>Key Concepts Recap</strong></h2>
<table>
<thead>
<tr>
<th>Concept</th>
<th>What It Does</th>
</tr>
</thead>
<tbody><tr>
<td><code>motion.create(Component)</code></td>
<td>Wraps any React component to accept Motion animation props</td>
</tr>
<tr>
<td><code>Variants</code></td>
<td>Named animation states (<code>hidden</code>, <code>visible</code>) defined outside JSX for reuse</td>
</tr>
<tr>
<td><code>custom={i}</code> + variant function</td>
<td>Passes a per-element value into the variant resolver for dynamic transitions</td>
</tr>
<tr>
<td><code>delay: i * 0.038</code></td>
<td>Stagger formula: each element's delay grows by its index</td>
</tr>
<tr>
<td><code>easeOutCubic</code> <code>[0.215, 0.61, 0.355, 1]</code></td>
<td>Fast start, smooth deceleration. Letter drop-in.</td>
</tr>
<tr>
<td><code>easeOutBack</code> <code>[0.175, 0.885, 0.32, 1.275]</code></td>
<td>Overshoots slightly, snaps back. Icon pop.</td>
</tr>
<tr>
<td>Three stacked radial gradients</td>
<td>Wide + soft outer glow, narrow + sharp inner line</td>
</tr>
<tr>
<td><code>overflow-visible</code> on the badge</td>
<td>Allows glow spans to extend outside the component's own bounds</td>
</tr>
</tbody></table>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>In this tutorial, you built a complete animated badge from scratch with a layered glow, bouncing icon, and staggered letter animation. Every part of it uses your existing Shadcn theme tokens, so it drops into any project without extra configuration.</p>
<p>You can browse more <a href="https://shadcnspace.com/components"><strong>Shadcn Components</strong></a> on Shadcn Space to apply the same animation patterns to other UI elements. If you work with external services and tooling in your stack, the <a href="https://shadcnspace.com/mcp"><strong>Shadcn MCP</strong></a> integration is worth looking at as a next step.</p>
<h2 id="heading-resources"><strong>Resources</strong></h2>
<ul>
<li><p><a href="https://shadcnspace.com/components/badge"><strong>Shadcn Space Badge Components</strong></a>: with all badge variants, including Pending, Failed, and more</p>
</li>
<li><p><a href="https://shadcnspace.com/docs/getting-started/how-to-use-shadcn-cli"><strong>Shadcn Space Getting Started Guide</strong></a>: how to use the Shadcn CLI with third-party registries</p>
</li>
<li><p><a href="https://motion.dev/"><strong>Motion Docs</strong></a>: official documentation for <code>motion/react</code></p>
</li>
<li><p><a href="https://lucide.dev/"><strong>Lucide React</strong></a>: icon library used in this tutorial</p>
</li>
<li><p><a href="https://ui.shadcn.com/docs"><strong>Shadcn/ui Documentation</strong></a></p>
</li>
<li><p><a href="https://youtu.be/n6dvjVxy02U?si=EXfClzSyI8D97VaI"><strong>YouTube: Shadcn Space CLI Walkthrough</strong></a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build an Admin Dashboard Sidebar with shadcn/ui and Base UI ]]>
                </title>
                <description>
                    <![CDATA[ Admin dashboards are one of the most common real-world UI components you will build as a React developer. At the heart of nearly every dashboard is a sidebar, a persistent navigation panel that organi ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-an-admin-dashboard-sidebar-with-shadcn-ui-and-base-ui/</link>
                <guid isPermaLink="false">69de6a6491716f3cfb542305</guid>
                
                    <category>
                        <![CDATA[ shadcn ]]>
                    </category>
                
                    <category>
                        <![CDATA[ UI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ baseui ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Tailwind CSS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Vaibhav Gupta ]]>
                </dc:creator>
                <pubDate>Tue, 14 Apr 2026 16:25:08 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/3ce152b1-9a34-4c72-85f0-cabf7d4f3460.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Admin dashboards are one of the most common real-world UI components you will build as a React developer. At the heart of nearly every dashboard is a sidebar, a persistent navigation panel that organizes pages, tools, and features into a clean, scannable structure.</p>
<p>Building a sidebar from scratch involves much more than an <code>&lt;nav&gt;</code> element. You need collapsible submenus, active state tracking across parent and child items, accessible keyboard navigation, a scroll area for long nav lists, and a consistent design system that holds together across screen sizes.</p>
<p>In this tutorial, you'll learn how to build a fully functional, accessible admin dashboard sidebar using shadcn/ui, a collection of beautifully designed, accessible React components, and a pre-built community block from Shadcn Space, which extends shadcn/ui with ready-to-use dashboard UI patterns.</p>
<p>By the end of this tutorial, you'll have a working sidebar that includes:</p>
<ul>
<li><p>Grouped navigation sections with uppercase labels</p>
</li>
<li><p>Collapsible parent menu items with child links</p>
</li>
<li><p>Active state tracking across both parent and child items</p>
</li>
<li><p>A floating sidebar with an independent scroll area</p>
</li>
<li><p>A promotional card pinned inside the sidebar footer</p>
</li>
</ul>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-you-will-build">What You Will Build</a></p>
</li>
<li><p><a href="#heading-why-shadcnui">Why shadcn/ui?</a></p>
</li>
<li><p><a href="#heading-what-is-shadcn-space">What is Shadcn Space?</a></p>
</li>
<li><p><a href="#heading-how-to-install-the-sidebar-block">How to Install the Sidebar Block</a></p>
</li>
<li><p><a href="#heading-how-to-understand-the-folder-structure">How to Understand the Folder Structure</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-page-layout">How to Build the Page Layout</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-appsidebar-component">How to Build the AppSidebar Component</a></p>
</li>
<li><p><a href="#heading-how-to-define-the-navigation-data">How to Define the Navigation Data</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-navmain-component">How to Build the NavMain Component</a></p>
</li>
<li><p><a href="#heading-how-to-handle-active-states-and-collapsible-menus">How to Handle Active States and Collapsible Menus</a></p>
</li>
<li><p><a href="#heading-how-to-style-the-sidebar">How to Style the Sidebar</a></p>
</li>
<li><p><a href="#heading-live-preview">Live Preview</a></p>
</li>
<li><p><a href="#heading-summary">Summary</a></p>
</li>
</ul>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<p>Before you start, make sure you have the following:</p>
<ul>
<li><p>Node.js 18+ installed on your machine</p>
</li>
<li><p>Basic knowledge of React and TypeScript</p>
</li>
<li><p>Familiarity with Tailwind CSS utility classes</p>
</li>
<li><p>A package manager installed (npm, pnpm, yarn, or bun)</p>
</li>
</ul>
<p>You don't need prior experience with shadcn/ui, but it helps to have read through <a href="https://ui.shadcn.com/docs">the official docs</a> at least once.</p>
<h2 id="heading-what-you-will-build"><strong>What You Will Build</strong></h2>
<p>In this article, you'll build a fully functional admin dashboard sidebar with the following features:</p>
<ol>
<li><p><strong>Floating sidebar shell</strong>: a card-style sidebar with rounded corners, a drop shadow, and a configurable width</p>
</li>
<li><p><strong>Grouped navigation</strong>: navigation items organized under section labels like Dashboards, Pages, Apps, and Form Elements</p>
</li>
<li><p><strong>Collapsible submenus</strong>: parent items like Blogs and Shadcn Forms that expand on click to reveal child links</p>
</li>
<li><p><strong>Active state tracking</strong>: visual highlighting of the selected parent and child item at all times</p>
</li>
<li><p><strong>Sidebar toggle</strong>: a trigger button in the page header that opens and closes the sidebar</p>
</li>
<li><p><strong>Promotional card</strong>: a "Get Premium" card at the bottom of the sidebar scroll area</p>
</li>
</ol>
<h2 id="heading-why-shadcnui"><strong>Why shadcn/ui?</strong></h2>
<p><a href="https://ui.shadcn.com/"><strong>shadcn/ui</strong></a> is a collection of beautifully designed, accessible React components built on top of Radix UI and styled with Tailwind CSS.</p>
<p>Instead of installing a traditional component library as a dependency, you copy components directly into your project using a CLI. This gives you full ownership of the code structure and styling. You can read every line, change anything, and the components never break because of a library update you didn't control.</p>
<p>Some key benefits of shadcn/ui include:</p>
<ul>
<li><p>Accessible by default, built on Radix and Base UI primitives</p>
</li>
<li><p>Fully styled with Tailwind CSS utility classes</p>
</li>
<li><p>Zero lock-in: the code lives in your project, not inside <code>node_modules</code></p>
</li>
<li><p>Works with Next.js, React, Astro, Vite, and other frameworks</p>
</li>
<li><p>A growing ecosystem of community-built blocks and registries</p>
</li>
</ul>
<p>The <code>Sidebar, Collapsible, ScrollArea, Card, and Button</code> Components you'll use in this tutorial all come from shadcn/ui.</p>
<h2 id="heading-what-is-shadcn-space"><strong>What is Shadcn Space?</strong></h2>
<p><strong>Shadcn Space</strong> is an open-source library of pre-built <a href="https://shadcnspace.com/blocks"><strong>UI blocks</strong></a> built on top of shadcn/ui. It provides ready-to-use dashboard layouts, sidebars, tables, cards, and other common admin UI patterns so you don't have to assemble them from individual primitives every time.</p>
<p>Each block in Shadcn Space is installable directly into your project using the shadcn CLI. Once installed, the code is yours: you can read it, extend it, and adapt it to your design system without any runtime dependency on Shadcn Space itself.</p>
<p>For this tutorial, you'll use the <code>sidebar-06</code> block (it’s free to use), which is a floating admin sidebar with grouped navigation, collapsible submenus, and an integrated scroll area.</p>
<p>Shadcn Space also provides a companion <a href="https://www.figma.com/community/file/1597967874273587400/shadcn-space-figma-ui-kit"><strong>Figma UI Kit</strong></a> that matches the design system used in the blocks, which is useful if you do design work alongside development.</p>
<p>You can explore the full block library and the getting-started documentation in the <a href="https://shadcnspace.com/docs/getting-started/blocks"><strong>official Shadcn Space docs</strong></a>.</p>
<h3 id="heading-how-to-set-up-the-project">How to Set Up the Project</h3>
<p>Start by creating a new Next.js project if you don't already have one:</p>
<pre><code class="language-javascript">npx shadcn@latest init --preset b0 --base base --template next
</code></pre>
<p>This command:</p>
<ul>
<li><p>Creates a Next.js project</p>
</li>
<li><p>Configures Tailwind CSS</p>
</li>
<li><p>Sets up Base UI as the component foundation</p>
</li>
<li><p>Uses Nova style preset</p>
</li>
<li><p>Configures Lucide icons</p>
</li>
<li><p>Uses Inter font</p>
</li>
<li><p>Applies neutral theme tokens</p>
</li>
</ul>
<p>Follow the prompts to configure your base color, CSS variables, and component output directory. This sets up the <code>components/ui</code> directory and the required Tailwind configuration that all shadcn/ui components depend on.</p>
<p>Once the initialization is complete, your <code>components.json</code> project will be created at the root of your project. This file tells the shadcn CLI where to place components, what path aliases you're using, and which styling configuration to follow.</p>
<p>Add this in <code>components.json</code>:</p>
<pre><code class="language-javascript">{
  "registries": {
    "@shadcn-space": {
      "url": "https://shadcnspace.com/r/{name}.json",
    }
  }
}
</code></pre>
<h2 id="heading-how-to-install-the-sidebar-block"><strong>How to Install the Sidebar Block</strong></h2>
<p>With shadcn/ui initialized, you can now pull in the <code>sidebar-06</code> block from Shadcn Space. While Shadcn Space provides components for both Radix UI and Base UI, this tutorial uses the Base UI version. Run one of the following commands depending on your package manager:</p>
<p><strong>npm:</strong></p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/sidebar-06
</code></pre>
<p><strong>pnpm:</strong></p>
<pre><code class="language-javascript">pnpm dlx shadcn@latest add @shadcn-space/sidebar-06
</code></pre>
<p><strong>yarn:</strong></p>
<pre><code class="language-javascript">yarn dlx shadcn@latest add @shadcn-space/sidebar-06
</code></pre>
<p><strong>bun:</strong></p>
<pre><code class="language-javascript">bunx --bun shadcn@latest add @shadcn-space/sidebar-06
</code></pre>
<p>This command fetches the block from the Shadcn Space registry and scaffolds all the required component files into your project automatically. It also installs any shadcn/ui primitives the block depends on (such as <code>Sidebar, ScrollArea, Card, Button,</code> and <code>Collapsible</code>) if they aren't already present in your components/ui directory.</p>
<p>You can preview the live block and find the installation command on their <a href="https://shadcnspace.com/blocks/dashboard-ui/sidebars"><strong>shadcn sidebar</strong></a> page.</p>
<h2 id="heading-how-to-understand-the-folder-structure"><strong>How to Understand the Folder Structure</strong></h2>
<p>After installation, your project will contain the following new files:</p>
<pre><code class="language-javascript">app/
  sidebar-06/
    page.tsx                  ← Route entry point
assets/
  logo/
    logo.tsx                  ← Logo component
components/
  shadcn-space/
    blocks/
      sidebar-06/
        app-sidebar.tsx       ← Main sidebar shell
        nav-main.tsx          ← Navigation logic and rendering
</code></pre>
<p>Each file has a clearly defined responsibility:</p>
<ul>
<li><p><code>app/sidebar-06/page.tsx</code>: the route entry point that wires the sidebar into a page layout using SidebarProvider</p>
</li>
<li><p><code>assets/logo/logo.tsx</code>: the logo component rendered in the sidebar header</p>
</li>
<li><p><code>components/shadcn-space/blocks/sidebar-06/app-sidebar.tsx</code>: the main sidebar shell, including the header, scroll area, nav data, and promotional card</p>
</li>
<li><p><code>components/shadcn-space/blocks/sidebar-06/nav-main.tsx</code>: all navigation rendering logic, including section labels, leaf items, collapsible parents, and active state management</p>
</li>
</ul>
<p>You'll work through each of these files in detail in the sections below.</p>
<h2 id="heading-how-to-build-the-page-layout"><strong>How to Build the Page Layout</strong></h2>
<p>Open <code>app/sidebar-06/page.tsx</code>. This file is the entry point for your dashboard page. It uses <code>SidebarProvider</code> to establish sidebar context across the page, and <code>SidebarTrigger</code> to render a toggle button inside the header.</p>
<pre><code class="language-javascript">import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { AppSidebar } from "@/components/shadcn-space/blocks/sidebar-06/app-sidebar";

const Page = () =&gt; {
  return (
    &lt;SidebarProvider
      className="p-4 bg-muted"
      style={{ "--sidebar-width": "300px" } as React.CSSProperties}
    &gt;
      &lt;AppSidebar /&gt;

      {/* Main content area */}
      &lt;div className="flex flex-1 flex-col gap-4"&gt;
        &lt;header className="flex h-14 shrink-0 items-center gap-2 rounded-xl bg-background px-4 shadow-sm"&gt;
          &lt;SidebarTrigger className="cursor-pointer" /&gt;
        &lt;/header&gt;
        &lt;main className="flex-1 rounded-xl bg-background" /&gt;
      &lt;/div&gt;
    &lt;/SidebarProvider&gt;
  );
};

export default Page;
</code></pre>
<p>Let's break down the key parts of this layout:</p>
<p><strong>SidebarProvider</strong> wraps everything on the page. It manages the sidebar's open/closed state and passes it down to child components via React context. Any component that needs to read or change the sidebar state, including <code>SidebarTrigger</code> and <code>AppSidebar</code>, must be a descendant of <code>SidebarProvider</code>.</p>
<p><strong>The</strong> <code>--sidebar-width</code> <strong>CSS custom property</strong> controls the rendered width of the sidebar. It's set inline using a type assertion (<code>as React.CSSProperties</code>) because TypeScript doesn't know about this custom property by default. Setting it here rather than in a CSS file keeps the width configurable on a per-page basis.</p>
<p><code>SidebarTrigger</code> is a toggle button component that reads the sidebar open/closed state from the nearest <code>SidebarProvider</code> context and flips it on click. It renders in the header so users always have access to the toggle regardless of scroll position.</p>
<p><code>bg-muted</code> on <code>SidebarProvider</code> creates the light gray outer background that makes the floating sidebar card visually stand out from the page.</p>
<h2 id="heading-how-to-build-the-appsidebar-component"><strong>How to Build the AppSidebar Component</strong></h2>
<p>Open <code>components/shadcn-space/blocks/sidebar-06/app-sidebar.tsx</code>. This component is the main sidebar shell. It composes shadcn/ui's <code>Sidebar</code>, <code>SidebarHeader</code>, and <code>SidebarContent</code> layout primitives and wraps the scrollable navigation area in a <code>ScrollArea</code> component to handle overflow independently.</p>
<pre><code class="language-javascript">"use client";

import {
  Sidebar,
  SidebarContent,
  SidebarHeader,
  SidebarMenu,
  SidebarMenuItem,
} from "@/components/ui/sidebar";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import Logo from "@/assets/logo/logo";
import { NavItem, NavMain } from "@/components/shadcn-space/blocks/sidebar-06/nav-main";
import {
  AlignStartVertical,
  PieChart,
  CircleUserRound,
  ClipboardList,
  Notebook,
  NotepadText,
  Table,
  Languages,
  Ticket,
} from "lucide-react";
</code></pre>
<p>The <code>"use client"</code> directive at the top is required because this component uses React state (through <code>NavMain</code>) and event handlers, both of which require the component to run in the browser rather than being server-rendered by Next.js.</p>
<h2 id="heading-how-to-define-the-navigation-data"><strong>How to Define the Navigation Data</strong></h2>
<p>Inside <code>app-sidebar.tsx</code>the navigation structure is defined as a flat array of <code>NavItem</code> objects. Each item belongs to one of three categories:</p>
<ol>
<li><p><strong>A section label</strong> marked with <code>isSection: true</code> and a <code>label</code> string. Renders as an uppercase group heading.</p>
</li>
<li><p><strong>A leaf item</strong> has a <code>title, icon</code>, and <code>href</code>, but no <code>children</code>. Renders as a direct navigation link.</p>
</li>
<li><p><strong>A parent item</strong> has a <code>title, icon</code>, and a <code>children</code> array of sub-items. Renders as a collapsible trigger.</p>
</li>
</ol>
<pre><code class="language-javascript">
export const navData: NavItem[] = [
  // Dashboards Section
  { label: "Dashboards", isSection: true },
  { title: "Analytics", icon: PieChart, href: "#" },
  { title: "CRM Dashboard", icon: ClipboardList, href: "#" },

  // Pages Section
  { label: "Pages", isSection: true },
  { title: "Tables", icon: Table, href: "#" },
  { title: "Forms", icon: ClipboardList, href: "#" },
  { title: "User Profile", icon: CircleUserRound, href: "#" },

  // Apps Section
  { label: "Apps", isSection: true },
  { title: "Notes", icon: Notebook, href: "#" },
  { title: "Tickets", icon: Ticket, href: "#" },
  {
    title: "Blogs",
    icon: Languages,
    children: [
      { title: "Blog Post", href: "#" },
      { title: "Blog Detail", href: "#" },
      { title: "Blog Edit", href: "#" },
      { title: "Blog Create", href: "#" },
      { title: "Manage Blogs", href: "#" },
    ],
  },

  // Form Elements Section
  { label: "Form Elements", isSection: true },
  {
    title: "Shadcn Forms",
    icon: NotepadText,
    children: [
      { title: "Button", href: "#" },
      { title: "Input", href: "#" },
      { title: "Select", href: "#" },
      { title: "Checkbox", href: "#" },
      { title: "Radio", href: "#" },
    ],
  },
  {
    title: "Form layouts",
    icon: AlignStartVertical,
    children: [
      { title: "Forms Horizontal", href: "#" },
      { title: "Forms Vertical", href: "#" },
      { title: "Forms Validation", href: "#" },
      { title: "Forms Examples", href: "#" },
      { title: "Forms Wizard", href: "#" },
    ],
  },
];
</code></pre>
<p>This flat array approach is intentionally simple to maintain. You don't need a nested tree structure because the <code>NavMain</code> component handles the rendering logic for each item type by inspecting each item's shape. Adding a new section, item, or submenu is as straightforward as appending a new object to the array.</p>
<h2 id="heading-how-to-build-the-navmain-component"><strong>How to Build the NavMain Component</strong></h2>
<p>Open <code>components/shadcn-space/blocks/sidebar-06/nav-main.tsx</code>. This file contains all the navigation rendering logic. Start with the type definition and the top-level <code>NavMain</code> function:</p>
<pre><code class="language-javascript">"use client";

import * as React from "react";
import { ChevronRight, LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import {
  Collapsible,
  CollapsibleTrigger,
  CollapsibleContent,
} from "@/components/ui/collapsible";
import {
  SidebarGroup,
  SidebarGroupLabel,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSub,
  SidebarMenuSubItem,
  SidebarMenuSubButton,
} from "@/components/ui/sidebar";

export type NavItem = {
  label?: string;
  isSection?: boolean;
  title?: string;
  icon?: LucideIcon;
  href?: string;
  children?: NavItem[];
};

export function NavMain({ items }: { items: NavItem[] }) {
  const [activeParent, setActiveParent] = React.useState&lt;string | null&gt;(
    items.find((i) =&gt; !i.isSection)?.title || null
  );
  const [activeChild, setActiveChild] = React.useState&lt;string | null&gt;(null);

  return (
    &lt;&gt;
      {items.map((item, index) =&gt; (
        &lt;NavMainItem
          key={item.title || item.label || index}
          item={item}
          activeParent={activeParent}
          setActiveParent={setActiveParent}
          activeChild={activeChild}
          setActiveChild={setActiveChild}
        /&gt;
      ))}
    &lt;/&gt;
  );
}
</code></pre>
<p><code>activeParent</code> tracks which top-level nav item is currently selected. It initializes to the title of the first non-section item, so the sidebar always has a selection on first render, and you never show the sidebar with nothing highlighted. <code>activeChild</code> tracks which sub-item inside a collapsible menu is selected.</p>
<p>Both state values are passed down as props to each <code>NavMainItem</code>, so every item in the list can read the current selection and trigger updates to it.</p>
<h2 id="heading-how-to-handle-active-states-and-collapsible-menus"><strong>How to Handle Active States and Collapsible Menus</strong></h2>
<p>The <code>NavMainItem</code> function branches into one of three rendering paths based on the shape of the incoming item.</p>
<h3 id="heading-how-to-render-section-labels">How to Render Section Labels</h3>
<pre><code class="language-javascript">if (item.isSection &amp;&amp; item.label) {
  return (
    &lt;SidebarGroup className="p-0 pt-5 first:pt-0"&gt;
      &lt;SidebarGroupLabel className="p-0 text-xs font-medium uppercase text-sidebar-foreground"&gt;
        {item.label}
      &lt;/SidebarGroupLabel&gt;
    &lt;/SidebarGroup&gt;
  );
}
</code></pre>
<p>Section labels use <code>first:pt-0</code> to remove the top padding from the very first section, so the nav starts flush with the header.</p>
<h3 id="heading-how-to-render-collapsible-parent-items">How to Render Collapsible Parent Items</h3>
<pre><code class="language-javascript">if (hasChildren &amp;&amp; item.title) {
  return (
    &lt;SidebarGroup className="p-0"&gt;
      &lt;SidebarMenu&gt;
        &lt;Collapsible open={isOpen} onOpenChange={setIsOpen}&gt;
          &lt;SidebarMenuItem&gt;
            &lt;CollapsibleTrigger
              className="w-full"
              render={
                &lt;SidebarMenuButton
                  id={`nav-main-trigger-${item.title.toLowerCase().replace(/\s+/g, '-')}`}
                  tooltip={item.title}
                  isActive={isParentActive}
                  onClick={() =&gt; setActiveParent(item.title!)}
                  className={cn(
                    "rounded-md text-sm font-medium px-3 py-2 h-9 transition-colors cursor-pointer",
                    isParentActive ? "bg-primary! text-primary-foreground!" : ""
                  )}
                &gt;
                  {item.icon &amp;&amp; &lt;item.icon size={16} /&gt;}
                  &lt;span&gt;{item.title}&lt;/span&gt;
                  &lt;ChevronRight
                    className={cn(
                      "ml-auto transition-transform duration-200",
                      isOpen &amp;&amp; "rotate-90"
                    )}
                  /&gt;
                &lt;/SidebarMenuButton&gt;
              }
            /&gt;
            &lt;CollapsibleContent&gt;
              &lt;SidebarMenuSub className="me-0 pe-0"&gt;
                {item.children!.map((child, index) =&gt; (
                  &lt;NavMainSubItem
                    key={child.title || index}
                    item={child}
                    activeParent={activeParent}
                    setActiveParent={setActiveParent}
                    activeChild={activeChild}
                    setActiveChild={setActiveChild}
                    parentTitle={item.title}
                  /&gt;
                ))}
              &lt;/SidebarMenuSub&gt;
            &lt;/CollapsibleContent&gt;
          &lt;/SidebarMenuItem&gt;
        &lt;/Collapsible&gt;
      &lt;/SidebarMenu&gt;
    &lt;/SidebarGroup&gt;
  );
}
</code></pre>
<p>A <code>useEffect</code> inside <code>NavMainItem</code> syncs the local <code>isOpen</code> state with <code>activeParent</code> so that when a different parent is activated, the previously open collapsible stays open until the user explicitly closes it:</p>
<pre><code class="language-javascript">React.useEffect(() =&gt; {
  if (isParentActive) {
    setIsOpen(true);
  }
}, [isParentActive]);
</code></pre>
<p>The <code>ChevronRight</code> icon rotates 90 degrees when the submenu is open, using a Tailwind transition class:</p>
<pre><code class="language-javascript">&lt;ChevronRight
  className={cn(
    "ml-auto transition-transform duration-200",
    isOpen &amp;&amp; "rotate-90"
  )}
/&gt;
</code></pre>
<h3 id="heading-how-to-render-leaf-items">How to Render Leaf Items</h3>
<pre><code class="language-javascript">if (item.title) {
  return (
    &lt;SidebarGroup className="p-0"&gt;
      &lt;SidebarMenu&gt;
        &lt;SidebarMenuItem&gt;
          &lt;SidebarMenuButton
            id={`nav-main-button-${item.title.toLowerCase().replace(/\s+/g, '-')}`}
            tooltip={item.title}
            isActive={isParentActive}
            onClick={() =&gt; {
              setActiveParent(item.title!);
              setActiveChild(null);
            }}
            className={cn(
              "rounded-md text-sm font-medium px-3 py-2 h-9 transition-colors cursor-pointer",
              isParentActive ? "bg-primary! text-primary-foreground!" : ""
            )}
            render={&lt;a href={item.href} /&gt;}
          &gt;
            {item.icon &amp;&amp; &lt;item.icon /&gt;}
            {item.title}
          &lt;/SidebarMenuButton&gt;
        &lt;/SidebarMenuItem&gt;
      &lt;/SidebarMenu&gt;
    &lt;/SidebarGroup&gt;
  );
}
</code></pre>
<p>The <code>render</code> prop on <code>SidebarMenuButton</code> replaces the default button element with an <code>&lt;a&gt;</code> tag. This preserves correct anchor link semantics and accessibility while keeping the button's visual styling. When a leaf item is clicked, <code>activeChild</code> is reset to <code>null</code> since there is no child to track.</p>
<h3 id="heading-how-to-render-child-items-in-a-submenu">How to Render Child Items in a Submenu</h3>
<p>The <code>NavMainSubItem</code> function handles sub-items inside a collapsible. When a child is clicked, it sets both <code>activeChild</code> to itself and <code>activeParent</code> to its parent's title so the parent item remains visually highlighted:</p>
<pre><code class="language-javascript">if (item.title) {
  return (
    &lt;SidebarMenuSubItem className="w-full"&gt;
      &lt;SidebarMenuSubButton
        id={`nav-sub-button-${item.title.toLowerCase().replace(/\s+/g, '-')}`}
        className={cn(
          "w-full rounded-md transition-colors",
          activeChild === item.title ? "bg-muted! text-foreground!" : ""
        )}
        isActive={activeChild === item.title}
        onClick={() =&gt; {
          setActiveParent(parentTitle || "");
          setActiveChild(item.title!);
        }}
        render={&lt;a href={item.href}&gt;{item.title}&lt;/a&gt;}
      /&gt;
    &lt;/SidebarMenuSubItem&gt;
  );
}
</code></pre>
<p>The child uses a different active style (<code>bg-muted</code> with <code>text-foreground</code>) compared to the parent (<code>bg-primary</code> with <code>text-primary-foreground</code>). This visual distinction makes it easy to see both which section you are in and which specific page is currently active.</p>
<p>Sub-items also support nesting. If a child item itself has a <code>children</code> array, <code>NavMainSubItem</code> renders another <code>Collapsible</code> with a nested <code>SidebarMenuSub</code>, allowing you to build multi-level navigation trees without any changes to the data structure.</p>
<h2 id="heading-how-to-style-the-sidebar"><strong>How to Style the Sidebar</strong></h2>
<p>The full <code>AppSidebar</code> render function puts all of the pieces together:</p>
<pre><code class="language-javascript">export function AppSidebar() {
  return (
    &lt;Sidebar variant="floating" className="p-4 h-full [&amp;_[data-slot=sidebar-inner]]:h-full"&gt;
      &lt;div className="flex flex-col gap-6 overflow-hidden"&gt;

        {/* Header with Logo */}
        &lt;SidebarHeader className="px-4"&gt;
          &lt;SidebarMenu&gt;
            &lt;SidebarMenuItem&gt;
              &lt;a href="#" className="w-full h-full"&gt;
                &lt;Logo /&gt;
              &lt;/a&gt;
            &lt;/SidebarMenuItem&gt;
          &lt;/SidebarMenu&gt;
        &lt;/SidebarHeader&gt;

        {/* Scrollable Navigation Content */}
        &lt;SidebarContent className="overflow-hidden"&gt;
          &lt;ScrollArea className="h-[calc(100vh-100px)]"&gt;
            &lt;div className="px-4"&gt;
              &lt;NavMain items={navData} /&gt;
            &lt;/div&gt;

            {/* Promotional Card */}
            &lt;div className="pt-5 px-4"&gt;
              &lt;Card className="shadow-none ring-0 bg-secondary px-4 py-6"&gt;
                &lt;CardContent className="p-0 flex flex-col gap-3 items-center"&gt;
                  &lt;img
                    src="https://images.shadcnspace.com/assets/backgrounds/download-img.png"
                    alt="sidebar-img"
                    width={74}
                    height={74}
                    className="h-20 w-20"
                  /&gt;
                  &lt;div className="flex flex-col gap-4 items-center"&gt;
                    &lt;div&gt;
                      &lt;p className="text-base font-semibold text-card-foreground text-center"&gt;
                        Grab Pro Now
                      &lt;/p&gt;
                      &lt;p className="text-sm font-regular text-muted-foreground text-center"&gt;
                        Customize your admin
                      &lt;/p&gt;
                    &lt;/div&gt;
                    &lt;Button className="w-fit h-9 px-4 py-2 shadow-none cursor-pointer rounded-xl hover:bg-primary/80"&gt;
                      Get Premium
                    &lt;/Button&gt;
                  &lt;/div&gt;
                &lt;/CardContent&gt;
              &lt;/Card&gt;
            &lt;/div&gt;
          &lt;/ScrollArea&gt;
        &lt;/SidebarContent&gt;

      &lt;/div&gt;
    &lt;/Sidebar&gt;
  );
}
</code></pre>
<p>Let's walk through the key styling decisions:</p>
<p><code>variant="floating"</code> gives the sidebar a card-like appearance with rounded corners and a subtle drop shadow. It visually lifts the sidebar off the background rather than making it flush with the page edge like a standard sidebar would.</p>
<p><code>[&amp;_[data-slot=sidebar-inner]]:h-full</code> is an arbitrary Tailwind variant selector that targets shadcn/ui's internal sidebar slot element. Without this, the sidebar inner container doesn't fill the full available height, which breaks the layout. The <code>data-slot</code> attribute is how shadcn/ui identifies internal sub-elements of compound components.</p>
<p><code>h-[calc(100vh-100px)]</code> on <code>ScrollArea</code> makes the navigation list independently scrollable. The 100px offset accounts for the sidebar header and padding, so the scroll area doesn't overflow the viewport. The rest of the page layout remains static while the nav scrolls.</p>
<p>The <code>bg-secondary</code> card at the bottom of the scroll area is a common admin dashboard pattern, a soft prompt for an upgrade or onboarding action that lives passively in the sidebar without blocking navigation.</p>
<p>For more details on the Sidebar component's API, variants, and configuration options, refer to the official shadcn/ui sidebar docs.</p>
<h2 id="heading-live-preview"><strong>Live Preview</strong></h2>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/f1538441-fa73-4eb0-af91-04f5bf4fab08.png" alt="f1538441-fa73-4eb0-af91-04f5bf4fab08" style="display:block;margin:0 auto" width="1440" height="892" loading="lazy">

<h2 id="heading-summary"><strong>Summary</strong></h2>
<p>Congratulations! You have now built a complete, production-ready admin dashboard sidebar using shadcn/ui and a community block from Shadcn Space.</p>
<p>Here is a recap of everything you covered:</p>
<ul>
<li><p>Setting up a Next.js project with shadcn/ui initialized and a pre-built sidebar block installed from Shadcn Space</p>
</li>
<li><p>Using <code>SidebarProvider</code> and <code>SidebarTrigger</code> to manage the sidebar open/closed state across a page layout through React context</p>
</li>
<li><p>Defining navigation data as a flat array of typed <code>NavItem</code> objects covering section labels, leaf items, and collapsible parent items</p>
</li>
<li><p>Rendering all three item types from a single <code>navData</code> source in the <code>NavMain</code> and <code>NavMainItem</code> components</p>
</li>
<li><p>Tracking <code>activeParent</code> and <code>activeChild</code> state in a single location and passing them as props so every item can read and update the shared selection state</p>
</li>
<li><p>Using <code>Collapsible</code> with a <code>useEffect</code> sync to keep parent items open when they are active, and animate the chevron icon on expand and collapse</p>
</li>
<li><p>Applying the <code>floating</code> variant, an arbitrary Tailwind slot selector, and <code>ScrollArea</code> with a calculated height to produce a polished, production-appropriate sidebar layout</p>
</li>
</ul>
<p>This pattern scales well beyond what you built here. You can extend <code>NavItem</code> with additional fields like badge counts, permission flags, or external link indicators. You can swap in real <code>href</code> values and connect <code>activeParent</code> and <code>activeChild</code> to your router so the selection always reflects the current URL. You can also add more sections to <code>navData</code> without touching any rendering logic.</p>
<p>For a quick checkout, we have used the Shadcn Space free Shadcn dashboard block in this <a href="https://shadcnspace.com/blocks/dashboard-ui/dashboard-shell"><strong>dashboard shell</strong></a>.</p>
<p>If you want to explore more pre-built admin UI blocks, components, and templates built on top of shadcn/ui, you can browse the full library at <a href="https://shadcnspace.com/"><strong>Shadcn Space</strong></a>.</p>
<h3 id="heading-resources"><strong>Resources</strong></h3>
<ul>
<li><p><a href="https://shadcnspace.com/blocks"><strong>Shadcn UI Blocks</strong></a></p>
</li>
<li><p><a href="https://shadcnspace.com/components"><strong>Shadcn UI Components</strong></a></p>
</li>
<li><p><a href="https://shadcnspace.com/docs/getting-started/blocks"><strong>Shadcn Space Getting Started Docs</strong></a></p>
</li>
<li><p><a href="https://www.figma.com/community/file/1597967874273587400/shadcn-space-figma-ui-kit"><strong>Figma UI Kit Design System</strong></a></p>
</li>
<li><p><a href="https://ui.shadcn.com/docs/components/sidebar"><strong>shadcn/ui Sidebar Docs</strong></a></p>
</li>
<li><p><a href="https://base-ui.com/"><strong>Base UI</strong></a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build an Animated Shadcn Tab Component with Shadcn/ui ]]>
                </title>
                <description>
                    <![CDATA[ Tab components are everywhere: dashboards, settings panels, product pages. But most implementations are static, lifeless, and forgettable. What if your tabs felt alive, with smooth spring animations,  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-an-animated-shadcn-tab-component-with-shadcn-ui/</link>
                <guid isPermaLink="false">69ca85f69fffa747403074fe</guid>
                
                    <category>
                        <![CDATA[ UI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ shadcn ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Vaibhav Gupta ]]>
                </dc:creator>
                <pubDate>Mon, 30 Mar 2026 14:17:26 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/961a288f-30b9-4085-a1fc-7da13ffce38f.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Tab components are everywhere: dashboards, settings panels, product pages. But most implementations are static, lifeless, and forgettable. What if your tabs felt alive, with smooth spring animations, a stacked card effect on hover, and a polished active indicator that glides between buttons?</p>
<p>A basic tab switcher can show and hide content. A better one gives users a clear active state, smooth transitions, and a little bit of motion that makes the interface feel alive. That's the idea behind this component: a reusable animated tab system built in the Shadcn style, with React, Tailwind CSS, and Motion.</p>
<p>In this tutorial, you’ll build exactly that: a fully animated tab component built by Shadcn/ui, Framer Motion, and a ready-to-use registry component from Shadcn Space.</p>
<p>By the end, you’ll have a reusable <code>&lt;Tabs/&gt;</code> component with:</p>
<ul>
<li><p>A spring-animated active pill indicator</p>
</li>
<li><p>A stacked card effect that fans out on hover</p>
</li>
<li><p>A smooth entrance animation when the active tab changes</p>
</li>
<li><p>Fully theme-aware styling using Shadcn/ui CSS variables</p>
</li>
</ul>
<p><strong>Video walkthrough</strong>: If you prefer to follow along visually, watch the full tutorial on YouTube:</p>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/n6dvjVxy02U" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>

<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-youll-build">What You’ll Build</a></p>
</li>
<li><p><a href="#heading-install-the-component-via-shadcn-space-cli">Install the Component via Shadcn Space CLI</a></p>
</li>
<li><p><a href="#heading-understand-the-component-structure">Understand the Component Structure</a></p>
</li>
<li><p><a href="#heading-step-1-define-the-tab-data-types">Step 1 - Define the Tab Data Types</a></p>
</li>
<li><p><a href="#heading-step-2-build-the-tab-data-array">Step 2 - Build the Tab Data Array</a></p>
</li>
<li><p><a href="#heading-step-3-build-the-tabs-component-tab-bar-state">Step 3 - Build the Tabs Component (Tab Bar + State)</a></p>
</li>
<li><p><a href="#heading-step-4-build-the-fadeinstack-component">Step 4 - Build the FadeInStack Component</a></p>
</li>
<li><p><a href="#heading-step-5-compose-the-page-component">Step 5 - Compose the Page Component</a></p>
</li>
<li><p><a href="#heading-step-6-customize-the-component">Step 6 - Customize the Component</a></p>
</li>
<li><p><a href="#heading-live-preview">Live Preview</a></p>
</li>
<li><p><a href="#heading-key-concepts-recap">Key Concepts Recap</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-resources">Resources</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you begin, make sure you have a working knowledge of:</p>
<ul>
<li><p>React and TypeScript basics</p>
</li>
<li><p>Tailwind CSS utility classes</p>
</li>
<li><p>The basics of Shadcn/ui (component installation and theming)</p>
</li>
</ul>
<p>You’ll also need a Next.js or Vite project with the following already set up:</p>
<ul>
<li><p>Shadcn/ui installed and initialized</p>
</li>
<li><p>Framer Motion (also referred to as motion/react) installed</p>
</li>
</ul>
<h2 id="heading-what-youll-build">What You’ll Build</h2>
<p>Here’s an overview of the component architecture you’ll create in this tutorial:</p>
<pre><code class="language-typescript">AnimatedTabMotion (page/demo entry point)
└── Tabs (tab bar + content orchestrator)
├── Tab buttons (with spring-animated active pill)
└── FadeInStack (stacked, animated content panels)
</code></pre>
<p>The key behaviors are:</p>
<ol>
<li><p><strong>Spring pill animation</strong> – A spring pill animation is a UI effect in which the active tab indicator, a rounded, pill-shaped highlight, physically moves from one button to another using a spring physics curve rather than a standard CSS transition. Instead of teleporting or fading, the pill slides between tabs with a subtle bounce at the end, mimicking the momentum of a real physical object.</p>
</li>
<li><p><strong>Stacked card effect</strong> – inactive tab panels are rendered behind the active one, scaled down and slightly faded, giving a layered depth illusion.</p>
</li>
<li><p><strong>Fan-out on hover</strong> – when the user hovers over the content area, the stacked cards spread out vertically.</p>
</li>
<li><p><strong>Bounce entrance</strong> – the top (active) card animates downward and back into place when a new tab is selected.</p>
</li>
</ol>
<h2 id="heading-install-the-component-via-shadcn-space-cli">Install the Component via Shadcn Space CLI</h2>
<p>Shadcn Space is a registry of production-ready Shadcn/ui-compatible components. Instead of scaffolding this component from scratch, you can pull it directly into your project using the Shadcn CLI.</p>
<p>Check out their <a href="https://shadcnspace.com/docs/getting-started/how-to-use-shadcn-cli">Getting Started guide</a> to learn how to use the Shadcn CLI with third-party registries.</p>
<p>Run <strong>one</strong> of the following commands, depending on your package manager:</p>
<p><strong>pnpm</strong></p>
<pre><code class="language-typescript">pnpm dlx shadcn@latest add @shadcn-space/tabs-01
</code></pre>
<p><strong>npm</strong></p>
<pre><code class="language-typescript">npx shadcn@latest add @shadcn-space/tabs-01
</code></pre>
<p><strong>Yarn</strong></p>
<pre><code class="language-typescript">yarn dlx shadcn@latest add @shadcn-space/tabs-01
</code></pre>
<p><strong>Bun</strong></p>
<pre><code class="language-typescript">bunx --bun shadcn@latest add @shadcn-space/tabs-01
</code></pre>
<p>This scaffolds the component file into your project, pre-wired to your existing Shadcn/ui theme tokens. You can then customize or extend it as needed, which is exactly what you’ll learn in this tutorial.</p>
<h2 id="heading-understand-the-component-structure">Understand the Component Structure</h2>
<p>Before writing any code, let’s review the full component and break it into logical pieces. Here is the complete implementation:</p>
<pre><code class="language-typescript">"use client";

import { useState } from "react";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";

type Tab = {
  title: string;
  value: string;
  content?: React.ReactNode;
};

type TabsProps = {
  tabs: Tab[];
    containerClassName?: string;
  activeTabClassName?: string;
  tabClassName?: string;
  contentClassName?: string;
};

const tabs = [
  {
    title: "Product",
    value: "product",
    content: (
      &lt;div className="w-full overflow-hidden relative rounded-2xl p-10 text-xl md:text-4xl font-bold text-foreground bg-muted h-[300px] border border-border"&gt;
        &lt;p&gt;Product Tab&lt;/p&gt;
      &lt;/div&gt;
    ),
  },
  {title: "Services",
    value: "services",
    content: (
      &lt;div className="w-full overflow-hidden relative rounded-2xl p-10 text-xl md:text-4xl font-bold text-foreground bg-muted h-[300px] border border-border"&gt;
        &lt;p&gt;Services tab&lt;/p&gt;
      &lt;/div&gt;
    ),
  },
  {
    title: "Playground",
    value: "playground",
    content: (
      &lt;div className="w-full overflow-hidden relative rounded-2xl p-10 text-xl md:text-4xl font-bold text-foreground bg-muted h-[300px] border border-border"&gt;
        &lt;p&gt;Playground tab&lt;/p&gt;
      &lt;/div&gt;
    ),
  },
 {
    title: "Content",
    value: "content",
    content: (
      &lt;div className="w-full overflow-hidden relative rounded-2xl p-10 text-xl md:text-4xl font-bold text-foreground bg-muted h-[300px] border border-border"&gt;
        &lt;p&gt;Content tab&lt;/p&gt;
      &lt;/div&gt;
    ),
  },
  {
    title: "Random",
    value: "random",
    content: (
      &lt;div className="w-full overflow-hidden relative rounded-2xl p-10 text-xl md:text-4xl font-bold text-foreground bg-muted h-[300px] border border-border"&gt;
        &lt;p&gt;Random tab&lt;/p&gt;
      &lt;/div&gt;
    ),
  },
];

const Tabs = ({
  tabs,
  containerClassName,
  activeTabClassName,
  tabClassName,
  contentClassName,
}: TabsProps) =&gt; {
  const [activeIdx, setActiveIdx] = useState(0);
  const [hovering, setHovering] = useState(false);

  const handleSelect = (idx: number) =&gt; {
    setActiveIdx(idx);
  };
const reorderedTabs = [
    tabs[activeIdx],
    ...tabs.filter((_, i) =&gt; i !== activeIdx),
  ];

  return (
    &lt;&gt;
      &lt;div
        className={cn(
          "flex flex-row items-center justify-start [perspective:1000px] relative overflow-auto sm:overflow-visible no-visible-scrollbar max-w-full w-full",
          containerClassName,
        )}
      &gt;
        {tabs.map((tab, idx) =&gt; {
          const isActive = idx === activeIdx;
          return (
            &lt;button
            key={tab.value}
              onClick={() =&gt; handleSelect(idx)}
              onMouseEnter={() =&gt; setHovering(true)}
              onMouseLeave={() =&gt; setHovering(false)}
              className={cn("relative px-4 py-2 rounded-full", tabClassName)}
              style={{ transformStyle: "preserve-3d" }}
            &gt;
              {isActive &amp;&amp; (
                &lt;motion.div
                  layoutId="clickedbutton"
                  transition={{ type: "spring", bounce: 0.3, duration: 0.6 }}
                  className={cn(
                    "absolute inset-0 bg-primary rounded-full",
                    activeTabClassName,
                  )}
                /&gt;
              )}
&lt;span
                className={cn(
                  "relative block text-sm",
                  isActive ? "text-background": "text-foreground",
                )}
              &gt;
                {tab.title}
              &lt;/span&gt;
            &lt;/button&gt;
          );
        })}
      &lt;/div&gt;
      &lt;FadeInStack
        tabs={reorderedTabs}
        hovering={hovering}
        className={cn("mt-10", contentClassName)}
      /&gt;
    &lt;/&gt;
  );
};

type FadeInStackProps = {
  className?: string;
  tabs: Tab[];
  hovering?: boolean;
};

const FadeInStack = ({ className, tabs, hovering }: FadeInStackProps) =&gt; {
  return (
    &lt;div className="relative w-full h-[300px]"&gt;
      {tabs.map((tab, idx) =&gt; (
        &lt;motion.div
          key={tab.value}
          layoutId={tab.value}
          style={{
            scale: 1 - idx * 0.1,
            top: hovering ? idx * -15 : 0,
            zIndex: -idx,
            opacity: idx &lt; 3 ? 1 - idx * 0.1 : 0,
          }}
          animate={{
            y: idx === 0 ? [0, 40, 0] : 0,
          }}
          className={cn("w-full h-full absolute top-0 left-0", className)}
        &gt;
          {tab.content}
        &lt;/motion.div&gt;
      ))}
    &lt;/div&gt;
  );
};

export default function AnimatedTabMotion() {
  return (
    &lt;&gt;
      &lt;div className="[perspective:1000px] relative flex flex-col max-w-5xl mx-auto w-full items-start justify-start mb-13"&gt;
        &lt;Tabs tabs={tabs} /&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );

}
</code></pre>
<p>Now, let’s break this down piece by piece.</p>
<h2 id="heading-step-1-define-the-tab-data-types">Step 1: Define the Tab Data Types</h2>
<pre><code class="language-typescript">type Tab = {
title: string;
value: string;
content?: React.ReactNode;
};
type TabsProps = {
tabs: Tab[];
containerClassName?: string;
activeTabClassName?: string;
tabClassName?: string;
contentClassName?: string;
};
</code></pre>
<p>The <code>Tab</code> type defines the shape of each tab item:</p>
<ul>
<li><p><code>title</code> – the label rendered in the tab button.</p>
</li>
<li><p><code>value</code> – a unique key used to identify each tab (and as the Framer Motion <code>layoutId</code>).</p>
</li>
<li><p><code>content</code> – an optional <code>React.ReactNode</code>, meaning you can pass any JSX as the panel body.</p>
</li>
</ul>
<p>The <code>TabsProps</code> type makes the <code>Tabs</code> component highly composable. Every visual layer has an override <code>className</code>, so you can restyle the active pill, individual tab buttons, and the content area independently without touching the core logic.</p>
<h2 id="heading-step-2-build-the-tab-data-array">Step 2: Build the Tab Data Array</h2>
<pre><code class="language-typescript">const tabs = [
{
title: “Product”,
value: “product”,
content: (

Product Tab

), }, // ... more tabs ];
</code></pre>
<p>Each tab’s <code>content</code> is a JSX element styled with Shadcn/ui semantic tokens like <code>bg-muted</code>, <code>text-foreground</code> and <code>border-border</code>. This is intentional: these tokens automatically adapt to your light/dark theme without any extra configuration.</p>
<p>You can replace these placeholder <code>&lt;div&gt;</code> panels with any real content: charts, forms, tables, media, whatever your use case demands.</p>
<h2 id="heading-step-3-build-the-tabs-component-tab-bar-state">Step 3: Build the Tabs Component (Tab Bar + State)</h2>
<pre><code class="language-typescript">const [activeIdx, setActiveIdx] = useState(0);
const [hovering, setHovering] = useState(false);
</code></pre>
<p>Two pieces of state drive the entire component:</p>
<ul>
<li><p><code>activeIdx</code> tracks which tab is currently selected (by array index).</p>
</li>
<li><p><code>hovering</code> tracks whether the user’s cursor is over any tab button, which is passed to <code>FadeInStack</code> to trigger the fan-out effect.</p>
</li>
</ul>
<h3 id="heading-reorder-tabs-for-the-stack-effect">Reorder Tabs for the Stack Effect</h3>
<pre><code class="language-typescript">const reorderedTabs = [
tabs[activeIdx],
…tabs.filter((_, i) =&gt; i !== activeIdx),
];
</code></pre>
<p>This is one of the most clever aspects of the architecture. Instead of showing only the active tab’s content, you <strong>always render all tab panels</strong> – but you put the active one first in the array. This is what enables the stacked-cards visual:</p>
<ul>
<li><p>Index 0 = the active panel, rendered on top with full scale and opacity.</p>
</li>
<li><p>Index 1, 2 = the next panels, stacked behind with reduced scale and opacity.</p>
</li>
<li><p>Index 3+ = hidden (opacity 0).</p>
</li>
</ul>
<h3 id="heading-render-the-tab-buttons-with-a-spring-pill">Render the Tab Buttons with a Spring Pill</h3>
<pre><code class="language-typescript">{tabs.map((tab, idx) =&gt; {
const isActive = idx === activeIdx;
return (
   &lt;button
    key={tab.value}
    onClick={() =&gt; handleSelect(idx)}
    onMouseEnter={() =&gt; setHovering(true)}
    onMouseLeave={() =&gt; setHovering(false)}
    className={cn(“relative px-4 py-2 rounded-full”, tabClassName)}
    style={{ transformStyle: “preserve-3d” }}
    &gt;
    {isActive &amp;&amp; (
       &lt;motion.div
        layoutId=“clickedbutton”
        transition={{ type: “spring”, bounce: 0.3, duration: 0.6 }}
        className={cn(
        “absolute inset-0 bg-primary rounded-full”,
        activeTabClassName,
     )}
   /&gt;
)}
&lt;span
    className={cn(
        “relative block text-sm”,
        isActive ? “text-background” : “text-foreground”,
    )}
    &gt;
      {tab.title}
     &lt;/span&gt;
  &lt;/button&gt;
);
})}
</code></pre>
<p>The magic here is <code>layoutId=“clickedbutton”</code> on the <code>motion.div</code>. When only one element with a given <code>layoutId</code> is mounted at a time, Framer Motion tracks its position in the DOM. When it unmounts from one button and mounts onto another, Framer Motion <code>automatically animates the transition</code> is between the two DOM positions. This creates the sliding pill effect with zero manual calculation.</p>
<p>The transition config uses a spring with <code>bounce: 0.3</code> a <code>duration: 0.6</code>, giving it a natural, slightly elastic feel rather than a mechanical linear slide.</p>
<p>The <code>transformStyle: “preserve-3d”</code> on the button enables 3D CSS transforms, which pair with the <code>[perspective:1000px]</code> on the container for a subtle depth effect.</p>
<h2 id="heading-step-4-build-the-fadeinstack-component">Step 4: Build the FadeInStack Component</h2>
<pre><code class="language-typescript">const FadeInStack = ({ className, tabs, hovering }: FadeInStackProps) =&gt; {
  return (
    &lt;div className="relative w-full h-[300px]"&gt;
      {tabs.map((tab, idx) =&gt; (
        &lt;motion.div
          key={tab.value}
          layoutId={tab.value}
          style={{
            scale: 1 - idx * 0.1,
            top: hovering ? idx * -15 : 0,
            zIndex: -idx,
            opacity: idx &lt; 3 ? 1 - idx * 0.1 : 0,
          }}
          animate={{
            y: idx === 0 ? [0, 40, 0] : 0,
          }}
          className={cn("w-full h-full absolute top-0 left-0", className)}
        &gt;
          {tab.content}
        &lt;/motion.div&gt;
      ))}
    &lt;/div&gt;
  );
};
</code></pre>
<p>Let’s unpack the visual logic for each <code>motion.div</code>:</p>
<h3 id="heading-scale-1-idx-01"><code>scale: 1 - idx * 0.1</code></h3>
<p>Each card behind the active one is scaled down by 10% per layer. So:</p>
<ul>
<li><p>Active card (idx 0): <code>scale: 1.0</code></p>
</li>
<li><p>Second card (idx 1): <code>scale: 0.9</code></p>
</li>
<li><p>Third card (idx 2): <code>scale: 0.8</code></p>
</li>
</ul>
<p>This creates clear depth separation between the stacked layers.</p>
<h3 id="heading-top-hovering-idx-15-0"><code>top: hovering ? idx * -15 : 0</code></h3>
<p>When <code>hovering</code> is <code>true</code>, each card shifts upward by <code>idx * 15px</code><em>. The active card doesn’t move</em> <code>(idx 15 = 0)</code>, but the cards behind it fan out at -15px, -30px, and so on. This gives a satisfying “deck spreading” effect on hover.</p>
<h3 id="heading-zindex-idx"><code>zIndex: -idx</code></h3>
<p>Negative z-index stacks cards in order: the active card sits on top (z-index 0), while subsequent cards descend further behind.</p>
<h3 id="heading-opacity-idx-lt-3-1-idx-01-0"><code>opacity: idx &lt; 3 ? 1 - idx * 0.1 : 0</code></h3>
<p>Cards at index 3 and beyond are hidden entirely. The first three cards fade progressively: 1.0, 0.9, 0.8.</p>
<h3 id="heading-animate-y-idx-0-0-40-0-0"><code>animate={{ y: idx === 0 ? [0, 40, 0] : 0 }}</code></h3>
<p>Only the active card (idx 0) gets this keyframe animation. When a tab is selected, and the <code>reorderedTabs</code> array is rebuilt, the new active card enters via a downward dip (<code>y: 40</code>) and bounces back to its rest position. This is a quick, tactile confirmation that the tab has changed.</p>
<h3 id="heading-layoutidtabvalue"><code>layoutId={tab.value}</code></h3>
<p>Each card also has a <code>layoutId</code> matching one <code>value</code>. When <code>reorderedTabs</code> is recomputed, and array positions shift, Framer Motion can track each card’s identity and animate it smoothly between positions, preventing jarring jumps.</p>
<h2 id="heading-step-5-compose-the-page-component">Step 5: Compose the Page Component</h2>
<pre><code class="language-typescript">export default function AnimatedTabMotion() {
  return (
    &lt;div className="[perspective:1000px] relative flex flex-col max-w-5xl mx-auto w-full items-start justify-start mb-13"&gt;
      &lt;Tabs tabs={tabs} /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>The outer wrapper applies <code>[perspective:1000px]</code> – a Tailwind arbitrary property that sets the CSS <code>perspective</code> value. This is what gives the 3D depth to the <code>transformStyle: “preserve-3d”</code> on the tab buttons.</p>
<p>The <code>max-w-5xl</code> and <code>mx-auto</code> center the component on wide screens while <code>items-start</code> left-aligns the tab bar, which matches most real-world UI patterns.</p>
<h2 id="heading-step-6-customize-the-component">Step 6: Customize the Component</h2>
<p>Because <code>Tabs</code> accepts class-name overrides for every visual layer, so you can fully restyle the component to match your design system. Here’s an example with a darker active pill and a tighter layout:</p>
<pre><code class="language-typescript">&lt;Tabs
  tabs={tabs}
  containerClassName="gap-1"
  tabClassName="text-xs px-3 py-1.5"
  activeTabClassName="bg-zinc-900 dark:bg-white"
  contentClassName="mt-6"
/&gt;
</code></pre>
<p>You can also replace the placeholder content panels with real content. Here’s an example using a card with a real description:</p>
<pre><code class="language-typescript">const tabs = [
  {
    title: "Overview",
    value: "overview",
    content: (
      &lt;div className="w-full rounded-2xl p-8 bg-muted border border-border h-[300px] flex flex-col gap-4"&gt;
        &lt;h2 className="text-2xl font-bold text-foreground"&gt;Product Overview&lt;/h2&gt;
        &lt;p className="text-muted-foreground text-sm leading-relaxed"&gt;
          Our platform helps teams ship faster with a fully integrated design-to-code workflow.
        &lt;/p&gt;
      &lt;/div&gt;
    ),
  },
  // ...
];
</code></pre>
<h2 id="heading-live-preview">Live Preview</h2>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/af4a2ba6-dd70-4e77-8c38-7d390060db0d.gif" alt="af4a2ba6-dd70-4e77-8c38-7d390060db0d" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h2 id="heading-key-concepts-recap">Key Concepts Recap</h2>
<p>Here’s a summary of the core Framer Motion techniques used in this component:</p>
<table style="min-width:415px"><colgroup><col style="min-width:25px"><col style="width:390px"></colgroup><tbody><tr><td><p><strong>Technique</strong></p></td><td><p><strong>What it does</strong></p></td></tr><tr><td><p><code>layoutId</code> on <code>motion.div</code></p></td><td><p>Animates a shared element between DOM positions (the sliding pill)</p></td></tr><tr><td><p><code>layoutId</code> on <code>motion.div</code> per tab</p></td><td><p>Tracks card identity during re-ordering, so Framer Motion animates position changes</p></td></tr><tr><td><p><code>animate={{ y: [0, 40, 0] }}</code></p></td><td><p>Keyframe animation for the bounce entrance on tab change</p></td></tr><tr><td><p><code>style={{ scale, top, zIndex, opacity }}</code></p></td><td><p>Inline reactive styles that create the stacked-card depth effect</p></td></tr><tr><td><p><code>transition={{ type: "spring" }}</code></p></td><td><p>Applies a physics-based spring curve instead of a CSS easing function</p></td></tr></tbody></table>

<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you built a fully animated, theme-aware tab component using Shadcn/ui and Framer Motion. You learned how to:</p>
<ul>
<li><p>Use <code>layoutId</code> to create a spring-animated sliding pill indicator</p>
</li>
<li><p>Render all tab panels simultaneously and reorder them to create a stacked card effect</p>
</li>
<li><p>Drive hover and depth effects with inline reactive <code>style</code> props</p>
</li>
<li><p>Apply Framer Motion keyframe animations for a tactile bounce entrance</p>
</li>
<li><p>Keep the component fully customizable via class name overrides</p>
</li>
</ul>
<p>This pattern, combining Shadcn/ui’s semantic design tokens with Framer Motion’s layout animations, scales well beyond tabs. You can apply the same <code>layoutId</code> and stack reorder technique to carousels, image galleries, notification toasts, and more.</p>
<p>You can explore the full component and more animated UI blocks at Shadcn Space, where the CLI command makes it trivial to drop production-quality components directly into your project.</p>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><a href="https://shadcnspace.com/components/tabs">Shadcn Space Tabs Component</a></p>
</li>
<li><p><a href="https://shadcnspace.com/docs/getting-started/how-to-use-shadcn-cli">Shadcn Space Getting Started Guide</a></p>
</li>
<li><p><a href="https://motion.dev/">Framer Motion Documentation</a></p>
</li>
<li><p><a href="https://ui.shadcn.com/">Shadcn/ui Documentation</a></p>
</li>
<li><p><a href="https://youtu.be/n6dvjVxy02U?si=pDpi2vC8oBjZlVsF">Video Tutorial on YouTube</a></p>
</li>
</ul>
<p>I wrote this article with the help of Mihir Koshti (Sr. Full Stack Developer) – <a href="https://www.linkedin.com/in/mihir-koshti/">Connect on LinkedIn</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build an Admin Dashboard with shadcn/ui and TanStack Start ]]>
                </title>
                <description>
                    <![CDATA[ In this guide, we’ll build a feature-rich admin dashboard using shadcn/ui for beautiful, reusable components and TanStack Start for a powerful, type-safe full-stack framework. By the end, you’ll have: A fully functional /dashboard layout A statisti... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-an-admin-dashboard-with-shadcnui-and-tanstack-start/</link>
                <guid isPermaLink="false">6931bd617fcd342128f08ed6</guid>
                
                    <category>
                        <![CDATA[ shadcn ]]>
                    </category>
                
                    <category>
                        <![CDATA[ shadcnui ]]>
                    </category>
                
                    <category>
                        <![CDATA[ shadcn ui ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tanstack-start ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tanstack ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ajay Patel ]]>
                </dc:creator>
                <pubDate>Thu, 04 Dec 2025 16:57:05 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1764780775287/b8cb826d-ac42-497c-8bb9-b9ffe797df83.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this guide, we’ll build a feature-rich admin dashboard using shadcn/ui for beautiful, reusable components and TanStack Start for a powerful, type-safe full-stack framework.</p>
<p>By the end, you’ll have:</p>
<ul>
<li><p>A fully functional <code>/dashboard</code> layout</p>
</li>
<li><p>A statistics-rich dashboard home page with charts and tables</p>
</li>
<li><p>A Products page using TanStack Query and TanStack Table</p>
</li>
<li><p>A Settings page with profile and notification controls</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764155564957/eda17d57-3f13-4526-be89-be55ec27453c.png" alt="TanStack Start dashboard" class="image--center mx-auto" width="1905" height="1050" loading="lazy"></p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-why-tanstack-start">Why TanStack Start?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-shadcnui">Why shadcn/ui?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-build-the-admin-dashboard-using-shadcnui-and-tanstack-start">How to Build the Admin Dashboard Using shadcn/ui and TanStack Start</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-create-a-new-tanstack-app">1. Create a new TanStack app</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-initial-cleanup">2. Initial Cleanup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-setting-up-shadcnstudio-blocks">3. Setting Up shadcn/studio Blocks</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-4-routing-structure-for-the-dashboard">4. Routing Structure for the Dashboard</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-5-creating-the-dashboard-layout">5. Creating the /dashboard Layout</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-6-building-the-dashboard-home-page">6. Building the Dashboard Home Page</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-7-set-up-the-products-page">7. Set up the Products Page.</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-8-settings-page">8. Settings Page</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-live-demo-amp-source-code">Live Demo &amp; Source Code</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-summary">Summary</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-whats-next">What’s Next?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-resources">Resources:</a></p>
</li>
</ul>
</li>
</ol>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>Before we start the guide, let’s understand the basic requirements of the project:</p>
<ul>
<li><p>Node.js 18+ installed</p>
</li>
<li><p>Basic knowledge of React and TypeScript</p>
</li>
<li><p>Familiarity with TailwindCSS</p>
</li>
</ul>
<h3 id="heading-what-we-will-build">What we will build</h3>
<p>In this article, we’ll build a fully functional admin dashboard with three main sections:</p>
<ol>
<li><p><strong>Dashboard overview</strong>: A home page that displays various charts showing sales metrics, product insights widgets, and a transaction history table.</p>
</li>
<li><p><strong>Products:</strong> A product page that demonstrates data fetching, server-side pagination, and advanced table features like column searching, sorting, and column filtering.</p>
</li>
<li><p><strong>Settings:</strong> A user-friendly settings page with profile management and notification preferences.</p>
</li>
</ol>
<p>The dashboard will include a responsive sidebar navigation, breadcrumb trails, a user profile dropdown, and a language selector.</p>
<h2 id="heading-why-tanstack-start">Why TanStack Start?</h2>
<p><a target="_blank" href="https://tanstack.com/start/latest">TanStack Start</a> is a modern full-stack React framework built on top of TanStack Router. It aims to be a flexible, type-safe alternative to traditional meta-frameworks like Next.js.</p>
<p>Some key benefits of TanStack Start include:</p>
<ul>
<li><p>Type-safe routing and data loading</p>
</li>
<li><p>Server-side rendering (SSR) out of the box</p>
</li>
<li><p>Built on TanStack Router, with file-based routing</p>
</li>
<li><p>Great DX with TypeScript and TanStack Query integration</p>
</li>
</ul>
<p>We’ll pair it with shadcn/ui to quickly build a polished admin dashboard.</p>
<h2 id="heading-why-shadcnui">Why shadcn/ui?</h2>
<p><a target="_blank" href="https://ui.shadcn.com/">shadcn/ui</a> is a collection of beautifully designed, accessible React components built on top of Radix UI and styled with Tailwind CSS.</p>
<p>Instead of installing a package, you can copy and paste the component's code directly into your project or use a CLI to generate it. This gives you full control over the code structure &amp; styling. This approach makes Shadcn highly customizable for frameworks like TanStack Start, Next.js, Astro, and so on.</p>
<h2 id="heading-how-to-build-the-admin-dashboard-using-shadcnui-and-tanstack-start">How to Build the Admin Dashboard Using shadcn/ui and TanStack Start</h2>
<h3 id="heading-1-create-a-new-tanstack-app">1. Create a new TanStack app</h3>
<p>To get started, you’ll need to create a new TanStack Start app. You can do that with the following command:</p>
<pre><code class="lang-typescript">pnpm create <span class="hljs-meta">@tanstack</span>/start<span class="hljs-meta">@latest</span>
</code></pre>
<p>During the CLI setup, when it asks about add-ons, make sure to select:</p>
<ul>
<li><p>Shadcn</p>
</li>
<li><p>Table</p>
</li>
<li><p>Query</p>
</li>
</ul>
<p>These will give you the shadcn/ui setup and the TanStack Query + Table integrations we’ll use later.</p>
<h3 id="heading-2-initial-cleanup">2. Initial Cleanup</h3>
<p>TanStack Start’s starter template comes with some demo routes and a header we don’t need.</p>
<p>Clean up the project as follows:</p>
<ol>
<li><p>Remove the demo folder inside the <code>src/routes</code> directory (or wherever your router directory lives).</p>
</li>
<li><p>Delete <code>Header.tsx</code> from <code>src/components</code>.</p>
</li>
<li><p>Remove the <code>Header</code> import and usage from <code>src/routes/__root.tsx</code>.</p>
</li>
<li><p>Clean up the <code>src/routes/index.tsx</code> file to something minimal (or leave a simple landing page).</p>
</li>
</ol>
<p>At this point, you can make the initial commit to your repo.</p>
<h3 id="heading-3-setting-up-shadcnstudio-blocks">3. Setting Up shadcn/studio Blocks</h3>
<p>Before we set up, let’s make sure you’re clear on what the shadcn/studio and Shadcn registries are.</p>
<h4 id="heading-what-is-shadcnstudio">What is shadcn/studio?</h4>
<p><a target="_blank" href="https://shadcnstudio.com">shadcn/studio</a> is an open-source collection of copy-and-paste shadcn/ui components, blocks, and templates. It’s paired with a powerful shadcn theme generator to help you craft, customize, and ship faster.</p>
<h4 id="heading-what-is-shadcn-registry">What is Shadcn Registry?</h4>
<p>A shadcn registry is a system for sharing and distributing reusable code assets such as UI components, hooks, and theme configurations across different projects. Running your own registry allows you to publish your custom components that others can then use. The registry uses a <code>registry.json</code> file to define and organize the components and their associated files. </p>
<p>If you want to know more about registries, you can refer to the <a target="_blank" href="https://ui.shadcn.com/docs/registry">official documentation here</a>.</p>
<p>For quick building, we will use shadcn/studio’s free shadcn block – dashboard shell.</p>
<p>First, configure the registries in your <code>components.json</code>:</p>
<pre><code class="lang-typescript">{
  <span class="hljs-comment">// ...existing config</span>
  <span class="hljs-string">"registries"</span>: {
    <span class="hljs-string">"@shadcn-studio"</span>: <span class="hljs-string">"https://shadcnstudio.com/r/{name}.json"</span>,
    <span class="hljs-string">"@ss-components"</span>: <span class="hljs-string">"https://shadcnstudio.com/r/components/{name}.json"</span>,
    <span class="hljs-string">"@ss-blocks"</span>: <span class="hljs-string">"https://shadcnstudio.com/r/blocks/{name}.json"</span>,
    <span class="hljs-string">"@ss-themes"</span>: <span class="hljs-string">"https://shadcnstudio.com/r/themes/{name}.json"</span>
  }
}
</code></pre>
<p>If you face any issues while setting up, you can refer to the <a target="_blank" href="https://shadcnstudio.com/docs/getting-started/how-to-use-shadcn-cli">docs</a>.</p>
<h4 id="heading-install-the-dashboard-shell-block">Install the Dashboard Shell Block</h4>
<p>To get started, visit <a target="_blank" href="https://shadcnstudio.com/blocks">Shadcn blocks</a> and navigate to the Dashboard and App section. Then select the <a target="_blank" href="https://shadcnstudio.com/blocks/dashboard-and-application/dashboard-shell#dashboard-shell-1">Dashboard Shell 1</a> block (it’s free to use).</p>
<p>On the top-right, you’ll see a command to install the block into your project:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764155098742/23d1bee2-e082-4b19-860a-8112fe6bf41c.png" alt="shadcn/stuidio dashboard shell " class="image--center mx-auto" width="1232" height="725" loading="lazy"></p>
<p>Copy that command, paste it into your terminal, and run it. This will install all the components needed for the dashboard layout (sidebar, header, dropdowns, and so on).</p>
<h3 id="heading-4-routing-structure-for-the-dashboard">4. Routing Structure for the Dashboard</h3>
<p>Next, we’ll set up the dashboard routes.</p>
<p>First, create a new layout route for <code>/dashboard</code> by adding a file at:</p>
<p><code>src/routes/dashboard.tsx</code></p>
<p>Then, inside a <code>dashboard</code> directory, create the three pages that will live under this layout:</p>
<ul>
<li><p><code>src/routes/dashboard/index.tsx</code> – main dashboard overview</p>
</li>
<li><p><code>src/routes/dashboard/products.tsx</code> – products table page</p>
</li>
<li><p><code>src/routes/dashboard/settings.tsx</code> – settings page</p>
</li>
</ul>
<p>Your <code>routes</code> folder should look like this:</p>
<pre><code class="lang-typescript">src/routes/
├── __root.tsx
├── index.tsx
├── dashboard.tsx          <span class="hljs-comment">// Layout for all /dashboard/* pages</span>
└── dashboard/
    ├── index.tsx          <span class="hljs-comment">// /dashboard</span>
    ├── products.tsx       <span class="hljs-comment">// /dashboard/products</span>
    └── settings.tsx       <span class="hljs-comment">// /dashboard/settings</span>
</code></pre>
<h3 id="heading-5-creating-the-dashboard-layout">5. Creating the <code>/dashboard</code> Layout</h3>
<p>This will set up the layout for the dashboard. Create <code>src/routes/dashboard.tsx</code> and paste:</p>
<p>file: <code>src/routes/dashboard.tsx</code></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> LanguageDropdown <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/shadcn-studio/blocks/dropdown-language'</span>
<span class="hljs-keyword">import</span> ProfileDropdown <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/shadcn-studio/blocks/dropdown-profile'</span>
<span class="hljs-keyword">import</span> { Avatar, AvatarImage } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/ui/avatar'</span>
<span class="hljs-keyword">import</span> {
    Breadcrumb,
    BreadcrumbItem,
    BreadcrumbLink,
    BreadcrumbList,
    BreadcrumbPage,
    BreadcrumbSeparator
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/ui/breadcrumb'</span>
<span class="hljs-keyword">import</span> { Button } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/ui/button'</span>
<span class="hljs-keyword">import</span> { Separator } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/ui/separator'</span>
<span class="hljs-keyword">import</span> {
    Sidebar,
    SidebarContent,
    SidebarGroup,
    SidebarGroupContent,
    SidebarGroupLabel,
    SidebarHeader,
    SidebarMenu,
    SidebarMenuButton,
    SidebarMenuItem,
    SidebarProvider,
    SidebarTrigger
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/ui/sidebar'</span>
<span class="hljs-keyword">import</span> { createFileRoute, Link, Outlet, useLocation } <span class="hljs-keyword">from</span> <span class="hljs-string">'@tanstack/react-router'</span>
<span class="hljs-keyword">import</span> {
    FacebookIcon,
    InstagramIcon,
    LanguagesIcon,
    LayoutDashboard,
    LinkedinIcon,
    LogIn,
    Package,
    Settings,
    TwitterIcon,
    User2
} <span class="hljs-keyword">from</span> <span class="hljs-string">'lucide-react'</span>
<span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Route = createFileRoute(<span class="hljs-string">'/dashboard'</span>)({
    component: DashboardLayout
})

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">DashboardLayout</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> location = useLocation()
    <span class="hljs-keyword">const</span> pathSegments = location.pathname.split(<span class="hljs-string">'/'</span>).filter(<span class="hljs-built_in">Boolean</span>)

    <span class="hljs-keyword">return</span> (
        &lt;div className=<span class="hljs-string">'flex min-h-dvh w-full'</span>&gt;
            &lt;SidebarProvider&gt;
                &lt;Sidebar&gt;
                    &lt;SidebarContent&gt;
                        &lt;SidebarHeader&gt;
                            &lt;SidebarMenu&gt;
                                &lt;SidebarMenuItem&gt;
                                    &lt;SidebarMenuButton size=<span class="hljs-string">"lg"</span> asChild&gt;
                                        &lt;Link to=<span class="hljs-string">"/"</span>&gt;
                                            &lt;div className=<span class="hljs-string">"flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground"</span>&gt;
                                                &lt;User2 className=<span class="hljs-string">"size-4"</span> /&gt;
                                            &lt;/div&gt;
                                            &lt;div className=<span class="hljs-string">"grid flex-1 text-left text-sm leading-tight"</span>&gt;
                                                &lt;span className=<span class="hljs-string">"truncate font-semibold"</span>&gt;Your App&lt;/span&gt;
                                                &lt;span className=<span class="hljs-string">"truncate text-xs"</span>&gt;Dashboard&lt;/span&gt;
                                            &lt;/div&gt;
                                        &lt;/Link&gt;
                                    &lt;/SidebarMenuButton&gt;
                                &lt;/SidebarMenuItem&gt;
                            &lt;/SidebarMenu&gt;
                        &lt;/SidebarHeader&gt;

                        &lt;SidebarGroup&gt;
                            &lt;SidebarGroupLabel&gt;General&lt;/SidebarGroupLabel&gt;
                            &lt;SidebarGroupContent&gt;
                                &lt;SidebarMenu&gt;
                                    &lt;SidebarMenuItem&gt;
                                        &lt;SidebarMenuButton asChild&gt;
                                            &lt;Link to=<span class="hljs-string">'/dashboard'</span>&gt;
                                                &lt;LayoutDashboard /&gt;
                                                &lt;span&gt;Dashboard&lt;/span&gt;
                                            &lt;/Link&gt;
                                        &lt;/SidebarMenuButton&gt;
                                    &lt;/SidebarMenuItem&gt;
                                    &lt;SidebarMenuItem&gt;
                                        &lt;SidebarMenuButton asChild&gt;
                                            &lt;Link to=<span class="hljs-string">'/dashboard/products'</span>&gt;
                                                &lt;Package /&gt;
                                                &lt;span&gt;Products&lt;/span&gt;
                                            &lt;/Link&gt;
                                        &lt;/SidebarMenuButton&gt;
                                    &lt;/SidebarMenuItem&gt;
                                    &lt;SidebarMenuItem&gt;
                                        &lt;SidebarMenuButton asChild&gt;
                                            &lt;Link to=<span class="hljs-string">'/dashboard/settings'</span>&gt;
                                                &lt;Settings /&gt;
                                                &lt;span&gt;Settings&lt;/span&gt;
                                            &lt;/Link&gt;
                                        &lt;/SidebarMenuButton&gt;
                                    &lt;/SidebarMenuItem&gt;
                                &lt;/SidebarMenu&gt;
                            &lt;/SidebarGroupContent&gt;
                        &lt;/SidebarGroup&gt;
                    &lt;/SidebarContent&gt;
                &lt;/Sidebar&gt;
                &lt;div className=<span class="hljs-string">'flex flex-1 flex-col'</span>&gt;
                    &lt;header className=<span class="hljs-string">'bg-card sticky top-0 z-50 border-b'</span>&gt;
                        &lt;div className=<span class="hljs-string">'mx-auto flex max-w-7xl items-center justify-between gap-6 px-4 py-2 sm:px-6'</span>&gt;
                            &lt;div className=<span class="hljs-string">'flex items-center gap-4'</span>&gt;
                                &lt;SidebarTrigger className=<span class="hljs-string">'[&amp;_svg]:h-5 [&amp;_svg]:w-5'</span> /&gt;
                                &lt;Separator orientation=<span class="hljs-string">'vertical'</span> className=<span class="hljs-string">'hidden h-4 sm:block'</span> /&gt;
                                &lt;Breadcrumb className=<span class="hljs-string">'hidden sm:block'</span>&gt;
                                    &lt;BreadcrumbList&gt;
                                        &lt;BreadcrumbItem&gt;
                                            &lt;BreadcrumbLink asChild&gt;
                                                &lt;Link to=<span class="hljs-string">'/'</span>&gt;Home&lt;/Link&gt;
                                            &lt;/BreadcrumbLink&gt;
                                        &lt;/BreadcrumbItem&gt;
                                        &lt;BreadcrumbSeparator /&gt;
                                        {pathSegments.map(<span class="hljs-function">(<span class="hljs-params">segment, index</span>) =&gt;</span> {
                                            <span class="hljs-keyword">const</span> path = <span class="hljs-string">`/<span class="hljs-subst">${pathSegments.slice(<span class="hljs-number">0</span>, index + <span class="hljs-number">1</span>).join(<span class="hljs-string">'/'</span>)}</span>`</span>
                                            <span class="hljs-keyword">const</span> isLast = index === pathSegments.length - <span class="hljs-number">1</span>
                                            <span class="hljs-keyword">const</span> title = segment.charAt(<span class="hljs-number">0</span>).toUpperCase() + segment.slice(<span class="hljs-number">1</span>)

                                            <span class="hljs-keyword">return</span> (
                                                &lt;React.Fragment key={path}&gt;
                                                    &lt;BreadcrumbItem&gt;
                                                        {isLast ? (
                                                            &lt;BreadcrumbPage&gt;{title}&lt;/BreadcrumbPage&gt;
                                                        ) : (
                                                            &lt;BreadcrumbLink asChild&gt;
                                                                &lt;Link to={path <span class="hljs-keyword">as</span> <span class="hljs-built_in">any</span>}&gt;{title}&lt;/Link&gt;
                                                            &lt;/BreadcrumbLink&gt;
                                                        )}
                                                    &lt;/BreadcrumbItem&gt;
                                                    {!isLast &amp;&amp; &lt;BreadcrumbSeparator /&gt;}
                                                &lt;/React.Fragment&gt;
                                            )
                                        })}
                                    &lt;/BreadcrumbList&gt;
                                &lt;/Breadcrumb&gt;
                            &lt;/div&gt;
                            &lt;div className=<span class="hljs-string">'flex items-center gap-1.5'</span>&gt;
                                &lt;LanguageDropdown
                                    trigger={
                                        &lt;Button variant=<span class="hljs-string">'ghost'</span> size=<span class="hljs-string">'icon'</span>&gt;
                                            &lt;LanguagesIcon /&gt;
                                        &lt;/Button&gt;
                                    }
                                /&gt;
                                &lt;ProfileDropdown
                                    trigger={
                                        &lt;Button variant=<span class="hljs-string">'ghost'</span> size=<span class="hljs-string">'icon'</span> className=<span class="hljs-string">'h-10 w-10'</span>&gt;
                                            &lt;Avatar className=<span class="hljs-string">'h-10 w-10 rounded-md'</span>&gt;
                                                &lt;AvatarImage src=<span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-1.png'</span> /&gt;
                                            &lt;/Avatar&gt;
                                        &lt;/Button&gt;
                                    }
                                /&gt;
                            &lt;/div&gt;
                        &lt;/div&gt;
                    &lt;/header&gt;
                    &lt;main className=<span class="hljs-string">'mx-auto w-full max-w-7xl flex-1 px-4 py-6 sm:px-6'</span>&gt;
                        &lt;Outlet /&gt;
                    &lt;/main&gt;
                    &lt;footer&gt;
                        &lt;div className=<span class="hljs-string">'text-muted-foreground mx-auto flex w-full items-center justify-between gap-3 px-4 py-3 flex-col sm:flex-row sm:gap-6 sm:px-6'</span>&gt;
                            &lt;p className=<span class="hljs-string">'text-sm text-center sm:text-left'</span>&gt;
                                {<span class="hljs-string">`©<span class="hljs-subst">${<span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().getFullYear()}</span>`</span>}{<span class="hljs-string">' '</span>}
                                &lt;a href=<span class="hljs-string">'#'</span> className=<span class="hljs-string">'text-primary'</span>&gt;
                                    TanStack Start
                                &lt;/a&gt;
                                , Made <span class="hljs-keyword">for</span> better web design
                            &lt;/p&gt;
                            &lt;div className=<span class="hljs-string">'flex items-center gap-5'</span>&gt;
                                &lt;a href=<span class="hljs-string">'#'</span>&gt;
                                    &lt;FacebookIcon className=<span class="hljs-string">'h-4 w-4'</span> /&gt;
                                &lt;/a&gt;
                                &lt;a href=<span class="hljs-string">'#'</span>&gt;
                                    &lt;InstagramIcon className=<span class="hljs-string">'h-4 w-4'</span> /&gt;
                                &lt;/a&gt;
                                &lt;a href=<span class="hljs-string">'#'</span>&gt;
                                    &lt;LinkedinIcon className=<span class="hljs-string">'h-4 w-4'</span> /&gt;
                                &lt;/a&gt;
                                &lt;a href=<span class="hljs-string">'#'</span>&gt;
                                    &lt;TwitterIcon className=<span class="hljs-string">'h-4 w-4'</span> /&gt;
                                &lt;/a&gt;
                            &lt;/div&gt;
                        &lt;/div&gt;
                    &lt;/footer&gt;
                &lt;/div&gt;
            &lt;/SidebarProvider&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p>You now have a full layout for all <code>/dashboard/*</code> routes.</p>
<p>Let's break down the key parts of our dashboard layout:</p>
<ul>
<li><p><strong>Sidebar structure:</strong> The <code>&lt;Sidebar&gt;</code> component wraps our navigation menu. Inside, we use <code>&lt;SidebarMenu&gt;</code> and <code>&lt;SidebarMenuItem&gt;</code> to create navigation links. Each menu item uses TanStack Router's <code>&lt;Link&gt;</code> component for type-safe navigation. We also have a header set up in the <code>&lt;SidebarProvider&gt;</code></p>
</li>
<li><p><strong>Dynamic breadcrumbs:</strong> The breadcrumb section uses <code>location.pathname</code> to split the current URL into segments, then maps over them to create breadcrumb links. The <code>isLast</code> check ensures the final breadcrumb renders as plain text rather than a link.</p>
</li>
<li><p><strong>Header actions</strong>: The header includes two dropdowns: <code>&lt;LanguageDropdown&gt;</code> for internationalization and <code>&lt;ProfileDropdown&gt;</code> for user account actions. These come from the <code>shadcn/studio</code> blocks we installed.</p>
</li>
<li><p><strong>Outlet component:</strong> The <code>&lt;Outlet /&gt;</code> component is where child routes (like <code>/dashboard</code>, <code>/dashboard/products</code>) will render. This makes our layout reusable across all dashboard pages. The layout uses Tailwind's utility classes for spacing, colors, and responsive behavior, making it easy to customize for your use case.</p>
</li>
</ul>
<p>For more details regarding the Sidebar component, you can <a target="_blank" href="https://ui.shadcn.com/docs/components/sidebar">refer to the official docs here</a>.</p>
<p>You now have a full layout for all <code>/dashboard/*</code> routes.</p>
<h3 id="heading-6-building-the-dashboard-home-page">6. Building the Dashboard Home Page</h3>
<p>Create <code>src/routes/dashboard/index.tsx</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { <span class="hljs-keyword">type</span> Item } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/shadcn-studio/blocks/datatable-transaction'</span>
<span class="hljs-keyword">import</span> { createFileRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">'@tanstack/react-router'</span>

<span class="hljs-keyword">import</span> { Card } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/ui/card'</span>

<span class="hljs-keyword">import</span> SalesMetricsCard <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/shadcn-studio/blocks/chart-sales-metrics'</span>
<span class="hljs-keyword">import</span> TransactionDatatable <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/shadcn-studio/blocks/datatable-transaction'</span>
<span class="hljs-keyword">import</span> StatisticsCard <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/shadcn-studio/blocks/statistics-card-01'</span>
<span class="hljs-keyword">import</span> ProductInsightsCard <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/shadcn-studio/blocks/widget-product-insights'</span>
<span class="hljs-keyword">import</span> TotalEarningCard <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/shadcn-studio/blocks/widget-total-earning'</span>

<span class="hljs-keyword">import</span> {
    CalendarX2Icon,
    TriangleAlertIcon,
    TruckIcon
} <span class="hljs-keyword">from</span> <span class="hljs-string">'lucide-react'</span>

<span class="hljs-comment">// Statistics card data</span>
<span class="hljs-keyword">const</span> StatisticsCardData = [
    {
        icon: &lt;TruckIcon className=<span class="hljs-string">'h-4 w-4'</span> /&gt;,
        value: <span class="hljs-string">'42'</span>,
        title: <span class="hljs-string">'Shipped Orders'</span>,
        changePercentage: <span class="hljs-string">'+18.2%'</span>
    },
    {
        icon: &lt;TriangleAlertIcon className=<span class="hljs-string">'h-4 w-4'</span> /&gt;,
        value: <span class="hljs-string">'8'</span>,
        title: <span class="hljs-string">'Damaged Returns'</span>,
        changePercentage: <span class="hljs-string">'-8.7%'</span>
    },
    {
        icon: &lt;CalendarX2Icon className=<span class="hljs-string">'h-4 w-4'</span> /&gt;,
        value: <span class="hljs-string">'27'</span>,
        title: <span class="hljs-string">'Missed Delivery Slots'</span>,
        changePercentage: <span class="hljs-string">'+4.3%'</span>
    }
]

<span class="hljs-comment">// Earning data for Total Earning card</span>
<span class="hljs-keyword">const</span> earningData = [
    {
        img: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/blocks/dashboard-application/widgets/zipcar.png'</span>,
        platform: <span class="hljs-string">'Zipcar'</span>,
        technologies: <span class="hljs-string">'Vuejs &amp; HTML'</span>,
        earnings: <span class="hljs-string">'-$23,569.26'</span>,
        progressPercentage: <span class="hljs-number">75</span>
    },
    {
        img: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/blocks/dashboard-application/widgets/bitbank.png'</span>,
        platform: <span class="hljs-string">'Bitbank'</span>,
        technologies: <span class="hljs-string">'Figma &amp; React'</span>,
        earnings: <span class="hljs-string">'-$12,650.31'</span>,
        progressPercentage: <span class="hljs-number">25</span>
    }
]

<span class="hljs-comment">// Transaction table data</span>
<span class="hljs-keyword">const</span> transactionData: Item[] = [
    {
        id: <span class="hljs-string">'1'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-1.png'</span>,
        avatarFallback: <span class="hljs-string">'JA'</span>,
        name: <span class="hljs-string">'Jack Alfredo'</span>,
        amount: <span class="hljs-number">315.0</span>,
        status: <span class="hljs-string">'paid'</span>,
        email: <span class="hljs-string">'jack@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'2'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-2.png'</span>,
        avatarFallback: <span class="hljs-string">'MG'</span>,
        name: <span class="hljs-string">'Maria Gonzalez'</span>,
        amount: <span class="hljs-number">253.4</span>,
        status: <span class="hljs-string">'pending'</span>,
        email: <span class="hljs-string">'maria.g@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    },
    {
        id: <span class="hljs-string">'3'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-3.png'</span>,
        avatarFallback: <span class="hljs-string">'JD'</span>,
        name: <span class="hljs-string">'John Doe'</span>,
        amount: <span class="hljs-number">852.0</span>,
        status: <span class="hljs-string">'paid'</span>,
        email: <span class="hljs-string">'john.doe@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'4'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-4.png'</span>,
        avatarFallback: <span class="hljs-string">'EC'</span>,
        name: <span class="hljs-string">'Emily Carter'</span>,
        amount: <span class="hljs-number">889.0</span>,
        status: <span class="hljs-string">'pending'</span>,
        email: <span class="hljs-string">'emily.carter@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    },
    {
        id: <span class="hljs-string">'5'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-5.png'</span>,
        avatarFallback: <span class="hljs-string">'DL'</span>,
        name: <span class="hljs-string">'David Lee'</span>,
        amount: <span class="hljs-number">723.16</span>,
        status: <span class="hljs-string">'paid'</span>,
        email: <span class="hljs-string">'david.lee@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'6'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-6.png'</span>,
        avatarFallback: <span class="hljs-string">'SP'</span>,
        name: <span class="hljs-string">'Sophia Patel'</span>,
        amount: <span class="hljs-number">612.0</span>,
        status: <span class="hljs-string">'failed'</span>,
        email: <span class="hljs-string">'sophia.patel@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'7'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-7.png'</span>,
        avatarFallback: <span class="hljs-string">'RW'</span>,
        name: <span class="hljs-string">'Robert Wilson'</span>,
        amount: <span class="hljs-number">445.25</span>,
        status: <span class="hljs-string">'paid'</span>,
        email: <span class="hljs-string">'robert.wilson@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    },
    {
        id: <span class="hljs-string">'8'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-8.png'</span>,
        avatarFallback: <span class="hljs-string">'LM'</span>,
        name: <span class="hljs-string">'Lisa Martinez'</span>,
        amount: <span class="hljs-number">297.8</span>,
        status: <span class="hljs-string">'processing'</span>,
        email: <span class="hljs-string">'lisa.martinez@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'9'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-9.png'</span>,
        avatarFallback: <span class="hljs-string">'MT'</span>,
        name: <span class="hljs-string">'Michael Thompson'</span>,
        amount: <span class="hljs-number">756.9</span>,
        status: <span class="hljs-string">'paid'</span>,
        email: <span class="hljs-string">'michael.thompson@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    },
    {
        id: <span class="hljs-string">'10'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-10.png'</span>,
        avatarFallback: <span class="hljs-string">'AJ'</span>,
        name: <span class="hljs-string">'Amanda Johnson'</span>,
        amount: <span class="hljs-number">189.5</span>,
        status: <span class="hljs-string">'pending'</span>,
        email: <span class="hljs-string">'amanda.johnson@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'11'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-11.png'</span>,
        avatarFallback: <span class="hljs-string">'KB'</span>,
        name: <span class="hljs-string">'Kevin Brown'</span>,
        amount: <span class="hljs-number">1024.75</span>,
        status: <span class="hljs-string">'paid'</span>,
        email: <span class="hljs-string">'kevin.brown@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    },
    {
        id: <span class="hljs-string">'12'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-12.png'</span>,
        avatarFallback: <span class="hljs-string">'SD'</span>,
        name: <span class="hljs-string">'Sarah Davis'</span>,
        amount: <span class="hljs-number">367.2</span>,
        status: <span class="hljs-string">'failed'</span>,
        email: <span class="hljs-string">'sarah.davis@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'13'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-13.png'</span>,
        avatarFallback: <span class="hljs-string">'CG'</span>,
        name: <span class="hljs-string">'Christopher Garcia'</span>,
        amount: <span class="hljs-number">598.45</span>,
        status: <span class="hljs-string">'processing'</span>,
        email: <span class="hljs-string">'christopher.garcia@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    },
    {
        id: <span class="hljs-string">'14'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-14.png'</span>,
        avatarFallback: <span class="hljs-string">'JR'</span>,
        name: <span class="hljs-string">'Jennifer Rodriguez'</span>,
        amount: <span class="hljs-number">821.3</span>,
        status: <span class="hljs-string">'paid'</span>,
        email: <span class="hljs-string">'jennifer.rodriguez@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'15'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-15.png'</span>,
        avatarFallback: <span class="hljs-string">'DM'</span>,
        name: <span class="hljs-string">'Daniel Miller'</span>,
        amount: <span class="hljs-number">156.75</span>,
        status: <span class="hljs-string">'pending'</span>,
        email: <span class="hljs-string">'daniel.miller@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    },
    {
        id: <span class="hljs-string">'16'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-16.png'</span>,
        avatarFallback: <span class="hljs-string">'NW'</span>,
        name: <span class="hljs-string">'Nicole White'</span>,
        amount: <span class="hljs-number">934.1</span>,
        status: <span class="hljs-string">'paid'</span>,
        email: <span class="hljs-string">'nicole.white@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'17'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-17.png'</span>,
        avatarFallback: <span class="hljs-string">'AL'</span>,
        name: <span class="hljs-string">'Anthony Lopez'</span>,
        amount: <span class="hljs-number">412.85</span>,
        status: <span class="hljs-string">'failed'</span>,
        email: <span class="hljs-string">'anthony.lopez@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    },
    {
        id: <span class="hljs-string">'18'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-18.png'</span>,
        avatarFallback: <span class="hljs-string">'MH'</span>,
        name: <span class="hljs-string">'Michelle Harris'</span>,
        amount: <span class="hljs-number">675.5</span>,
        status: <span class="hljs-string">'processing'</span>,
        email: <span class="hljs-string">'michelle.harris@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'19'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-19.png'</span>,
        avatarFallback: <span class="hljs-string">'JC'</span>,
        name: <span class="hljs-string">'James Clark'</span>,
        amount: <span class="hljs-number">289.95</span>,
        status: <span class="hljs-string">'paid'</span>,
        email: <span class="hljs-string">'james.clark@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    },
    {
        id: <span class="hljs-string">'20'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-20.png'</span>,
        avatarFallback: <span class="hljs-string">'RL'</span>,
        name: <span class="hljs-string">'Rachel Lewis'</span>,
        amount: <span class="hljs-number">1156.25</span>,
        status: <span class="hljs-string">'pending'</span>,
        email: <span class="hljs-string">'rachel.lewis@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'21'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-21.png'</span>,
        avatarFallback: <span class="hljs-string">'TY'</span>,
        name: <span class="hljs-string">'Thomas Young'</span>,
        amount: <span class="hljs-number">543.6</span>,
        status: <span class="hljs-string">'paid'</span>,
        email: <span class="hljs-string">'thomas.young@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    },
    {
        id: <span class="hljs-string">'22'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-22.png'</span>,
        avatarFallback: <span class="hljs-string">'SB'</span>,
        name: <span class="hljs-string">'Stephanie Brown'</span>,
        amount: <span class="hljs-number">789.3</span>,
        status: <span class="hljs-string">'processing'</span>,
        email: <span class="hljs-string">'stephanie.brown@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'23'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-23.png'</span>,
        avatarFallback: <span class="hljs-string">'BM'</span>,
        name: <span class="hljs-string">'Brandon Moore'</span>,
        amount: <span class="hljs-number">425.75</span>,
        status: <span class="hljs-string">'failed'</span>,
        email: <span class="hljs-string">'brandon.moore@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    },
    {
        id: <span class="hljs-string">'24'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-24.png'</span>,
        avatarFallback: <span class="hljs-string">'KT'</span>,
        name: <span class="hljs-string">'Kelly Taylor'</span>,
        amount: <span class="hljs-number">1203.5</span>,
        status: <span class="hljs-string">'paid'</span>,
        email: <span class="hljs-string">'kelly.taylor@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'mastercard'</span>
    },
    {
        id: <span class="hljs-string">'25'</span>,
        avatar: <span class="hljs-string">'https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-25.png'</span>,
        avatarFallback: <span class="hljs-string">'MA'</span>,
        name: <span class="hljs-string">'Mark Anderson'</span>,
        amount: <span class="hljs-number">356.2</span>,
        status: <span class="hljs-string">'pending'</span>,
        email: <span class="hljs-string">'mark.anderson@shadcnstudio.com'</span>,
        paidBy: <span class="hljs-string">'visa'</span>
    }
]

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Route = createFileRoute(<span class="hljs-string">'/dashboard/'</span>)({
    component: RouteComponent,
})

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">RouteComponent</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> (
        &lt;div className=<span class="hljs-string">'grid grid-cols-2 gap-6 lg:grid-cols-3'</span>&gt;
            {<span class="hljs-comment">/* Statistics Cards */</span>}
            &lt;div className=<span class="hljs-string">'col-span-full grid gap-6 sm:grid-cols-3 md:max-lg:grid-cols-1'</span>&gt;
                {StatisticsCardData.map(<span class="hljs-function">(<span class="hljs-params">card, index</span>) =&gt;</span> (
                    &lt;StatisticsCard
                        key={index}
                        icon={card.icon}
                        title={card.title}
                        value={card.value}
                        changePercentage={card.changePercentage}
                    /&gt;
                ))}
            &lt;/div&gt;

            &lt;div className=<span class="hljs-string">'grid gap-6 max-xl:col-span-full lg:max-xl:grid-cols-2'</span>&gt;
                {<span class="hljs-comment">/* Product Insights Card */</span>}
                &lt;ProductInsightsCard className=<span class="hljs-string">'justify-between gap-3 *:data-[slot=card-content]:space-y-5'</span> /&gt;

                {<span class="hljs-comment">/* Total Earning Card */</span>}
                &lt;TotalEarningCard
                    title=<span class="hljs-string">'Total Earning'</span>
                    earning={<span class="hljs-number">24650</span>}
                    trend=<span class="hljs-string">'up'</span>
                    percentage={<span class="hljs-number">10</span>}
                    comparisonText=<span class="hljs-string">'Compare to last year ($84,325)'</span>
                    earningData={earningData}
                    className=<span class="hljs-string">'justify-between gap-5 sm:min-w-0 *:data-[slot=card-content]:space-y-7'</span>
                /&gt;
            &lt;/div&gt;

            &lt;SalesMetricsCard className=<span class="hljs-string">'col-span-full xl:col-span-2 *:data-[slot=card-content]:space-y-6'</span> /&gt;
            &lt;Card className=<span class="hljs-string">'col-span-full w-full py-0'</span>&gt;
                &lt;TransactionDatatable data={transactionData} /&gt;
            &lt;/Card&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p>Our dashboard homepage uses various shadcn-studio blocks like:</p>
<ul>
<li><p><strong>Statistics cards</strong> display KPIs (Shipped Orders, Damaged Returns, and so on) with trend indicators. Each card receives props for the icon, value, title, and percentage change, making them reusable for any metric.</p>
</li>
<li><p><strong>Chart components</strong> like <code>&lt;SalesMetricsCard&gt;</code> use <code>recharts</code> under the hood to visualize data. The styling comes from shadcn/ui's card component and Tailwind utilities.</p>
</li>
<li><p><strong>Transaction data table</strong> demonstrates TanStack Table integration. We pass an array of transaction objects, and the <code>&lt;TransactionDatatable&gt;</code> component handles rendering, sorting, and pagination. Notice how we use TypeScript's <code>Item[]</code> type for full type safety.</p>
</li>
</ul>
<p>If you now navigate to <code>/dashboard</code>, you should see an admin dashboard with KPI statistics, charts, a dashboard, and a transaction table. Here is what it would look like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764747793227/ca1c0e10-e295-45c4-8e3c-15702583c887.jpeg" alt="tanstack start dashboard demo" class="image--center mx-auto" width="1454" height="1388" loading="lazy"></p>
<p>We have built this beautiful dashboard quickly by using the shadcn/studio’s pre-built blocks.</p>
<h3 id="heading-7-set-up-the-products-page">7. Set up the Products Page.</h3>
<p>Before building our products table, we need to install <strong>Zod</strong>, a TypeScript-first schema validation library. We'll use it to validate the data structure of requests to our server function.</p>
<h4 id="heading-why-zod">Why Zod?</h4>
<p>TanStack Start's server functions use Zod to ensure type-safe data transfer between client and server. When we request to fetch products, Zod validates that the request includes the correct types for <code>page</code>, <code>pageSize</code>, <code>sortBy</code>, and <code>filters</code>. This catches errors at runtime and provides excellent TypeScript inference.</p>
<p>Now, let’s set up the products page with a products table. But before that, let’s install the zod package dependency. Here is the command for it:</p>
<pre><code class="lang-bash">pnpm add zod
</code></pre>
<h4 id="heading-creating-mock-product-data">Creating Mock Product Data</h4>
<p>We will need to store our mock products’ data somewhere. For that, we will create a new file <code>data/products.ts</code> and paste the code below. This will help us mock the product data for our products table.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { createServerFn } <span class="hljs-keyword">from</span> <span class="hljs-string">"@tanstack/react-start"</span>;
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">"zod"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> Product = {
    id: <span class="hljs-built_in">string</span>
    name: <span class="hljs-built_in">string</span>
    category: <span class="hljs-built_in">string</span>
    price: <span class="hljs-built_in">number</span>
    stock: <span class="hljs-built_in">number</span>
    status: <span class="hljs-string">'active'</span> | <span class="hljs-string">'draft'</span> | <span class="hljs-string">'archived'</span>
    image: <span class="hljs-built_in">string</span>
}

<span class="hljs-comment">// Define the type for the data parameter</span>
<span class="hljs-keyword">type</span> ProductQueryParams = {
    page: <span class="hljs-built_in">number</span>;
    pageSize: <span class="hljs-built_in">number</span>;
    sortBy?: <span class="hljs-built_in">string</span>;
    sortOrder?: <span class="hljs-string">"asc"</span> | <span class="hljs-string">"desc"</span>;
    filters?: {
        name?: <span class="hljs-built_in">string</span>;
        category?: <span class="hljs-built_in">string</span>;
        status?: <span class="hljs-built_in">string</span>;
    };
};

<span class="hljs-keyword">const</span> products: Product[] = [
    {
        id: <span class="hljs-string">'PROD-001'</span>,
        name: <span class="hljs-string">'Wireless Noise Cancelling Headphones'</span>,
        category: <span class="hljs-string">'Electronics'</span>,
        price: <span class="hljs-number">299.99</span>,
        stock: <span class="hljs-number">45</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-002'</span>,
        name: <span class="hljs-string">'Ergonomic Office Chair'</span>,
        category: <span class="hljs-string">'Furniture'</span>,
        price: <span class="hljs-number">199.50</span>,
        stock: <span class="hljs-number">12</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1592078615290-033ee584e267?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-003'</span>,
        name: <span class="hljs-string">'Mechanical Gaming Keyboard'</span>,
        category: <span class="hljs-string">'Electronics'</span>,
        price: <span class="hljs-number">129.99</span>,
        stock: <span class="hljs-number">0</span>,
        status: <span class="hljs-string">'archived'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1587829741301-dc798b91add1?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-004'</span>,
        name: <span class="hljs-string">'Smart Fitness Watch'</span>,
        category: <span class="hljs-string">'Wearables'</span>,
        price: <span class="hljs-number">149.00</span>,
        stock: <span class="hljs-number">89</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-005'</span>,
        name: <span class="hljs-string">'Minimalist Desk Lamp'</span>,
        category: <span class="hljs-string">'Lighting'</span>,
        price: <span class="hljs-number">45.00</span>,
        stock: <span class="hljs-number">23</span>,
        status: <span class="hljs-string">'draft'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1507473888900-52e1ad14723b?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-006'</span>,
        name: <span class="hljs-string">'Portable Bluetooth Speaker'</span>,
        category: <span class="hljs-string">'Electronics'</span>,
        price: <span class="hljs-number">79.99</span>,
        stock: <span class="hljs-number">150</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1608043152269-423dbba4e7e1?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-007'</span>,
        name: <span class="hljs-string">'Ceramic Coffee Mug Set'</span>,
        category: <span class="hljs-string">'Kitchen'</span>,
        price: <span class="hljs-number">24.99</span>,
        stock: <span class="hljs-number">200</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1514228742587-6b1558fcca3d?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-008'</span>,
        name: <span class="hljs-string">'Leather Messenger Bag'</span>,
        category: <span class="hljs-string">'Accessories'</span>,
        price: <span class="hljs-number">129.50</span>,
        stock: <span class="hljs-number">15</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1553062407-98eeb64c6a62?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-009'</span>,
        name: <span class="hljs-string">'Wireless Charging Pad'</span>,
        category: <span class="hljs-string">'Electronics'</span>,
        price: <span class="hljs-number">39.99</span>,
        stock: <span class="hljs-number">75</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1586816879360-004f5b0c51e3?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-010'</span>,
        name: <span class="hljs-string">'Succulent Plant Set'</span>,
        category: <span class="hljs-string">'Home &amp; Garden'</span>,
        price: <span class="hljs-number">29.99</span>,
        stock: <span class="hljs-number">30</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1485955900006-10f4d324d411?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-011'</span>,
        name: <span class="hljs-string">'Professional Chef Knife'</span>,
        category: <span class="hljs-string">'Kitchen'</span>,
        price: <span class="hljs-number">89.95</span>,
        stock: <span class="hljs-number">42</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1593618998160-e34014e67546?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-012'</span>,
        name: <span class="hljs-string">'Yoga Mat'</span>,
        category: <span class="hljs-string">'Fitness'</span>,
        price: <span class="hljs-number">35.00</span>,
        stock: <span class="hljs-number">100</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1601925260368-ae2f83cf8b7f?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-013'</span>,
        name: <span class="hljs-string">'Smart Thermostat'</span>,
        category: <span class="hljs-string">'Home Automation'</span>,
        price: <span class="hljs-number">199.00</span>,
        stock: <span class="hljs-number">0</span>,
        status: <span class="hljs-string">'archived'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1567789884554-0b844b597180?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-014'</span>,
        name: <span class="hljs-string">'Vintage Film Camera'</span>,
        category: <span class="hljs-string">'Photography'</span>,
        price: <span class="hljs-number">450.00</span>,
        stock: <span class="hljs-number">3</span>,
        status: <span class="hljs-string">'draft'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1526170375885-4d8ecf77b99f?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-015'</span>,
        name: <span class="hljs-string">'Cotton T-Shirt Pack'</span>,
        category: <span class="hljs-string">'Apparel'</span>,
        price: <span class="hljs-number">49.99</span>,
        stock: <span class="hljs-number">150</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-016'</span>,
        name: <span class="hljs-string">'Electric Toothbrush'</span>,
        category: <span class="hljs-string">'Personal Care'</span>,
        price: <span class="hljs-number">69.99</span>,
        stock: <span class="hljs-number">55</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1559656914-a30970c1affd?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-017'</span>,
        name: <span class="hljs-string">'Gaming Mouse'</span>,
        category: <span class="hljs-string">'Electronics'</span>,
        price: <span class="hljs-number">59.99</span>,
        stock: <span class="hljs-number">88</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1527864550417-7fd91fc51a46?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-018'</span>,
        name: <span class="hljs-string">'Essential Oil Diffuser'</span>,
        category: <span class="hljs-string">'Home &amp; Garden'</span>,
        price: <span class="hljs-number">34.50</span>,
        stock: <span class="hljs-number">25</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1602928321679-560bb453f190?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-019'</span>,
        name: <span class="hljs-string">'Running Shoes'</span>,
        category: <span class="hljs-string">'Footwear'</span>,
        price: <span class="hljs-number">119.99</span>,
        stock: <span class="hljs-number">60</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-020'</span>,
        name: <span class="hljs-string">'Digital Drawing Tablet'</span>,
        category: <span class="hljs-string">'Electronics'</span>,
        price: <span class="hljs-number">249.00</span>,
        stock: <span class="hljs-number">18</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1561525140-c2a4cc68e4bd?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-021'</span>,
        name: <span class="hljs-string">'Bamboo Cutting Board'</span>,
        category: <span class="hljs-string">'Kitchen'</span>,
        price: <span class="hljs-number">22.99</span>,
        stock: <span class="hljs-number">95</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1594385208974-2e75f8d7bb48?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-022'</span>,
        name: <span class="hljs-string">'Sunglasses'</span>,
        category: <span class="hljs-string">'Accessories'</span>,
        price: <span class="hljs-number">159.00</span>,
        stock: <span class="hljs-number">40</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1511499767150-a48a237f0083?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-023'</span>,
        name: <span class="hljs-string">'Water Bottle'</span>,
        category: <span class="hljs-string">'Fitness'</span>,
        price: <span class="hljs-number">19.99</span>,
        stock: <span class="hljs-number">300</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1602143407151-01114192003f?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-024'</span>,
        name: <span class="hljs-string">'Throw Pillow Set'</span>,
        category: <span class="hljs-string">'Home Decor'</span>,
        price: <span class="hljs-number">45.99</span>,
        stock: <span class="hljs-number">28</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1584100936595-c0654b55a2e6?w=100&amp;q=80'</span>,
    },
    {
        id: <span class="hljs-string">'PROD-025'</span>,
        name: <span class="hljs-string">'Wireless Earbuds'</span>,
        category: <span class="hljs-string">'Electronics'</span>,
        price: <span class="hljs-number">89.99</span>,
        stock: <span class="hljs-number">120</span>,
        status: <span class="hljs-string">'active'</span>,
        image: <span class="hljs-string">'https://images.unsplash.com/photo-1590658268037-6bf12165a8df?w=100&amp;q=80'</span>,
    }
]

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getProducts = createServerFn({ method: <span class="hljs-string">"GET"</span> })
    .inputValidator(
        z.object({
            page: z.number().default(<span class="hljs-number">0</span>),
            pageSize: z.number().default(<span class="hljs-number">10</span>),
            sortBy: z.string().optional(),
            sortOrder: z.enum([<span class="hljs-string">"asc"</span>, <span class="hljs-string">"desc"</span>]).optional(),
            filters: z
                .object({
                    name: z.string().optional(),
                    category: z.string().optional(),
                    status: z.string().optional(),
                })
                .optional(),
        })
    )
    .handler(<span class="hljs-keyword">async</span> ({ data }: { data: ProductQueryParams }) =&gt; {
        <span class="hljs-keyword">const</span> { page, pageSize, sortBy, sortOrder, filters } = data;

        <span class="hljs-comment">// Apply filters</span>
        <span class="hljs-keyword">let</span> filteredProducts = [...products];

        <span class="hljs-keyword">if</span> (filters) {
            <span class="hljs-keyword">if</span> (filters.name) {
                filteredProducts = filteredProducts.filter(<span class="hljs-function">(<span class="hljs-params">product</span>) =&gt;</span>
                    product.name.toLowerCase().includes(filters.name!.toLowerCase())
                );
            }

            <span class="hljs-keyword">if</span> (filters.category) {
                filteredProducts = filteredProducts.filter(
                    <span class="hljs-function">(<span class="hljs-params">product</span>) =&gt;</span>
                        product.category.toLowerCase() === filters.category!.toLowerCase()
                );
            }

            <span class="hljs-keyword">if</span> (filters.status) {
                filteredProducts = filteredProducts.filter(
                    <span class="hljs-function">(<span class="hljs-params">product</span>) =&gt;</span> product.status === filters.status
                );
            }
        }

        <span class="hljs-comment">// Apply sorting</span>
        <span class="hljs-keyword">if</span> (sortBy) {
            filteredProducts.sort(<span class="hljs-function">(<span class="hljs-params">a, b</span>) =&gt;</span> {
                <span class="hljs-keyword">const</span> aValue = a[sortBy <span class="hljs-keyword">as</span> keyof Product];
                <span class="hljs-keyword">const</span> bValue = b[sortBy <span class="hljs-keyword">as</span> keyof Product];

                <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> aValue === <span class="hljs-string">"string"</span> &amp;&amp; <span class="hljs-keyword">typeof</span> bValue === <span class="hljs-string">"string"</span>) {
                    <span class="hljs-keyword">return</span> sortOrder === <span class="hljs-string">"desc"</span>
                        ? bValue.localeCompare(aValue)
                        : aValue.localeCompare(bValue);
                }

                <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> aValue === <span class="hljs-string">"number"</span> &amp;&amp; <span class="hljs-keyword">typeof</span> bValue === <span class="hljs-string">"number"</span>) {
                    <span class="hljs-keyword">return</span> sortOrder === <span class="hljs-string">"desc"</span> ? bValue - aValue : aValue - bValue;
                }

                <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
            });
        }

        <span class="hljs-comment">// Calculate pagination</span>
        <span class="hljs-keyword">const</span> totalCount = filteredProducts.length;
        <span class="hljs-keyword">const</span> totalPages = <span class="hljs-built_in">Math</span>.ceil(totalCount / pageSize);
        <span class="hljs-keyword">const</span> paginatedProducts = filteredProducts.slice(
            page * pageSize,
            (page + <span class="hljs-number">1</span>) * pageSize
        );

        <span class="hljs-comment">// Simulate network delay</span>
        <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve</span>) =&gt;</span> <span class="hljs-built_in">setTimeout</span>(resolve, <span class="hljs-number">500</span>));

        <span class="hljs-keyword">return</span> {
            products: paginatedProducts,
            pagination: {
                page,
                pageSize,
                totalCount,
                totalPages,
            },
        };
    });
</code></pre>
<p>Let’s understand the server function and break down what's happening in <code>getProducts</code>:</p>
<ul>
<li><p><strong>Input validation</strong>: The <code>.inputValidator()</code> method uses a Zod schema to validate incoming requests. It ensures <code>page</code> and <code>pageSize</code> are numbers, <code>sortOrder</code> is either "asc" or "desc", and filters are optional strings.</p>
</li>
<li><p><strong>Filtering products</strong>: The function filters the products array based on the provided filters (name, category, status). This simulates what a real database query would do.</p>
</li>
<li><p><strong>Sorting</strong>: Products are sorted by the specified column (<code>sortBy</code>) in ascending or descending order (<code>sortOrder</code>).</p>
</li>
<li><p><strong>Pagination</strong>: We calculate which slice of products to return based on <code>page</code> and <code>pageSize</code>, along with metadata like <code>totalCount</code> and <code>totalPages</code>.</p>
</li>
</ul>
<h4 id="heading-create-the-products-table">Create the Products table:</h4>
<p>Once the data is done, let’s create a table in <code>/dashboard/products.tsx</code>. This table will use our mock product data and will provide multiple functions in the table, like search, sort, and filter. This table demonstrates the powerful combination of TanStack Query for data management and TanStack Table for rendering.</p>
<p>Paste the code below in the <code>products.tsx</code> file:</p>
<pre><code class="lang-bash">import { useQuery } from <span class="hljs-string">'@tanstack/react-query'</span>
import { createFileRoute } from <span class="hljs-string">'@tanstack/react-router'</span>
import {
    ColumnDef,
    ColumnFiltersState,
    flexRender,
    getCoreRowModel,
    getFilteredRowModel,
    getPaginationRowModel,
    getSortedRowModel,
    SortingState,
    useReactTable,
    VisibilityState,
} from <span class="hljs-string">'@tanstack/react-table'</span>
import {
    ArrowUpDown,
    ChevronDown,
    Filter,
    Loader2,
    MoreHorizontal,
    Plus,
    Search
} from <span class="hljs-string">'lucide-react'</span>
import { useState } from <span class="hljs-string">'react'</span>

import { Badge } from <span class="hljs-string">'@/components/ui/badge'</span>
import { Button } from <span class="hljs-string">'@/components/ui/button'</span>
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from <span class="hljs-string">'@/components/ui/card'</span>
import {
    DropdownMenu,
    DropdownMenuCheckboxItem,
    DropdownMenuContent,
    DropdownMenuItem,
    DropdownMenuLabel,
    DropdownMenuSeparator,
    DropdownMenuTrigger,
} from <span class="hljs-string">'@/components/ui/dropdown-menu'</span>
import { Input } from <span class="hljs-string">'@/components/ui/input'</span>
import {
    Table,
    TableBody,
    TableCell,
    TableHead,
    TableHeader,
    TableRow,
} from <span class="hljs-string">'@/components/ui/table'</span>
import { getProducts, <span class="hljs-built_in">type</span> Product } from <span class="hljs-string">'@/data/products'</span>

<span class="hljs-built_in">export</span> const Route = createFileRoute(<span class="hljs-string">'/dashboard/products'</span>)({
    component: ProductsPage,
})

<span class="hljs-built_in">export</span> const columns: ColumnDef&lt;Product&gt;[] = [
    {
        accessorKey: <span class="hljs-string">'name'</span>,
        header: ({ column }) =&gt; {
            <span class="hljs-built_in">return</span> (
                &lt;Button
                    variant=<span class="hljs-string">"ghost"</span>
                    onClick={() =&gt; column.toggleSorting(column.getIsSorted() === <span class="hljs-string">"asc"</span>)}
                &gt;
                    Product Name
                    &lt;ArrowUpDown className=<span class="hljs-string">"ml-2 h-4 w-4"</span> /&gt;
                &lt;/Button&gt;
            )
        },
        cell: ({ row }) =&gt; (
            &lt;div className=<span class="hljs-string">"flex items-center gap-3"</span>&gt;
                &lt;img
                    src={row.original.image}
                    alt={row.getValue(<span class="hljs-string">'name'</span>)}
                    className=<span class="hljs-string">"h-10 w-10 rounded-md object-cover"</span>
                /&gt;
                &lt;div className=<span class="hljs-string">"flex flex-col"</span>&gt;
                    &lt;span className=<span class="hljs-string">"font-medium"</span>&gt;{row.getValue(<span class="hljs-string">'name'</span>)}&lt;/span&gt;
                    &lt;span className=<span class="hljs-string">"text-xs text-muted-foreground"</span>&gt;{row.original.id}&lt;/span&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        ),
    },
    {
        accessorKey: <span class="hljs-string">'category'</span>,
        header: <span class="hljs-string">'Category'</span>,
        cell: ({ row }) =&gt; &lt;div&gt;{row.getValue(<span class="hljs-string">'category'</span>)}&lt;/div&gt;,
    },
    {
        accessorKey: <span class="hljs-string">'status'</span>,
        header: <span class="hljs-string">'Status'</span>,
        cell: ({ row }) =&gt; {
            const status = row.getValue(<span class="hljs-string">'status'</span>) as string
            <span class="hljs-built_in">return</span> (
                &lt;Badge variant={status === <span class="hljs-string">'active'</span> ? <span class="hljs-string">'default'</span> : status === <span class="hljs-string">'draft'</span> ? <span class="hljs-string">'secondary'</span> : <span class="hljs-string">'outline'</span>}&gt;
                    {status}
                &lt;/Badge&gt;
            )
        },
    },
    {
        accessorKey: <span class="hljs-string">'price'</span>,
        header: () =&gt; &lt;div className=<span class="hljs-string">"text-right"</span>&gt;Price&lt;/div&gt;,
        cell: ({ row }) =&gt; {
            const amount = parseFloat(row.getValue(<span class="hljs-string">'price'</span>))
            const formatted = new Intl.NumberFormat(<span class="hljs-string">'en-US'</span>, {
                style: <span class="hljs-string">'currency'</span>,
                currency: <span class="hljs-string">'USD'</span>,
            }).format(amount)

            <span class="hljs-built_in">return</span> &lt;div className=<span class="hljs-string">"text-right font-medium"</span>&gt;{formatted}&lt;/div&gt;
        },
    },
    {
        accessorKey: <span class="hljs-string">'stock'</span>,
        header: () =&gt; &lt;div className=<span class="hljs-string">"text-right"</span>&gt;Stock&lt;/div&gt;,
        cell: ({ row }) =&gt; {
            const stock = parseFloat(row.getValue(<span class="hljs-string">'stock'</span>))
            <span class="hljs-built_in">return</span> &lt;div className={`text-right <span class="hljs-variable">${stock === 0 ? 'text-red-500 font-medium' : ''}</span>`}&gt;{stock}&lt;/div&gt;
        },
    },
    {
        id: <span class="hljs-string">'actions'</span>,
        enableHiding: <span class="hljs-literal">false</span>,
        cell: ({ row }) =&gt; {
            const product = row.original

            <span class="hljs-built_in">return</span> (
                &lt;DropdownMenu&gt;
                    &lt;DropdownMenuTrigger asChild&gt;
                        &lt;Button variant=<span class="hljs-string">"ghost"</span> className=<span class="hljs-string">"h-8 w-8 p-0"</span>&gt;
                            &lt;span className=<span class="hljs-string">"sr-only"</span>&gt;Open menu&lt;/span&gt;
                            &lt;MoreHorizontal className=<span class="hljs-string">"h-4 w-4"</span> /&gt;
                        &lt;/Button&gt;
                    &lt;/DropdownMenuTrigger&gt;
                    &lt;DropdownMenuContent align=<span class="hljs-string">"end"</span>&gt;
                        &lt;DropdownMenuLabel&gt;Actions&lt;/DropdownMenuLabel&gt;
                        &lt;DropdownMenuItem
                            onClick={() =&gt; navigator.clipboard.writeText(product.id)}
                        &gt;
                            Copy Product ID
                        &lt;/DropdownMenuItem&gt;
                        &lt;DropdownMenuSeparator /&gt;
                        &lt;DropdownMenuItem&gt;Edit Product&lt;/DropdownMenuItem&gt;
                        &lt;DropdownMenuItem&gt;View Details&lt;/DropdownMenuItem&gt;
                    &lt;/DropdownMenuContent&gt;
                &lt;/DropdownMenu&gt;
            )
        },
    },
]

<span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">ProductsPage</span></span>() {
    const [sorting, setSorting] = useState&lt;SortingState&gt;([])
    const [columnFilters, setColumnFilters] = useState&lt;ColumnFiltersState&gt;([])
    const [columnVisibility, setColumnVisibility] = useState&lt;VisibilityState&gt;({})
    const [rowSelection, setRowSelection] = useState({})
    const [pagination, setPagination] = useState({
        pageIndex: 0,
        pageSize: 10,
    })

    const { data, isLoading } = useQuery({
        queryKey: [<span class="hljs-string">'products'</span>, pagination, sorting, columnFilters],
        queryFn: () =&gt; getProducts({
            data: {
                page: pagination.pageIndex,
                pageSize: pagination.pageSize,
                sortBy: sorting[0]?.id,
                sortOrder: sorting[0]?.desc ? <span class="hljs-string">'desc'</span> : <span class="hljs-string">'asc'</span>,
                filters: {
                    name: (columnFilters.find((f) =&gt; f.id === <span class="hljs-string">'name'</span>)?.value as string) || undefined,
                    status: (columnFilters.find((f) =&gt; f.id === <span class="hljs-string">'status'</span>)?.value as string) || undefined,
                }
            }
        }),
    })

    const products = data?.products || []
    const totalPages = data?.pagination.totalPages || 0
    const totalCount = data?.pagination.totalCount || 0

    const table = useReactTable({
        data: products,
        columns,
        pageCount: totalPages,
        manualPagination: <span class="hljs-literal">true</span>,
        manualSorting: <span class="hljs-literal">true</span>,
        manualFiltering: <span class="hljs-literal">true</span>,
        onSortingChange: setSorting,
        onColumnFiltersChange: setColumnFilters,
        getCoreRowModel: getCoreRowModel(),
        getPaginationRowModel: getPaginationRowModel(),
        getSortedRowModel: getSortedRowModel(),
        getFilteredRowModel: getFilteredRowModel(),
        onColumnVisibilityChange: setColumnVisibility,
        onRowSelectionChange: setRowSelection,
        onPaginationChange: setPagination,
        state: {
            sorting,
            columnFilters,
            columnVisibility,
            rowSelection,
            pagination,
        },
    })

    <span class="hljs-built_in">return</span> (
        &lt;div className=<span class="hljs-string">"w-full space-y-4"</span>&gt;
            &lt;div className=<span class="hljs-string">"flex items-center justify-between"</span>&gt;
                &lt;h2 className=<span class="hljs-string">"text-2xl font-bold tracking-tight"</span>&gt;Products&lt;/h2&gt;
                &lt;div className=<span class="hljs-string">"flex items-center gap-2"</span>&gt;
                    &lt;Button variant=<span class="hljs-string">"outline"</span> size=<span class="hljs-string">"sm"</span>&gt;
                        &lt;Filter className=<span class="hljs-string">"mr-2 h-4 w-4"</span> /&gt;
                        Filter
                    &lt;/Button&gt;
                    &lt;Button size=<span class="hljs-string">"sm"</span>&gt;
                        &lt;Plus className=<span class="hljs-string">"mr-2 h-4 w-4"</span> /&gt;
                        Add Product
                    &lt;/Button&gt;
                &lt;/div&gt;
            &lt;/div&gt;

            &lt;Card&gt;
                &lt;CardHeader&gt;
                    &lt;CardTitle&gt;Product Management&lt;/CardTitle&gt;
                    &lt;CardDescription&gt;
                        Manage your product catalog, track inventory, and update prices.
                    &lt;/CardDescription&gt;
                &lt;/CardHeader&gt;
                &lt;CardContent&gt;
                    &lt;div className=<span class="hljs-string">"flex items-center py-4 gap-2"</span>&gt;
                        &lt;div className=<span class="hljs-string">"relative flex-1"</span>&gt;
                            &lt;Search className=<span class="hljs-string">"absolute left-2 top-2.5 h-4 w-4 text-muted-foreground"</span> /&gt;
                            &lt;Input
                                placeholder=<span class="hljs-string">"Filter products..."</span>
                                value={(table.getColumn(<span class="hljs-string">"name"</span>)?.getFilterValue() as string) ?? <span class="hljs-string">""</span>}
                                onChange={(event) =&gt;
                                    table.getColumn(<span class="hljs-string">"name"</span>)?.setFilterValue(event.target.value)
                                }
                                className=<span class="hljs-string">"pl-8 max-w-sm"</span>
                            /&gt;
                        &lt;/div&gt;
                        &lt;DropdownMenu&gt;
                            &lt;DropdownMenuTrigger asChild&gt;
                                &lt;Button variant=<span class="hljs-string">"outline"</span> className=<span class="hljs-string">"ml-auto"</span>&gt;
                                    Columns &lt;ChevronDown className=<span class="hljs-string">"ml-2 h-4 w-4"</span> /&gt;
                                &lt;/Button&gt;
                            &lt;/DropdownMenuTrigger&gt;
                            &lt;DropdownMenuContent align=<span class="hljs-string">"end"</span>&gt;
                                {table
                                    .getAllColumns()
                                    .filter((column) =&gt; column.getCanHide())
                                    .map((column) =&gt; {
                                        <span class="hljs-built_in">return</span> (
                                            &lt;DropdownMenuCheckboxItem
                                                key={column.id}
                                                className=<span class="hljs-string">"capitalize"</span>
                                                checked={column.getIsVisible()}
                                                onCheckedChange={(value) =&gt;
                                                    column.toggleVisibility(!!value)
                                                }
                                            &gt;
                                                {column.id}
                                            &lt;/DropdownMenuCheckboxItem&gt;
                                        )
                                    })}
                            &lt;/DropdownMenuContent&gt;
                        &lt;/DropdownMenu&gt;
                    &lt;/div&gt;
                    &lt;div className=<span class="hljs-string">"rounded-md border"</span>&gt;
                        &lt;Table&gt;
                            &lt;TableHeader&gt;
                                {table.getHeaderGroups().map((headerGroup) =&gt; (
                                    &lt;TableRow key={headerGroup.id}&gt;
                                        {headerGroup.headers.map((header) =&gt; {
                                            <span class="hljs-built_in">return</span> (
                                                &lt;TableHead key={header.id}&gt;
                                                    {header.isPlaceholder
                                                        ? null
                                                        : flexRender(
                                                            header.column.columnDef.header,
                                                            header.getContext()
                                                        )}
                                                &lt;/TableHead&gt;
                                            )
                                        })}
                                    &lt;/TableRow&gt;
                                ))}
                            &lt;/TableHeader&gt;
                            &lt;TableBody&gt;
                                {isLoading ? (
                                    &lt;TableRow&gt;
                                        &lt;TableCell colSpan={columns.length} className=<span class="hljs-string">"h-24 text-center"</span>&gt;
                                            &lt;div className=<span class="hljs-string">"flex items-center justify-center gap-2"</span>&gt;
                                                &lt;Loader2 className=<span class="hljs-string">"h-6 w-6 animate-spin"</span> /&gt;
                                                &lt;span&gt;Loading products...&lt;/span&gt;
                                            &lt;/div&gt;
                                        &lt;/TableCell&gt;
                                    &lt;/TableRow&gt;
                                ) : table.getRowModel().rows?.length ? (
                                    table.getRowModel().rows.map((row) =&gt; (
                                        &lt;TableRow
                                            key={row.id}
                                            data-state={row.getIsSelected() &amp;&amp; <span class="hljs-string">"selected"</span>}
                                        &gt;
                                            {row.getVisibleCells().map((cell) =&gt; (
                                                &lt;TableCell key={cell.id}&gt;
                                                    {flexRender(
                                                        cell.column.columnDef.cell,
                                                        cell.getContext()
                                                    )}
                                                &lt;/TableCell&gt;
                                            ))}
                                        &lt;/TableRow&gt;
                                    ))
                                ) : (
                                    &lt;TableRow&gt;
                                        &lt;TableCell
                                            colSpan={columns.length}
                                            className=<span class="hljs-string">"h-24 text-center"</span>
                                        &gt;
                                            No results.
                                        &lt;/TableCell&gt;
                                    &lt;/TableRow&gt;
                                )}
                            &lt;/TableBody&gt;
                        &lt;/Table&gt;
                    &lt;/div&gt;
                    &lt;div className=<span class="hljs-string">"flex items-center justify-end space-x-2 py-4"</span>&gt;
                        &lt;div className=<span class="hljs-string">"flex-1 text-sm text-muted-foreground"</span>&gt;
                            {table.getFilteredSelectedRowModel().rows.length} of{<span class="hljs-string">" "</span>}
                            {totalCount} row(s) selected.
                        &lt;/div&gt;
                        &lt;div className=<span class="hljs-string">"space-x-2"</span>&gt;
                            &lt;Button
                                variant=<span class="hljs-string">"outline"</span>
                                size=<span class="hljs-string">"sm"</span>
                                onClick={() =&gt; table.previousPage()}
                                disabled={!table.getCanPreviousPage()}
                            &gt;
                                Previous
                            &lt;/Button&gt;
                            &lt;Button
                                variant=<span class="hljs-string">"outline"</span>
                                size=<span class="hljs-string">"sm"</span>
                                onClick={() =&gt; table.nextPage()}
                                disabled={!table.getCanNextPage()}
                            &gt;
                                Next
                            &lt;/Button&gt;
                        &lt;/div&gt;
                    &lt;/div&gt;
                &lt;/CardContent&gt;
            &lt;/Card&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p>Now you can see the fully functional products page by navigating the <code>/products</code> where you can search and sort the products.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764748681745/6f73dc04-ac9a-4f75-a1ab-88ed1fc5c6f3.jpeg" alt="tanstack start dashboard demo" class="image--center mx-auto" width="1454" height="1059" loading="lazy"></p>
<h4 id="heading-how-do-tanstack-query-and-tanstack-table-work-in-the-products-table">How do TanStack Query and TanStack Table Work in the products table?</h4>
<p>Our products page uses TanStack Query for data fetching and TanStack Table for rendering.</p>
<p><code>useQuery</code> is a fundamental hook in TanStack Query for managing server state in web applications. It simplifies data fetching, caching, and synchronization.</p>
<p>The below code snippet below shows how we have used useQuery in our product table:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useQuery } <span class="hljs-keyword">from</span> <span class="hljs-string">'@tanstack/react-query'</span>;

<span class="hljs-keyword">const</span> { data, isLoading } = useQuery({
    queryKey: [<span class="hljs-string">'products'</span>, pagination, sorting, columnFilters],
    queryFn: <span class="hljs-function">() =&gt;</span> getProducts({...})
}
</code></pre>
<p>The <code>useQuery</code> hook manages data fetching in our application. For more details, you can <a target="_blank" href="https://tanstack.com/query/latest">refer to the official docs here</a>.</p>
<p><strong>useReactTable:</strong></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useReactTable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@tanstack/react-table'</span>

<span class="hljs-keyword">const</span> table = useReactTable({
    data: products,
    columns,
    manualPagination: <span class="hljs-literal">true</span>,
    manualSorting: <span class="hljs-literal">true</span>,
    manualFiltering: <span class="hljs-literal">true</span>,
})
</code></pre>
<p><strong>TanStack Table</strong> manages the UI state and rendering. By setting <code>manualPagination</code>, <code>manualSorting</code>, and <code>manualFiltering</code> to <code>true</code>, we tell the table that server-side logic handles these operations.</p>
<p>When users sort, filter, or paginate, the table updates its states, and React Query detects the state change in the <code>queryKey</code>. It refetches data from the server, and the table re-renders with fresh data.</p>
<p>This architecture is production-ready and scales to thousands of rows. You just need to replace the mock API endpoint with your real API endpoint.</p>
<h3 id="heading-8-settings-page">8. Settings Page</h3>
<p>Finally, let’s add a simple Settings page with a profile section and some basic notification preferences.</p>
<p>Below is the code for the Settings Page. You can paste it into <code>/dashboard/settings.tsx</code>:</p>
<pre><code class="lang-bash">import { Avatar, AvatarFallback, AvatarImage } from <span class="hljs-string">'@/components/ui/avatar'</span>
import { Button } from <span class="hljs-string">'@/components/ui/button'</span>
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from <span class="hljs-string">'@/components/ui/card'</span>
import { Checkbox } from <span class="hljs-string">"@/components/ui/checkbox"</span>
import { Input } from <span class="hljs-string">'@/components/ui/input'</span>
import { Separator } from <span class="hljs-string">'@/components/ui/separator'</span>
import { createFileRoute } from <span class="hljs-string">'@tanstack/react-router'</span>

<span class="hljs-built_in">export</span> const Route = createFileRoute(<span class="hljs-string">'/dashboard/settings'</span>)({
  component: SettingsPage,
})

<span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">SettingsPage</span></span>() {
  <span class="hljs-built_in">return</span> (
    &lt;div className=<span class="hljs-string">"space-y-6"</span>&gt;
      &lt;div&gt;
        &lt;h3 className=<span class="hljs-string">"text-lg font-medium"</span>&gt;Settings&lt;/h3&gt;
        &lt;p className=<span class="hljs-string">"text-sm text-muted-foreground"</span>&gt;
          Manage your account settings and <span class="hljs-built_in">set</span> e-mail preferences.
        &lt;/p&gt;
      &lt;/div&gt;
      &lt;Separator /&gt;

      &lt;div className=<span class="hljs-string">"grid gap-6"</span>&gt;
        &lt;Card&gt;
          &lt;CardHeader&gt;
            &lt;CardTitle&gt;Profile&lt;/CardTitle&gt;
            &lt;CardDescription&gt;
              This is how others will see you on the site.
            &lt;/CardDescription&gt;
          &lt;/CardHeader&gt;
          &lt;CardContent className=<span class="hljs-string">"space-y-4"</span>&gt;
            &lt;div className=<span class="hljs-string">"flex items-center gap-4"</span>&gt;
              &lt;Avatar className=<span class="hljs-string">"h-20 w-20"</span>&gt;
                &lt;AvatarImage src=<span class="hljs-string">"https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-1.png"</span> /&gt;
                &lt;AvatarFallback&gt;JD&lt;/AvatarFallback&gt;
              &lt;/Avatar&gt;
              &lt;Button variant=<span class="hljs-string">"outline"</span>&gt;Change Avatar&lt;/Button&gt;
            &lt;/div&gt;
            &lt;div className=<span class="hljs-string">"space-y-1"</span>&gt;
              &lt;label htmlFor=<span class="hljs-string">"username"</span> className=<span class="hljs-string">"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"</span>&gt;Username&lt;/label&gt;
              &lt;Input id=<span class="hljs-string">"username"</span> defaultValue=<span class="hljs-string">"jdoe"</span> /&gt;
            &lt;/div&gt;
            &lt;div className=<span class="hljs-string">"space-y-1"</span>&gt;
              &lt;label htmlFor=<span class="hljs-string">"email"</span> className=<span class="hljs-string">"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"</span>&gt;Email&lt;/label&gt;
              &lt;Input id=<span class="hljs-string">"email"</span> defaultValue=<span class="hljs-string">"john.doe@example.com"</span> /&gt;
            &lt;/div&gt;
            &lt;div className=<span class="hljs-string">"space-y-1"</span>&gt;
              &lt;label htmlFor=<span class="hljs-string">"bio"</span> className=<span class="hljs-string">"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"</span>&gt;Bio&lt;/label&gt;
              &lt;Input id=<span class="hljs-string">"bio"</span> placeholder=<span class="hljs-string">"Tell us a little bit about yourself"</span> /&gt;
            &lt;/div&gt;
          &lt;/CardContent&gt;
          &lt;CardFooter&gt;
            &lt;Button&gt;Save Changes&lt;/Button&gt;
          &lt;/CardFooter&gt;
        &lt;/Card&gt;

        &lt;Card&gt;
          &lt;CardHeader&gt;
            &lt;CardTitle&gt;Notifications&lt;/CardTitle&gt;
            &lt;CardDescription&gt;
              Configure how you receive notifications.
            &lt;/CardDescription&gt;
          &lt;/CardHeader&gt;
          &lt;CardContent className=<span class="hljs-string">"space-y-4"</span>&gt;
            &lt;div className=<span class="hljs-string">"flex items-center justify-between rounded-lg border p-4"</span>&gt;
              &lt;div className=<span class="hljs-string">"space-y-0.5"</span>&gt;
                &lt;label className=<span class="hljs-string">"text-base font-medium"</span>&gt;Communication emails&lt;/label&gt;
                &lt;p className=<span class="hljs-string">"text-sm text-muted-foreground"</span>&gt;
                  Receive emails about your account activity.
                &lt;/p&gt;
              &lt;/div&gt;
              {/* Toggle would go here, using a simple checkbox <span class="hljs-keyword">for</span> now */}
              &lt;Checkbox defaultChecked /&gt;
            &lt;/div&gt;
            &lt;div className=<span class="hljs-string">"flex items-center justify-between rounded-lg border p-4"</span>&gt;
              &lt;div className=<span class="hljs-string">"space-y-0.5"</span>&gt;
                &lt;label className=<span class="hljs-string">"text-base font-medium"</span>&gt;Marketing emails&lt;/label&gt;
                &lt;p className=<span class="hljs-string">"text-sm text-muted-foreground"</span>&gt;
                  Receive emails about new products, features, and more.
                &lt;/p&gt;
              &lt;/div&gt;
              &lt;Checkbox /&gt;
            &lt;/div&gt;
          &lt;/CardContent&gt;
          &lt;CardFooter&gt;
            &lt;Button variant=<span class="hljs-string">"outline"</span>&gt;Update Preferences&lt;/Button&gt;
          &lt;/CardFooter&gt;
        &lt;/Card&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}
</code></pre>
<p>In this page, we have created two sections:</p>
<ol>
<li><p>Profile Section</p>
</li>
<li><p>Notification Section</p>
</li>
</ol>
<p>These two sections have been built using shadcn/ui components like Card, Footer, Checkbox, Avatar, Input, and so on.</p>
<p>At this point, we have:</p>
<ul>
<li><p>A dashboard layout with sidebar, header, breadcrumbs, and footer</p>
</li>
<li><p>A Dashboard page with charts, insights, and a transaction table</p>
</li>
<li><p>A Products page powered by:</p>
<ul>
<li><p>TanStack Start server functions</p>
</li>
<li><p>TanStack Query</p>
</li>
<li><p>TanStack Table</p>
</li>
</ul>
</li>
<li><p>A clean Settings page using shadcn/ui components</p>
</li>
</ul>
<h2 id="heading-live-demo-amp-source-code">Live Demo &amp; Source Code</h2>
<p>You can check out the full source code on GitHub here:</p>
<ul>
<li><p>GitHub Repository: <a target="_blank" href="https://github.com/themeselection/tanstack-dashboard-demo">https://github.com/themeselection/tanstack-dashboard-demo</a></p>
</li>
<li><p>Live Demo: <a target="_blank" href="https://tanstack-dashboard-demo.vercel.app/dashboard">https://tanstack-dashboard-demo.vercel.app/dashboard</a></p>
</li>
</ul>
<p>Feel free to clone, experiment, and extend it to fit your own application needs!</p>
<h2 id="heading-summary">Summary</h2>
<p>Congratulations! You've built a complete, production-ready admin dashboard using TanStack Start, TanStack Table, TanStack Query, Shadcn/ui, and shadcn/studio.</p>
<p>Throughout this tutorial, you’ve gained some hands-on experience in:</p>
<ul>
<li><p><strong>Full-stack application development with type safety</strong>: We’ve developed a full-stack application with TanStack Start's server functions with Zod validation to create type-safe APIs.</p>
</li>
<li><p><strong>Advanced data fetching</strong>: We’ve implemented TanStack Query for data fetching with automatic caching and background updates.</p>
</li>
<li><p><strong>Complex table interactions</strong>: We’ve built feature-rich data tables with TanStack Table, including server-side pagination, sorting, and filtering.</p>
</li>
<li><p><strong>Building UI quicker</strong>: We’ve leveraged shadcn/ui and shadcn/studio blocks to quickly build polished interfaces.</p>
</li>
<li><p><strong>Responsive layouts</strong>: And we’ve created adaptive designs that work seamlessly from mobile to desktop</p>
</li>
</ul>
<h3 id="heading-whats-next">What’s Next?</h3>
<p>Now that you have a solid foundation, consider implementing some or all of the below features if you want to work more on this:</p>
<ul>
<li><p><strong>Authentication</strong>: Add user authentication with Clerk, NextAuth, or Auth.js</p>
</li>
<li><p><strong>Real database</strong>: Replace mock data with Prisma + PostgreSQL or Drizzle + SQLite</p>
</li>
<li><p><strong>Form validation</strong>: Integrate React Hook Form with Zod for robust form handling</p>
</li>
<li><p><strong>Theming</strong>: Implement dark mode and custom color schemes using shadcn/ui's theming system</p>
</li>
<li><p><strong>API routes for CRUD</strong>: Add CRUD operations for products (create, update, delete)</p>
</li>
<li><p><strong>Internationalization:</strong> Make the dashboard compatible with multiple languages by integrating internationalization.</p>
</li>
</ul>
<p>We shipped a scalable and production-ready dashboard much faster than starting from scratch. Hope you enjoyed the process – and thanks for reading!</p>
<h3 id="heading-resources">Resources:</h3>
<ul>
<li><p><a target="_blank" href="https://tanstack.com/start">TanStack Start Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://tanstack.com/table">TanStack Table Docs</a></p>
</li>
<li><p><a target="_blank" href="https://tanstack.com/query">TanStack Query Docs</a></p>
</li>
<li><p><a target="_blank" href="https://shadcnstudio.com/components">Shadcn UI Components</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Set Up a Registry in shadcn ]]>
                </title>
                <description>
                    <![CDATA[ In this guide, you’ll learn how to set up a registry in shadcn. If you’re not familiar with this tool, shadcn is a collection of reusable and accessible components you can use in your projects. You’ll learn about essential concepts such as setting up... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-set-up-a-registry-in-shadcn/</link>
                <guid isPermaLink="false">68ff81486f611e7895c9f9ca</guid>
                
                    <category>
                        <![CDATA[ shadcn ]]>
                    </category>
                
                    <category>
                        <![CDATA[ shadcn ui ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Abhijeet Dave ]]>
                </dc:creator>
                <pubDate>Mon, 27 Oct 2025 14:27:20 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1761575215365/54597001-a10f-4a3d-a082-3eb5ac8b9a7d.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this guide, you’ll learn how to set up a registry in shadcn. If you’re not familiar with this tool, shadcn is a collection of reusable and accessible components you can use in your projects.</p>
<p>You’ll learn about essential concepts such as setting up and configuring the registry, adding authentication, CLI commands you can use, and more.</p>
<h2 id="heading-table-of-contents">Table of Contents:</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-a-registry-in-shadcn">What is a Registry in shadcn?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-and-configure-your-registry">How to Create and Configure Your Registry</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-namespace-system">Namespace System</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-authentication-for-private-registries">Authentication for Private Registries</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-cli-commands">CLI Commands</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-dependency-resolution">Dependency Resolution</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-error-handling">Error Handling</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-a-registry-in-shadcn"><strong>What is a registry in shadcn?</strong></h2>
<p>A <strong>registry</strong> in shadcn is a central place for sharing and managing your reusable components, utilities, and UI elements (along with other resources) across different projects. It lets developers give numbers to and organize components in a standard way. This makes it easier to integrate and share resources within and across teams.</p>
<p>The registry system helps make these components easily reusable. It also helps teams keep their code clean and manage dependencies more effectively.</p>
<p>shadcn's main system uses the <code>registry.json</code> file. The file provides key information about the registry, such as resource names and places along with the files that go with them.</p>
<h3 id="heading-why-use-a-shadcn-registry">Why use a shadcn registry?</h3>
<p>Using registries is helpful because it helps you standardize rules for your components. Every component, UI element, or utility follows a clear plan, which makes it easier to integrate and manage them.</p>
<p>Also, version numbers let you manage different versions of a component. This makes sure various parts work together and lets you update them without trouble.</p>
<p>Registries also give you the ability to organize resources into groups while managing what depends on what. Everything is flexible this way.</p>
<p>And if you create a registry, it lets you share your components with other developers (either everyone or just certain people). This allows for both inside and open-source work.</p>
<h2 id="heading-how-to-create-and-configure-your-registry">How to Create and Configure Your Registry</h2>
<p>Creating a shadcn registry involves setting up a configuration file (<code>registry.json</code>) at the root of your project. This file contains the metadata and structure of your registry, helping define components and their relationships.</p>
<p>shadcn provides this <a target="_blank" href="https://github.com/shadcn-ui/registry-template">starter template</a> to help you understand how registries work.</p>
<h3 id="heading-step-by-step-guide-to-create-a-registry">Step by Step Guide to Create a Registry</h3>
<h4 id="heading-1-define-the-registrys-metadata">1. Define the Registry's Metadata</h4>
<p>You’ll need to fill in the following information:</p>
<ul>
<li><p><code>name</code>: A unique name for the registry.</p>
</li>
<li><p><code>homepage</code>: A URL pointing to the homepage for the registry.</p>
</li>
<li><p><code>items</code>: An array that contains all available components, UI elements, or utilities in the registry.</p>
</li>
</ul>
<p>Here’s an example of a simple <code>registry.json</code>:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"$schema"</span>: <span class="hljs-string">"&lt;https://ui.shadcn.com/schema/registry.json&gt;"</span>,
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"acme"</span>,
  <span class="hljs-attr">"homepage"</span>: <span class="hljs-string">"&lt;https://acme.com&gt;"</span>,
  <span class="hljs-attr">"items"</span>: [
    <span class="hljs-comment">// Components will go here</span>
  ]
}
</code></pre>
<h4 id="heading-2-components-structure">2. Components Structure</h4>
<p>Each item in the registry can be a component, theme, hook, or utility. These items have the following properties:</p>
<ul>
<li><p><code>name</code>: Unique name of the component.</p>
</li>
<li><p><code>type</code>: Specifies the type of item (for example, <code>registry:component</code>, <code>registry:block</code>).</p>
</li>
<li><p><code>files</code>: An array of files that make up the component.</p>
</li>
</ul>
<p>Here’s an example:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"name"</span>,
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"registry:block"</span>,
  <span class="hljs-attr">"title"</span>: <span class="hljs-string">"title"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Simple description"</span>,
  <span class="hljs-attr">"files"</span>: [
    {
      <span class="hljs-attr">"path"</span>: <span class="hljs-string">"registry/new-york/..."</span>,
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"registry:component"</span>
    }
  ]
}
</code></pre>
<p>This component contains a simple button. You can keep adding more components to the <code>items</code> array.</p>
<h4 id="heading-3-adding-components">3. Adding Components</h4>
<p>After creating the <code>registry.json</code> file, you can add components to the registry. These components can be UI elements, functions, or utilities.</p>
<p><strong>Example 1: Adding a Simple Button Component</strong></p>
<p>First, create the component file. You can define your component in a separate directory. For example, we will create a <code>HelloWorld</code> component.</p>
<pre><code class="lang-json"><span class="hljs-comment">// registry/new-york/hello-world/hello-world.tsx</span>
import { Button } from <span class="hljs-string">"@/components/ui/button"</span>

export function HelloWorld() {
  return &lt;Button&gt;Hello World&lt;/Button&gt;
}
</code></pre>
<p>Reference the component in your <code>registry.json</code> like this:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"hello-world"</span>,
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"registry:block"</span>,
  <span class="hljs-attr">"title"</span>: <span class="hljs-string">"Hello World"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">"A simple hello world component."</span>,
  <span class="hljs-attr">"files"</span>: [
    {
      <span class="hljs-attr">"path"</span>: <span class="hljs-string">"registry/new-york/hello-world/hello-world.tsx"</span>,
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"registry:component"</span>
    }
  ]
}
</code></pre>
<p>The <code>"files"</code> key points to the path where the component is stored, while the <code>type</code> helps categorize the item.</p>
<p><strong>Example 2: Adding Multiple Components</strong></p>
<p>You can add multiple components to the registry as well. For instance, a button and a form could be part of a UI package:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"ui-kit"</span>,
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"registry:block"</span>,
  <span class="hljs-attr">"title"</span>: <span class="hljs-string">"UI Kit"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">"A collection of basic UI components."</span>,
  <span class="hljs-attr">"files"</span>: [
    {
      <span class="hljs-attr">"path"</span>: <span class="hljs-string">"registry/ui-kit/button/button.tsx"</span>,
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"registry:component"</span>
    },
    {
      <span class="hljs-attr">"path"</span>: <span class="hljs-string">"registry/ui-kit/form/form.tsx"</span>,
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"registry:component"</span>
    }
  ]
}
</code></pre>
<p>This modular approach allows for scalable development, enabling easy additions or updates to the registry without affecting other parts of the application.</p>
<blockquote>
<p>You can learn more about <a target="_blank" href="https://ui.shadcn.com/docs/registry/getting-started">registry basics</a> in the shadcn UI docs.</p>
</blockquote>
<h2 id="heading-namespace-system"><strong>Namespace System</strong></h2>
<p>Namespaces in shadcn help arrange components, utilities, themes, or other resources. The goal is to avoid conflicts and provide a good structure for your resources.</p>
<h3 id="heading-what-is-a-namespace">What is a Namespace?</h3>
<p>A namespace groups resources under a plain identifier, usually prefixed with an '@'. With this, you can separate different types of resources, teams, or even public versus private components.</p>
<p><strong>For instance:</strong></p>
<ul>
<li><p><code>@shadcn/button</code> could represent a button component from shadcn's registry.</p>
</li>
<li><p><code>@acme/auth-utils</code> could represent authentication utilities developed by the Acme company.</p>
</li>
</ul>
<h3 id="heading-how-to-configure-multiple-registries-using-namespaces">How to Configure Multiple Registries Using Namespaces</h3>
<p>You can configure multiple registries under different namespaces, which helps organize resources by type or team:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"registries"</span>: {
    <span class="hljs-attr">"@acme-ui"</span>: <span class="hljs-string">"&lt;https://registry.acme.com/ui/{name}.json&gt;"</span>,
    <span class="hljs-attr">"@acme-docs"</span>: <span class="hljs-string">"&lt;https://registry.acme.com/docs/{name}.json&gt;"</span>,
    <span class="hljs-attr">"@acme-ai"</span>: <span class="hljs-string">"&lt;https://registry.acme.com/ai/{name}.json&gt;"</span>,
    <span class="hljs-attr">"@acme-internal"</span>: {
      <span class="hljs-attr">"url"</span>: <span class="hljs-string">"&lt;https://internal.acme.com/registry/{name}.json&gt;"</span>,
      <span class="hljs-attr">"headers"</span>: {
        <span class="hljs-attr">"Authorization"</span>: <span class="hljs-string">"Bearer ${INTERNAL_TOKEN}"</span>
      }
    }
  }
}
</code></pre>
<p>This setup allows you to:</p>
<ul>
<li><p>Keep UI components, documentation, AI resources, and internal libraries separate.</p>
</li>
<li><p>Easily manage public and private resources within the same registry configuration.</p>
</li>
</ul>
<p>You can learn more about <a target="_blank" href="https://ui.shadcn.com/docs/registry/namespace#authentication--security"><strong>Namespace</strong></a> in the shadcn UI docs.</p>
<h2 id="heading-authentication-for-private-registries">Authentication for Private Registries</h2>
<p>If you have private registries, shadcn offers several authentication methods to ensure that only authorized users can access them. These include Bearer Token (OAuth 2.0), API Key, Basic Authentication, and Query Parameter Authentication. Let’s look at each one in more detail.</p>
<h3 id="heading-bearer-token-oauth-20">Bearer Token (OAuth 2.0)</h3>
<p>Bearer tokens are ideal for integrating with external APIs like GitHub or internal services that support OAuth 2.0.</p>
<p>You include the token in the <code>Authorization</code> header of the request. You typically get this token through an OAuth 2.0 flow, and it grants access to protected resources.</p>
<p>Here’s an example:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"@github"</span>: {
    <span class="hljs-attr">"url"</span>: <span class="hljs-string">"&lt;https://api.github.com/repos/org/registry/contents/{name}.json&gt;"</span>,
    <span class="hljs-attr">"headers"</span>: {
      <span class="hljs-attr">"Authorization"</span>: <span class="hljs-string">"Bearer ${GITHUB_TOKEN}"</span>
    }
  }
}
</code></pre>
<h3 id="heading-api-key">API Key</h3>
<p>You commonly use API keys for private NPM registries or internal APIs where a simple key is sufficient for access control.</p>
<p>An API key is included in the request headers, often under <code>X-API-Key</code>. This key is issued by the service and you should keep it confidential.</p>
<p><strong>Here’s an example</strong>:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"@private"</span>: {
    <span class="hljs-attr">"url"</span>: <span class="hljs-string">"&lt;https://api.company.com/registry/{name}&gt;"</span>,
    <span class="hljs-attr">"headers"</span>: {
      <span class="hljs-attr">"X-API-Key"</span>: <span class="hljs-string">"${API_KEY}"</span>
    }
  }
}
</code></pre>
<h3 id="heading-basic-authentication">Basic Authentication</h3>
<p>You’ll typically use basic authentication in legacy systems that require basic HTTP authentication.</p>
<p>The <code>Authorization</code> header contains a base64-encoded string of the format <code>username:password</code>. While it’s pretty easy to implement, it’s less secure than more modern methods like OAuth 2.0.</p>
<p><strong>Here’s an example</strong>:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"@internal"</span>: {
    <span class="hljs-attr">"url"</span>: <span class="hljs-string">"&lt;https://registry.company.com/{name}.json&gt;"</span>,
    <span class="hljs-attr">"headers"</span>: {
      <span class="hljs-attr">"Authorization"</span>: <span class="hljs-string">"Basic ${BASE64_CREDENTIALS}"</span>
    }
  }
}
</code></pre>
<h3 id="heading-query-parameter-authentication">Query Parameter Authentication</h3>
<p>Query parameter auth is a simpler form of authentication using query parameters for APIs.</p>
<p>It works by passing authentication details as query parameters in the URL. While this is convenient, it’s less secure than other methods since query parameters can be exposed in logs or URLs.</p>
<p><strong>Here’s an example:</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"@secure"</span>: {
    <span class="hljs-attr">"url"</span>: <span class="hljs-string">"&lt;https://registry.example.com/{name}.json&gt;"</span>,
    <span class="hljs-attr">"params"</span>: {
      <span class="hljs-attr">"api_key"</span>: <span class="hljs-string">"${API_KEY}"</span>,
      <span class="hljs-attr">"client_id"</span>: <span class="hljs-string">"${CLIENT_ID}"</span>,
      <span class="hljs-attr">"signature"</span>: <span class="hljs-string">"${REQUEST_SIGNATURE}"</span>
    }
  }
}
</code></pre>
<h3 id="heading-multiple-authentication-methods">Multiple Authentication Methods</h3>
<p>Some registries require multiple authentication methods simultaneously – for example, a combination of a Bearer token and an API key.</p>
<p>The request includes multiple headers and possibly query parameters to satisfy all required authentication mechanisms. This is common in enterprise environments where different layers of security are enforced.</p>
<p><strong>Here’s an example</strong>:</p>
<pre><code class="lang-bash">{
  <span class="hljs-string">"@enterprise"</span>: {
    <span class="hljs-string">"url"</span>: <span class="hljs-string">"https://api.enterprise.com/v2/registry/{name}"</span>,
    <span class="hljs-string">"headers"</span>: {
      <span class="hljs-string">"Authorization"</span>: <span class="hljs-string">"Bearer <span class="hljs-variable">${ACCESS_TOKEN}</span>"</span>,
      <span class="hljs-string">"X-API-Key"</span>: <span class="hljs-string">"<span class="hljs-variable">${API_KEY}</span>"</span>,
      <span class="hljs-string">"X-Workspace-Id"</span>: <span class="hljs-string">"<span class="hljs-variable">${WORKSPACE_ID}</span>"</span>
    },
    <span class="hljs-string">"params"</span>: {
      <span class="hljs-string">"version"</span>: <span class="hljs-string">"latest"</span>
    }
  }
}
</code></pre>
<p>You can learn more about <a target="_blank" href="https://ui.shadcn.com/docs/registry/namespace#authentication--security"><strong>Authentication and Security</strong></a> in the shadcn UI docs.</p>
<h2 id="heading-cli-commands">CLI Commands</h2>
<p>The shadcn CLI lets you interact with the registry directly from the command line. With commands like <code>add</code>, <code>view</code>, <code>search</code>, and <code>list</code>, you can easily install and manage resources. Let’s look at some examples to see how this works.</p>
<h3 id="heading-install-resources-from-the-registry">Install Resources from the Registry</h3>
<p>Use the following commands to add resources to your project:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Install a specific component</span>
npx shadcn@latest add @acme/button

<span class="hljs-comment"># Install multiple components at once</span>
npx shadcn@latest add @acme/button @lib/utils @ai/prompt
</code></pre>
<p>These commands fetch the specified components from the registry and integrate them into your project, ensuring all necessary dependencies are also installed.</p>
<h3 id="heading-viewing-metadata">Viewing Metadata</h3>
<p>Before integrating a component, it's crucial to understand its structure and dependencies. The <code>view</code> command allows you to inspect a component's metadata:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># View a specific component</span>
npx shadcn@latest view @acme/button

<span class="hljs-comment"># View multiple components</span>
npx shadcn@latest view @acme/button @lib/utils @ai/prompt

<span class="hljs-comment"># View from a URL directly</span>
npx shadcn@latest view https://registry.example.com/button.json

<span class="hljs-comment"># View from a local file</span>
npx shadcn@latest view ./local-registry/button.json
</code></pre>
<p>So what does the <code>view</code> command display?</p>
<ul>
<li><p>Resource metadata: Information such as the component's name, type, and description.</p>
</li>
<li><p>Dependencies: Lists both direct and registry dependencies required by the component.</p>
</li>
<li><p>File contents: Displays the actual code that will be installed.</p>
</li>
<li><p>CSS variables and Tailwind configuration: Shows any styling configurations associated with the component.</p>
</li>
<li><p>Required environment variables: Lists any environment variables needed for the component to function correctly.</p>
</li>
</ul>
<p>This command is invaluable for reviewing a component's details before installation, ensuring compatibility and understanding its requirements.</p>
<h3 id="heading-searching-resources"><strong>Searching Resources</strong></h3>
<p>To discover components within a registry, you can use these commands:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Search a specific registry</span>
npx shadcn@latest search @v0

<span class="hljs-comment"># Search with a query</span>
npx shadcn@latest search @acme --query <span class="hljs-string">"auth"</span>

<span class="hljs-comment"># Search multiple registries</span>
npx shadcn@latest search @v0 @acme @lib

<span class="hljs-comment"># Limit results</span>
npx shadcn@latest search @v0 --<span class="hljs-built_in">limit</span> 10 --offset 20

<span class="hljs-comment"># List all items (alias for search)</span>
npx shadcn@latest list @acme
</code></pre>
<p>They help you find components based on criteria like registry, query terms, and result limits.</p>
<p>Learn more about <a target="_blank" href="https://ui.shadcn.com/docs/registry/namespace#cli-commands"><strong>CLI Commands</strong></a> in the shadcn UI docs.</p>
<h2 id="heading-dependency-resolution">Dependency Resolution</h2>
<p>The CLI automatically resolves and installs all dependencies from their respective registries.</p>
<p>Understanding how dependencies are resolved internally is important if you're developing registries or need to customize third-party resources.</p>
<p>In shadcn, components often rely on other resources from various registries. When you install a component, shadcn ensures that all its dependencies are also installed, even if they reside in different registries. This process is known as <strong>dependency resolution</strong>.</p>
<h3 id="heading-what-does-it-mean-to-resolve-dependencies"><strong>What Does It Mean to "Resolve Dependencies"?</strong></h3>
<p>So to be clear resolving dependencies involves identifying, fetching, and installing dependencies.</p>
<p>First, shadcn determines which components a resource requires to function correctly. Then it retrieves these dependent components from their respective registries. Finally, it ensures that all dependencies are installed before the main component, maintaining the correct order.</p>
<p>This process guarantees that when you install a component, all its prerequisites are also installed, ensuring smooth functionality.</p>
<h3 id="heading-understanding-topological-sorting-in-dependency-resolution"><strong>Understanding Topological Sorting in Dependency Resolution</strong></h3>
<p><strong>Topological sorting</strong> might sound complicated, but it's essentially a method of organizing tasks (or components) in a way that makes sure everything gets done in the right order.</p>
<p>Imagine you have a list of tasks, and some tasks depend on others to be completed first. For example, you can't make a cake without first measuring out and then mixing the ingredients. So, the tasks “measure ingredients” and "mix ingredients" need to be completed before "bake the cake."</p>
<p>In the context of shadcn, topological sorting works in a similar way to organizing the installation of components:</p>
<ul>
<li><p>Each component (like <code>dashboard</code>) can depend on other components (like <code>@shadcn/card</code> or <code>@acme/data-table</code>).</p>
</li>
<li><p>Topological sorting arranges the components so that each one is installed only after the components it depends on have been installed.</p>
</li>
</ul>
<h4 id="heading-why-is-topological-sorting-important">Why Is Topological Sorting Important?</h4>
<p>Topological sorting takes care of a couple key things. First, it makes sure that components are installed in the correct order. For example, if Component A depends on Component B, then Component B will be installed first, followed by Component A.</p>
<p>It also prevents circular dependencies. If two components depend on each other, topological sorting detects this and prevents a never-ending loop (also called a circular dependency).</p>
<h4 id="heading-example-of-dependency-resolution">Example of Dependency Resolution:</h4>
<p>Consider the following component with its dependencies:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"dashboard"</span>,
  <span class="hljs-attr">"registryDependencies"</span>: [
    <span class="hljs-string">"@shadcn/card"</span>,
    <span class="hljs-string">"@v0/chart"</span>,
    <span class="hljs-string">"@acme/data-table"</span>
  ]
}
</code></pre>
<p>In this example, we have a <strong>component</strong>: <code>dashboard</code> and its <strong>dependencies</strong>: <code>@shadcn/card</code>, <code>@v0/chart</code>, and <code>@acme/data-table</code>.</p>
<p>In this case, shadcn will first identify the dependencies by recognizing that <code>dashboard</code> depends on <code>@shadcn/card</code>, <code>@v0/chart</code>, and <code>@acme/data-table</code>. Then it will fetch these components from their respective registries. Finally, it’ll install <code>@shadcn/card</code>, <code>@v0/chart</code>, and <code>@acme/data-table</code> first, before installing <code>dashboard</code>, ensuring all prerequisites are met.</p>
<p>You can learn more about <a target="_blank" href="https://ui.shadcn.com/docs/registry/namespace#authentication--security">Dependency Resolution</a> in the shadcn UI docs.</p>
<h2 id="heading-error-handling"><strong>Error Handling</strong></h2>
<p>shadcn’s CLI is equipped to handle several types of errors. Here are some common scenarios and how to resolve them:</p>
<h3 id="heading-common-errors"><strong>Common Errors</strong></h3>
<p><strong>1. Unknown Registry</strong></p>
<p>This error occurs when the registry isn’t defined in the configuration.</p>
<p>Here’s an example:</p>
<pre><code class="lang-json">Error: Unknown registry <span class="hljs-string">"@non-existent"</span>
</code></pre>
<p><strong>Solution:</strong> To fix this, just add the registry in the <code>registries</code> section of your configuration.</p>
<p><strong>2. Missing Environment Variables</strong></p>
<p>If your registry requires certain environment variables that are not set, you'll get an error.</p>
<p>Here’s an example:</p>
<pre><code class="lang-json">Registry <span class="hljs-string">"@private"</span> requires REGISTRY_TOKEN
</code></pre>
<p><strong>Solution:</strong> To fix this, just add the required environment variables to <code>.env</code> or <code>.env.local</code>.</p>
<p><strong>3. 404 Not Found</strong></p>
<p>The resource may not exist or the URL could be incorrect.</p>
<p>Here’s an example:</p>
<pre><code class="lang-json">Error: The resource was not found at &lt;https:<span class="hljs-comment">//api.company.com/button.json&gt;</span>
</code></pre>
<p><strong>4. Authentication Failures (401/403)</strong></p>
<p>If you’re not authorized to access a resource, you’ll see 401 or 403 errors.</p>
<p>To fix this, make sure your tokens, API keys, or credentials are valid.</p>
<p>You can learn more about <a target="_blank" href="https://ui.shadcn.com/docs/registry/namespace#authentication--security">Error Handling</a> in the shadcn UI docs.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>The shadcn registry system provides a good, modular solution for managing and sharing components or utilities across projects. If you’re looking to explore practical implementations, platforms like <a target="_blank" href="https://shadcnstudio.com/">shadcn/studio</a> which showcase how you can leverage <a target="_blank" href="https://shadcnstudio.com/components">shadcn components</a> to build sleek, modern UI solutions with minimal setup.</p>
<p>With its structured approach to dependencies, flexible namespaces, good authentication, and CLI commands, registries enable teams to share secure resources and customize them along the way.</p>
<p>I have prepared this article with the help of <a target="_blank" href="https://github.com/PruthviPraj00">Pruthvi Prajapati</a>, a front-end developer with 3 years of experience.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Make a Dropdown Menu with shadcn/ui ]]>
                </title>
                <description>
                    <![CDATA[ Dropdown menus are little pop-up menus that help you show more options without cluttering your screen. They’re super helpful in websites and apps. In this guide, you’ll learn how to build a dropdown menu using shadcn/ui. It’s a tool that works well w... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/shadcn-ui-dropdown-menu/</link>
                <guid isPermaLink="false">687964f6562b6ce291fda4e2</guid>
                
                    <category>
                        <![CDATA[ shadcn ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Tailwind CSS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ UI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ajay Kalal ]]>
                </dc:creator>
                <pubDate>Thu, 17 Jul 2025 21:02:46 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1752786132476/fef60fd2-ad5e-4f9d-9dcf-de4b99adac99.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Dropdown menus are little pop-up menus that help you show more options without cluttering your screen. They’re super helpful in websites and apps.</p>
<p>In this guide, you’ll learn how to build a dropdown menu using shadcn/ui. It’s a tool that works well with Tailwind CSS and Radix UI to help you make nice-looking, easy-to-use menus.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-shadcnui">What is shadcn/ui?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-use-shadcnui-for-dropdowns">Why Use shadcn/ui for Dropdowns?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-lets-build-a-dropdown-step-by-step">Let’s Build a Dropdown Step-by-Step</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-step-1-start-a-new-project">Step 1: Start a New Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-add-the-dropdown-menu-component">Step 2: Add the Dropdown Menu Component</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-import-what-you-need">Step 3: Import What You Need</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-4-build-a-simple-dropdown">Step 4: Build a Simple Dropdown</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-5-make-it-look-better">Step 5: Make It Look Better</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-6-make-it-work-on-all-screens">Step 6: Make It Work on All Screens</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-7-add-cool-icons">Step 7: Add Cool Icons</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-8-its-already-accessible">Step 8: It’s Already Accessible!</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-real-world-use-case-country-dropdown-with-flags">Real-World Use Case: Country Dropdown with Flags</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-final-thoughts">Final Thoughts</a></p>
</li>
</ul>
<h3 id="heading-prerequisites">💡 Prerequisites</h3>
<p>Before we start, make sure you have:</p>
<ul>
<li><p>Basic knowledge of React and JavaScript</p>
</li>
<li><p>Node.js and a package manager like npm, pnpm, or yarn are installed</p>
</li>
<li><p>Familiarity with Tailwind CSS is a bonus, but not required</p>
</li>
</ul>
<p>We’ll walk through everything step by step, so don’t worry if you’re not an expert yet.</p>
<h2 id="heading-what-is-shadcnui">What is shadcn/ui?</h2>
<p><a target="_blank" href="https://ui.shadcn.com/docs/installation">shadcn/ui</a> is a group of tools (called components) that help you build parts of a website, like buttons, modals, and dropdowns. It’s built with Radix UI and styled using Tailwind CSS. It’s perfect if you’re using React or Next.js.</p>
<p>With shadcn/ui, you don’t get just styled components, you get full control over how everything works and looks. That makes it perfect for teams that want consistency in design without giving up flexibility.</p>
<h3 id="heading-why-use-shadcnui-for-dropdowns">Why Use shadcn/ui for Dropdowns?</h3>
<p>Dropdown menus are a great use case for shadcn/ui because:</p>
<ul>
<li><p>It’s easy to use with keyboard and screen readers</p>
</li>
<li><p>You can create custom looks using Tailwind CSS</p>
</li>
<li><p>You control how it works and looks</p>
</li>
<li><p>It works great in real websites and apps</p>
</li>
<li><p>It integrates well with modern React workflows</p>
</li>
</ul>
<h2 id="heading-lets-build-a-dropdown-step-by-step">Let’s Build a Dropdown Step-by-Step</h2>
<h3 id="heading-step-1-start-a-new-project-with-shadcnui">Step 1: Start a New Project with shadcn/ui</h3>
<p>You don’t need to set up React, Next.js, or Tailwind manually. Just run this command:</p>
<pre><code class="lang-bash">pnpm dlx shadcn@latest init
</code></pre>
<p>This will automatically create a new Next.js app with Tailwind CSS and shadcn/ui preconfigured.</p>
<p>Tip: You can also use <code>npx</code> instead of <code>pnpm dlx</code> if you prefer:</p>
<pre><code class="lang-bash">npx shadcn@latest init
</code></pre>
<h3 id="heading-step-2-add-the-dropdown-menu-component">Step 2: Add the Dropdown Menu Component</h3>
<p>After your project is ready, add the dropdown component using:</p>
<pre><code class="lang-bash">npx shadcn@latest add dropdown-menu
</code></pre>
<p>This will pull in all the necessary components to create a dropdown menu.</p>
<h3 id="heading-step-3-import-what-you-need">Step 3: Import What You Need</h3>
<p>In your React file, import the full dropdown module so you can access all its features:</p>
<pre><code class="lang-tsx">import {
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuGroup,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuPortal,
} from "@/components/ui/dropdown-menu"
</code></pre>
<h3 id="heading-step-4-build-a-simple-dropdown">Step 4: Build a Simple Dropdown</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752690572839/4cb2bd61-b843-4fe3-8530-4b341d38a633.jpeg" alt="Screenshot of basic dropdown we're building" class="image--center mx-auto" width="630" height="577" loading="lazy"></p>
<p>Here’s a basic dropdown example:</p>
<pre><code class="lang-tsx">export function ProfileMenu() {
  return (
    &lt;DropdownMenu&gt;
      &lt;DropdownMenuTrigger asChild&gt;
        &lt;button className="px-4 py-2 bg-primary text-white rounded"&gt;
          Open Menu
        &lt;/button&gt;
      &lt;/DropdownMenuTrigger&gt;
      &lt;DropdownMenuContent className="w-56"&gt;
        &lt;DropdownMenuLabel&gt;My Account&lt;/DropdownMenuLabel&gt;
        &lt;DropdownMenuSeparator /&gt;
        &lt;DropdownMenuItem&gt;Profile&lt;/DropdownMenuItem&gt;
        &lt;DropdownMenuItem&gt;Settings&lt;/DropdownMenuItem&gt;
        &lt;DropdownMenuItem&gt;Log out&lt;/DropdownMenuItem&gt;
      &lt;/DropdownMenuContent&gt;
    &lt;/DropdownMenu&gt;
  )
}
</code></pre>
<p>This is just the start. You can add groups, submenus, and keyboard shortcuts for power users.</p>
<h3 id="heading-step-5-make-it-look-better">Step 5: Make It Look Better</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752690441156/0c2b8e39-72ca-4823-8dd2-6af305f02275.jpeg" alt="Screenshot showing dropdown with styling applied" class="image--center mx-auto" width="671" height="601" loading="lazy"></p>
<p>Use Tailwind CSS to style your dropdown, and hover effects like this:</p>
<pre><code class="lang-tsx">&lt;DropdownMenu&gt;
        &lt;DropdownMenuTrigger asChild&gt;
          &lt;button className="px-3 py-1.5 bg-primary text-white text-sm font-medium rounded-md hover:bg-primary/90 transition-colors"&gt;
            Open Menu
          &lt;/button&gt;
        &lt;/DropdownMenuTrigger&gt;
        &lt;DropdownMenuContent className="w-52 border-gray-200 shadow-lg rounded-md space-y-0.5"&gt;
          &lt;DropdownMenuLabel className="text-xs text-gray-500"&gt;
            My Account
          &lt;/DropdownMenuLabel&gt;
          &lt;DropdownMenuSeparator className="border-t border-gray-100" /&gt;
          &lt;DropdownMenuItem className="px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-100 rounded-md cursor-pointer transition-colors"&gt;
            Profile
          &lt;/DropdownMenuItem&gt;
          &lt;DropdownMenuItem className="px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-100 rounded-md cursor-pointer transition-colors"&gt;
            Settings
          &lt;/DropdownMenuItem&gt;
          &lt;DropdownMenuItem className="px-3 py-1.5 text-sm text-red-600 hover:bg-red-50 rounded-md cur
</code></pre>
<h3 id="heading-step-6-make-it-work-on-all-screens">Step 6: Make It Work on All Screens</h3>
<p>Want your dropdown to be responsive? Use Tailwind’s responsive classes:</p>
<pre><code class="lang-tsx">&lt;DropdownMenuContent className="w-full md:w-64"&gt;
</code></pre>
<p>You can also dynamically position the dropdown using Radix's built-in portal support.</p>
<h3 id="heading-step-7-add-cool-icons">Step 7: Add Cool Icons</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752691587711/0a96c5ca-0fa2-4916-92d2-087f2071d40e.jpeg" alt="Screenshot of dropdown with icons added" class="image--center mx-auto" width="618" height="565" loading="lazy"></p>
<p>Install Lucide icons:</p>
<pre><code class="lang-bash">npm install lucide-react
</code></pre>
<p>Then use them in your menu:</p>
<pre><code class="lang-tsx">import { User, Settings, LogOut } from "lucide-react"

&lt;DropdownMenuItem&gt;
  &lt;User className="mr-2 h-4 w-4" /&gt; Profile
&lt;/DropdownMenuItem&gt;
</code></pre>
<p>Icons help users scan options quickly – a great touch for UX.</p>
<h3 id="heading-step-8-its-already-accessible">Step 8: It’s Already Accessible!</h3>
<p>shadcn/ui (thanks to Radix UI) makes your dropdown menu:</p>
<ul>
<li><p>Keyboard friendly</p>
</li>
<li><p>Screen-reader ready</p>
</li>
<li><p>Following best web practices</p>
</li>
</ul>
<p>You don’t need to configure accessibility – it just works :)</p>
<h2 id="heading-real-world-use-case-country-dropdown-with-flags">Real-World Use Case: Country Dropdown with Flags</h2>
<p>Looking for a more advanced dropdown? Here’s an amazing example that includes search, flag icons, and grouping:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752598285627/6cb8b27e-7cba-4d92-95c5-3bea44e0c01c.png" alt="Shadcn dropdown example" class="image--center mx-auto" width="879" height="483" loading="lazy"></p>
<p>👉 <a target="_blank" href="https://shadcn-country-dropdown.vercel.app/">shadcn-country-dropdown.vercel.app</a></p>
<p>It’s open-source and a great place to see what’s possible with shadcn/ui.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>Using shadcn/ui to create a dropdown menu is fast, simple, and powerful. You get great styling, accessibility, and full control over how things look and work. Whether you’re just starting out or building for production, this is a solid tool to use.</p>
<p>Dropdowns are just the beginning. shadcn/ui offers a whole library of headless components for building modern UIs.</p>
<p>I hope you found this article helpful! If you're building a SaaS product or any web app that involves user interaction or conversion, consider enhancing user trust with real-time notifications like modal pop-ups, <a target="_blank" href="http://toastie.saasindie.com">sales pop up</a>, etc.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Mastering Shadcn UI Components ]]>
                </title>
                <description>
                    <![CDATA[ We just published a course on the freeCodeCamp.org YouTube channel that will help you master Shadcn. This comprehensive course dives deep into the versatile world of Shadcn, a powerful toolset for building modern web applications with customizable an... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/mastering-shadcn-ui-components/</link>
                <guid isPermaLink="false">667ee3fd8663f80bd6d96481</guid>
                
                    <category>
                        <![CDATA[ shadcn ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Fri, 28 Jun 2024 16:25:33 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1719591920182/350c938e-9faa-4292-9a9c-826cbcf43bc7.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>We just published a course on the <a target="_blank" href="http://freeCodeCamp.org">freeCodeCamp.org</a> YouTube channel that will help you master Shadcn. This comprehensive course dives deep into the versatile world of Shadcn, a powerful toolset for building modern web applications with customizable and reusable components. Mathew from CodeByDesign designed this course.</p>
<h3 id="heading-what-is-shadcn">What is Shadcn?</h3>
<p>Shadcn is not your typical component library. Instead of installing it as a dependency through npm, Shadcn offers a collection of beautifully designed, accessible, and customizable React components that you can copy and paste directly into your project. This unique approach allows developers to have complete control over their components, enabling greater customization and integration flexibility.</p>
<p>The Shadcn components are designed to be easily integrated into any React or Next.js project. They come with built-in styling options and support theming through CSS variables or Tailwind CSS utility classes, making it simple to maintain a consistent look and feel across your application.</p>
<p>This course covers all the main UI components provided by Shadcn, offering a thorough introduction and practical examples to help you get started quickly.</p>
<h3 id="heading-why-learn-shadcn">Why Learn Shadcn?</h3>
<p>Shadcn offers a flexible and efficient way to build modern web applications with React and Next.js. By mastering Shadcn, you can:</p>
<ul>
<li><p>Speed up your development process by reusing high-quality, pre-built components.</p>
</li>
<li><p>Maintain complete control over your component styling and behavior.</p>
</li>
<li><p>Ensure your application is accessible and user-friendly with components designed with accessibility in mind.</p>
</li>
</ul>
<p>Whether you are a beginner looking to get started with React UI components or an experienced developer aiming to enhance your toolkit, this course provides valuable insights and practical knowledge to help you succeed. Watch the <a target="_blank" href="https://youtu.be/oidnyW71W0A">course on the freeCodeCamp.org YouTube channel</a> (3-hour watch).</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/oidnyW71W0A" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
