# Make shadcn/ui Look Less Generic: 7 Small Tweaks

> Open the files in components/ui and change a few defaults: swap transition-all for named properties, add one --ease-* token in @theme and use it everywhere, give buttons a small press, make menus zoom from 98% and close faster, stop dialogs zooming from the center, use a soft ring and shadow on cards, and add tabular-nums to changing numbers. No new packages.

- URL: https://qodesign.dev/blog/make-shadcn-ui-look-less-generic-small-tweaks
- Author: Teodor Avadani (https://qodesign.dev/blog/author/teodor-avadani)
- Published: 2026-10-21
- Updated: 2026-10-21
- Category: AI-built UI
- Publisher: qodesign (https://qodesign.dev), a shop of animated React + Tailwind CSS components for Next.js, each sold with its full source code and an AI prompt.

Most shadcn/ui apps look alike for a boring reason: nobody opened the component files after `add`. The theme gets new colors, but the button still fades on `transition-all`, every menu zooms from 95%, every dialog grows out of the middle of the screen, and every card is a 1px border with `shadow-sm`. Those defaults are fine. They are also on thousands of other sites, so people notice them.

I make qodesign, a small shop of animated React and Tailwind pieces, so I spend a lot of time in these files. The tweaks below are small edits to the shadcn components you already own: class strings and one theme token, no new packages. The code in this post was written for it and needs only React and Tailwind v4. It compiles with strict TypeScript, and I checked that our own Tailwind 4 install generates every core utility it uses.

If your problem is the page as a whole (the default theme, one font everywhere, the centered gradient hero), start with [How to Make UI Not Look AI-Generated: A v0 Checklist](/blog/make-v0-app-not-look-ai-generated). This post picks up after that, inside `components/ui`.

## What do the shadcn defaults actually do?

It helps to read the source before changing it. In the current new-york-v4 style:

- [Button](https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/ui/button.tsx) uses `transition-all` with no easing or duration class, so it gets Tailwind's defaults: 150ms on `cubic-bezier(0.4, 0, 0.2, 1)` for every property that changes.
- [DropdownMenuContent](https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/ui/dropdown-menu.tsx) animates in with `fade-in-0 zoom-in-95` and out with `fade-out-0 zoom-out-95`, plus a 2 unit slide from the side it opens on. It sets `origin-(--radix-dropdown-menu-content-transform-origin)`, so it already scales out of the trigger. Keep that class.
- [DialogContent](https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/ui/dialog.tsx) uses the same `zoom-in-95` with `duration-200`, and the overlay is `bg-black/50`.

Those `animate-in` and `zoom-in-95` classes come from [tw-animate-css](https://github.com/Wombosvideo/tw-animate-css). Its source is worth a look for one detail: the animations read `--tw-duration` and `--tw-ease`, falling back to 150ms and plain CSS `ease`. So Tailwind's normal `duration-*` and `ease-*` classes work on these animations too, and if you set neither, every popover in your app runs on `ease`.

## 1. Pick one easing curve and use it everywhere

The fastest way to make motion feel designed is to stop mixing curves. Add one to your theme:

`globals.css`:

```css
@import "tailwindcss";

@theme {
  /* one curve for every UI transition: starts fast, settles gently */
  --ease-snappy: cubic-bezier(0.2, 0, 0, 1);
}
```

In Tailwind v4 any `--ease-*` variable in `@theme` becomes a utility, so this gives you `ease-snappy`. I checked the generated CSS: it sets both `transition-timing-function` and `--tw-ease`, which means the same class drives Tailwind transitions and tw-animate-css animations. The exact numbers are taste. What matters is that buttons, menus, dialogs and cards all share them.

## 2. Replace transition-all on the button

`transition-all` animates anything that changes, including things you never meant to animate, like padding when a loading spinner appears. Name the properties instead, give the button a press state, and keep the focus ring. Here is a button with the same job as shadcn's `buttonVariants`, written without dependencies so you can paste the strings into your own file:

`button.tsx`:

```tsx
import * as React from "react"

// Same job as shadcn's buttonVariants, written out so the post has no dependencies.
// If you use cva, paste these strings into your existing buttonVariants instead.
const base =
  "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap " +
  "transition-[color,background-color,box-shadow,scale] duration-150 ease-snappy " +
  "active:not-aria-[haspopup]:scale-[0.97] motion-reduce:transition-none motion-reduce:active:scale-100 " +
  "outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 " +
  "disabled:pointer-events-none disabled:opacity-50"

const variants = {
  default:
    "bg-primary text-primary-foreground shadow-[inset_0_1px_0_rgb(255_255_255/0.15),0_1px_2px_rgb(0_0_0/0.2)] hover:bg-primary/90",
  outline:
    "bg-background ring-1 ring-black/10 shadow-xs hover:bg-accent hover:text-accent-foreground dark:ring-white/15",
  ghost: "hover:bg-accent hover:text-accent-foreground",
} as const

const sizes = {
  default: "h-9 px-4",
  sm: "h-8 px-3",
  lg: "h-10 px-6",
} as const

type ButtonProps = React.ComponentProps<"button"> & {
  variant?: keyof typeof variants
  size?: keyof typeof sizes
}

export function Button({ className = "", variant = "default", size = "default", ...props }: ButtonProps) {
  return (
    <button
      data-slot="button"
      className={`${base} ${variants[variant]} ${sizes[size]} ${className}`}
      {...props}
    />
  )
}
```

A few choices in there:

- `active:scale-[0.97]` gives a small press. Newer shadcn styles do something similar with a 1px `translate-y-px` on `:active`. Either works; pick one and keep it.
- `not-aria-[haspopup]` skips the press on buttons that open a menu, so the trigger does not shrink while the menu scales out of it. That idea comes from shadcn's own newer button classes.
- The default variant gets a faint inset highlight on top and a tight shadow under it, so it looks raised instead of flat. The outline variant uses a ring at 10% black instead of a solid border, which is softer on light backgrounds.
- `motion-reduce:` turns the press and the transition off for people who ask their OS for less motion.

## 3. Menus: scale less, leave faster

A 95% zoom is a lot for a menu that is 200px wide. Drop it to 98%, and let the menu close faster than it opens. People wait for something to appear, but they have already moved on when it disappears.

`motion-classes.ts`:

```ts
// Drop these into the className of the matching shadcn component (tw-animate-css utilities).

// DropdownMenuContent, PopoverContent, SelectContent: smaller scale, quicker exit.
// Keep shadcn's origin-(--radix-...-transform-origin) class so it still grows out of the trigger.
export const menuContentMotion =
  "data-[state=open]:zoom-in-98 data-[state=closed]:zoom-out-98 " +
  "data-[state=open]:duration-150 data-[state=closed]:duration-100 ease-snappy"

// DialogContent: rise a few pixels instead of zooming out of the middle of the screen.
export const dialogContentMotion =
  "data-[state=open]:zoom-in-100 data-[state=closed]:zoom-out-100 " +
  "data-[state=open]:slide-in-from-bottom-2 data-[state=closed]:slide-out-to-bottom-1 " +
  "data-[state=open]:duration-200 data-[state=closed]:duration-150 ease-snappy"

// DialogOverlay: lighter tint plus a small blur reads calmer than flat black at 50%.
export const dialogOverlayLook = "bg-black/30 backdrop-blur-[2px]"
```

The cleanest way to use these is to edit the class string in `components/ui/dropdown-menu.tsx` (and the popover, select and dialog files): replace `zoom-in-95` and `zoom-out-95` with the classes above. Passing them through `className` also adds them, but `cn` uses tailwind-merge, which may not know the tw-animate-css classes, so both zoom values can end up on the element and you are left guessing which one wins. The duration classes are safe either way, because a `data-[state=...]:` variant is more specific than a plain `duration-200`. In tw-animate-css a number means a percentage, so `zoom-in-98` starts the menu at 98% scale.

## 4. Dialogs: rise a little instead of zooming

The default dialog grows from 95% in the center of the screen, which is the motion most people now read as a template. `dialogContentMotion` above sets the scale to 100% so there is no zoom at all, and slides the dialog up 2 spacing units as it fades in. It drops 1 unit on the way out, a bit faster.

This works with shadcn's centering because the two use different CSS properties. Tailwind v4's `translate-x-[-50%]` sets the `translate` property, and the tw-animate-css keyframes animate `transform`, so the slide adds to the centering instead of replacing it.

For the overlay, `bg-black/30 backdrop-blur-[2px]` is lighter than the default black at 50% and keeps a hint of the page behind it. Blur can be heavier to paint than a flat color, so keep it small and try it on a cheap phone. If you want to push that blur all the way to frosted glass, the [liquid glass post](/blog/liquid-glass-react-tailwind-browser-support) covers what renders in Chrome, Safari and Firefox and what to fall back to.

## 5. Cards: a ring and a soft shadow instead of a border

The stock card is `border` plus `shadow-sm`. A 1px ring at 5% black with a long, soft shadow under it looks more like a surface and less like a box. On hover, lift it half a spacing unit and deepen the shadow:

`stat-card.tsx`:

```tsx
import * as React from "react"

type StatCardProps = {
  label: string
  value: string
  href: string
}

export function StatCard({ label, value, href }: StatCardProps) {
  return (
    <a
      href={href}
      className={
        "group block rounded-xl bg-card p-5 ring-1 ring-black/5 dark:ring-white/10 " +
        "shadow-[0_1px_2px_rgb(0_0_0/0.04),0_8px_24px_-12px_rgb(0_0_0/0.12)] " +
        "transition-[translate,box-shadow] duration-200 ease-snappy " +
        "hover:-translate-y-0.5 hover:shadow-[0_1px_2px_rgb(0_0_0/0.04),0_16px_32px_-12px_rgb(0_0_0/0.18)] " +
        "motion-reduce:transition-none motion-reduce:hover:translate-y-0"
      }
    >
      <p className="text-sm text-muted-foreground">{label}</p>
      <p className="mt-1 text-3xl font-semibold tracking-tight tabular-nums">{value}</p>
      <span className="mt-3 inline-flex items-center gap-1 text-sm font-medium">
        View report
        <svg
          aria-hidden="true"
          viewBox="0 0 16 16"
          className="size-4 transition-transform duration-200 ease-snappy group-hover:translate-x-0.5 motion-reduce:transition-none"
          fill="none"
          stroke="currentColor"
          strokeWidth={1.5}
        >
          <path d="M6 3l5 5-5 5" />
        </svg>
      </span>
    </a>
  )
}
```

Note `transition-[translate,box-shadow]`. In Tailwind v4, `-translate-y-0.5` sets the `translate` property, not `transform`, so `translate` is the property to name. I checked that in the generated CSS too.

## 6. Use tabular numbers anywhere digits change

Prices, counters, timers and stats should use `tabular-nums`. It sets `font-variant-numeric: tabular-nums`, which gives every digit the same width, so a value going from 1,111 to 9,999 does not shift the text next to it. shadcn uses it in its own docs examples. It is one class and it makes a dashboard feel steadier right away. If you animate numbers, [Animated Number Counter in React, No Library](/blog/animated-number-counter-react-no-library) shows the counting part.

## 7. Nudge icons, not whole components

The arrow in the card above moves 2px to the right when you hover the card, using `group` on the link and `group-hover:translate-x-0.5` on the icon. Small movement on a small element tells people something is clickable without the whole layout moving. The same trick works for a chevron in a button or a "View all" link.

## Don't skip reduced motion

Every snippet here has a `motion-reduce:` class next to its motion. That is the minimum. If you want the full picture for shadcn components, including whether Radix dialogs still trap focus with their animations turned off, read [shadcn/ui Animated Components: Add Motion Without Breaking Accessibility](/blog/shadcn-ui-animation-accessibility-reduced-motion-focus).

## A prompt for Cursor, v0 or Lovable

AI tools add shadcn components with the stock classes every time, so put the rules where the tool reads them. Something like:

```text
When you add or edit a shadcn/ui component:
- Never use transition-all. Name the properties (color, background-color, box-shadow, scale, translate).
- Use ease-snappy and duration-150 for small UI, duration-200 for dialogs.
- Popovers and menus: zoom-in-98 / zoom-out-98, exit faster than enter. Keep the origin-(--radix-...) class.
- Dialogs: no zoom, slide-in-from-bottom-2.
- Every motion class gets a motion-reduce: counterpart.
- Use tabular-nums for any number that changes.
```

## One piece with more going on

Once the basics feel right, one component with real motion gives the page something people remember. The Hold to Delete button below is one of mine: a destructive action that fills while you hold it, so nobody deletes by accident. If you would rather build that idea yourself, [Hold to Delete Button in React and Tailwind](/blog/hold-to-delete-button-react-tailwind) walks through it, and the paid piece is at [/components/hold-to-delete](/components/hold-to-delete).

## FAQ

### How do I make shadcn/ui look less generic?

Change the theme first, then the component files. In the components, the biggest wins are small: replace `transition-all`, use one easing curve, make menus scale less and close faster, stop dialogs zooming from the center, and swap card borders for a soft ring and shadow.

### Do Tailwind's duration and ease classes work on shadcn animations?

Yes. shadcn's `animate-in` and `animate-out` come from tw-animate-css, which reads `--tw-duration` and `--tw-ease`. Tailwind's `duration-*` and `ease-*` classes set those variables, so they control the enter and exit animations as well as normal transitions.

### Do I need Framer Motion for these tweaks?

No. Everything here is Tailwind classes plus the tw-animate-css utilities shadcn already installs. Reach for a motion library when you need gestures, layout animation or springs.

### Will my changes survive running the shadcn CLI again?

The files in `components/ui` are plain source in your repo, so they are yours to edit. If you run `add` again for a component you changed, read the diff before you keep it, and keep the shared bits (the easing token, the class strings in `motion-classes.ts`) in your own files so they are easy to put back.
