@classmatejs/react

中文文档
0.3.0发布于 2026/06/28MITTypeScriptESM + CJS近 30 天下载 2.3万

A react tool to separate class name logic, create variants and manage styles.

此软件包已被标记为弃用

Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.

原始使用文档
文档目录23 节
  1. 🚩 Transform this
  2. 🌤️ Into
  3. Features
  4. Contents
  5. Getting started
  6. Basic
  7. Use with props
  8. Prefix incoming props with $
  9. Create Variants
  10. Typescript: Separate base props and variants with a second …
  11. Extend
  12. Transform
  13. Add CSS Styles
  14. Use inside React components - useClassmate
  15. Add logic headers
  16. Recipes for cm.extend
  17. extend from variants
  18. Auto infer types for props
  19. Extending other lib components / any as Input
  20. CommonJS
  21. Tailwind Merge
  22. Upcoming
  23. Inspiration

@classmatejs/react

A tool for managing React component class names, variants and styles.

🚩 Transform this

const SomeButton = ({ isLoading, ...props }) => {
  const activeClass = isLoading
    ? "bg-blue-400 text-white"
    : "bg-blue-800 text-blue-200";

  return (
    <button
      {...props}
      className={`transition-all mt-5 border-1 md:text-lg text-normal ${someConfig.transitionDurationEaseClass} ${activeClass} ${
        props.className || ""
      }`}
    >
      {props.children}
    </button>
  );
};

🌤️ Into

const SomeButton = cm.button`
  text-normal
  md:text-lg
  mt-5
  border-1
  transition-all
  ${someConfig.transitionDurationEaseClass}
  ${({ $isLoading }) => $isLoading && "opacity-90 pointer-events-none"}
`;

Features

  • Class name-focused components
  • Variants
  • Extend components
  • Transform components
  • Dynamic styles
  • TypeScript support
  • Tested with SSR Frameworks
  • Classname merging

Contents

Getting started

Make sure you have installed React (> 16.8.0) in your project.

npm i @classmatejs/react
# or
yarn add @classmatejs/react

For bundled apps, you can also install ClassmateJS as a dev dependency:

npm i -D @classmatejs/react
# or
yarn add -D @classmatejs/react

Use a regular dependency for packages/libraries or deployments that resolve node_modules at runtime after installing only production dependencies.

Basic

Create a component by calling cm with a tag name and a template literal string.

import cm from "@classmatejs/react";

const Container = cm.div`
  py-2
  px-5
  min-h-24
`;
// transforms to: <div className="py-2 px-5 min-h-24" />

Use with props

Pass props to the component and use them in the template literal string and in the component prop validation.

// hey typescript
interface ButtonProps {
  $isActive?: boolean;
  $isLoading?: boolean;
}
const SomeButton = cm.button<ButtonProps>`
  text-lg
  mt-5
  ${({ $isActive }) => ($isActive ? "bg-blue-400 text-white" : "bg-blue-400 text-blue-200")}
  ${({ $isLoading }) => $isLoading && "opacity-90 pointer-events-none"}
`;
// transforms to <button className="text-lg mt-5 bg-blue-400 text-white opacity-90 pointer-events-none" />

Prefix incoming props with $

we prefix the props incoming to dc with a $ sign. This is a important convention to distinguish dynamic props from the ones we pass to the component.

This pattern should also avoid conflicts with reserved prop names.

Create Variants

Create variants by passing an object to the variants key like in cva. The key should match the prop name and the value should be a function that returns a string. You could also re-use the props in the function.

interface AlertProps {
  $severity: "info" | "warning" | "error";
  $isActive?: boolean;
}
const Alert = cm.div.variants<AlertProps>({
  // optional
  base: (p) => `
    ${p.isActive ? "custom-active" : "custom-inactive"}
    p-4
    rounded-md
  `,
  // required
  variants: {
    $severity: {
      warning: "bg-yellow-100 text-yellow-800",
      info: (p) =>
        `bg-blue-100 text-blue-800 ${p.$isActive ? "shadow-lg" : ""}`,
      error: (p) =>
        `bg-red-100 text-red-800 ${p.$isActive ? "ring ring-red-500" : ""}`,
    },
  },
  // optional - used if no variant was found
  defaultVariant: {
    $severity: "info",
  },
});

export default () => <Alert $severity="info" $isActive />;
// outputs: <div className="custom-active p-4 rounded-md bg-blue-100 text-blue-800 shadow-lg" />

Typescript: Separate base props and variants with a second type parameter

As seen above, we also pass AlertProps to the variants, which can cause loose types. If you want to separate the base props from the variants, you can pass a second type to the variants function so that only those props are available in the variants.

interface AlertProps {
  $isActive?: boolean;
}
interface AlertVariants {
  $severity: "info" | "warning" | "error";
}
const Alert = cm.div.variants<AlertProps, AlertVariants>({
  base: `p-4 rounded-md`,
  variants: {
    // in here there are only the keys from AlertVariants available
    $severity: {
      // you can use the props from AlertProps here again
      warning: "bg-yellow-100 text-yellow-800",
      info: (p) =>
        `bg-blue-100 text-blue-800 ${p.$isActive ? "shadow-lg" : ""}`,
      error: (p) =>
        `bg-red-100 text-red-800 ${p.$isActive ? "ring ring-red-500" : ""}`,
    },
  },
  // optional - used if no variant was found
  defaultVariant: {
    $severity: "info",
  },
});

Extend

Extend a component directly by passing the component and the tag name.

import MyOtherComponent from "./MyOtherComponent"; // () => <button className="text-lg mt-5" />
import cm from "@classmatejs/react";

const Container = cm.extend(MyOtherComponent)`
  py-2
  px-5
  min-h-24
`;
// transforms to: <button className="text-lg mt-5 py-2 px-5 min-h-24" />

Transform

Transform renders an existing classmate component as another intrinsic element. It keeps the original class logic and $ props, but TypeScript validates props against the new element.

import cm from "@classmatejs/react";

const MyButton = cm.button<{ $toggleCta?: boolean }>`
  absolute
  min-w-300
  ${({ $toggleCta }) => ($toggleCta ? "bg-red-500" : "bg-blue-500")}
`;

export default () => <MyButton $_as="span" $toggleCta />;
// outputs: <span className="absolute min-w-300 bg-red-500" />

Use the builder API when you want a reusable transformed component, or add more classes with a tagged template.

const SpanButton = cm.transform(MyButton).span;
const TextSpanButton = cm.transform(MyButton).span`
  text-neutral
  tracking-wide
`;

export default () => <TextSpanButton $toggleCta />;
// outputs: <span className="absolute min-w-300 bg-red-500 text-neutral tracking-wide" />

Transform also works with variants and extended classmate components.

const VariantDiv = cm.div.variants<{ $size: "sm" | "md" }>({
  base: "absolute min-w-300",
  variants: {
    $size: {
      sm: "w-10 h-10",
      md: "w-20 h-20",
    },
  },
  defaultVariants: {
    $size: "md",
  },
});

const Main = cm.transform(VariantDiv).main`
  text-neutral
  tracking-wide
`;

export default () => <Main $size="sm" role="main" />;
// outputs: <main className="absolute min-w-300 w-10 h-10 text-neutral tracking-wide" role="main" />

cm.transform only accepts classmate components in v1. Regular React components are not transformable because classmate cannot know where their classes are attached.

Add CSS Styles

You can use CSS styles in the template literal string with the style function. This function takes an object with CSS properties and returns a string. We can use the props from before.

// Base:
const StyledButton = cm.button<{ $isDisabled: boolean }>`
  text-blue
  ${(p) =>
  p.style({
    boxShadow: "0 0 5px rgba(0, 0, 0, 0.1)",
    cursor: p.$isDisabled ? "not-allowed" : "pointer",
  })}
`;
export default () => <StyledButton $isDisabled />;
// outputs: <button className="text-blue" style="box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); cursor: not-allowed;" />
// Extended:
const BaseButton = cm.button<{ $isActive?: boolean }>`
  ${(p) =>
  p.style({
    backgroundColor: p.$isActive ? "green" : "red",
  })}
`;
const ExtendedButton = cm.extend(BaseButton)<{ $isLoading?: boolean }>`
  ${(p) =>
  p.style({
    opacity: p.$isLoading ? 0.5 : 1,
    pointerEvents: p.$isLoading ? "none" : "auto",
  })}
`;
export default () => <ExtendedButton $isActive $isLoading />;
// outputs: <button className="bg-red" style="opacity: 0.5; pointer-events: none;" />

Use inside React components - useClassmate

When you need to create a classmate component inside another React component (for example, when the configuration depends on runtime-only values), wrap the factory with useClassmate. This memoizes the result and avoids creating a brand-new component on every render.

import cm, { useClassmate } from "@classmatejs/react";

const WorkoutDay = ({ status }: { status: "completed" | "pending" }) => {
  const StyledDay = useClassmate(
    () =>
      cm.div.variants({
        base: "rounded border p-4 text-sm",
        variants: {
          $status: {
            completed: "border-green-400 bg-green-50",
            pending: "border-yellow-400 bg-yellow-50",
          },
        },
      }),
    [status], // recompute when dependencies change
  );

  return <StyledDay $status={status}>Workout details</StyledDay>;
};

The dependency array behaves like React.useMemo. Pass everything the factory closes over if you expect the component to update when those values change.

Add logic headers

Use .logic() to run arbitrary JavaScript once per render before your class names or variants are computed. The return value is shallow-merged back into the props, so you can derive $ props, DOM attributes, or anything else your component needs without additional hooks.

type DayStatus = "completed" | "pending"

interface WorkoutProps {
  workouts: unknown[]
  allResolved: boolean
  hasCompleted: boolean
  hasSkipped: boolean
  $status?: DayStatus
}

const WorkoutDay = cm.div
  .logic<WorkoutProps>((props) => {
    const status = deriveDayStatus(props)
    return {
      $status: status,
      ["data-status"]: status,
    }
  })
  .variants<WorkoutProps, { $status: DayStatus }>({
    base: "rounded border p-4",
    variants: {
      $status: {
        completed: "bg-green-50 border-green-400",
        pending: "bg-white border-slate-200",
      },
    },
  })

// Consumers only pass raw workout data – the logic header derives $status for you.
<WorkoutDay workouts={workouts} allResolved hasCompleted hasSkipped={false} />

Return values from .logic() are merged in order, so later logic calls can reference earlier results or override them.

Recipes for cm.extend

With cm.extend, you can build upon any base React component, adding new styles and even supporting additional props. This makes it easy to create reusable component variations without duplicating logic.

import { ArrowBigDown } from "lucide-react";
import cm from "@classmatejs/react";

const StyledLucideArrow = cm.extend(ArrowBigDown)`
  md:-right-4.5
  right-1
  slide-in-r-20
`;

// ts: we can pass only props which are accessible on a `lucid-react` Component
export default () => <StyledLucideArrow stroke="3" />;

⚠️ Having problems by extending third party components, see: Extending other lib components

Now we can define a base component, extend it with additional styles and classes, and pass properties. You can pass the types to the extend function to get autocompletion and type checking.

import cm from "@classmatejs/react";

interface StyledSliderItemBaseProps {
  $active: boolean;
}
const StyledSliderItemBase = cm.button<StyledSliderItemBaseProps>`
  absolute
  h-full
  w-full
  left-0
  top-0
  ${(p) => (p.$active ? "animate-in fade-in" : "animate-out fade-out")}
`;

interface NewStyledSliderItemProps extends StyledSliderItemBaseProps {
  $secondBool: boolean;
}
const NewStyledSliderItemWithNewProps = cm.extend(
  StyledSliderItemBase,
)<NewStyledSliderItemProps>`
  rounded-lg
  text-lg
  ${(p) => (p.$active ? "bg-blue" : "bg-red")}
  ${(p) => (p.$secondBool ? "text-underline" : "some-class-here")}
`;

export default () => (
  <NewStyledSliderItemWithNewProps $active $secondBool={false} />
);
// outputs: <button className="absolute h-full w-full left-0 top-0 animate-in fade-in rounded-lg text-lg bg-blue" />

extend from variants

interface ButtonProps extends InputHTMLAttributes<HTMLInputElement> {
  $severity: "info" | "warning" | "error";
  $isActive?: boolean;
}

const Alert = cm.input.variants<ButtonProps>({
  base: "p-4",
  variants: {
    $severity: {
      info: (p) =>
        `bg-blue-100 text-blue-800 ${p.$isActive ? "shadow-lg" : ""}`,
    },
  },
});

const ExtendedButton = cm.extend(Alert)<{ $test: boolean }>`
  ${(p) => (p.$test ? "bg-green-100 text-green-800" : "")}
`;

export default () => <ExtendedButton $severity="info" $test />;
// outputs: <input className="p-4 bg-blue-100 text-blue-800 shadow-lg bg-green-100 text-green-800" />

Auto infer types for props

By passing the component, we can validate the component to accept tag related props. This is useful if you wanna rely on the props for a specific element without the $ prefix.

// if you pass rc component it's types are validated
const ExtendedButton = cm.extend(cm.button``)`
  some-class
  ${(p) => (p.type === "submit" ? "font-normal" : "font-bold")}
`;

// infers the type of the input element + add new props
const MyInput = ({ ...props }: HTMLAttributes<HTMLInputElement>) => (
  <input {...props} />
);
const StyledDiv = cm.extend(MyInput)<{ $trigger?: boolean }>`
  bg-white
  ${(p) => (p.$trigger ? "!border-error" : "")}
  ${(p) => (p.type === "submit" ? "font-normal" : "font-bold")}
`;

Extending other lib components / any as Input

Unfortunately we cannot infer the type directly of the component if it's any or loosely typed. But we can use a intermediate step to pass the type to the extend function.

import { ComponentProps } from 'react'
import { MapContainer } from 'react-leaflet'
import { Field, FieldConfig } from 'formik'
import cm, { CmBaseComponent } from 'react-classmate'

// we need to cast the type to ComponentProps
type StyledMapContainerType = ComponentProps<typeof MapContainer>
const StyledMapContainer: CmBaseComponent<StyledMapContainerType> = cm.extend(MapContainer)`
  absolute
  h-full
  w-full
  text-white
  outline-0
`

export const Component = () => <StyledMapContainer bounds={...} />

// or with Formik

import { Field, FieldConfig } from 'formik'

type FieldComponentProps = ComponentProps<'input'> & FieldConfig
const FieldComponent = ({ ...props }: FieldComponentProps) => <Field {...props} />

const StyledField = cm.extend(FieldComponent)<{ $error: boolean }>`
  theme-form-field
  w-full
  ....
  ${p => (p.$error ? '!border-error' : '')}
`

export const Component = () => <StyledField placeholder="placeholder" as="select" name="name" $error />

CommonJS

If you are using CommonJS, you can import the library like this:

const cm = require("@classmatejs/react").default;

// or

const { default: cm } = require("@classmatejs/react");

Tailwind Merge

React-classmate uses tailwind-merge under the hood to merge class names. The last class name will always win, so you can use it to override classes.

Upcoming

  • bug / troubleshoot: classnames set by ref.current (useRef) will be overwritten as soon component rerenders
    • needs at least a small article in the docs
  • cm.raw() and cm.raw.variants() for only using rc syntax for classnames (output as string)
  • Variants for cm.extend
  • named lib import for CommonJS (currently only .default) -- Means we need to remove the named export in the ts file to not duplicate IDE import suggestions: --- Change postbuild script to remove named esm export
  • Integrate more tests, benchmarks focused on SSR and React
  • Advanced IDE integration
    • show generated default class on hover
    • enforce autocompletion and tooltips from the used libs

Inspiration

版本与发布记录

@classmatejs/react 共发布 10 个版本,首次发布于 2025/11/25;过去 12 个月发布了 10 个版本。

版本发布日期距今
0.3.0latest2026/06/284 个月前
0.2.12026/02/079 个月前
0.1.12026/02/079 个月前
0.1.02025/11/2911 个月前
0.0.62025/11/2711 个月前
0.0.52025/11/2711 个月前

关于 @classmatejs/react 的常见问题

@classmatejs/react 是什么?

A react tool to separate class name logic, create variants and manage styles.注意:该包已被作者标记为弃用——Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.

如何安装 @classmatejs/react?

在项目目录运行 npm install @classmatejs/react,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(>=16.8.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@classmatejs/react 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@classmatejs/react 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@classmatejs/react 可以商用吗?

@classmatejs/react 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@classmatejs/react 的下载量有多少?

根据 npm 官方下载统计,@classmatejs/react 在 2026/10/10 – 2026/11/08 这 30 天内被下载 23,257 次,日均约 775 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@classmatejs/react 还在维护吗?

最新版本 0.3.0 发布于 2026/06/28(4 个月前);至今共发布 10 个版本,其中过去 12 个月发布了 10 个;首次发布于 2025/11/25。作者已将其标记为弃用,新项目建议寻找替代方案。