{
"path": "/posts/2025/llm-tailwind-react/index",
"site": "at://did:plc:mracrip6qu3vw46nbewg44sm/site.standard.publication/self",
"$type": "site.standard.document",
"title": "The Unreasonable Effectiveness of Generating UI with React and Tailwind",
"updatedAt": "2025-12-30T23:06:48.913Z",
"description": "Writing React/Tailwind with LLMs is effortless",
"publishedAt": "2025-01-06T00:54:48.000Z",
"textContent": "import Aside from '@components/prose/Aside.astro';\nimport Chat from '@components/prose/Chat.astro';\nimport CodeBlock from '@components/prose/CodeBlock.astro';\nimport { Image } from 'astro:assets';\n\nIf you've experimented with Claude Artifacts or v0.dev, maybe you've been delighted (as I have) that the language model can conjure a UI for an idea you describe.\nMost of my experience building software, especially professionally, comes from working on the \"backend\".\nBuilding frontend (read: user interfaces in a browser) is harder for me (or doesn't come as easily), because while I have experience writing software, I don't have as much experience writing _this type_ of software.\n\nThe \"Magic\" Stack\n\nWhen generating UI, one specific stack has proven to be both popular and effective relative to the technologies I have tried.\nThat stack is React and Tailwind.\n\nBoth Claude Artifacts and v0 use these technologies by default and there is a reason why.\nFor the language model, co-locating the component styling with the structural markup is highly effective and steerable by prompting.\n\nComparing Different Approaches\n\nGiven the following prompt, here's what claude-3-5-sonnet-20241022 generates in Cursor (the components below are real, working React code - try them out!).\n\n<Aside type=\"note\" mobileOnly>\n I made a few minor adjustments post-generation to the components to ensure\n they render nicely on small screens\n</Aside>\n\n<Aside type=\"note\" darkModeOnly>\n I made minor adjustments to the color schemes of the components after\n generation to play nice with dark mode of this site. The changes I made keep\n the color scheme true to what the model output rather than match the site\n style.\n</Aside>\n\n<Chat\n model=\"claude-3-5-sonnet-20241022\"\n messages={[\n {\n role: 'user',\n content:\n 'use react, tailwind and lucide-react to create an interface for a bullet-journal inspired calendar',\n },\n ]}\n/>\n\nimport { default as TailwindCalendarV1 } from './components/TailwindCalendarv1';\nimport TailwindCalendarV1Source from './components/TailwindCalendarv1?raw';\n\n<TailwindCalendarV1 client:load />\n\n<CodeBlock\n code={TailwindCalendarV1Source}\n lang=\"tsx\"\n title=\"TailwindCalendar.v1.tsx\"\n/>\n\n<Chat\n model=\"claude-3-5-sonnet-20241022\"\n messages={[\n {\n role: 'user',\n content:\n 'use react and lucide-react to create an interface for a bullet-journal inspired calendar. output the react component with styles and a corresponding css module',\n },\n ]}\n/>\n\nimport { default as ReactCalendarV1 } from './components/ReactCalendar.v1';\nimport ReactCalendarV1Source from './components/ReactCalendar.v1?raw';\n\n<ReactCalendarV1 client:load />\n\n<CodeBlock\n code={ReactCalendarV1Source}\n lang=\"tsx\"\n title=\"ReactCalendar.v1.tsx\"\n/>\n\nNot much difference between these two from the user's perspective.\nPretty straightforward LLM UI output with lots of opportunities for improvement.\n\nThe former component exists entirely within a single .tsx file.\nThe latter has the markup and style separated into .tsx and .css.module files.\n\nIterating on the Initial Design\n\nInevitably, we will want to make changes to the first iteration from the model.\nLet's add a calendar component so we can see the number of tasks that have been input on each day.\n\n<Chat\n model=\"claude-3-5-sonnet-20241022\"\n messages={[\n {\n role: 'user',\n content:\n 'update the component to add a calendar ui element. the calendar should display the count of the number of tasks and their types each day',\n },\n ]}\n/>\n\nimport { default as TailwindCalendarV2 } from './components/TailwindCalendarv2';\nimport TailwindCalendarV2Source from './components/TailwindCalendarv2?raw';\n\n<TailwindCalendarV2 client:load />\n\n<CodeBlock\n code={TailwindCalendarV2Source}\n lang=\"tsx\"\n title=\"TailwindCalendar.v2.tsx\"\n/>\n\nThe Multi-File Challenge\n\nTo do the same for our two-file approach, we now need to start thinking about what context we're going to provide to the model.\nIdeally, we would generate diffs to the existing files, as this is one of the faster and more efficient ways we could make changes.\nThere are many new tools available that can facilitate multi-file changes but needing to coordinate changes across multiple files adds complexity compared to the single-file approach.\nFor simplicity, let's send both files to the LLM and make the same ask (I'll @-ref both files in Cursor in that chat, then send the same prompt from above).\n\n!Screenshot showing how to reference multiple files in Cursor by using @ symbol\n\nCursor outputs code like\n\nand\n\nso now I manually need to apply these changes (Cursor Pro has a model that supports one-click application of code changes from chat, but I'm trying to stick to just using the model for now).\n\nOh, and it looks like the model actually introduced new code using dependencies we don't have installed\n\nThis type of things (arguably a hallucination of sorts) happens a lot less when making changes in a single file.\nI hear you, it's not a fair comparison: Tailwind in-file to CSS with a separate module file.\nLet's put all the CSS styles in the same file then compare what it's like to iterate.\n\n<Chat\n messages={[\n {\n role: 'user',\n content:\n 'use react and lucide-react to create an interface for a bullet-journal inspired calendar. output the react component with css styles all in one file',\n },\n ]}\n/>\n\nimport { default as CssCalendarV1 } from './components/CssCalendarv1';\nimport CssCalendarV1Source from './components/CssCalendarv1?raw';\n\n<CssCalendarV1 client:load />\n\n<CodeBlock code={CssCalendarV1Source} lang=\"tsx\" title=\"CssCalendar.v1.tsx\" />\n\nNow, let's transform that single file with the second prompt\n\n<Chat\n messages={[\n {\n role: 'user',\n content:\n 'update the component to add a calendar ui element. the calendar should display the count of the number of tasks and their types each day',\n },\n ]}\n/>\n\nimport { default as CssCalendarV2 } from './components/CssCalendarv2';\nimport CssCalendarV2Source from './components/CssCalendarv2?raw';\n\n<CssCalendarV2 client:load />\n\n<CodeBlock code={CssCalendarV2Source} lang=\"tsx\" title=\"CssCalendarv2.tsx\" />\n\nNot bad!\nSo what am I complaining about?\n\nUsing a model to prompt for edits to a React component with styles defined as CSS, even in the same file, pretty much always requires a full-file rewrite.\n\nWhy?\n\nThe React component with in-file CSS styles has the following structure\n\nComparing diffs\n\nWhen we prompt to make updates in the UI, we will almost always be making changes in areas 1 and 3 to implement new or modified functionality.\n\nWith Tailwind, the styles live right on the React markup as classNames.\nWe can easily highlight and select smaller regions of the code and prompt the model using a tool like Cursor to make changes.\nThis approach is more token-efficient and as a result, is faster and cheaper.\nIt also encourages a more holistic understanding of the code you're working with as a developer.\nIn my experience using models to generate code, the less thinking you do or the less you understand, the less likely what you're attempting is going to work.\n\nHere is the diff between the two versions of the component with CSS styles in-file:\n\nCompare these two distinct areas of change to the changes in the versions using Tailwind:\n\nThe Efficiency of Tailwind with LLMs\n\nA diff of about half the number of lines is needed to make the prompted changes in the Tailwind component.\nThis does not suggest a Tailwind approach is necessarily better than others, but rather that a language model is more effective at following instructions requiring small, more localized modifications.\nThus, if you use Tailwind/React, you have an easier time iterating and building with a language partner than several other approaches and project structures.\nThis understanding is implicitly reflected in the approaches taken by default (presumably prompted into) tools like Claude Artifacts and v0.\nHowever, not all models and tools take this approach by default - it's not the only way to quickly build UIs with language models, but it is a highly effective and fast way.\n\nFor completeness, here are some tools that _don't_ seem to use React and Tailwind by default:\n\n- Val Town's Townie\n- ChatGPT Canvas\n\nModel Biases and Context Influence\n\nIt's also possible that claude-3-5-sonnet-20241022, one of the most popular models for coding, has a propensity for writing React/Tailwind code, which could influence the approaches taken by popular tools.\nWhen prompting Claude with similar prompts as above, I usually get React code, even when I don't ask for it.\nWith other popular models like deepseek-chat or gpt-4, I don't always get React.\nSometimes I just get plain HTML or other frameworks like Vue (when I prompt for everything in a single file).\n\nPart of the behavior we're seeing is context-specific as well.\nWhen you prompt the model to create a component like the one we've described, if I am working in Cursor and in a file called <whatever>.tsx, that gets passed to the model as context and influences the code it generates.\nIt would definitely _not_ be what we wanted if we got Vue code in our .tsx file.\n\nAs someone who has leaned heavily on LLMs to write UIs for me, I've found React and Tailwind to be a particularly potent combination for fast iteration.\nAfter diving into the actual code structure above, why that is now makes a bit more sense.",
"canonicalUrl": "https://www.danielcorin.com/posts/2025/llm-tailwind-react/index"
}