<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Preet Suthar</title>
    <description>The latest articles on DEV Community by Preet Suthar (@preetsuthar17).</description>
    <link>https://dev.to/preetsuthar17</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1128396%2F537a9e4d-d47c-410a-9595-550ea19e7393.png</url>
      <title>DEV Community: Preet Suthar</title>
      <link>https://dev.to/preetsuthar17</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/preetsuthar17"/>
    <language>en</language>
    <item>
      <title>How to build a shadcn/ui registry that Claude Code and Cursor can actually use</title>
      <dc:creator>Preet Suthar</dc:creator>
      <pubDate>Fri, 16 Oct 2026 04:25:32 +0000</pubDate>
      <link>https://dev.to/preetsuthar17/how-to-build-a-shadcnui-registry-that-claude-code-and-cursor-can-actually-use-19n3</link>
      <guid>https://dev.to/preetsuthar17/how-to-build-a-shadcnui-registry-that-claude-code-and-cursor-can-actually-use-19n3</guid>
      <description>&lt;p&gt;Most shadcn/ui components are now installed by an AI coding agent, not a person typing the command. If you publish a registry, the agent is your user. It needs to find your components, read their props without guessing, and run the right install command. A registry that only works for humans clicking through a docs site is a registry that gets misused.&lt;/p&gt;

&lt;p&gt;I run &lt;a href="https://hextaui.com" rel="noopener noreferrer"&gt;HextaUI&lt;/a&gt;, a shadcn/ui registry, and I spent a chunk of this year making it work for Claude Code, Cursor and the shadcn MCP server. This is everything that turned out to matter, in the order I would do it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Ship a registry that passes validation
&lt;/h2&gt;

&lt;p&gt;Start with the basics. Every item is a JSON file that follows the registry schema, with &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;files&lt;/code&gt;, &lt;code&gt;dependencies&lt;/code&gt; and &lt;code&gt;registryDependencies&lt;/code&gt;. Put the index at a stable URL and every item at a predictable one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://your-site.com/r/registry.json
https://your-site.com/r/button.json
https://your-site.com/r/all.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;shadcn build&lt;/code&gt; to generate them from a &lt;code&gt;registry.json&lt;/code&gt; source, and validate before you deploy. The CLI gained a registry validate step in May 2026. A registry that fails validation fails silently for agents, who will then write the component by hand and get it wrong.&lt;/p&gt;

&lt;p&gt;Two details that make a real difference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;registryDependencies&lt;/code&gt; with full URLs.&lt;/strong&gt; When your Dialog depends on your Button, point to &lt;code&gt;https://your-site.com/r/button.json&lt;/code&gt;, not &lt;code&gt;button&lt;/code&gt;, or the CLI pulls the shadcn/ui Button instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An &lt;code&gt;all.json&lt;/code&gt; item&lt;/strong&gt; that depends on everything. Agents asked to "set up the design system" will reach for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Register a namespace
&lt;/h2&gt;

&lt;p&gt;Since the registry namespace feature landed, a user adds this to &lt;code&gt;components.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"registries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@hextaui"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://hextaui.com/r/{name}.json"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then installs with a short name:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Document this snippet on your installation page and in your &lt;code&gt;llms.txt&lt;/code&gt;. The short form is what the shadcn MCP server searches across, so a registry without a namespace is invisible to the one tool most agents use to browse registries.&lt;/p&gt;

&lt;p&gt;If the registry is private, the object form takes headers with environment variables that the CLI expands and never logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"registries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@acme-pro"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://acme.com/r/pro/{name}.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer ${ACME_TOKEN}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Give every docs page a Markdown twin
&lt;/h2&gt;

&lt;p&gt;Agents fetch HTML badly. They fetch Markdown well. Make every docs URL answer at &lt;code&gt;.md&lt;/code&gt; with the same content: description, install command, full source, usage, examples, keyboard support and the API table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://hextaui.com/docs/button      -&amp;gt; HTML for people
https://hextaui.com/docs/button.md   -&amp;gt; Markdown for agents
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not hand-write the Markdown. Generate it from the same source as the page so the two never drift. On HextaUI the &lt;code&gt;.md&lt;/code&gt; route walks the page's React tree and serializes it, so adding an example to the page adds it to the Markdown for free.&lt;/p&gt;

&lt;p&gt;The API reference matters most here. An agent that can read a props table does not hallucinate a &lt;code&gt;size="xl"&lt;/code&gt; that does not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Publish llms.txt and llms-full.txt
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;llms.txt&lt;/code&gt; is a Markdown index at your site root: what the project is, when to use it, when not to, how to install, and one line per docs page with its &lt;code&gt;.md&lt;/code&gt; link. &lt;code&gt;llms-full.txt&lt;/code&gt; is every docs page concatenated, for agents that want to load everything once.&lt;/p&gt;

&lt;p&gt;The "when not to use it" section is the part people skip and the part that saves you support tickets. If your registry is React and Tailwind only, say so, or an agent will try to install it into a Vue project.&lt;/p&gt;

&lt;p&gt;Keep the install command in the first ten lines. Agents read the top.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Run an MCP server, read-only, no key
&lt;/h2&gt;

&lt;p&gt;The shadcn CLI has its own MCP server (&lt;code&gt;npx shadcn@latest mcp&lt;/code&gt;) that searches any registry in &lt;code&gt;components.json&lt;/code&gt;. That covers installs. It does not cover docs, keyboard maps or examples.&lt;/p&gt;

&lt;p&gt;So I run a second one at &lt;code&gt;https://hextaui.com/mcp&lt;/code&gt;, Streamable HTTP, read-only, no API key, with five tools: &lt;code&gt;search_docs&lt;/code&gt;, &lt;code&gt;get_component&lt;/code&gt;, &lt;code&gt;get_component_api&lt;/code&gt;, &lt;code&gt;get_examples&lt;/code&gt; and &lt;code&gt;get_install_command&lt;/code&gt;. Claude Code connects with one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http hextaui https://hextaui.com/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cursor and VS Code take the same URL in their MCP config. Once connected, "add a HextaUI combobox with async results" turns into a docs search, an API read and the right install command, with no guessing in between.&lt;/p&gt;

&lt;p&gt;Make it discoverable too. Publish a server card at &lt;code&gt;/.well-known/mcp/server-card.json&lt;/code&gt; and a &lt;code&gt;server.json&lt;/code&gt; in the MCP registry format. Agents and the tools around them are starting to look there first.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Describe the HTTP surface with OpenAPI and real errors
&lt;/h2&gt;

&lt;p&gt;Everything above is also plain HTTP, and agents increasingly call HTTP directly. Publish an &lt;code&gt;openapi.json&lt;/code&gt; with operation ids and typed responses. Return errors as RFC 9457 &lt;code&gt;application/problem+json&lt;/code&gt; with a &lt;code&gt;resolution&lt;/code&gt; field that says what to do next:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://hextaui.com/docs/api#not-found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Component not found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"No component named 'modal'."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Try 'dialog' or 'alert-dialog'. The catalog is at /mcp/index.json."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An agent that gets a 404 with a hint recovers. An agent that gets an HTML error page writes the component from memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Ship a skill file
&lt;/h2&gt;

&lt;p&gt;Claude Code and a growing list of agents read &lt;code&gt;SKILL.md&lt;/code&gt; files that explain how to use a tool. Put one at &lt;code&gt;/.well-known/agent-skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt;: how to find a component, read its docs, and install it, with the exact commands. It is a page of Markdown and it is the highest-leverage page on the site.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Test with the agents, not with yourself
&lt;/h2&gt;

&lt;p&gt;Open a fresh Next.js project in Claude Code with your MCP server connected and no other context. Ask it to "add a date picker from  with a range and a disabled weekend rule". Watch what it does. Every wrong guess is a gap in your &lt;code&gt;.md&lt;/code&gt; docs or your &lt;code&gt;llms.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then do the same in Cursor with only &lt;code&gt;llms.txt&lt;/code&gt; and no MCP. Then with nothing but the URL. The registry should degrade gracefully through all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this gets you
&lt;/h2&gt;

&lt;p&gt;An agent with the MCP server or the &lt;code&gt;.md&lt;/code&gt; docs connected reads the props table instead of guessing it, runs the install command instead of rewriting the component, and installs the dependencies in the right order. The registry side of HextaUI was about a week of work. The agent side took longer, and it is the part that made the registry usable by the people who now do most of the installing.&lt;/p&gt;

&lt;p&gt;If you want to see all of it live: &lt;a href="https://hextaui.com/llms.txt" rel="noopener noreferrer"&gt;hextaui.com/llms.txt&lt;/a&gt;, any docs page with &lt;code&gt;.md&lt;/code&gt; appended, the &lt;a href="https://hextaui.com/docs/mcp" rel="noopener noreferrer"&gt;MCP docs&lt;/a&gt;, and the &lt;a href="https://hextaui.com/docs/api" rel="noopener noreferrer"&gt;API docs&lt;/a&gt; with the OpenAPI spec.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need my own MCP server if the shadcn CLI has one?&lt;/strong&gt; The shadcn MCP covers search and install across registries in &lt;code&gt;components.json&lt;/code&gt;. Your own server covers docs, APIs and examples. Most registries benefit from both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should the MCP server require an API key?&lt;/strong&gt; Not for public docs. Every key you require is an agent that gives up. Keep auth for private items and use bearer tokens through &lt;code&gt;components.json&lt;/code&gt; headers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I generate the Markdown docs without duplicating content?&lt;/strong&gt; Render the same component tree the page uses and serialize it to Markdown. One source, two formats.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What goes in llms.txt?&lt;/strong&gt; A description, when to use and when not to, the install command, the namespace snippet, and one line per docs page linking to its &lt;code&gt;.md&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>shadcn</category>
      <category>ai</category>
      <category>react</category>
      <category>devtools</category>
    </item>
    <item>
      <title>The states shadcn/ui leaves to you: loading, success, error, empty and reduced motion</title>
      <dc:creator>Preet Suthar</dc:creator>
      <pubDate>Fri, 16 Oct 2026 04:25:10 +0000</pubDate>
      <link>https://dev.to/preetsuthar17/the-states-shadcnui-leaves-to-you-loading-success-error-empty-and-reduced-motion-26ce</link>
      <guid>https://dev.to/preetsuthar17/the-states-shadcnui-leaves-to-you-loading-success-error-empty-and-reduced-motion-26ce</guid>
      <description>&lt;p&gt;Search "shadcn button loading state" and you will find a hundred snippets that do the same thing: add an &lt;code&gt;isLoading&lt;/code&gt; prop, swap the label for a spinner, disable the button. Ship that and your app will feel worse than before you added it.&lt;/p&gt;

&lt;p&gt;That is not a knock on shadcn/ui. It gives you a clean starting point and leaves the states to you, on purpose. But "the states" are where most of the feel of an interface lives, and almost nobody budgets time for them. This is the list of states I handle in every component I ship, and the specific timings that separate "finished" from "works".&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Loading that does not flicker
&lt;/h2&gt;

&lt;p&gt;The naive loading button shows a spinner the instant you click. Most requests finish in under 200ms, so what the user sees is a flash of spinner and then the result. It reads as jitter.&lt;/p&gt;

&lt;p&gt;Two rules fix it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wait before you show the spinner.&lt;/strong&gt; If the request finishes within about 150ms, never show it at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Once shown, keep it up for a minimum time.&lt;/strong&gt; Around 400ms. A spinner that appears for 30ms is worse than no spinner.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spinnerDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;minimumSpinnerTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a hook, not a prop. Here is the shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;buttonProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useButtonFeedback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="p"&gt;{...&lt;/span&gt;&lt;span class="nx"&gt;buttonProps&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;())}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;Save&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;track&lt;/code&gt; takes the promise. The hook decides whether the request was fast enough to skip the spinner, holds the spinner long enough not to flicker, and moves the button into success or error when the promise settles. The HextaUI &lt;a href="https://hextaui.com/docs/use-button-feedback" rel="noopener noreferrer"&gt;useButtonFeedback&lt;/a&gt; hook and &lt;a href="https://hextaui.com/docs/use-delayed-loading" rel="noopener noreferrer"&gt;useDelayedLoading&lt;/a&gt; do exactly this, and they are small enough to read in one sitting.&lt;/p&gt;

&lt;p&gt;The same delay rule applies to skeletons. A skeleton that appears for 50ms and gets replaced by the content is a flash of grey. Delay it by 150ms, and when the content arrives, fade it in without moving anything around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Success and error, in place
&lt;/h2&gt;

&lt;p&gt;After a save succeeds, the button should say so. "Saved" with a check, for about two seconds, then back to idle. After it fails, the button should say that too, and hold the error longer, about four seconds, because people need time to read an error and decide what to do.&lt;/p&gt;

&lt;p&gt;Two details that matter more than they sound:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Do not reset while the pointer is on the button.&lt;/strong&gt; If someone is hovering, they are reading. Reset after they leave.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignore clicks while the request is in flight.&lt;/strong&gt; Not with &lt;code&gt;disabled&lt;/code&gt;, which removes the button from the tab order and makes screen readers skip it. Use &lt;code&gt;aria-busy&lt;/code&gt; and ignore the click.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Switch has the same problem in a smaller space. Flip it, send the request, and if the request fails, flip it back. The user needs to see the rollback, so animate it, and show a brief pending state while the save is out.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Invalid, without punishing typing
&lt;/h2&gt;

&lt;p&gt;Shake the field when a submit attempt finds it invalid. Never shake while someone is still typing. The rule is the trigger: validation on submit or on blur gets the shake, validation on change does not.&lt;/p&gt;

&lt;p&gt;The shake itself should be short, three or four oscillations in about 300ms, and under &lt;code&gt;prefers-reduced-motion&lt;/code&gt; it becomes a color change on the border and nothing else.&lt;/p&gt;

&lt;p&gt;Native &lt;code&gt;:user-invalid&lt;/code&gt; helps here. It only matches after the user has interacted, so you can style the invalid state in CSS without a &lt;code&gt;touched&lt;/code&gt; flag in React.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Empty is a state, not an absence
&lt;/h2&gt;

&lt;p&gt;An empty list is where new users land first, and the default is a blank area with a header above it. Every empty state needs three things: an icon or illustration so the space reads as intentional, one sentence that says what will appear here, and the action that makes something appear.&lt;/p&gt;

&lt;p&gt;This is cheap to build and almost always skipped because the person building the feature has test data. Clear your database once in a while.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Reduced motion is not "no motion"
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;prefers-reduced-motion: reduce&lt;/code&gt; is on for a meaningful share of users, and the lazy implementation is a global &lt;code&gt;* { transition: none }&lt;/code&gt;. That removes the information motion was carrying. A dialog that snaps open with no fade is harder to parse than one that fades.&lt;/p&gt;

&lt;p&gt;The rule I use: anything that moves across the screen becomes a fade. Anything that fades stays a fade. Anything that loops stops. Durations get shorter, not zero.&lt;/p&gt;

&lt;p&gt;Put the check in one place so every component reads it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;prefersReducedMotion&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;(prefers-reduced-motion: reduce)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And share the easing curves and durations too. If every component picks its own &lt;code&gt;cubic-bezier&lt;/code&gt;, the app will feel like it was built by six people, because it was.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Motion you can interrupt
&lt;/h2&gt;

&lt;p&gt;Open an accordion, then close it halfway through the opening animation. In a lot of implementations, it finishes opening and then closes, or it jumps. The right behavior is to reverse from wherever it is.&lt;/p&gt;

&lt;p&gt;The Web Animations API makes this easy: keep a reference to the running animation, and on the next toggle, read its current time and animate from the current computed height. CSS transitions on &lt;code&gt;height: auto&lt;/code&gt; cannot do this, and &lt;code&gt;grid-template-rows: 0fr&lt;/code&gt; to &lt;code&gt;1fr&lt;/code&gt; can but is harder to reverse mid-way. Pick the approach that lets you interrupt.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Touch targets and the iOS zoom
&lt;/h2&gt;

&lt;p&gt;Two things break on phones that you will never see on a laptop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inputs under 16px zoom the page on iOS Safari.&lt;/strong&gt; Keep input text at 16px on coarse pointers even if your theme is 15px elsewhere. A single &lt;code&gt;@media (pointer: coarse)&lt;/code&gt; rule covers it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tap targets under 44px miss.&lt;/strong&gt; Buttons can be 32px tall on desktop and grow on touch screens through the same media query.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Right-to-left
&lt;/h2&gt;

&lt;p&gt;If you ever ship to Arabic or Hebrew users, every &lt;code&gt;ml-&lt;/code&gt; and &lt;code&gt;pl-&lt;/code&gt; and &lt;code&gt;left-&lt;/code&gt; in your components is a bug. Use logical properties: &lt;code&gt;ms-&lt;/code&gt;, &lt;code&gt;ps-&lt;/code&gt;, &lt;code&gt;start-&lt;/code&gt;. shadcn/ui added RTL support in January 2026, so new components you pull are fine, but the ones you copied in 2024 are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;None of these is hard. All of them together is a few weeks of work on a component set the size of shadcn/ui, and the work is invisible when it is done, which is why it gets cut.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://hextaui.com" rel="noopener noreferrer"&gt;HextaUI&lt;/a&gt; because I kept doing this work on every project. It is a free, MIT-licensed registry you add to a shadcn/ui project. Same CLI, same component names, same theme tokens, and every one of the states above already handled, down to the 150ms spinner delay. If you would rather own the code and write the states yourself, the hooks are standalone and you can install just those.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx shadcn@latest add https://hextaui.com/r/use-button-feedback.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Should I disable a button while loading?&lt;/strong&gt; Set &lt;code&gt;aria-busy="true"&lt;/code&gt; and ignore clicks. &lt;code&gt;disabled&lt;/code&gt; removes it from the tab order and hides it from screen readers mid-action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long should a success state show?&lt;/strong&gt; About two seconds, longer if the pointer is still on the control. Errors about four seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is a skeleton always better than a spinner?&lt;/strong&gt; A skeleton is better when you know the shape of what is coming. A spinner is better for actions. Both need the 150ms delay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does reduced motion mean I should remove all animation?&lt;/strong&gt; No. Replace movement with fades, stop loops and shorten durations. Keep the information.&lt;/p&gt;

</description>
      <category>react</category>
      <category>shadcn</category>
      <category>ux</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Vercel's AI Elements is being deprecated. Here are the alternatives for shadcn/ui AI chat UI</title>
      <dc:creator>Preet Suthar</dc:creator>
      <pubDate>Fri, 16 Oct 2026 04:25:05 +0000</pubDate>
      <link>https://dev.to/preetsuthar17/vercels-ai-elements-is-being-deprecated-here-are-the-alternatives-for-shadcnui-ai-chat-ui-4n5f</link>
      <guid>https://dev.to/preetsuthar17/vercels-ai-elements-is-being-deprecated-here-are-the-alternatives-for-shadcnui-ai-chat-ui-4n5f</guid>
      <description>&lt;p&gt;If you built an AI chat with Vercel's AI Elements this year, the repository now carries a notice: the library will be deprecated at the end of November 2026, replaced by AI Elements from shadcn/ui.&lt;/p&gt;

&lt;p&gt;This post covers what that means for your code, what the replacement is, and the other options if you are starting an AI chat UI with shadcn/ui in late 2026. I maintain one of those options, &lt;a href="https://hextaui.com" rel="noopener noreferrer"&gt;HextaUI&lt;/a&gt;, so I will say where it fits and where it does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  First: your code keeps working
&lt;/h2&gt;

&lt;p&gt;AI Elements was a shadcn-style registry. The components were copied into your project. There is no package to stop receiving updates, so nothing breaks on 1 December. The deprecation means no new components and no fixes from Vercel, and the docs will eventually point elsewhere.&lt;/p&gt;

&lt;p&gt;You have time. Do not rush a migration this week.&lt;/p&gt;

&lt;h2&gt;
  
  
  What replaces it
&lt;/h2&gt;

&lt;p&gt;In October 2026 shadcn/ui published its own &lt;a href="https://ui.shadcn.com/docs/ai-elements" rel="noopener noreferrer"&gt;AI Elements&lt;/a&gt;: 21 patterns for agentic interfaces built from shadcn/ui components. Chat, Prompt Input, Reasoning, Tool, Plan, Subagent and more. Each pattern page shows how to render it from AI SDK message parts. There is no package. You copy each pattern in.&lt;/p&gt;

&lt;p&gt;They ship alongside two new components, Trace and Code Block, that work across Base UI, React Aria and Radix.&lt;/p&gt;

&lt;p&gt;Migration from the Vercel library is meant to be low-friction. Export names and props are kept where the pattern was rebuilt, and each pattern page includes a migration prompt you can hand to an AI coding agent. Elements that were not rebuilt are not covered, so check the list against what you use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest difference between pieces and screens
&lt;/h2&gt;

&lt;p&gt;Every option below falls into one of two camps, and knowing which you want saves a week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pieces&lt;/strong&gt; give you a Message, a Prompt Input, a Tool component. You design the screen and decide what happens when a reply fails, when a stream is stopped, when a tool needs approval, when an upload dies. Both versions of AI Elements, and prompt-kit, are pieces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screens&lt;/strong&gt; give you the chat thread, composer, tool call list and settings pages with those decisions already made. You wire data in and restyle with your theme tokens. HextaUI's blocks and complete apps like Vercel's AI Chatbot template are screens.&lt;/p&gt;

&lt;p&gt;Pieces are free and flexible. Screens are faster and opinionated. Neither is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five options compared
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. AI Elements from shadcn/ui
&lt;/h3&gt;

&lt;p&gt;Free, MIT, maintained by the shadcn team, built for Base UI, React Aria and Radix. Pieces, not screens. The natural default if you want to compose your own and stay on the most-used registry. New as of October 2026, so expect the pattern list to grow and the rough edges to be sanded over the next few months.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. prompt-kit
&lt;/h3&gt;

&lt;p&gt;Free, MIT, installs with the shadcn CLI. Prompt Input, Message, Reasoning, Chain of Thought, Tool, File Upload and example blocks including a chat app with a history sidebar. Pieces, with good examples. Built on Radix, so a Base UI project carries both primitive layers while you use it.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. assistant-ui
&lt;/h3&gt;

&lt;p&gt;Free, MIT, with an optional paid cloud for saved threads. This one is different in kind: it is a chat framework with its own runtime that manages the conversation, plus headless primitives and styled components that read from it. Adapters for the AI SDK, LangGraph, LangChain, AG-UI and others. Pick it if you want the runtime to own the chat state. Skip it if your state already lives in &lt;code&gt;useChat&lt;/code&gt; or your own store and you do not want anything wrapping your app.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. shadcn-chatbot-kit and the AI Chatbot template
&lt;/h3&gt;

&lt;p&gt;Blazity's shadcn-chatbot-kit is a smaller set of chatbot components. Vercel's AI Chatbot is a complete Next.js app with sign-in and a database, which is the canonical reference if you want to see every piece wired. Both are useful to read even if you do not adopt them.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. HextaUI blocks
&lt;/h3&gt;

&lt;p&gt;Screens. A Chat Thread where failed replies offer Try again, stopped replies say so, and edits and retries become versions you can switch between. Tool Calls with every AI SDK tool state including approvals with a "do this instead" reason. Streaming and Markdown that never show half-typed syntax. A Prompt Input that is safe with Japanese and Chinese input, shows upload progress and retry, queues messages typed during a reply and keeps drafts across reloads. Plus the screens around a chat product: settings, models, API keys, billing, team, security.&lt;/p&gt;

&lt;p&gt;The components underneath are free and MIT. The AI blocks are HextaUI Pro, a one-time payment with every later block included, and Prompt Input is free. Built on Base UI, reads AI SDK message parts without importing the SDK. Not for you if you want everything free, or if you want Radix.&lt;/p&gt;

&lt;h2&gt;
  
  
  At a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Camp&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Primitives&lt;/th&gt;
&lt;th&gt;AI SDK&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AI Elements (shadcn/ui)&lt;/td&gt;
&lt;td&gt;Pieces&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Base UI, React Aria, Radix&lt;/td&gt;
&lt;td&gt;Used in examples&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI Elements (Vercel)&lt;/td&gt;
&lt;td&gt;Pieces&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Radix&lt;/td&gt;
&lt;td&gt;Required by setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prompt-kit&lt;/td&gt;
&lt;td&gt;Pieces&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Radix&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assistant-ui&lt;/td&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;Free, paid cloud&lt;/td&gt;
&lt;td&gt;Own&lt;/td&gt;
&lt;td&gt;Adapter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HextaUI&lt;/td&gt;
&lt;td&gt;Screens&lt;/td&gt;
&lt;td&gt;Components free, blocks paid once&lt;/td&gt;
&lt;td&gt;Base UI&lt;/td&gt;
&lt;td&gt;Optional, same parts shape&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How to decide
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You are starting fresh and want to compose your own: &lt;strong&gt;AI Elements from shadcn/ui&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;You already have Vercel's AI Elements in your tree: keep it until you need something new, then move pattern by pattern using the migration prompts.&lt;/li&gt;
&lt;li&gt;You want a runtime to manage threads, branching and persistence for you: &lt;strong&gt;assistant-ui&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;You want the chat and the product screens around it done, with the failure states handled, and you are fine paying once: &lt;strong&gt;HextaUI&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of these copy source into your project and use the shadcn/ui theme tokens, so you can mix them one screen at a time. The one caution is primitives: mixing a Radix-based kit into a Base UI project works, but you ship both.&lt;/p&gt;

&lt;p&gt;I keep a more detailed comparison with the full piece-by-piece mapping at &lt;a href="https://hextaui.com/compare/ai-elements" rel="noopener noreferrer"&gt;hextaui.com/compare/ai-elements&lt;/a&gt;, and a seven-way look at &lt;a href="https://hextaui.com/compare/shadcn-ai-chat-templates" rel="noopener noreferrer"&gt;shadcn AI chat templates&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;When exactly is Vercel's AI Elements deprecated?&lt;/strong&gt; The repository says the end of November 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will my existing AI Elements components break?&lt;/strong&gt; No. They live in your repository. They stop receiving updates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the new AI Elements a package?&lt;/strong&gt; No. It is a set of patterns you copy in with the shadcn CLI, the same model as shadcn/ui itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use HextaUI blocks without the Vercel AI SDK?&lt;/strong&gt; Yes. They take messages as props in the AI SDK's parts shape, and do not import the SDK.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>shadcn</category>
      <category>react</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>How to build an AI chat UI with shadcn/ui and the Vercel AI SDK that survives real users</title>
      <dc:creator>Preet Suthar</dc:creator>
      <pubDate>Fri, 16 Oct 2026 04:25:00 +0000</pubDate>
      <link>https://dev.to/preetsuthar17/how-to-build-an-ai-chat-ui-with-shadcnui-and-the-vercel-ai-sdk-that-survives-real-users-1ln3</link>
      <guid>https://dev.to/preetsuthar17/how-to-build-an-ai-chat-ui-with-shadcnui-and-the-vercel-ai-sdk-that-survives-real-users-1ln3</guid>
      <description>&lt;p&gt;The demo takes an afternoon. &lt;code&gt;useChat&lt;/code&gt; from the AI SDK, a list of messages, a textarea, and tokens stream onto the screen. Everyone on the team is impressed.&lt;/p&gt;

&lt;p&gt;Then a real person uses it. They hit Enter while composing Japanese and send half a word. They stop a reply and the UI shows nothing. A tool call wants approval mid-stream and there is no UI for that. They scroll up to re-read something and the view yanks them back to the bottom with every token.&lt;/p&gt;

&lt;p&gt;I have built this screen more times than I would like, most recently as the blocks behind &lt;a href="https://hextaui.com" rel="noopener noreferrer"&gt;HextaUI&lt;/a&gt;. This is the checklist of what a chat UI has to handle before it is done, with the shadcn/ui and AI SDK specifics for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start from message parts, not strings
&lt;/h2&gt;

&lt;p&gt;The AI SDK's &lt;code&gt;useChat&lt;/code&gt; returns messages made of parts: text, reasoning, tool calls with their state, sources, files. Render the parts, not a concatenated string. Every feature below depends on it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isToolUIPart&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isToolUIPart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ToolCall&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;part&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Markdown&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reasoning&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Thinking&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;source-url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Source&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tool parts are typed &lt;code&gt;tool-&amp;lt;name&amp;gt;&lt;/code&gt;, so &lt;code&gt;isToolUIPart&lt;/code&gt; is the reliable check.&lt;/p&gt;

&lt;p&gt;Keep your blocks free of the SDK import. Take parts as props. Then the same components work with a LangGraph backend or your own server once you map its output to the same shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  The composer
&lt;/h2&gt;

&lt;p&gt;The textarea is where most of the bugs live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enter sends, Shift+Enter breaks a line.&lt;/strong&gt; Everyone does this. Almost nobody handles composition. When someone types Japanese, Chinese or Korean, the IME is open and Enter confirms the candidate, not the message. Check &lt;code&gt;event.nativeEvent.isComposing&lt;/code&gt; and bail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onKeyDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;KeyboardEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLTextAreaElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Enter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shiftKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nativeEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isComposing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Grow with the text, within limits.&lt;/strong&gt; One line to start, up to about eight rows, then scroll. Animate the height change, and never through React state on every keystroke. Measure &lt;code&gt;scrollHeight&lt;/code&gt; in a layout effect and set the height on the DOM node. Any setState in a native input listener will eventually drop or delay keys under load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drafts survive a reload.&lt;/strong&gt; Save the draft to &lt;code&gt;localStorage&lt;/code&gt; keyed by chat id, debounced. People lose long prompts to accidental refreshes and never forgive you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queue messages typed during a reply.&lt;/strong&gt; If the model is still writing and the user sends another message, either disable sending or queue it and send when the stream ends. Silently dropping it is the worst option and it is the default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attachments show progress and retry.&lt;/strong&gt; Uploads fail. Show the failure on the attachment card with a retry, not as a toast that disappears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming that reads well
&lt;/h2&gt;

&lt;p&gt;Models emit tokens in bursts. If you render exactly what arrives, text stutters: nothing, then twelve words, then nothing. Smooth it. Reveal characters at a steady pace that speeds up when the buffer grows, so you never fall far behind the stream but never stutter either.&lt;/p&gt;

&lt;p&gt;Markdown during streaming has its own trap: half-typed syntax. The model has written &lt;code&gt;**bold&lt;/code&gt; and not yet closed it, so your renderer shows literal asterisks for a frame, then bold text. Hold back incomplete inline syntax until it closes, or until the stream ends.&lt;/p&gt;

&lt;p&gt;Code blocks need the same care. Highlight incrementally and never re-highlight the whole block on every token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop, retry, edit and versions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stop&lt;/strong&gt; should leave the partial reply on screen, with a visible "Stopped" marker so the user knows it is incomplete. Do not delete it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retry&lt;/strong&gt; on a failed reply is a button on the message, not a hidden gesture. Connect it to &lt;code&gt;regenerate()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edit&lt;/strong&gt; an earlier message creates a branch. Both the old and new reply exist. Show a small &lt;code&gt;1 / 2&lt;/code&gt; switcher on the message so the user can flip between versions. This is where a lot of homegrown chat UIs give up, and it is the feature people miss most from ChatGPT and Claude.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failed&lt;/strong&gt; replies get a reason and a retry. "Something went wrong" with no action is a dead end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scrolling
&lt;/h2&gt;

&lt;p&gt;Follow new tokens when the user is at the bottom. The moment they scroll up, stop following, and show a "jump to latest" button with a count of what they missed. When they jump, follow again.&lt;/p&gt;

&lt;p&gt;Loading older history at the top must not move the view. Measure the scroll height before and after you prepend, and adjust &lt;code&gt;scrollTop&lt;/code&gt; by the difference in the same frame.&lt;/p&gt;

&lt;p&gt;If the user sends a message, scroll so their message pins to the top of the viewport and the reply streams below it. That is how the two big chat apps behave now and users expect it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool calls and approvals
&lt;/h2&gt;

&lt;p&gt;The AI SDK gives each tool part a state: &lt;code&gt;input-streaming&lt;/code&gt;, &lt;code&gt;input-available&lt;/code&gt;, &lt;code&gt;output-available&lt;/code&gt;, &lt;code&gt;output-error&lt;/code&gt;, and with human-in-the-loop, &lt;code&gt;approval-requested&lt;/code&gt;, &lt;code&gt;approval-responded&lt;/code&gt; and &lt;code&gt;output-denied&lt;/code&gt;. Every state needs a look.&lt;/p&gt;

&lt;p&gt;Reads and searches can fold into a one-line summary. Edits, commands and errors should stay expanded. Approvals block the stream and need a clear Allow and Deny, plus a way to say "do this instead" that sends text back to the model as the denial reason.&lt;/p&gt;

&lt;p&gt;Show the result when it is useful. A file edit opens into a diff. A command opens into a terminal with the output. A search opens into results. A tool call that only says "Done" teaches users to ignore tool calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thinking and sources
&lt;/h2&gt;

&lt;p&gt;Reasoning parts should be collapsible and collapsed by default once the answer starts. While the model is still thinking, a live label that follows each step ("Searching docs", "Reading three files") is far more reassuring than a spinner.&lt;/p&gt;

&lt;p&gt;Citations belong next to the claims they support, inline, not in a list at the bottom nobody reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility you cannot skip
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;New messages announce through a &lt;code&gt;role="log"&lt;/code&gt; region, so screen reader users hear replies.&lt;/li&gt;
&lt;li&gt;The stop button is reachable by keyboard and has a label.&lt;/li&gt;
&lt;li&gt;Streaming text should not fire a hundred live-region announcements. Announce when the message completes, or at sentence boundaries.&lt;/li&gt;
&lt;li&gt;Every icon-only button has &lt;code&gt;aria-label&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;If you want the pieces, shadcn/ui shipped AI Elements in October 2026: 21 patterns you copy into your project and compose yourself, including Chat, Prompt Input, Reasoning and Tool. Vercel's earlier AI Elements library is being deprecated at the end of November 2026 in favor of them.&lt;/p&gt;

&lt;p&gt;If you want the screens already assembled with the states above worked out, that is what the &lt;a href="https://hextaui.com/blocks" rel="noopener noreferrer"&gt;HextaUI blocks&lt;/a&gt; are: a Chat Thread with stop, retry and versions, Tool Calls with every AI SDK state and approvals, Streaming and Markdown that never show half-typed syntax, and a Prompt Input that handles CJK input, uploads, queues and drafts. Prompt Input is free. The rest is a one-time purchase. All of it reads AI SDK message parts and none of it imports the SDK, so it works with any backend you can map.&lt;/p&gt;

&lt;p&gt;You can try the whole thing assembled as a chat app at &lt;a href="https://hextaui.com/blocks/hexta-ai" rel="noopener noreferrer"&gt;hextaui.com/blocks/hexta-ai&lt;/a&gt; before installing anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need the Vercel AI SDK to use these patterns?&lt;/strong&gt; No. The message-parts shape is the useful part. Any backend works once you map its output to parts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I handle Enter with an IME?&lt;/strong&gt; Check &lt;code&gt;event.nativeEvent.isComposing&lt;/code&gt; in your keydown handler and return early.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should streaming text update a live region on every token?&lt;/strong&gt; No. Announce at completion or at sentence boundaries, or screen readers become unusable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens to a stopped reply?&lt;/strong&gt; Keep the partial text and mark it as stopped. Never delete what the user has already read.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>react</category>
      <category>shadcn</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>shadcn/ui switched to Base UI. Here is what actually changes in your code</title>
      <dc:creator>Preet Suthar</dc:creator>
      <pubDate>Fri, 16 Oct 2026 04:24:28 +0000</pubDate>
      <link>https://dev.to/preetsuthar17/shadcnui-switched-to-base-ui-here-is-what-actually-changes-in-your-code-18li</link>
      <guid>https://dev.to/preetsuthar17/shadcnui-switched-to-base-ui-here-is-what-actually-changes-in-your-code-18li</guid>
      <description>&lt;p&gt;On 3 July 2026, &lt;code&gt;npx shadcn init&lt;/code&gt; started picking Base UI instead of Radix. If you have read the headlines and nothing else, you probably think one of two things: that Radix is dead, or that nothing changed. Neither is true.&lt;/p&gt;

&lt;p&gt;I maintain &lt;a href="https://hextaui.com" rel="noopener noreferrer"&gt;HextaUI&lt;/a&gt;, a shadcn/ui registry built on Base UI, so I have spent most of this year inside both primitive layers. This is the guide I wished existed in July: what changed, what did not, every API difference you will hit, and the order to migrate in if you decide to.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed, in one paragraph
&lt;/h2&gt;

&lt;p&gt;New shadcn/ui projects get Base UI primitives. Radix is still fully supported. The shadcn team said every update and new component ships for both libraries, unless a component only exists in Base UI. Existing projects are untouched. If you want Radix on a fresh project, you pass one flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx shadcn@latest init &lt;span class="nt"&gt;-b&lt;/span&gt; radix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have CI or scripts that call &lt;code&gt;init&lt;/code&gt; non-interactively and expect Radix, add that flag now, before a fresh checkout surprises you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the default moved
&lt;/h2&gt;

&lt;p&gt;Two reasons were given. Base UI had reached a stable 1.6.0 with over six million weekly downloads, and projects created through &lt;code&gt;shadcn/create&lt;/code&gt; were already choosing Base UI over Radix about two to one. The default followed what people were picking.&lt;/p&gt;

&lt;p&gt;Base UI is built by the people behind Radix, Material UI and Floating UI, and the lineage shows. It covers primitives Radix never shipped, such as Combobox, Autocomplete, Number Field, Toast and Preview Card. As of September 2026 it sits at 1.8.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you migrate an existing app?
&lt;/h2&gt;

&lt;p&gt;Probably not this quarter, and not all at once.&lt;/p&gt;

&lt;p&gt;Stay on Radix if the app is stable, you are not blocked on a missing primitive, and nobody on the team has a specific complaint. Nothing you depend on is going away.&lt;/p&gt;

&lt;p&gt;Migrate if you need Combobox or Number Field without reaching for a second library, if you are already rewriting the components for Tailwind v4 anyway, or if you want to be on the layer that new shadcn/ui components target first.&lt;/p&gt;

&lt;p&gt;Either way, read the next section before you decide, because the differences are not cosmetic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every API difference you will actually hit
&lt;/h2&gt;

&lt;p&gt;I grouped these by how they fail. The first group breaks your build. The second group compiles fine and changes behavior silently, which is worse.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build-breaking changes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;asChild&lt;/code&gt; becomes &lt;code&gt;render&lt;/code&gt;.&lt;/strong&gt; This is the one everyone meets first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Radix&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DialogTrigger&lt;/span&gt; &lt;span class="na"&gt;asChild&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Open&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;DialogTrigger&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// Base UI&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DialogTrigger&lt;/span&gt; &lt;span class="na"&gt;render&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Open&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;DialogTrigger&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;render&lt;/code&gt; prop takes an element or a function. The function form gives you the state, which is useful for things like a trigger that changes its icon when open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anchored popups gain a &lt;code&gt;Positioner&lt;/code&gt;.&lt;/strong&gt; Menu, Select, Popover and Tooltip go from &lt;code&gt;Portal &amp;gt; Content&lt;/code&gt; to &lt;code&gt;Portal &amp;gt; Positioner &amp;gt; Popup&lt;/code&gt;. Dialog has no Positioner; it uses &lt;code&gt;Backdrop&lt;/code&gt; and &lt;code&gt;Popup&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Overlay&lt;/code&gt; is now &lt;code&gt;Backdrop&lt;/code&gt;.&lt;/strong&gt; Rename and move on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Select values are nullable.&lt;/strong&gt; A controlled Select holds &lt;code&gt;Value | null&lt;/code&gt;, not &lt;code&gt;string&lt;/code&gt;. Your &lt;code&gt;useState&amp;lt;string&amp;gt;("")&lt;/code&gt; becomes &lt;code&gt;useState&amp;lt;string | null&amp;gt;(null)&lt;/code&gt;, and every place that read the value as a string needs a guard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accordion and Toggle Group values are always arrays.&lt;/strong&gt; Even in single mode, &lt;code&gt;value="a"&lt;/code&gt; becomes &lt;code&gt;value={["a"]}&lt;/code&gt;. Tabs keep a single value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;onOpenChange&lt;/code&gt; has a second argument.&lt;/strong&gt; Radix lets you call &lt;code&gt;event.preventDefault()&lt;/code&gt; to stop a close. Base UI passes &lt;code&gt;(open, details)&lt;/code&gt; and you call &lt;code&gt;details.cancel()&lt;/code&gt;. The &lt;code&gt;details.reason&lt;/code&gt; field tells you whether it was the Escape key, an outside press or something else, which is handy for "unsaved changes" dialogs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open-state selectors change.&lt;/strong&gt; &lt;code&gt;data-[state=open]:&lt;/code&gt; variants become &lt;code&gt;data-open:&lt;/code&gt;. This one hides in your &lt;code&gt;className&lt;/code&gt; strings and in &lt;code&gt;cva&lt;/code&gt; variants, so grep for &lt;code&gt;data-[state=&lt;/code&gt; before you call the migration done.&lt;/p&gt;

&lt;h3&gt;
  
  
  Silent behavior changes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Tabs activate manually by default.&lt;/strong&gt; In Radix, arrow keys move focus and activate the tab. In Base UI, arrow keys move focus and you press Enter or Space to activate. If your tests or users expect the Radix behavior, set &lt;code&gt;activateOnFocus&lt;/code&gt; on the list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkbox and radio menu items keep the menu open.&lt;/strong&gt; &lt;code&gt;closeOnClick&lt;/code&gt; defaults to &lt;code&gt;false&lt;/code&gt; for &lt;code&gt;CheckboxItem&lt;/code&gt; and &lt;code&gt;RadioItem&lt;/code&gt;. Plain items still close. This is arguably the better default, but it is a change your users will notice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Navigation menus open faster.&lt;/strong&gt; Base UI's hover &lt;code&gt;delay&lt;/code&gt; defaults to 50ms. Radix's &lt;code&gt;delayDuration&lt;/code&gt; defaulted to 200ms. Fast menus feel great until someone sweeps their mouse across the header and three panels fire in a row. Tune it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A migration order that does not hurt
&lt;/h2&gt;

&lt;p&gt;Keep both libraries installed. Move one component per commit. Run your tests between each. In this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Leaf components with no popup&lt;/strong&gt;: Switch, Checkbox, Slider, Toggle, Progress. Mostly rename work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tabs and Accordion.&lt;/strong&gt; Fix the array values and decide on &lt;code&gt;activateOnFocus&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dialog and Alert Dialog.&lt;/strong&gt; Learn the &lt;code&gt;Backdrop&lt;/code&gt; and &lt;code&gt;Popup&lt;/code&gt; structure and the &lt;code&gt;details.cancel()&lt;/code&gt; pattern here, where there is no Positioner to confuse things.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Popover and Tooltip.&lt;/strong&gt; Add the &lt;code&gt;Positioner&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Select, Combobox, Dropdown Menu, Context Menu.&lt;/strong&gt; Nullable values, &lt;code&gt;closeOnClick&lt;/code&gt;, and the whole popup tree. Do these last, with the most tests.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Replacing a component with the Base UI version is one CLI command, and your source is in git, so review the diff rather than trusting it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx shadcn@latest add dialog &lt;span class="nt"&gt;--overwrite&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;git diff components/ui/dialog.tsx&lt;/code&gt; and read every line before you keep it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would not do
&lt;/h2&gt;

&lt;p&gt;I would not run a codemod across the whole tree and hope. The build-breaking changes are easy to find. The silent ones are not, and a codemod cannot tell you that your settings page now needs two keystrokes to switch tabs.&lt;/p&gt;

&lt;p&gt;I would also not switch primitive layers and redesign the components in the same PR. The point of shadcn/ui is that you own the code. Owning it means reading the diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want the migration done for you
&lt;/h2&gt;

&lt;p&gt;HextaUI is a registry you add to a shadcn/ui project. Every component is already written on Base UI, keeps the shadcn/ui names and props, and uses the same theme tokens, so &lt;code&gt;npx shadcn@latest add https://hextaui.com/r/dialog.json --overwrite&lt;/code&gt; lands a Base UI Dialog in &lt;code&gt;components/ui&lt;/code&gt; next to your other files. The components are MIT and free. The &lt;a href="https://hextaui.com/compare/shadcn-ui" rel="noopener noreferrer"&gt;HextaUI vs shadcn/ui&lt;/a&gt; page lists what is different and when plain shadcn/ui is the better pick.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is Radix deprecated?&lt;/strong&gt; No. shadcn/ui ships every component for both libraries, and the team still runs Radix in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I mix Radix and Base UI components in one project?&lt;/strong&gt; Yes. You carry both dependencies while you migrate. Many projects will stay mixed for a long time and that is fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to change my theme tokens?&lt;/strong&gt; No. &lt;code&gt;--background&lt;/code&gt;, &lt;code&gt;--primary&lt;/code&gt;, &lt;code&gt;--radius&lt;/code&gt; and the rest are unchanged. The primitive layer is independent of the Tailwind side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Base UI support React Server Components?&lt;/strong&gt; Base UI primitives are client components, the same as Radix. Keep the &lt;code&gt;"use client"&lt;/code&gt; directive where shadcn/ui puts it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where is the official change log?&lt;/strong&gt; The July 2026 entry is at &lt;a href="https://ui.shadcn.com/docs/changelog/2026-07-base-ui-default" rel="noopener noreferrer"&gt;ui.shadcn.com/docs/changelog/2026-07-base-ui-default&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>react</category>
      <category>shadcn</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Typeform was too expensive so I built my own forms</title>
      <dc:creator>Preet Suthar</dc:creator>
      <pubDate>Fri, 11 Jul 2025 05:56:24 +0000</pubDate>
      <link>https://dev.to/preetsuthar17/typeform-was-too-expensive-so-i-built-my-own-forms-3oe8</link>
      <guid>https://dev.to/preetsuthar17/typeform-was-too-expensive-so-i-built-my-own-forms-3oe8</guid>
      <description>&lt;p&gt;I'm a solopreneur and run a web design agency.&lt;/p&gt;

&lt;p&gt;I create open-source apps, but I also work as a freelancer and designer. I was accepting any new freelance project via forms on my agency website.&lt;/p&gt;

&lt;p&gt;I was using Typeform, but as time went by and more people submitted forms, it got more and more expensive. That time, I thought to use Google Form, but it was way too blocky and looked very unprofessional on my agency website.&lt;/p&gt;

&lt;p&gt;So I thought to build my own forms for my own usage, and it turns out it almost doubled form submissions and inquiry calls.&lt;/p&gt;

&lt;p&gt;I was happy, so I thought to build it for everyone and make it open-source.&lt;/p&gt;

&lt;p&gt;I added AI functionalities using Vercel AISDK. I can generate forms almost instantly using AI and also added analytics AI so that users can talk with their forms—more like talk with their analytics data.&lt;/p&gt;

&lt;p&gt;[&lt;a href="https://ikiform.com%5D(https://ikiform.com" rel="noopener noreferrer"&gt;https://ikiform.com](https://ikiform.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've been building this publicly, sharing updates on my X account &lt;a href="https://x.com/preetsuthar17" rel="noopener noreferrer"&gt;preetsuthar17&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I hope this product will be as helpful to you as it was for me. Would love your feedback pls&lt;/p&gt;

&lt;p&gt;Preet&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Prevent Spam Bot Attacks in Form Submissions</title>
      <dc:creator>Preet Suthar</dc:creator>
      <pubDate>Sat, 24 May 2025 08:18:18 +0000</pubDate>
      <link>https://dev.to/preetsuthar17/prevent-spam-bot-attacks-in-form-submissions-493j</link>
      <guid>https://dev.to/preetsuthar17/prevent-spam-bot-attacks-in-form-submissions-493j</guid>
      <description>&lt;p&gt;Spam bot attacks on forms can be a nuisance, leading to spam submissions and potential security risks. Here's a simple method to prevent spam bot attacks in form submissions using a hidden field.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Add a Hidden Field
&lt;/h3&gt;

&lt;p&gt;Add a hidden field to your form. This field should be invisible to users but visible to bots. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;honey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setHoney&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;honey&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setHoney&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;tabIndex&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"-1"&lt;/span&gt;
  &lt;span class="na"&gt;area-hidden&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Validate the Hidden Field
&lt;/h3&gt;

&lt;p&gt;When processing the form submission, check if the hidden field is empty. If it's not empty, it's likely a bot submission.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;honey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// This is a bot submission&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Spam bot detected!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Process the form submission&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Form submitted successfully!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Complete Example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;honey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setHoney&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;honey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Spam bot detected!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Form submitted successfully!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleSubmit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;honey&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setHoney&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;tabIndex&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"-1"&lt;/span&gt;
        &lt;span class="na"&gt;area-hidden&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Submit&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Actual users won't see the hidden field, but bots will fill it out, allowing you to filter out spam submissions easily. This method is simple and effective for preventing spam bot attacks in form submissions.&lt;/p&gt;

&lt;p&gt;Actual Twitter thread: &lt;a href="https://x.com/preetsuthar17/status/1926188828169015371" rel="noopener noreferrer"&gt;link&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Add a badge to the filename in Fumadocs</title>
      <dc:creator>Preet Suthar</dc:creator>
      <pubDate>Sat, 24 May 2025 08:16:58 +0000</pubDate>
      <link>https://dev.to/preetsuthar17/add-a-badge-to-the-filename-in-fumadocs-2kk3</link>
      <guid>https://dev.to/preetsuthar17/add-a-badge-to-the-filename-in-fumadocs-2kk3</guid>
      <description>&lt;p&gt;This is the type of badge you can add to the filename in Fumadocs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpbs.twimg.com%2Fmedia%2FGqGozTTbAAAj1ay%3Fformat%3Dpng%26name%3D360x360" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpbs.twimg.com%2Fmedia%2FGqGozTTbAAAj1ay%3Fformat%3Dpng%26name%3D360x360" alt="image" width="286" height="132"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is very easy to add a badge to the filename in Fumadocs.&lt;/p&gt;

&lt;p&gt;first step is to extend the frontmatter schema.&lt;/p&gt;

&lt;p&gt;go to &lt;code&gt;source.config.ts&lt;/code&gt; file in root folder of fumadocs project and add the following code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineDocs&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;frontmatterSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;new&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;now we can basically add the frontmatter prop in &lt;code&gt;lib/source.ts&lt;/code&gt; file.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Make sure to rename the &lt;code&gt;lib/source.ts&lt;/code&gt; to &lt;code&gt;lib/source.tsx&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/docs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFumadocsSource&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;pageTree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;attachFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;new&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;

      &lt;span class="c1"&gt;// JSX nodes are allowed&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bg-fd-primary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;NEW&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;now to use this we can just use &lt;code&gt;new&lt;/code&gt; prop in the frontmatter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
title: "File Upload"
description: "Animated and interactive file upload component."
new: true
---
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;final result would be like this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpbs.twimg.com%2Fmedia%2FGqGozTTbAAAj1ay%3Fformat%3Dpng%26name%3D360x360" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpbs.twimg.com%2Fmedia%2FGqGozTTbAAAj1ay%3Fformat%3Dpng%26name%3D360x360" alt="image" width="286" height="132"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>documentation</category>
      <category>webdev</category>
      <category>nextjs</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Guide to minimal website design</title>
      <dc:creator>Preet Suthar</dc:creator>
      <pubDate>Sat, 24 May 2025 08:15:51 +0000</pubDate>
      <link>https://dev.to/preetsuthar17/guide-to-minimal-website-design-a8l</link>
      <guid>https://dev.to/preetsuthar17/guide-to-minimal-website-design-a8l</guid>
      <description>&lt;p&gt;Minimalist website design is a refined art form focused on simplicity, clarity, and functionality. By stripping away nonessential elements, it highlights what truly matters and creates a seamless experience for visitors.&lt;/p&gt;

&lt;h2&gt;
  
  
  7 Principles of Minimalist Web Design
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Embrace Simplicity&lt;/li&gt;
&lt;li&gt;Prioritize User Experience&lt;/li&gt;
&lt;li&gt;Showcase Quality Visuals&lt;/li&gt;
&lt;li&gt;Leverage Whitespace&lt;/li&gt;
&lt;li&gt;Highlight Content&lt;/li&gt;
&lt;li&gt;Ensure Fast Load Times&lt;/li&gt;
&lt;li&gt;Implement Clear Calls to Action&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  1. Embrace Simplicity
&lt;/h3&gt;

&lt;p&gt;At the heart of minimalist design is simplicity. A straightforward, uncluttered layout with a restrained color palette, fonts, and graphics creates an inviting environment. Generous whitespace not only enhances aesthetics but also improves readability, allowing your content to shine.&lt;/p&gt;

&lt;p&gt;A good example is SquareSpace website, simple and clean showing only content that is important&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsg1so26fcwu0hxip3mka.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsg1so26fcwu0hxip3mka.png" alt="squarespace" width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Prioritize User Experience
&lt;/h3&gt;

&lt;p&gt;A user-centric approach is essential in minimalist design. Your website should be intuitive, with streamlined navigation and legible typography. Given the increasing use of mobile devices, ensure that your site is responsive and offers a smooth experience on all screen sizes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1an371ycgoerhfnbs3ib.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1an371ycgoerhfnbs3ib.png" alt="vercel's website" width="799" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Showcase Quality Visuals
&lt;/h3&gt;

&lt;p&gt;Minimalism does not equate to a lack of visual appeal. Instead, select high-caliber images or illustrations that resonate with your brand’s identity. Opt for unique visuals rather than generic stock photos to add a touch of personality and distinction.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Leverage Whitespace
&lt;/h3&gt;

&lt;p&gt;Whitespace, or negative space, is a critical design element that lends structure and focus to your layout. It helps guide the visitor’s eye toward key content and creates a sense of balance and harmony throughout the site.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Highlight Content
&lt;/h3&gt;

&lt;p&gt;In minimalist design, content remains the focal point. Employ concise and precise language to communicate your message effectively. Use headings and subheadings to break up text, making it both engaging and easy to navigate, while avoiding distractions that detract from your core message.&lt;/p&gt;

&lt;p&gt;Stripe‘s website is both functional and visually appealing. The website features a clean white background, straightforward typography, and plenty of negative space, making it easy to navigate and understand. The design highlights the company’s products and services without distracting users with unnecessary graphics or information.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn8jrg542af9alunnij60.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn8jrg542af9alunnij60.png" alt="stripe website" width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Ensure Fast Load Times
&lt;/h3&gt;

&lt;p&gt;A hallmark of minimalist design is its ability to enhance performance. By using a lightweight theme, compressing images, and minimizing extraneous code, you can achieve faster load times, thereby improving overall user satisfaction and search engine performance.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Implement Clear Calls to Action
&lt;/h3&gt;

&lt;p&gt;While the design is minimalist, your calls to action (CTAs) should be unmistakably clear and compelling. Use direct language and contrasting elements to ensure that your CTAs stand out, guiding visitors seamlessly toward the desired actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In summary, minimalist website design is both an art and a science—a pursuit of elegance through simplicity. By adhering to these seven principles, you can craft a website that not only captivates visually but also delivers a superb user experience.&lt;/p&gt;

&lt;p&gt;A minimalist approach ensures that every element has a purpose, transforming your website into a sophisticated, efficient, and enjoyable digital space that leaves a lasting impression on visitors. Embrace simplicity, enhance functionality, and let your content take center stage while delighting your audience with a touch of fun and refinement.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ui</category>
      <category>ux</category>
      <category>design</category>
    </item>
    <item>
      <title>Force refresh page in Next.js</title>
      <dc:creator>Preet Suthar</dc:creator>
      <pubDate>Sat, 24 May 2025 08:13:50 +0000</pubDate>
      <link>https://dev.to/preetsuthar17/force-refresh-page-in-nextjs-1o4f</link>
      <guid>https://dev.to/preetsuthar17/force-refresh-page-in-nextjs-1o4f</guid>
      <description>&lt;p&gt;If you’re inside a React component, grab the router instance with the &lt;code&gt;useRouter&lt;/code&gt; hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useRouter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/router&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;MyComponent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRouter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// reload the current page&lt;/span&gt;
  &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need to trigger a reload from outside a component—for example, in a plain utility module—just import the singleton &lt;code&gt;Router&lt;/code&gt; instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Router&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/router&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// reload the current page&lt;/span&gt;
&lt;span class="nx"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>nextjs</category>
      <category>programming</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Prefetching content in Next.js</title>
      <dc:creator>Preet Suthar</dc:creator>
      <pubDate>Wed, 23 Apr 2025 04:50:18 +0000</pubDate>
      <link>https://dev.to/preetsuthar17/prefetching-content-in-nextjs-2hb9</link>
      <guid>https://dev.to/preetsuthar17/prefetching-content-in-nextjs-2hb9</guid>
      <description>&lt;p&gt;When linking two pages in a Next.js application, the built-in &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt; component allows for seamless client-side navigation. Instead of triggering a full page reload like traditional websites, Next.js uses frontend routing to display the new page instantly, making for a faster user experience.&lt;/p&gt;

&lt;p&gt;But that’s not all—Next.js also prefetches linked pages automatically. As soon as a &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt; element enters the viewport (i.e., becomes visible on screen), Next.js begins preloading the linked page in the background—provided it’s an internal link within your site. This makes navigation nearly instant when users click the link.&lt;/p&gt;

&lt;p&gt;However, this prefetching behavior only works in &lt;strong&gt;production mode&lt;/strong&gt;. If you're running your app in development with &lt;code&gt;npm run dev&lt;/code&gt;, prefetching won't be triggered. To see it in action, stop the dev server, run &lt;code&gt;npm run build&lt;/code&gt; to generate your production bundle, and start the app with &lt;code&gt;npm run start&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can observe this behavior in your browser’s Network tab. Once the page finishes loading (after the &lt;code&gt;load&lt;/code&gt; event fires), links that are already visible (i.e., above the fold) start prefetching. Links that are out of view will be prefetched as the user scrolls them into the viewport.&lt;/p&gt;

&lt;p&gt;Prefetching happens automatically on fast internet connections (like Wi-Fi or 3G+), unless the browser sends a &lt;code&gt;Save-Data&lt;/code&gt; header indicating a preference for reduced data usage.&lt;/p&gt;

&lt;p&gt;If you want to disable prefetching for a specific link, you can do so by passing &lt;code&gt;prefetch={false}&lt;/code&gt; to the &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt; component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/contact"&lt;/span&gt; &lt;span class="na"&gt;prefetch&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  Contact
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How User Feedback Can Transform Your SaaS Product</title>
      <dc:creator>Preet Suthar</dc:creator>
      <pubDate>Thu, 02 Jan 2025 04:40:52 +0000</pubDate>
      <link>https://dev.to/preetsuthar17/how-user-feedback-can-transform-your-saas-product-k6e</link>
      <guid>https://dev.to/preetsuthar17/how-user-feedback-can-transform-your-saas-product-k6e</guid>
      <description>&lt;p&gt;Your product’s success doesn’t come from guessing what users want—it comes from listening to what they actually need. Let’s explore how you can turn user feedback into your secret weapon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why User Feedback is a Game-Changer
&lt;/h2&gt;

&lt;p&gt;Imagine this: you spend months building what you think is the perfect SaaS product. It looks great, it works flawlessly, but after launch, users aren’t as thrilled as you hoped. That’s when feedback becomes your guiding star.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Collect Useful Feedback
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; Don’t sit back and wait. Go out and ask your users for their thoughts.&lt;/p&gt;

&lt;p&gt;Here are some effective ways to gather feedback:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;In-app Surveys&lt;/strong&gt;: Add short surveys that pop up at the right time—like right after users try a new feature. Ask something simple, like, “Did this feature solve your problem?” Fresh experiences lead to honest answers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;User Interviews&lt;/strong&gt;: Chat directly with users, both happy and unhappy ones. These conversations often uncover issues that numbers alone can’t. One founder completely revamped their onboarding process after learning users felt overwhelmed in the first 10 minutes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Usage Data&lt;/strong&gt;: Pay attention to how users interact with your product. If a feature you thought was a must-have barely gets used, that’s valuable information.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Managing Feedback Without Losing Your Mind
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Challenge:&lt;/strong&gt; Juggling feedback from emails, chats, surveys, and more can feel chaotic.&lt;/p&gt;

&lt;p&gt;Here’s a simple solution: &lt;a href="https://ratethistool.vercel.app" rel="noopener noreferrer"&gt;Rate This Tool&lt;/a&gt;. It’s a platform where you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Collect all your feedback in one place&lt;/li&gt;
&lt;li&gt;Spot trends across platforms&lt;/li&gt;
&lt;li&gt;Rank feature requests by priority&lt;/li&gt;
&lt;li&gt;Make smarter decisions based on real data&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Turning Feedback into Real Improvements
&lt;/h2&gt;

&lt;p&gt;Here’s how top SaaS companies use feedback to stay ahead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Prioritize Smartly&lt;/strong&gt;: Not all feedback is created equal. Use a scoring system to rank requests based on how often they come up, how much impact they’ll have, and how hard they are to implement. If lots of users mention the same issue, it’s probably worth tackling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start Small&lt;/strong&gt;: Instead of rolling out huge changes, test small tweaks first. One SaaS company simplified their settings page after users complained it was confusing—and saw a 25% boost in user retention.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to Do When Things Don’t Go as Planned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Lesson Learned:&lt;/strong&gt; Feedback can guide your next big idea Even if your product doesn’t take off.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Document Everything&lt;/strong&gt;: Record all feedback, including the context. Sometimes a failed idea plants the seed for a successful one. For example, Slack grew out of feedback from a failed gaming project.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Find the Patterns&lt;/strong&gt;: Look for common threads in feedback from different users. Sometimes, the problem worth solving isn’t the one you originally set out to fix.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing the Loop with Your Users
&lt;/h2&gt;

&lt;p&gt;Always let your users know their voices matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Thank them for their feedback&lt;/li&gt;
&lt;li&gt;Update them on what you’ve changed because of it&lt;/li&gt;
&lt;li&gt;Be transparent about why some suggestions might not work&lt;/li&gt;
&lt;li&gt;Keep them involved in your journey&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The takeaway:&lt;/strong&gt; You don’t need to get everything right on day one. Success comes from creating a feedback loop that helps you keep improving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ready to Take Action?
&lt;/h2&gt;

&lt;p&gt;Start managing your feedback better today with &lt;a href="https://ratethistool.vercel.app" rel="noopener noreferrer"&gt;Rate This Tool&lt;/a&gt;. When all your user insights are in one place, spotting what matters, making better decisions, and building a product your users love is easier.&lt;/p&gt;

&lt;p&gt;Remember, your users are your best product advisors. All you have to do is listen.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>saas</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
