Skip to main content

ss

The main helper: group Tailwind classes by breakpoint and state, compose many arguments in one call, and nest groups for compound variants.

Grouping by key

base holds the classes with no further prefix. Every other key is a breakpoint, a max-* range, or a state variant, and its value comes out with that key in front of it.

Grouping
ss({ base: "text-xl flex", sm: "block", md: "text-2xl" });// → "text-xl flex sm:block md:text-2xl" ss({ base: "grid", "max-md": "gap-2", "group-hover": "underline" });// → "grid max-md:gap-2 group-hover:underline"

ss accepts base plus Tailwind's own keys — 149 in total, and nothing else — so autocomplete is exhaustive and a typo cannot compile. The full list is on the Keys page, and the same 149 are available inside a nested group, which is how a compound variant is spelled.

Every prefix here is assembled at runtime, so Tailwind never sees the finished class in your source. That is why ss needs the build plugin from Setup.

At the size a real component reaches

The two-key example above is the mechanism. An ordinary component reaches six groups without trying — three breakpoints, two states and a dark mode with its own hover. Both spellings below emit exactly the same class string.

One flat string
<button  className="flex gap-2 rounded-lg border px-3 py-1.5 text-sm sm:px-4 md:gap-3 md:rounded-xl md:px-5 md:text-base lg:px-6 hover:bg-neutral-50 focus-visible:outline-2 dark:border-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-900"/>
The same classes, grouped
<button  className={ss({    base: "flex gap-2 rounded-lg border px-3 py-1.5 text-sm",    sm: "px-4",    md: "gap-3 rounded-xl px-5 text-base",    lg: "px-6",    hover: "bg-neutral-50",    "focus-visible": "outline-2",    dark: {      base: "border-neutral-800 text-neutral-100",      hover: "bg-neutral-900",    },  })}/>

Key order is fixed

Write the keys in whatever order reads best in the component. They are emitted in one fixed order regardless, and the finished string runs through cn.

Emission order inside one ss group.
OrderKeys
1base — classes with no further prefix
2Breakpoints, mobile-first — sm, md, lg, xl, 2xl
3Max-width ranges, largest first — max-2xl down to max-sm
4State variants — hover, dark, group-hover and the rest

Stable order is what keeps tailwind-merge's last-one-wins resolution predictable. Two components that spell the same groups in a different order produce the same class string, so which class survives a conflict never depends on how the object was typed.

Falsy values drop the group

Values are clsx-style, so a condition goes inline instead of around the call. A falsy value drops the whole group, prefix included — there is no orphaned md: left in the output.

Conditions inline
ss({ base: "text-sm", md: isActive && "text-2xl" });// isActive === false → "text-sm"

Many arguments, one call

ss is variadic, and an argument is anything a bucket accepts: another map, a class string, a clsx array, or a condition that produces one. This is what a className looks like in practice — and why nothing needs to wrap it.

One call
ss(  { base: "rounded-lg border p-4", md: "p-6" },  isDisabled && { base: "opacity-50", sm: "bg-red-500" },  match(tone, { info: "bg-blue-50", danger: "bg-red-50" }),  className,);

The lookup in the middle is match, and the caller's className is simply the last argument — no cn around the outside, no second ss(). Framework examples shows the same shape as a whole component in React, Vue and Svelte.

The last argument wins

Keys are sorted inside each map; the arguments themselves are never reordered. That is what makes the last argument win, exactly as it does in cn.

Argument order
ss({ base: "p-4", md: "p-6" }, "md:p-10");  // → "p-4 md:p-10"ss({ base: "p-4" }, { base: "p-8" });       // → "p-8"

Sorting a bare string into the base bucket instead would put a caller's md:p-10 ahead of your own md:p-6, where it would quietly lose. A string stays an argument, so an override passed in from outside is still the last thing merged.

Given only class values, ss is cn:

No groups at all
ss("px-2 py-1", isActive && "bg-blue-500", "px-4");  // → "py-1 bg-blue-500 px-4"

Nested groups, for compound variants

A bucket's value can be another map, which stacks the prefixes. Each breakpoint gets its own group, with the same keys and the same rules.

Compound variants
ss({  base: "text-black p-4",  md: {    base: "p-6",       // → md:p-6    hover: "p-8",      // → md:hover:p-8    "max-lg": "grid",  // → md:max-lg:grid  },  dark: {    base: "text-white",      // → dark:text-white    hover: "text-blue-300",  // → dark:hover:text-blue-300  },});

md: "p-6" and md: { base: "p-6" } mean the same thing, so nothing has to change to start nesting. A falsy nested bucket drops, prefix included, like any other.

Note

A plain object is always a nested map, and an array is always clsx classes. Nothing is decided by looking at your key names, so the same source always means the same thing.

Put a clsx dictionary inside an array, where there is nothing to confuse it with.

Dictionary in an array
// a clsx dictionary, inside an array — nothing left to confuse it withss({ base: "text-sm", md: [{ "text-lg": isLarge }] });