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

use-cascade

v0.0.3

Published

Manipulate class names in React through a cascading context.

Readme

use-cascade

typescript biome vitest codecov npm

A better way to manipulate class names in React with support for Tailwind and CSS Modules.

Motivation

classNames is a great tool to manipulate classes in React. However, every time I use classNames, I need to create a new prop to provide additional classes from a parent component:

const TextInput = ({ className, ...props }) => {
    return <input type="text" className={classNames('input', className)}>; 
}

use-cascade intend to remove className props by using the context instead.

Quick example

The createCascade function returns a tuple with a consumer function and a Provider component.

import { createCascade } from 'use-cascade';

const [ useCascade, CascadeProvider ] = createCascade();

The consumer function

The consumer function combines the class names passed as direct arguments with the class names provided in the context.

The direct arguments can be used to give the "base" style to your element:

export function TextInput() {
    return <input type="text" className={useCascade('input')} />
}

The Provider component

The Provider component is a context provider that will provide the class names to its children.

First we need to export the Provider component from the file:

export const TextInputCascade = CascadeProvider;

Then we can use it in our component tree, to provide new class names to the cascade:

import { TextInput, TextInputCascade } from './TextInput';

function InputField() {
    return (<TextInputCascade className="fieldInput">
        <TextInput />
    </TextInputCascade>);
}

In this example, the input element will receive, as className prop the value "input fieldInput".

The providers Cascade

In react, the context provider the closest to the consumer will override the other providers above in the components tree.

function App() {
    return <Provider value="ignored value">
        <Provider value="consumed value">
            <Consumer />
        </Provider>
    </Provider>
}

In use-cascade however, providers are "cascading". Every class names provided in the tree will be received by the consumer. Using the previous inputField example, we can add another provider to the component tree:

import { TextInputCascade } from './TextInput';
import { InputField } from './InputField';

function Form() {
    return (<TextInputCascade className="formInput">
        <InputField />
    </TextInputCascade>);
}

In this example, the input element will receive, as className prop the value "input formInput fieldInput".

Providers priority

Class names order is only relevant using tools like tailwind-merge. In tailwind-merge the first class names have less priority than the last ones.

The consumerFunction returns the class names passed as direct arguments first, then the class names provided in the cascade starting from the top of the tree. That means, the more "specific" is a provider, the more priority will have the provided class names.

If that logic does not suite your use case, you may want to create a new cascade:

const [ useCascade, CascadeProvider ] = createCascade();

export const InputFieldCascade = CascadeProvider;

function InputField() {
    return (<TextInputCascade className={useCascade(/*...*/)}>
        <TextInput />
    </TextInputCascade>); 
}

In this case, the InputFieldCascade provider will have the priority over TextInputCascade.

API

The createCascade function can be called in different ways.

createCascade()

The createCascade function can be called without any argument. It returns a tuple with a consumer function and a Provider component:

[
    (className: string) => string, 
    FunctionComponent<{ className: string; children: ReactNode; }>
]

By default, the consumer function only accepts a single string.

createCascade(options)

The createCascade function can also be called with an options object.

type Options = {
	in?: (...args: any[]) => string;
	out?: (a: string) => string;
};

The options allows to transform the arguments and the return value of the consumer function.

The in function

The in function allows to transform the arguments passed to the consumer function into a single string.

Using classnames lib as an example, you can rewrite the following code:

import classNames from "classnames";

const [ useCascade ] = createCascade();
const className = useCascade(classNames('input', { isFocused }));

Like this:

const [ useCascade ] = createCascade({ in: classNames });
const className = useCascade('input', { isFocused });

The consumer function inherits the signature of in function provided in the options object. This way, any function can be used as long as it returns a string.

The out function

The out function allows to transform the return value of the consumer function into a single string.

Using tailwind-merge lib as an example, you can rewrite the following code:

import { twMerge } from "tailwind-merge";

const [ useCascade ] = createCascade();
const className = twMerge(useCascade('shadow-lg bg-slate-600 rounded-xl'));

Like this:

const [ useCascade ] = createCascade({ out: twMerge });
const className = useCascade('shadow-lg bg-slate-600 rounded-xl');

The out function must accept at least a string as the first argument and must return a string.

createCascade(...elements)

The createCascade function can be called with the list of elements in the cascade.

const [ useCascade, CascadeProvider ] = createCascade(
    'wrapper', 'title', 'description',
);

export const ArticleCascade = CascadeProvider;

This creates a consumer function for each element:

<article className={useCascade.wrapper('wrapper')}>
    <h1 className={useCascade.title('title')}>Title</h1>
    <p className={useCascade.description('description')}>
        {/* description */}
    </p>
</article>

This also creates a provider component for each element. This ensures targeting a specific element when providing new class to the cascade:

<ArticleCascade.wrapper className={'listArticle'}>
    {articles.map((item) => <Article {...item} />)}
</ArticleCascade.wrapper>

createCascade(options, ...elements)

The createCascade function can be called with both the list of elements in the cascade. The is combines the behaviors described in the above sections.

const [ useCascade, CascadeProvider ] = createCascade(
    { in: classNames.bind(styles) },
    'wrapper', 'title', 'description',
);

Guide

Using with CSS modules

If you already use classnames, you may be familliar with its "bind" approach:

import classNames from 'classnames/bind';

const cx = classNames.bind(styles);

This approach can be used to transform the class names provided to the consumer function into their hashed version.

const [ useCascade ] = createCascade({ in: classNames.bind(styles) });

This does not transform the class names provided outside the component. To achieve this behavior you must use the out function. classnames requires to split the classes to work properly:

const [ useCascade ] = createCascade({ 
   out: (classes: string) => classNames.bind(styles)(classes.split(" ")) 
});

Using Tailwind

If you use Tailwind, you may want to deduplicate classes and override already defined rules. tailwind-merge is the tool for you:

twMerge('px-2 py-1 bg-red hover:bg-dark-red', 'p-3 bg-[#B91C1C]')
// returns → 'hover:bg-dark-red p-3 bg-[#B91C1C]'

twMerge must be used as the out function:

const [ useCascade, CascadeProvider ] = createCascade({ out: twMerge });

export const TextInputCascade = CascadeProvider;

Using the same values as the above example:

// TextInput component
<input type="text" className={useCascade('px-2 py-1 bg-red hover:bg-dark-red')} />

// parent component
<TextInputCascade className="p-3 bg-[#B91C1C]">
    {/* ... */}
</TextInputCascade>

Under the hood, the twMerge helper will produce the expected class combination.