npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

slot-variants

v1.1.0

Published

Type-safe class name variants with slots

Readme

slot-variants

A lightweight, zero-dependency, type-safe library for managing class name variants with slots support.

Installation

npm install slot-variants

Overview

slot-variants exports two functions:

  • sv() - creates variant-based class name generators with optional slots
  • cn() - a utility for conditionally merging class names

Quick Start

import { sv } from 'slot-variants';

const button = sv('btn font-medium rounded-lg', {
  variants: {
    size: {
      sm: 'text-sm py-1 px-2',
      md: 'text-base py-2 px-4',
      lg: 'text-lg py-3 px-6'
    },
    intent: {
      primary: 'bg-blue-500 text-white',
      secondary: 'bg-gray-200 text-gray-800',
      danger: 'bg-red-500 text-white'
    }
  },
  defaultVariants: {
    size: 'md',
    intent: 'primary'
  }
});

button();
// 'btn font-medium rounded-lg text-base py-2 px-4 bg-blue-500 text-white'

button({ size: 'lg', intent: 'danger' });
// 'btn font-medium rounded-lg text-lg py-3 px-6 bg-red-500 text-white'

cn() - Class Name Utility

A utility for conditionally joining class names together.

import { cn } from 'slot-variants';

// Strings
cn('foo', 'bar');                             // 'foo bar'

// Arrays (including nested)
cn(['foo', 'bar']);                           // 'foo bar'
cn(['foo', ['bar', 'baz']]);                  // 'foo bar baz'

// Objects (truthy values are included)
cn({ foo: true, bar: false, baz: true });     // 'foo baz'

// Mixed
cn('base', ['responsive'], { active: true }); // 'base active responsive'

// Falsy values are filtered out
cn('foo', null, undefined, false, 'bar');     // 'foo bar'

Supported Input Types

| Type | Behavior | | --- | --- | | string | Included as-is | | string[] | Flattened recursively | | Record<string, unknown> | Keys with truthy values included | | boolean, number, bigint | Ignored | | null, undefined | Ignored |

sv() - Slot Variants

Base Only (No Config)

When called without a config object, sv() processes the base classes through cn() and returns a string:

sv('btn btn-primary');              // 'btn btn-primary'
sv(['btn', 'btn-primary']);         // 'btn btn-primary'
sv({ btn: true, disabled: false }); // 'btn'

Variants

When a config object is provided, sv() returns a function that accepts variant props and returns the computed class string.

const badge = sv('badge', {
  variants: {
    color: {
      gray: 'bg-gray-100 text-gray-800',
      red: 'bg-red-100 text-red-800',
      green: 'bg-green-100 text-green-800'
    },
    size: {
      sm: 'text-xs px-2 py-0.5',
      lg: 'text-base px-3 py-1'
    }
  }
});

badge({ color: 'green', size: 'sm' });
// 'badge bg-green-100 text-green-800 text-xs px-2 py-0.5'

Variant values can be strings, arrays, or objects:

const button = sv('btn', {
  variants: {
    size: {
      sm: ['px-2', 'py-1', 'text-sm'],                    // array
      lg: 'px-6 py-3 text-lg',                            // string
      xl: { 'px-8': true, 'py-4': true, 'text-xl': true } // object
    }
  }
});

Boolean Variants

Variants with true/false keys accept boolean prop values:

const input = sv('input border', {
  variants: {
    disabled: {
      true: 'opacity-50 cursor-not-allowed',
      false: 'cursor-text'
    },
    error: {
      true: 'border-red-500',
      false: 'border-gray-300'
    }
  },
  defaultVariants: {
    disabled: false,
    error: false
  }
});

input({ disabled: true, error: true });
// 'input border opacity-50 cursor-not-allowed border-red-500'

Boolean shorthand - provide a ClassValue directly instead of a true/false record. The value is applied when true, and nothing is applied when false:

const button = sv('btn', {
  variants: {
    loading: 'animate-spin pointer-events-none',
    disabled: 'opacity-50 cursor-not-allowed'
  }
});

button({ loading: true, disabled: false });
// 'btn animate-spin pointer-events-none'

Numeric Variant Keys

Variant keys can be numbers:

const heading = sv('font-bold', {
  variants: {
    level: {
      1: 'text-4xl',
      2: 'text-3xl',
      3: 'text-2xl'
    }
  }
});

heading({ level: 1 }); // 'font-bold text-4xl'

Default Variants

Set fallback values that are used when a variant prop is not provided:

const button = sv('btn', {
  variants: {
    size: {
      sm: 'text-sm',
      md: 'text-base',
      lg: 'text-lg'
    },
    rounded: {
      true: 'rounded-full',
      false: 'rounded-md'
    }
  },
  defaultVariants: {
    size: 'md',
    rounded: false
  }
});

button();                   // 'btn text-base rounded-md'
button({ size: 'lg' });     // 'btn text-lg rounded-md'
button({ rounded: true });  // 'btn text-base rounded-full'

Passing undefined for a prop falls back to the default:

button({ size: undefined }); // 'btn text-base rounded-md'

Function-Based Default Variants

Default variants can be functions that receive the current props and return a value dynamically. Return undefined to skip the variant entirely:

const button = sv('btn', {
  variants: {
    size: {
      sm: 'text-sm',
      lg: 'text-lg'
    },
    intent: {
      primary: 'bg-blue-500',
      danger: 'bg-red-500'
    }
  },
  defaultVariants: {
    size: 'sm',
    intent: (props) => (props.size === 'lg' ? 'danger' : 'primary')
  }
});

button();               // 'btn text-sm bg-blue-500'
button({ size: 'lg' }); // 'btn text-lg bg-red-500'

Compound Variants

Apply additional classes when multiple variant conditions are met simultaneously:

const button = sv('btn', {
  variants: {
    intent: {
      primary: 'bg-blue-500',
      secondary: 'bg-gray-200'
    },
    size: {
      sm: 'text-sm',
      lg: 'text-lg'
    }
  },
  compoundVariants: [
    {
      intent: 'primary',
      size: 'lg',
      class: 'uppercase font-bold'
    }
  ]
});

button({ intent: 'primary', size: 'lg' });
// 'btn bg-blue-500 text-lg uppercase font-bold'

button({ intent: 'secondary', size: 'lg' });
// 'btn bg-gray-200 text-lg'

Compound variant conditions support array matching (OR logic):

compoundVariants: [
  {
    intent: ['primary', 'secondary'],
    size: 'sm',
    class: 'tracking-tight'
  }
]

Multiple compound variants can match simultaneously, and all matching classes are applied.

Compound variants also support className as an alternative to class:

compoundVariants: [
  {
    size: 'sm',
    className: 'shadow-sm'
  }
]

Required Variants

Mark variants as required so they must be provided at call time. Required variants cannot have default values:

const button = sv('btn', {
  variants: {
    size: {
      sm: 'text-sm',
      lg: 'text-lg'
    },
    intent: {
      primary: 'bg-blue-500',
      danger: 'bg-red-500'
    }
  },
  requiredVariants: ['intent']
});

button({ intent: 'primary' });              // OK
button({ intent: 'primary', size: 'lg' });  // OK
button({ size: 'lg' });                     // Throws: Missing required variant: "intent"

Presets

Presets are predefined named combinations of variant values. Use them to create reusable variant shortcuts:

const button = sv('btn', {
  variants: {
    size: {
      sm: 'text-sm',
      lg: 'text-lg'
    },
    intent: {
      primary: 'bg-blue-500',
      danger: 'bg-red-500'
    },
    rounded: {
      true: 'rounded-full',
      false: 'rounded-md'
    }
  },
  presets: {
    cta: { size: 'lg', intent: 'primary', rounded: true },
    subtle: { size: 'sm', intent: 'primary' }
  },
  defaultVariants: {
    rounded: false
  }
});

button({ preset: 'cta' });
// 'btn text-lg bg-blue-500 rounded-full'

button({ preset: 'subtle' });
// 'btn text-sm bg-blue-500 rounded-md'

Explicit props override preset values, and presets override defaults. The priority order is: defaultVariants < preset < explicit props:

button({ preset: 'cta', size: 'sm' });
// 'btn text-sm bg-blue-500 rounded-full'
// size overridden to 'sm', rest from preset

Presets can satisfy required variants at runtime — if a preset provides a required variant, it does not need to be passed explicitly.

An invalid preset name throws an error:

button({ preset: 'nonexistent' }); // Throws: Invalid preset "nonexistent"

Slots

Slots allow you to define multiple named class targets for multi-element components. When slots are defined, the returned function produces an object with base and each named slot as keys:

const card = sv('card border rounded-lg', {
  slots: {
    header: 'card-header font-semibold',
    body: 'card-body',
    footer: 'card-footer border-t'
  }
});

const { base, header, body, footer } = card();
// base:   'card border rounded-lg'
// header: 'card-header font-semibold'
// body:   'card-body'
// footer: 'card-footer border-t'

The base slot can also be defined explicitly in the slots config, and it merges with the first argument:

const card = sv('border', {
  slots: {
    base: 'rounded-lg shadow-md',
    header: 'font-bold'
  }
});

card().base; // 'border rounded-lg shadow-md'

Slots with Variants

Variant values can target specific slots by providing an object with slot keys:

const card = sv('card border rounded-lg', {
  slots: {
    header: 'font-bold',
    body: 'py-4',
    footer: 'border-t'
  },
  variants: {
    size: {
      sm: {
        base: 'p-2 text-sm',
        header: 'pb-1',
        body: 'py-1',
        footer: 'pt-1'
      },
      lg: {
        base: 'p-6 text-lg',
        header: 'pb-4',
        body: 'py-4',
        footer: 'pt-4'
      }
    }
  },
  defaultVariants: {
    size: 'sm'
  }
});

const { base, header, body, footer } = card({ size: 'lg' });
// base:   'card border rounded-lg p-6 text-lg'
// header: 'font-bold pb-4'
// body:   'py-4 py-4'
// footer: 'border-t pt-4'

Variants don't need to target every slot - untargeted slots remain unchanged:

variants: {
  size: {
    sm: { base: 'p-2', header: 'text-sm' }
    // body and footer are unaffected
  }
}

Boolean Shorthand with Slots

When using slots, a boolean shorthand variant can be a slot object:

const card = sv('border rounded-lg', {
  slots: {
    header: 'font-bold',
    body: 'py-4'
  },
  variants: {
    highlighted: {
      base: 'ring-2 ring-blue-500',
      header: 'bg-blue-100'
    }
  }
});

card({ highlighted: true });
// base:   'border rounded-lg ring-2 ring-blue-500'
// header: 'font-bold bg-blue-100'

card({ highlighted: false });
// base:   'border rounded-lg'
// header: 'font-bold'

Compound Slots

Apply classes to multiple slots at once, optionally conditioned on variant values:

const dialog = sv('fixed inset-0', {
  slots: {
    overlay: 'bg-black/50',
    content: 'bg-white rounded-lg',
    title: 'text-lg font-bold',
    actions: 'flex gap-2'
  },
  variants: {
    size: {
      sm: 'max-w-sm',
      lg: 'max-w-lg'
    }
  },
  compoundSlots: [
    {
      slots: ['content', 'title', 'actions'],
      class: 'px-6'
    },
    {
      size: 'sm',
      slots: ['title', 'actions'],
      class: 'text-sm'
    }
  ]
});

const result = dialog({ size: 'sm' });
// base:    'fixed inset-0 max-w-sm'
// overlay: 'bg-black/50'
// content: 'bg-white rounded-lg px-6'
// title:   'text-lg font-bold px-6 text-sm'
// actions: 'flex gap-2 px-6 text-sm'

Compound slots support the same array matching as compound variants:

compoundSlots: [
  {
    size: ['sm', 'md'],
    slots: ['cell', 'header'],
    class: 'px-3'
  }
]

Class Override at Runtime

Append additional classes at call time using class or className:

const button = sv('btn', {
  variants: {
    size: {
      sm: 'text-sm',
      lg: 'text-lg'
    }
  }
});

// String
button({ size: 'sm', class: 'mt-4 mx-auto' });
// 'btn text-sm mt-4 mx-auto'

// Array
button({ size: 'sm', class: ['mt-4', 'mx-auto'] });
// 'btn text-sm mt-4 mx-auto'

// Object
button({ size: 'sm', class: { 'mt-4': true, hidden: false } });
// 'btn text-sm mt-4'

With slots, a string class appends to the base slot. Use a slot object to target specific slots:

const card = sv('border', {
  slots: {
    header: 'font-bold',
    body: 'py-4'
  }
});

// String targets the base slot
card({ class: 'shadow-xl' });
// base: 'border shadow-xl', header: 'font-bold', body: 'py-4'

// Object targets specific slots
card({ class: { base: 'shadow-xl', header: 'text-blue-700', body: 'min-h-24' } });
// base: 'border shadow-xl', header: 'font-bold text-blue-700', body: 'py-4 min-h-24'

Both class and className are supported, but class is prioritized when both are used in the same time.

Post-Processing

Apply a custom transformation to the final class strings using postProcess. This is useful for integrating with libraries like tailwind-merge:

import { sv } from 'slot-variants';
import { twMerge } from 'tailwind-merge';

const button = sv('px-4 py-2 bg-blue-500', {
  variants: {
    size: {
      sm: 'px-2 py-1 text-sm',
      lg: 'px-6 py-3 text-lg'
    }
  },
  postProcess: twMerge
});

The postProcess function is applied to each slot's final class string independently.

Caching

Results are cached automatically for performance. The default cache size is 256 entries. Calls with class or className props bypass the cache.

const button = sv('btn', {
  variants: {
    size: {
      sm: 'text-sm',
      lg: 'text-lg'
    }
  },
  cacheSize: 512        // customize the cache size
});

button.getCacheSize();  // current number of cached entries
button.clearCache();    // clear all cached entries

Introspection

The returned function exposes configuration properties for runtime introspection:

const button = sv('btn', {
  slots: {
    icon: 'w-4 h-4'
  },
  variants: {
    size: {
      sm: 'text-sm',
      lg: 'text-lg'
    },
    intent: {
      primary: 'bg-blue-500',
      danger: 'bg-red-500'
    }
  },
  defaultVariants: {
    size: 'sm'
  },
  requiredVariants: ['intent'],
  presets: {
    cta: { size: 'lg', intent: 'primary' }
  }
});

button.variantKeys;      // ['size', 'intent']
button.variants;         // { size: { sm: 'text-sm', lg: 'text-lg' }, intent: { ... } }
button.slotKeys;         // ['base', 'icon']
button.slots;            // { icon: 'w-4 h-4' }
button.defaultVariants;  // { size: 'sm' }
button.requiredVariants; // ['intent']
button.presetKeys;       // ['cta']
button.presets;          // { cta: { size: 'lg', intent: 'primary' } }

TypeScript

slot-variants is fully typed. Variant props are inferred from your config:

import { sv, type VariantProps } from 'slot-variants';

const button = sv('btn', {
  variants: {
    size: {
      sm: 'text-sm',
      lg: 'text-lg'
    },
    intent: {
      primary: 'bg-blue-500',
      danger: 'bg-red-500'
    }
  },
  requiredVariants: ['intent']
});

// Extract the variant props type (excludes class/className)
type ButtonProps = VariantProps<typeof button>;
// { size?: 'sm' | 'lg' | undefined; intent: 'primary' | 'danger' }

Excluding Variants from Props

VariantProps accepts an optional second type parameter to exclude specific variants from the extracted props. This is useful when some variants are controlled internally by a component and should not be exposed to consumers:

const button = sv('btn', {
  variants: {
    size: {
      sm: 'text-sm',
      lg: 'text-lg'
    },
    intent: {
      primary: 'bg-blue-500',
      danger: 'bg-red-500'
    },
    internalState: {
      active: 'ring-2',
      idle: ''
    }
  }
});

type ButtonProps = VariantProps<typeof button, 'internalState'>;
// { size?: 'sm' | 'lg' | undefined; intent?: 'primary' | 'danger' | undefined }

Multiple variants can be excluded using a union:

type ButtonProps = VariantProps<typeof button, 'internalState' | 'intent'>;
// { size?: 'sm' | 'lg' | undefined }

Exported Types

| Type | Description | | --- | --- | | ClassValue | Valid input types for cn() | | VariantProps<T, E> | Extracts variant props from an sv() return type, optionally excluding keys in E |

Return Type

  • Without slots - the function returns a string
  • With slots - the function returns a Record with base and each slot name as keys, all typed as string

Config Reference

| Option | Type | Description | | --- | --- | --- | | variants | SVVariants | Variant definitions mapping variant names to their possible values | | slots | SVSlots | Named slot definitions for multi-element components | | compoundVariants | Array | Additional classes applied when multiple variant conditions match | | compoundSlots | Array | Classes applied to multiple slots based on variant conditions | | defaultVariants | Object | Default values for variants (static values or functions) | | requiredVariants | string[] | Variant names that must be provided at call time | | presets | Record<string, Partial<VariantProps>> | Named combinations of variant values selectable via preset prop | | postProcess | (className: string) => string | Custom transformation applied to final class strings | | cacheSize | number | Maximum number of cached results (default: 256) |