There's a specific kind of frustration I think every frontend dev has felt at least once.
You install a UI library. It's great for the first week. Everything looks consistent, the docs are nice, you're moving fast. Then your designer, or your own brain, asks for one small change. The dropdown should open a little slower. The tab indicator should be rounder. The button shouldn't scale on hover, it should do this other thing.
And you go looking for the prop. There's no prop. You look for a theme override. There's a theme override, but it only covers colors. You look at the source on GitHub and find the animation is hardcoded three components deep. You think about forking. You think about !important. You think about wrapping it in a div and fighting it with CSS.
An hour later you've written forty lines of override code to change one number.
That experience is a big part of why useLayouts works the way it does. When you add a component, you don't install a package. You get a file. It's yours. Change the number.
I'm Urvish, I make useLayouts. It's a free, MIT licensed collection of animated React components built with Tailwind and Motion, distributed as a shadcn registry. It got picked for the Vercel Open Source Program Winter 2026 cohort, which I'm still kind of processing. This post is about one specific idea: copy, customize, ship. Why I chose it over the traditional npm package model, what it's actually like to live with, where it's worse, and how I'd recommend using it so you don't end up with a mess.
I'm going to try to be fair here. Locked packages aren't evil. There are real reasons they exist. But for the kind of components I make, I think owning the source is the better deal, and I want to show you why with real examples instead of vibes.
The two models, plainly
Let's define terms so we're talking about the same thing.
The package model. You run npm install some-ui-kit. The library lives in node_modules. You import components from it. You configure them through props, theme objects, or CSS variables the library chose to expose. When the library updates, you bump the version and get the new behavior. You don't really own the code. You rent it.
The copy model. You run a command, or literally copy paste, and the component's source code ends up in your project. Usually in something like components/ui/. It's a normal file. It imports its dependencies like any other file. You can edit it directly. When the original author updates it, nothing changes in your project unless you go get the new version yourself.
Shadcn/ui made the copy model mainstream. Before that, a lot of people were doing it informally, copying snippets from blog posts and CodePens. Shadcn turned it into a real workflow with a CLI and a registry format. And now other people, like me, can publish registries that plug into the same CLI.
For useLayouts, it looks like this. You add the registry once in components.json:
{
"registries": {
"@uselayouts": "https://uselayouts.com/r/{name}.json"
}
}
Then you add components by name:
npx shadcn@latest add @uselayouts/discrete-tabs
The file lands in your project. Motion and whatever else it needs get installed for you as regular dependencies. That's it. There's no uselayouts package in your package.json. There's no runtime that belongs to me.
Why motion components specifically want to be owned
I think the copy model is good in general, but I think it's especially good for animated components. Here's my argument.
A regular button is mostly structure and accessibility. It needs to be a real button element, it needs focus styles, it needs disabled states, it needs to handle keyboard input. Those things have right answers. There's a correct way to do them, and a library that does them correctly is genuinely valuable, because you probably don't want to reinvent that.
Motion is different. Motion is taste.
What's the right spring stiffness for a tab indicator? There isn't one. It depends on your brand. A meditation app and a trading dashboard should not have the same tab animation. It depends on your users' devices. It depends on how much else is moving on the page. It depends on whether your designer likes things snappy or soft.
If I ship a locked component with a hardcoded spring, I'm forcing my taste on your product. If I expose the spring as a prop, okay, now you can change stiffness. But what about damping? Mass? What about the blur on the label? What about the fact that you want the icon to move but not the text? What about the fact that you want the label to just appear, no blur at all?
Every one of those becomes a prop. The API balloons. And it's still never enough, because the one thing you want to change is always the one thing I didn't think to expose.
With the copy model, I don't have to predict what you'll want. You open the file and change it. The spring is right there. The blur is right there. Delete it. Done.
That's why I say, half seriously, that the components are a starting point you can ruin on purpose. I'm not giving you a finished product. I'm giving you a really good first draft that you finish.
A real example: Discrete Tabs out of the box
Let's look at an actual component so this isn't abstract. Discrete Tabs is probably the one people add most, and it's a good one to show because it's short.
When you add it, the file has a few icon components at the top, then this:
// Change Here
const TABS = [
{ id: "Inbox", title: "Inbox", icon: Inbox },
{ id: "Planner", title: "Planner", icon: Calendar },
{ id: "Alerts", title: "Alerts", icon: Alert },
];
That // Change Here comment is very intentional. It's the first thing you'll want to edit, so I put a sign on it.
Then the main component, which is just state and a map:
export default function DiscreteTabs() {
const [activeButton, setActiveButton] = useState(TABS[0].id);
return (
<div className="flex gap-4 items-center">
{TABS.map((tab) => (
<Button
key={tab.id}
title={tab.title}
ButtonIcon={tab.icon}
isActive={activeButton === tab.id}
setActiveButton={setActiveButton}
/>
))}
</div>
);
}
And then a Button function that does the actual motion work. Each tab is a pill. Inactive tabs show just the icon. The active tab opens up to show the icon plus the label. When you switch, the pills resize and slide using Motion's layout animations, and the label fades in with a quick blur.
The spring looks like this:
transition={{
layout: {
type: "spring",
damping: 20,
stiffness: 230,
mass: 1.2,
},
}}
Notice what's not there. There's no <DiscreteTabs springConfig={...} labelAnimation={...} labelBlur={false} /> API. There's no config object. It's just the code that does the thing, written as plainly as I could.
Some people look at that and think it's less "professional" than a configurable component. I think it's more honest. You can read the whole thing in a couple of minutes, and then you know exactly what you're shipping.
Customizing it: five real edits
Okay, so you've added Discrete Tabs. Here's what customizing actually looks like. These are edits I've made myself in real projects, or that people have asked me how to do. None of them need anything from me. That's the point.
Edit 1: Make it calmer
Say your product is calmer than my demo. Maybe it's a writing app, or something people use for hours and you don't want the UI feeling bouncy.
Open the file, find the spring, and change it. Lower stiffness makes it slower to get going. Higher damping makes it settle without overshoot.
layout: {
type: "spring",
damping: 30,
stiffness: 180,
mass: 1,
}
Try it, click around, adjust. There's no right answer, there's just "does this feel like my product." I'd recommend changing one number at a time, because if you change all three at once you won't know which one made it feel better or worse.
With a locked package, this edit either needs a prop that may not exist, or a fork. Here it's a ten second change.
Edit 2: Make it controlled and hook it to your router
The default version keeps its own state with useState. That's great for a demo, but in a real app your tabs probably need to match the URL, or a parent component needs to know which tab is active.
So you change the main component to take props:
type DiscreteTabsProps = {
value: string;
onValueChange: (id: string) => void;
};
export default function DiscreteTabs({ value, onValueChange }: DiscreteTabsProps) {
return (
<div className="flex gap-4 items-center">
{TABS.map((tab) => (
<Button
key={tab.id}
title={tab.title}
ButtonIcon={tab.icon}
isActive={value === tab.id}
setActiveButton={() => onValueChange(tab.id)}
/>
))}
</div>
);
}
And now the parent owns the state. You could wire that to a search param, a route segment, or whatever store you use. You'd also want to tidy up the Button prop types so setActiveButton is just a function that takes no arguments, but that's a two line change.
This is a really common edit and it's a good example of why I don't try to make the defaults do everything. If I'd shipped a controlled and uncontrolled API with every possible integration, the file would be three times longer and harder to read. Instead, it's short, and you add the shape your app needs.
Edit 3: Clean out what you don't use
If you read the Button function closely, you'll find a showShine state and a useEffect that flips it on for 800ms whenever a tab becomes active after you've interacted. Then you'll notice nothing in the render actually reads showShine. It's a hook for a shine effect that isn't drawn in the current file. Honestly, it's leftover scaffolding from me experimenting.
In a package, that kind of thing would just sit in node_modules forever and you'd never know. Here, you can see it. So you get to choose. Either use it, like conditionally rendering a little highlight sweep while showShine is true, or delete the state and the effect. It's a useState and a useEffect with a timeout. Remove them and you're done.
Same goes for the label blur. If your product wants the label to just appear, remove the filter values from initial and animate. You don't need to set labelBlur={false}. You don't need to file a feature request asking me to make it optional. You just delete the lines.
I kind of like that this example is a little embarrassing for me. It's the most honest demo of the copy model I can give you. You see everything, including my leftovers.
I know that sounds obvious, but I think a lot of developers are trained by package culture to not touch the component. It feels like you're breaking something. You're not. It's your file now.
Edit 4: Use your icons
The default file defines a few small SVG icon components at the top, so it doesn't force an icon library on you. But a lot of projects already use Lucide, and Lucide fits shadcn projects really naturally.
The Button expects an icon component that accepts a size prop. Lucide icons accept size. So you can basically swap:
import { Inbox, Calendar, Bell } from "lucide-react";
const TABS = [
{ id: "Inbox", title: "Inbox", icon: Inbox },
{ id: "Planner", title: "Planner", icon: Calendar },
{ id: "Alerts", title: "Alerts", icon: Bell },
];
Then delete the inline SVG components you're not using anymore. The file gets shorter and it matches the rest of your app.
Edit 5: Match your design system
The pills use theme classes like bg-secondary, outline-border, and text-primary for the active state. If you're on a standard shadcn theme, those already pick up your colors, which is why I use them.
But maybe your design system has its own rules. Maybe your tabs aren't pills, they're more rectangular. Maybe you don't use monospace uppercase labels, because that's a pretty specific look I like and you might not.
The border radius is set inline as borderRadius: "25px". Change it. The label has font-mono uppercase. Remove those classes and it'll use your normal font. The shadow is shadow-md. Swap it for whatever your system uses, or drop it.
Fun detail on the radius: it's set as an inline style instead of a Tailwind class on purpose. Motion's layout animations can correct border radius during the animation when it's set that way, so the corners don't look stretched while the pill resizes. If you move the radius into a class and the corners start looking weird mid animation, that's why. This is exactly the kind of thing that's easy to understand when you can read the file, and really confusing when it's buried in a package.
After those five edits, the component might look nothing like my version. Good. That's what I wanted.
The night I fought a library
I want to tell a story about the package model going wrong for me, because I think it explains why I care about this so much.
A while back, before useLayouts was a real thing, I was building a dashboard for a project. I was using a popular component package for a lot of the UI. I'm not going to name it, because it's a good library and this isn't its fault. It just wasn't built for what I wanted.
The dashboard had a sidebar with sections, and I wanted the active section indicator to slide between items instead of jumping. Pretty normal request.
The library's nav component didn't support that. It had an active state, and you could style it with a class, but the indicator was just a background on the active item. No shared element, no transition between items.
So I tried to add it myself. I wrapped the nav items, measured their positions with refs, and absolutely positioned a separate indicator element behind them. Then I animated that element's position when the active item changed.
It kind of worked. Then I resized the window and the measurements went stale. So I added a resize observer. Then the library's own focus ring started rendering on top of my indicator in a weird way. So I overrode the focus styles. Then I realized the library re-rendered the items in a way that occasionally reset my refs. So I added some janky effect logic to re-measure.
By around 2am I had something like a hundred lines of code whose only job was to fight the library into doing a slide animation. And it was fragile. Every time the library updated, I was scared it'd break.
If I'd owned the nav component's source, the whole thing would have been: add layoutId to the active indicator, done. Motion handles the shared layout animation. Maybe ten lines.
That's the core of my argument. The package model is great when the package does what you want. When it doesn't, the cost of bending it can be way bigger than the cost of just owning the code from the start.
What you give up
I promised to be fair, so here's the real list of downsides. If someone tells you the copy model has no tradeoffs, they're selling something.
Updates don't come to you
If I fix a bug in a component, or improve it, your copy doesn't change. You have to go get it.
For some people that's a dealbreaker. They want npm update and done. I get it.
My take is that for this specific kind of component, it's actually a feature. Motion is something you tuned. If I could push an update that silently changed your spring, or your timing, or removed an effect, I could change how your product feels without you knowing. That seems worse than missing a fix.
But it does mean you have to care a little. More on how to handle that below.
You can break it
When you own the file, nothing stops you from making it worse. You can delete the "use client" directive and get confused errors in the App Router. You can remove something that looked unnecessary but was doing important work, like that isLoaded flag that stops the component animating on first render. You can introduce bugs.
That's the cost of freedom. I try to make the files readable so you can understand what each piece is for before you remove it. But I can't stop you, and I wouldn't want to.
Drift across projects
If you use useLayouts in five projects and customize it differently in each, you now have five versions. Some will have fixes the others don't. This is exactly the copy paste problem I had before useLayouts existed, when I was dragging a folder between repos.
On a team, this can get messy if nobody owns it. Two developers each add the same component to different parts of the app and tweak it differently, and now your app has two slightly different tab animations.
More code in your repo
Your project gets bigger. Each component is a real file you have to maintain, lint, type check, and review. With a package, that code is in node_modules and it's somebody else's problem.
For most useLayouts components this is pretty small, a single file, but it's still real. If you add twenty components you have twenty files you're responsible for.
You need to read the code
This is a soft one but it matters. The copy model works best when you actually open the file and understand it. If you're the kind of developer who wants to never look inside a component, a package might suit you better.
How to live with it without making a mess
So, given those tradeoffs, here's how I'd actually recommend working with copy model components. This is what I do in my own projects.
Commit the pristine version first
When you add a component, commit it immediately, before you change anything.
npx shadcn@latest add @uselayouts/discrete-tabs
git add -A && git commit -m "add discrete-tabs from uselayouts"
Then make your edits and commit those separately. Now your history has a clean record of what came from the registry and what you changed. When you want to pull an update later, this makes it way easier to see what's mine and what's yours.
Pulling an update
When there's a newer version of a component you want, the CLI can add it again. It'll ask before overwriting an existing file. My workflow is:
- Make sure my working tree is clean.
- Re-add the component and let it overwrite.
- Look at the git diff. Now I can see exactly what changed between my version and the new one.
- Keep the upstream changes I want, restore my customizations, commit.
It's more manual than a version bump, yeah. But it's also way more transparent. You see every single line that changes. Nothing sneaks in.
Honestly for a lot of components you'll never need to do this. If it works and it feels right in your product, there's no reason to update it.
Keep one copy per app
On a team, agree on one location and one version of each component. If the dashboard and the settings page both need Discrete Tabs, they should import the same file, not each have their own. If one needs different behavior, add a prop or a variant to the shared file. That's normal component hygiene, it just matters more when the component didn't come from a package.
Rename it if you changed it a lot
If you've customized a component so much it's basically a different thing, rename it. discrete-tabs.tsx becomes section-switcher.tsx or whatever it actually is in your app. That way nobody on your team thinks it's the stock version, and nobody accidentally overwrites it by re-adding from the registry.
Leave a note about where it came from
A one line comment at the top of the file saying it started as a useLayouts component is nice. It helps future you remember where to look for updates, and it helps teammates understand why it's written the way it is. MIT doesn't make you do this in a comment specifically, but keeping the attribution around is good practice and I appreciate it.
When a locked package is actually the better choice
I don't want to pretend the copy model wins every time. There are cases where I'd reach for a regular package myself.
Hard, correctness heavy primitives. Things like a fully accessible combobox, a date picker with keyboard navigation and screen reader support, focus trapping in dialogs. These have a lot of edge cases and a "right" answer. You want those to be maintained centrally by people who've thought about every weird assistive tech quirk. That's what Radix is great at. Even shadcn components that you copy into your project usually wrap Radix primitives that stay as a package dependency. The copy layer is the styling and composition. The hard behavior stays in a maintained package.
Huge surface area you'll never customize. If you need fifty basic components and you're happy with how they look, a package is less code in your repo.
Teams with zero appetite for owning UI code. If nobody on the team wants to read component source, ever, the copy model will slowly rot. A package with a stable API might be a better fit.
Things with security or protocol implications. Auth flows, payment widgets, anything where you really want the vendor's fixes to arrive automatically.
So the way I think about it: the deep, correctness heavy stuff can live in packages. The surface layer, the part that defines how your product feels, should live in your code. useLayouts is entirely that surface layer. It's tabs, buttons, cards, galleries, small interactions. It's the feel. That's exactly the part you should own.
You can actually see what you're shipping
This one's underrated. When a component is a file in your project, you can read every line of it before it ships to your users.
With a package, you usually trust it. You check that it's popular, maybe glance at the GitHub, and import it. What's actually inside? Its own dependencies, its own effects, maybe some global CSS it injects, maybe a polyfill. You probably never look.
With a useLayouts component, the whole thing is right there. You can see which dependencies it pulls in, because the registry item declares them. For Discrete Tabs that's motion, clsx, and tailwind-merge. You can see every effect, every timeout, every style. Nothing hidden.
I also put the source on every component's docs page, so you can read it before you even run the CLI. I really don't want anyone to be surprised by what lands in their project. If you read the file and something seems off, you can not add it. That's a level of control you just don't get with a black box.
What "ship" actually means here
The name is copy, customize, ship. I've talked a lot about copy and customize. Let me talk about the ship part, because that's the part that matters.
The whole reason this model exists is so you can ship faster without shipping something dead. Not so you can spend a week tuning springs. If copying a component turns into an endless customization rabbit hole, that's a failure too.
So here's a real shipping night, or close to one, to show how it plays out.
I was helping out on a small project that needed an admin table where you could delete rows. It was late, the deadline was the next morning, and the delete action was just a red button with a browser confirm dialog. It worked. It felt like 2009.
Here's what I did.
npx shadcn@latest add @uselayouts/delete-button
Commit. Then I opened the file and did three things.
First, I changed the copy. My demo text didn't match their product's voice, so I rewrote the labels to match what they used everywhere else.
Second, I hooked up the actual delete. The demo version counts down and gives you a cancel option, but it doesn't actually delete anything, because there's nothing to delete on a docs page. I made the real mutation fire when the countdown finishes, and made cancel actually cancel.
Third, I shortened the countdown. The timing values all sit in one config object at the top of the file, which made this a one number change. In a table with lots of rows, you see the delete button a lot, and anything long starts to feel slow when you're deleting five things in a row.
That's it. Maybe forty minutes including testing. The browser confirm was gone, deleting felt deliberate, and the table still felt fast. Shipped it, went to bed.
The key thing: I didn't redesign the component. I didn't try to make it perfect. I copied it, made the three edits that mattered for that product, and shipped. The customization was in service of shipping, not a hobby.
I think that's the healthy way to use this stuff. Make the edits that make it yours. Stop there. You can always come back to it.
Bundle size and dependencies
People sometimes ask whether adding a bunch of animated components will bloat their app. Fair question.
The main dependency is Motion. If your project already uses it, adding useLayouts components barely changes anything, because they all share it. If it doesn't, you're adding Motion once, and every component after that reuses it.
Beyond that, the components themselves are small files. And because they're your code, your bundler treats them like any other component. Tree shaking works normally. If you add a component and never import it, it doesn't end up in your bundle. If you only use it on one page, code splitting can keep it on that page.
That's actually another quiet advantage over some packages. With a big monolithic UI package, you're sometimes at the mercy of how well it's set up for tree shaking. With source in your project, the bundler sees exactly what you use.
And if you're worried about a specific component, you can open it and see what it imports. No guessing.
How this plays with shadcn and Radix
useLayouts isn't trying to replace shadcn/ui. It sits next to it.
A typical setup I'd recommend: use shadcn for your base components. Buttons, inputs, dialogs, dropdowns, the stuff every app needs. Those give you solid structure and accessibility, often through Radix under the hood. Then use useLayouts for the moments you want to feel special. The main nav tabs. The delete action. The save button. The pricing card on your landing page. The gallery on your portfolio.
Because useLayouts components follow the same conventions, they fit right in. Same components.json. Same CLI. Same cn helper from @/lib/utils. Same theme tokens. They don't feel like a foreign library dropped into your app. They feel like more of your components, because they are.
It's also why I kept the "add the registry once" setup so short. You've probably already got a components.json from shadcn. Adding useLayouts is one more line in it.
On teams: who owns the motion
Something I've seen when teams use copy model components: the question of who owns the motion becomes really clear, really fast.
With a package, the answer is basically "the package author." Your team just accepts whatever animations ship with it. Nobody on the team really decides how your tabs feel. It just is what it is.
When the code is in your repo, someone has to own it. And I think that's healthy. Somebody on your team becomes the person who cares about how the UI moves. They tune the springs, they make sure the timing is consistent across components, they review PRs that touch animations.
A practical tip if you're on a team: pull your shared motion values into one place once you've customized a few components. Like a small file with your app's springs:
export const springs = {
snappy: { type: "spring", damping: 22, stiffness: 260, mass: 1 },
calm: { type: "spring", damping: 30, stiffness: 180, mass: 1 },
} as const;
Then update the components you've added to use those instead of their inline values. Now your whole app moves with the same personality, and changing it is one edit.
I don't ship components that way by default, because I want each file to be self contained and readable on its own. If Discrete Tabs imported springs from some shared file you don't have, it'd break when you added it. But once it's in your project, pulling values out like that is totally the right move.
That's kind of the copy model in a nutshell. I give you something self contained. You integrate it into your system however makes sense for you.
Why not offer both?
Someone asked me once why I don't just publish an npm package too. Give people the choice. Registry for people who want to own it, package for people who want updates.
I thought about it. I decided not to, at least for now, for a couple of reasons.
The first is focus. I'm one person. Maintaining a package means thinking about public APIs, semver, breaking changes, backwards compatibility, peer dependency ranges, and all the props that I'd need to expose to make it configurable. That's a whole different job. The registry lets me keep each component as a plain, readable file, which is the thing I actually want to be good at.
The second is that a package would quietly change how I design the components. The minute something's a package, I'd start adding props for everything, because people can't edit the source. The files would get longer and more abstract. I'd be designing for configurability instead of for how it feels. I think the components would get worse.
The third is honesty about what these are. They're not infrastructure. They're starting points with good taste baked in. A package says "trust me, don't look inside." A registry says "here's my best version, now make it yours." That second one is the actual relationship I want with people who use this.
Mistakes people make after copying
Since the code is in your hands, here are the things I see go wrong most often. None of these are your fault exactly, they're just the sharp edges of owning the code.
Removing "use client". The components use state, effects, and Motion, so they're client components. The file starts with "use client". If you delete it, or move the logic into a server component, you'll get errors in the Next.js App Router. Keep it. If you want a server rendered wrapper, put that in a separate file that imports the client component.
Path aliases that don't match. The components import cn from @/lib/utils, which is the standard shadcn setup. If your project uses a different alias, the CLI usually handles it based on your components.json, but if you copy paste manually from the docs page instead of using the CLI, you might need to fix the import.
Overwriting your own changes. If you customized a component and then re-add it from the registry without committing first, you can lose your edits. The CLI asks before overwriting, but it's easy to hit yes on autopilot. Commit first. Always.
Deleting the "boring" lines. Some lines look unnecessary but aren't. In Discrete Tabs, the isLoaded state is what stops the label from doing its entrance animation on first render. The willChange: "transform" styles help the browser prepare for the motion. The inline border radius helps Motion keep corners looking right while the pill resizes. If you delete these, it still works, it just gets a little worse in ways that are hard to spot. Read before you cut.
Customizing in five places at once. People open the file and change the spring, the colors, the padding, the font, and the icons all in one go. Then something feels off and they don't know which change caused it. Change one thing, look at it, then change the next.
Forgetting people who prefer reduced motion. Some users have reduced motion turned on at the OS level. Motion has tools for respecting that, like the useReducedMotion hook and MotionConfig with reducedMotion="user". Since the component is yours, you can decide how to handle it in your app. A common approach is wrapping your app in MotionConfig so transforms get toned down for people who asked for that. It's a few lines and it's worth doing.
The package mindset vs the owner mindset
There's a mental shift that happens when people start using copy model components, and I think it's the most important part of this whole thing.
In the package mindset, a component is something you consume. You read the docs, you find the props, you use it as intended. If it doesn't do what you want, you either live with it or you file an issue and wait. The component belongs to someone else and you're a guest.
In the owner mindset, a component is something you start from. You read the code, you understand what each part does, and you make it fit. If it doesn't do what you want, you change it. The component belongs to you and the original author is more like someone who gave you a head start.
A lot of developers, especially newer ones, are really trained into the package mindset. I was too. It feels wrong to edit something that came from somewhere else, like you're voiding a warranty. There's no warranty. MIT literally says so. Edit it.
Once that clicks, the copy model gets a lot more fun. You stop thinking "what can this component do" and start thinking "what do I want my product to do." The component is just the fastest way to get there.
MIT, sponsors, and staying honest
useLayouts is MIT licensed and free. You can use it in personal projects, client work, commercial products, whatever. Keep the license notice around, and that's basically it.
I chose MIT because it's the only license that actually matches the copy model. If I'm telling you the file is yours, the license needs to back that up. Anything more restrictive would make "it's yours" a lie.
The project runs on my time, some sponsors, and the support from being in the Vercel Open Source Program Winter 2026 cohort. That program gave the project credits and support from Vercel, which matters more than you might think for a registry. Every time someone runs the CLI, it fetches a JSON file from uselayouts.com. Every docs page has live previews. All of that is hosted, and having Vercel back it means I don't stress about traffic spikes when someone with a big audience shares a component.
If your company gets real value out of useLayouts, sponsoring is how you keep it free for the solo devs and students who can't pay. If you're building something small, just use it. That's how I want it to work. No pro tier. No locked components. No upsell inside the file.
And just to clear it up since people mix it up all the time: useLayouts is not UI Layouts. Different creators, different projects. UI Layouts is at ui-layouts.com. Both happen to be in the Vercel OSS program, which hasn't helped with the confusion.
What people usually change in other components
Discrete Tabs is the easy example, but the same idea applies everywhere. Here's what I'd expect you to touch first in a few other components, based on what I end up changing myself when I drop them into real projects.
Status Button (it's save-button in the registry). The states are the obvious thing. My demo walks through a simple idle, working, done flow, but your app might need an error state that actually means something, or a state that says "queued" instead of "working." The other big edit is wiring it to your real async call instead of a simulated delay. And timing. How long the success state sticks around before resetting is a product decision, not a library decision. A settings page can show "Saved" for a while. A chat input probably shouldn't.
Delete Button. Copy, first. The words on a destructive action matter a lot and they should sound like your product. Then the countdown. The stock version flips into a cancel state with a countdown, so you get a few seconds to back out before anything happens. How long that window should be is a product call. Deleting an account deserves a long one. Archiving a row in a list probably wants a short one. The colors and timing live in a config object at the top of the file, so this is a quick edit. Since it's your file, you can also make it conditional, like skipping the countdown for things you can undo anyway.
Morphing Input. Usually the content that it morphs between. It's built to show how an input can change shape instead of being replaced, and what it changes into depends entirely on your feature. Search, inline add, a quick compose box. You'll also probably want to connect it to your form library if you use one.
Pricing Card. Almost everything content wise. Plan names, prices, features, the highlighted plan. The layout is a starting point, but pricing pages are very brand specific, so I'd expect you to change spacing and typography more here than anywhere else. The interaction details are the part to keep.
Multi-Step Form. The steps, obviously, and validation. My version shows the flow and the transitions between steps. Your version needs real fields, real validation rules, and probably a real submit. The motion between steps is the part that makes it feel like progress instead of paperwork, so I'd leave that mostly alone and focus your edits on the content.
Bento Card. Grid sizing. Bento layouts live or die by how the cards are sized relative to each other, and that depends on your content. Change the spans until the most important thing is the biggest thing.
Smooth Dropdown and Bottom Menu. Items, icons, and what happens on select. These are navigation pieces, so they need to plug into your routing. Same idea as making Discrete Tabs controlled.
Notice a pattern? In almost every case, the first edits are about content and wiring, not motion. The motion is the part that took me hours to get right, and it's the part you probably shouldn't need to touch much. The content is the part that's always specific to your product, and it's the part a package would make hardest to change. That's the whole case for the copy model in one observation.
A quick checklist
If you're about to add your first useLayouts component, here's the short version of this whole post:
- Add the registry to
components.jsononce. - Read the source on the component's docs page before you add it.
- Run the add command.
- Commit the pristine file.
- Make the edits that matter for your product. Copy, icons, colors, maybe the spring.
- Commit your edits separately.
- Ship it.
- Come back later if you want to tune more.
That's the loop. Copy, customize, ship.
Questions I get about this
Isn't copying code an anti pattern? DRY and all that?
DRY is about not repeating logic inside your own codebase. Copying a component into your project once, and then using it everywhere from that one file, is totally DRY. It's not different from writing the component yourself, you just didn't have to start from zero.
What if you find a security issue in a component?
These are UI components, mostly state and animation, so the surface area is small. But if something important needed fixing, I'd announce it on the repo and the site so people know to update their copies. The diff workflow above makes that easy to apply.
Can I copy paste from the docs page instead of using the CLI?
Yeah, the source is right there. The CLI is just more convenient because it handles dependencies and file placement for you. If you copy manually, make sure you install the dependencies listed for that component and fix any import paths.
Can I publish my modified version?
It's MIT, so yes, as long as you keep the license notice. I'd love it if you mentioned where it started, but the license only asks for the notice.
Will you ever break my app with an update?
No. I literally can't. Your copy only changes when you change it.
Does it work with pnpm, yarn, bun?
All of them:
pnpm dlx shadcn@latest add @uselayouts/discrete-tabs
yarn dlx shadcn@latest add @uselayouts/discrete-tabs
bunx --bun shadcn@latest add @uselayouts/discrete-tabs
Can I add a few at once?
npx shadcn@latest add @uselayouts/delete-button @uselayouts/save-button
Go own something
If you've been living in package land and fighting overrides, try this once. Pick one component where your current UI feels a little dead. A tab bar, a delete action, a save button. Add the useLayouts version, commit it, change three things, ship it.
I think you'll feel the difference not just in the UI, but in how you work. You stop asking a library for permission and you just build.
Browse the components at uselayouts.com, read the intro in the docs, and if it ends up being useful, a star on GitHub genuinely helps. The repo's at hundreds of stars and climbing, and every one of them helps someone else find it.
- Site: https://uselayouts.com/
- Docs: https://uselayouts.com/docs/introduction
- GitHub: https://github.com/iurvish/uselayouts
Thanks for reading.
Urvish (@0xUrvish)


Top comments (1)
The interesting trade-off with copyable source isn't really “dependency vs no dependency”. It's that ownership moves to the consumer. That can be great for customization, but it also means the update path becomes part of the architecture: someone has to notice security fixes, compare upstream changes, and decide what is safe to merge. For teams using AI heavily, that maintenance boundary probably matters as much as the initial copy.