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

@grainshape/fold

v0.0.2

Published

Type-safe class-variant utility library

Downloads

41

Readme

@grainshape/fold

npm version license

Type-safe class variant resolver. Define component variants as a config object, get back a strongly-typed function that resolves the right class string at runtime.

Based on CVA by Joe Bell. Adds foldConfig() for lossless type preservation, a partition() method for framework-friendly prop splitting, and fixes several TypeScript inference issues present in CVA's type signatures.


Install

pnpm add @grainshape/fold

If you are using Tailwind and want automatic class conflict resolution, use @grainshape/tw-fold instead.


Basic usage

import { fold } from '@grainshape/fold'

const button = fold({
  base: 'font-medium rounded',
  variants: {
    intent: {
      primary: 'bg-blue-500 text-white',
      secondary: 'bg-gray-100 text-gray-900',
    },
    size: {
      sm: 'px-2 py-1 text-sm',
      md: 'px-4 py-2',
      lg: 'px-6 py-3 text-lg',
    },
  },
  defaultVariants: {
    intent: 'primary',
    size: 'md',
  },
})

button()                              // 'font-medium rounded bg-blue-500 text-white px-4 py-2'
button({ intent: 'secondary' })       // 'font-medium rounded bg-gray-100 text-gray-900 px-4 py-2'
button({ size: 'sm' })                // 'font-medium rounded bg-blue-500 text-white px-2 py-1 text-sm'
button({ class: 'w-full' })           // appends 'w-full' to the resolved string

Compound variants

Apply additional classes when a specific combination of variants is active.

const button = fold({
  base: 'font-medium rounded',
  variants: {
    intent: {
      primary: 'bg-blue-500',
      secondary: 'bg-gray-100',
    },
    size: {
      sm: 'text-sm',
      lg: 'text-lg',
    },
  },
  compoundVariants: [
    // single value match
    { intent: 'primary', size: 'lg', class: 'uppercase tracking-wide' },
    // array match — applies when size is 'sm' or 'lg'
    { intent: 'secondary', size: ['sm', 'lg'], class: 'border border-gray-300' },
  ],
})

Compose

Merge multiple fold functions into one. All variant keys are combined; each component only receives the props it knows about.

import { fold, compose } from '@grainshape/fold'

const box = fold({
  variants: {
    shadow: { sm: 'shadow-sm', md: 'shadow-md', lg: 'shadow-lg' },
  },
})

const stack = fold({
  variants: {
    gap: { 1: 'gap-1', 2: 'gap-2', 4: 'gap-4' },
    direction: { row: 'flex-row', col: 'flex-col' },
  },
})

const card = compose(box, stack)

card({ shadow: 'md', gap: 2, direction: 'col' })
// 'shadow-md gap-2 flex-col'

The composed function is fully typed — shadow, gap, and direction are all inferred from their respective configs.


partition()

Every fold and compose result exposes a partition() method that splits an arbitrary props object into [variantProps, otherProps]. This is particularly useful in Svelte, where spreading unknown props onto a DOM element causes attribute warnings.

<!-- Button.svelte -->
<script lang="ts">
  import type { Snippet } from 'svelte'
  import { fold, type VariantProps } from '@grainshape/fold'

  const styles = fold({
    base: 'font-medium rounded',
    variants: {
      intent: {
        primary: 'bg-blue-500 text-white',
        secondary: 'bg-gray-100 text-gray-900',
      },
      size: { sm: 'px-2 py-1', md: 'px-4 py-2' },
    },
    defaultVariants: { intent: 'primary', size: 'md' },
  })

  type Props = VariantProps<typeof styles> & {
    children?: Snippet
    // any other props, e.g. native button attributes
    [key: string]: unknown
  }

  let { children, ...props }: Props = $props()

  // variantProps = { intent, size }  (only what fold knows about)
  // restProps   = { onclick, disabled, ... } (everything else — reactive)
  let [variantProps, restProps] = $derived(styles.partition(props))
</script>

<button class={styles(variantProps)} {...restProps}>
  {@render children?.()}
</button>

partition() is fully typed — variantProps contains only the known variant keys, restProps contains everything else, both inferred from the shape of the input.


cx()

A clsx-compatible utility, passed through any onResolve hook defined via defineConfig.

import { cx } from '@grainshape/fold'

cx('foo', undefined, false, 'bar')   // 'foo bar'
cx({ active: true, disabled: false }) // 'active'

foldConfig()

A typed identity function that captures a config's full generic type without instantiating a fold. This was one of the original motivations for building fold over CVA.

The problem with plain objects: if you define a config as a plain object literal and pass it around, TypeScript widens the type — you lose the exact variant key literals. foldConfig() preserves them.

// Without foldConfig — type is widened, variant keys are lost
const config = {
  variants: { size: { sm: 'small', lg: 'large' } }
}
// typeof config.variants.size → Record<string, string> — 'sm' | 'lg' is gone

// With foldConfig — full type is preserved
import { foldConfig, fold } from '@grainshape/fold'

const buttonConfig = foldConfig({
  base: 'font-medium rounded',
  variants: {
    intent: { primary: 'bg-blue-500', secondary: 'bg-gray-100' },
  },
})
// typeof buttonConfig knows exactly: intent is 'primary' | 'secondary'

const button = fold(buttonConfig)
// button's props are still fully typed: { intent?: 'primary' | 'secondary' }

This matters most when sharing configs across components or when using compose — each component's .config property is typed exactly, and compose.configs returns a typed tuple rather than a generic array:

const boxConfig = foldConfig({
  variants: { shadow: { sm: 'shadow-sm', md: 'shadow-md' } },
})
const stackConfig = foldConfig({
  variants: { gap: { 1: 'gap-1', 2: 'gap-2' } },
})

const box = fold(boxConfig)
const stack = fold(stackConfig)
const card = compose(box, stack)

// card.configs is typed as [typeof boxConfig, typeof stackConfig]
// — not a generic array, the exact tuple shape is preserved
type Configs = typeof card.configs
// [Readonly<FoldConfig<...boxConfig shape...>>, Readonly<FoldConfig<...stackConfig shape...>>]

VariantProps

Extract the variant props type from a fold or compose function.

import { fold, type VariantProps } from '@grainshape/fold'

const button = fold({
  variants: {
    intent: { primary: '...', secondary: '...' },
    size: { sm: '...', md: '...' },
  },
})

type ButtonProps = VariantProps<typeof button>
// { intent?: 'primary' | 'secondary'; size?: 'sm' | 'md' }

Custom resolver (defineConfig)

Use defineConfig to create a custom instance of fold, compose, and cx with a post-processing hook on the resolved class string. This is how @grainshape/tw-fold adds tailwind-merge support.

import { defineConfig } from '@grainshape/fold'
import { twMerge } from 'tailwind-merge'

const { fold, compose, cx, foldConfig } = defineConfig({
  hooks: {
    onResolve: (className) => twMerge(className),
  },
})

export { fold, compose, cx, foldConfig }

Credits

Built on top of the ideas and type patterns from CVA by Joe Bell. fold extends CVA's approach with foldConfig() for preserved generic types, partition() for prop splitting, and a defineConfig() factory for custom resolvers.


License

MIT © John van Dijk

This package contains code derived from CVA by Joe Bell (Apache 2.0). See NOTICE for details.