@use-pico/cls
Version:
Type-safe, composable styling system for React, Vue, Svelte, and vanilla JS
1,437 lines (1,214 loc) โข 71 kB
Markdown
# CLS - Type-Safe Styling System ๐
> **The styling system that finally makes sense** - because we're tired of CSS chaos! ๐จโจ
[](https://badge.fury.io/js/@use-pico%2Fcls)
[](https://opensource.org/licenses/MIT)
## ๐ Table of Contents
- [๐ฏ What is CLS?](#-what-is-cls)
- [๐ Installation](#-installation)
- [๐ Quick Start](#-quick-start)
- [โจ Core Features](#-core-features)
- [๐ Quality & Reliability](#-quality--reliability)
- [๐๏ธ Contract Builder API](#๏ธ-contract-builder-api)
- [๐ Basic Button Example](#-basic-button-example)
- [๐ฏ Using Your Button](#-using-your-button)
- [๐ง Contract Builder Methods](#-contract-builder-methods)
- [๐จ Definition Builder Methods](#-definition-builder-methods)
- [๐ง Smart Type Safety](#-smart-type-safety)
- [๐จ Design Tokens](#-design-tokens)
- [๐ฏ Type-Safety Magic](#-type-safety-magic)
- [๐ Basic Concepts](#-basic-concepts)
- [๐ง Mental Model](#-mental-model)
- [๐ท๏ธ Tokens](#-tokens)
- [๐ช Slots](#-slots)
- [๐ญ Variants](#-variants)
- [๐ฏ Rules](#-rules)
- [๐งฌ Inheritance](#-inheritance)
- [โ๏ธ React Integration](#๏ธ-react-integration)
- [๐ฏ Advanced Features](#-advanced-features)
- [๐ Performance](#-performance)
- [๐ฏ When to Use CLS](#-when-to-use-cls)
- [๐ Comparison with Other Solutions](#-comparison-with-other-solutions)
- [๐ Real-World Comparison Examples](#-real-world-comparison-examples)
- [๐ Community & Support](#-community--support)
- [๐ค LLM Compatibility](#-llm-compatibility)
## ๐ฏ What is CLS?
**CLS** stands for **Class List System** (because we're creative with acronyms, obviously! ๐). Think of it as that **smart friend** who shows up to a construction site with a perfectly organized toolbox instead of just throwing random tools in a bag ๐งฐ.
While other styling libraries are like *"here's a hammer, good luck building a house"*, CLS is like *"here's a blueprint, here are the materials, and here's how they all work together โ oh, and everything is type-safe because we're not savages"* ๐.
### Why CLS? Because we're tired of:
- ๐จ **Styling chaos** - hunting down inconsistent colors across codebases
- ๐ **Runtime surprises** - styling errors that should've been caught at compile time
- ๐ **Multiple sources of truth** - maintaining CSS variables AND TypeScript types
- ๐ **Vendor lock-in** - being stuck with heavy, opinionated styling solutions
## ๐ Installation
```bash
npm install @use-pico/cls
# or
bun add @use-pico/cls
# or
yarn add @use-pico/cls
```
## ๐ Quick Start
Let's build a button component from scratch! This example shows the **Contract Builder API** - the recommended way to create CLS instances:
```typescript
import { contract } from '@use-pico/cls';
// Step 1: Create a contract (define what can be styled)
const ButtonCls = contract()
.tokens(["color.bg.primary", "color.text.primary"]) // Design tokens
.slots(["root", "label"]) // Component parts
.variant("size", ["sm", "md", "lg"]) // Size variants
.bool("disabled") // Boolean variant
.def() // Start definition phase
.token({ // Define token values
"color.bg.primary": { class: ["bg-blue-600"] },
"color.text.primary": { class: ["text-white"] },
})
.root({ // Base styling
root: {
token: ["color.bg.primary", "color.text.primary"],
class: ["px-4", "py-2", "rounded", "font-medium"],
},
label: { class: ["select-none"] },
})
.rule({ size: "lg" }, { root: { class: ["px-6", "py-3"] } }) // Conditional rules
.rule({ disabled: true }, { root: { class: ["opacity-50", "cursor-not-allowed"] } })
.defaults({ size: "md", disabled: false }) // Default values
.cls(); // Create CLS instance
// Step 2: Use it! (variadic tweak parameters, undefined values cleaned up)
const slots = ButtonCls.create({
variant: { size: "lg" }
});
console.log(slots.root()); // "bg-blue-600 text-white px-4 py-2 rounded font-medium px-6 py-3"
// Individual slots also support tweaks!
const customSlots = ButtonCls.create();
console.log(customSlots.root({ variant: { size: "sm" } })); // "bg-blue-600 text-white px-3 py-1 text-sm"
```
### With React (because who doesn't love React? ๐)
```tsx
import { useCls } from '@use-pico/cls';
function MyButton({ size = "md", disabled = false, tweak }) {
const { slots, variant } = useCls(ButtonCls,
{ variant: { size, disabled } }, // Component props (lower precedence)
tweak // User tweak (highest precedence)
);
return (
<button className={slots.root()}>
<span className={slots.label()}>Click me</span>
</button>
);
}
```
## โจ Core Features
- **๐ Type Safety First** - Catch styling errors at compile time (no more midnight debugging sessions! ๐)
- **๐งฉ Composable** - Build design systems with inheritance and composition
- **โก Performance** - Lazy evaluation, smart caching, and minimal runtime overhead
- **๐ Framework Agnostic** - Works with React, Vue, Svelte, vanilla JS, or any framework
- **๐จ Design System Ready** - Tokens, variants, and slots for scalable styling
- **๐ฏ Granular Control** - Individual slots support tweaks for fine-grained customization
- **๐ Developer Experience** - Excellent IDE support and intuitive API
- **๐ก๏ธ Battle-Tested** - Over 300 comprehensive tests ensuring rock-solid behavior and preventing regressions
## ๐ Quality & Reliability
CLS isn't just another styling library - it's a **heavily polished, battle-tested solution** that's ready for production use.
### โจ Heavily Polished & Production-Ready
CLS has been refined through *countless iterations* to achieve **maximum convenience** and developer experience. Every piece has been *carefully crafted* and polished to perfection. While the project powers several **larger applications**, its primary development is driven by *side projects* โ ensuring the library maintains the **highest quality standards*.
**Production-ready stability** is guaranteed with **zero breaking changes** going forward. When we added the powerful Contract Builder API, it was built *on top of* the main `cls()` function without affecting the overall API - ensuring existing code continues to work perfectly while new features enhance the experience. **Your investment in CLS is safe** - we're committed to maintaining backward compatibility and stable APIs.
### ๐งช Extensively Tested
CLS is *thoroughly tested* with comprehensive test suites covering all features, edge cases, and integration scenarios. The test coverage deeply encompasses all **three main APIs** of CLS: the **Contract Builder API** (fluent contract and definition building), the **low-level `cls()` function** (direct contract and definition usage), and the **React integration** (hooks, context, and component patterns). **Every bug that has been found gets a new test** to ensure it never happens again - creating a robust safety net that grows stronger with each discovery. This rigorous testing approach means you can trust CLS to work reliably in production environments.
## ๐๏ธ Contract Builder API
The **Contract Builder** is CLS's main API - it's fluent, type-safe, and incredibly powerful! Think of it as your styling toolkit that guides you every step of the way.
### ๐ Basic Button Example
```typescript
import { contract } from '@use-pico/cls';
const ButtonCls = contract()
.slots(["root", "icon", "label"]) // Define component parts
.variant("size", ["sm", "md", "lg"]) // Define size options
.variant("tone", ["default", "primary"]) // Define tone options
.bool("disabled") // Add boolean variant
.def() // Start defining styles
.root({ // Base styles (no conditions)
root: { class: ["inline-flex", "items-center", "gap-2"] },
icon: { class: ["w-4", "h-4"] },
label: { class: ["font-medium"] }
})
.match("size", "sm", { // Size-specific styles
root: { class: ["px-3", "py-1", "text-sm"] }
})
.match("size", "md", {
root: { class: ["px-4", "py-2", "text-base"] }
})
.match("size", "lg", {
root: { class: ["px-6", "py-3", "text-lg"] }
})
.match("tone", "primary", { // Tone-specific styles
root: { class: ["bg-blue-600", "text-white"] }
})
.match("disabled", true, { // Boolean variant
root: { class: ["opacity-50", "cursor-not-allowed"] }
})
.defaults({ // Default values
size: "md",
tone: "default",
disabled: false
})
.cls(); // Create the CLS instance
```
### ๐ฏ Using Your Button
```typescript
// Create styled slots and variants (variadic tweak parameters)
const { slots, variant } = ButtonCls.create({
variant: {
size: "lg",
tone: "primary",
disabled: false
}
});
// Use in your component
<button className={slots.root()}>
<Icon className={slots.icon()} />
<span className={slots.label()}>Click me!</span>
</button>
// Access resolved variants for component logic
console.log(variant.size); // "lg"
console.log(variant.tone); // "primary"
console.log(variant.disabled); // false
```
### ๐ง Contract Builder Methods
**Define the structure** (used before `.def()`):
| Method | Purpose | Example |
|--------|---------|---------|
| `.tokens()` | Define multiple tokens | `.tokens(["color.primary", "spacing.md"])` |
| `.token()` | Define single token | `.token("color.primary")` |
| `.slots()` | Define component parts | `.slots(["root", "icon", "label"])` |
| `.slot()` | Add single slot | `.slot("badge")` |
| `.variant()` | Add string/enum variants | `.variant("size", ["sm", "md", "lg"])` |
| `.variants()` | Define multiple variants | `.variants({ size: ["sm", "md"], tone: ["default", "primary"] })` |
| `.bool()` | Add boolean variants | `.bool("disabled")` |
### ๐จ Definition Builder Methods
**Define the styling** (used after `.def()`):
| Method | Purpose | Example |
|--------|---------|---------|
| `.root()` | Base styles (no conditions) | `.root({ root: { class: ["flex"] } })` |
| `.rule()` | Complex condition matching | `.rule({ size: "lg", disabled: true }, { root: { class: ["opacity-50"] } })` |
| `.match()` | Single variant matching | `.match("size", "lg", { root: { class: ["px-6"] } })` |
| `.switch()` | Boolean variant helper | `.switch("disabled", { root: { class: ["opacity-50"] } }, { root: { class: ["opacity-100"] } })` |
| `.tokens.rule()` | Token conditional rules | `.tokens.rule({ tone: "primary" }, { "color.bg": { class: ["bg-blue-600"] } })` |
| `.tokens.match()` | Single variant token matching | `.tokens.match("tone", "primary", { "color.bg": { class: ["bg-blue-600"] } })` |
| `.tokens.switch()` | Boolean variant token helper | `.tokens.switch("disabled", { "color.bg": { class: ["bg-gray-300"] } }, { "color.bg": { class: ["bg-blue-600"] } })` |
| `.defaults()` | Set default variant values | `.defaults({ size: "md", disabled: false })` |
| `.cls()` | Create final CLS instance | `.cls()` |
### ๐ง Smart Type Safety
CLS enforces correctness at every step:
```typescript
const ButtonCls = contract()
.variant("size", ["sm", "md", "lg"])
.bool("disabled")
.def()
.match("size", "xl", { root: { class: ["px-8"] } }) // โ TypeScript error! "xl" not in variants
.match("loading", true, { root: { class: ["animate-spin"] } }) // โ TypeScript error! "loading" not defined
.cls();
```
### ๐จ Design Tokens
Create reusable design values:
```typescript
const ThemeCls = contract()
.token({
"color.bg.primary": { class: ["bg-blue-600"] },
"color.text.primary": { class: ["text-white"] },
"spacing.padding.md": { class: ["px-4", "py-2"] }
})
.def()
.root({ root: { class: ["font-sans"] } })
.cls();
// Use tokens in other components
const ButtonCls = contract(ThemeCls.contract)
.def()
.root({
root: {
token: ["color.bg.primary", "color.text.primary", "spacing.padding.md"]
}
})
.cls();
```
## ๐ Basic Concepts
### ๐ง Mental Model
CLS is built on **two powerful pillars**: **contract-first design** and **heavy typechecking**. Think of it like building a house โ you start with solid blueprints, then everything flows naturally! ๐๏ธ
#### Contract = Structure (What can be styled?)
```typescript
// Contract defines WHAT can be styled using Contract Builder API
const MyComponentCls = contract()
.tokens(["color.text", "color.bg"]) // What design tokens exist?
.slots(["root", "label"]) // What parts can be styled?
.variant("size", ["sm", "md", "lg"]) // What size variations are possible?
.variant("variant", ["default", "primary"]) // What style variations are possible?
.build(); // This creates the contract structure
```
#### Definition = Values (How is it styled?)
```typescript
// Definition defines HOW it's styled using Definition Builder API
const MyComponentCls = contract()
.tokens(["color.text", "color.bg"])
.slots(["root", "label"])
.variant("size", ["sm", "md", "lg"])
.variant("variant", ["default", "primary"])
.def() // Start definition phase
.token({ // Define token values
"color.text.default": { class: ["text-gray-900"] },
"color.text.primary": { class: ["text-white"] },
"color.bg.default": { class: ["bg-gray-100"] },
"color.bg.primary": { class: ["bg-blue-600"] }
})
.root({ // Base styles (no conditions)
root: { class: ["inline-flex", "items-center"] }
})
.match("size", "lg", { // Size-specific styles
root: { class: ["px-6", "py-3"] }
})
.match("variant", "primary", { // Variant-specific styles
root: { token: ["color.text.primary", "color.bg.primary"] }
})
.defaults({ size: "md", variant: "default" }) // Default values
.cls(); // This creates the complete CLS instance
```
> **๐ก Key Insight**: Think of **Contract** as the "interface" and **Definition** as the "implementation"
### ๐ท๏ธ Tokens
Tokens are **named design values** that can be referenced throughout your components:
```typescript
const ButtonCls = contract()
.tokens(["color.bg.primary", "color.text.primary", "spacing.padding.md"]) // Define tokens
.slots(["root"])
.def()
.token({ // Define token values
"color.bg.primary": { class: ["bg-blue-600"] },
"color.text.primary": { class: ["text-white"] },
"spacing.padding.md": { class: ["px-4", "py-2"] }
})
.root({ // Reference tokens in slots
root: { token: ["color.bg.primary", "color.text.primary"] }
})
.cls();
```
### ๐ฏ Token Rules
Tokens support **conditional rules** just like slots! Use `.tokens.rule()` to apply different token values based on variants:
```typescript
const ButtonCls = contract()
.tokens(["color.bg", "color.text"])
.slots(["root"])
.variant("tone", ["default", "primary", "danger"])
.bool("disabled")
.def()
.token({ // Base token values
"color.bg": { class: ["bg-gray-100"] },
"color.text": { class: ["text-gray-900"] }
})
.tokens.rule({ tone: "primary" }, { // Primary tone tokens
"color.bg": { class: ["bg-blue-600"] },
"color.text": { class: ["text-white"] }
})
.tokens.rule({ tone: "danger" }, { // Danger tone tokens
"color.bg": { class: ["bg-red-600"] },
"color.text": { class: ["text-white"] }
})
.tokens.rule({ disabled: true }, { // Disabled state tokens
"color.bg": { class: ["bg-gray-300"] },
"color.text": { class: ["text-gray-500"] }
})
.root({
root: { token: ["color.bg", "color.text"] }
})
.cls();
```
**๐จ Token Rule Helpers:**
```typescript
// Match specific variant combinations
.tokens.match("tone", "primary", {
"color.bg": { class: ["bg-blue-600"] }
})
// Boolean variant switch (generates two rules)
.tokens.switch("disabled",
{ "color.bg": { class: ["bg-gray-300"] } }, // when disabled: true
{ "color.bg": { class: ["bg-blue-600"] } } // when disabled: false
)
// Override previous token rules
.tokens.rule({ tone: "primary" }, {
"color.bg": { class: ["bg-purple-600"] }
}, true) // override: true
```
### ๐ช Slots
Slots represent **named parts** of a component that can receive independent styling:
```typescript
const ButtonCls = contract()
.slots(["root", "icon", "label", "badge"]) // Define component parts
.def()
.root({ // Style each slot independently
root: { class: ["flex", "items-center"] },
icon: { class: ["w-4", "h-4"] },
label: { class: ["font-medium"] },
badge: { class: ["ml-2", "px-1", "rounded"] }
})
.cls();
```
### ๐ญ Variants
Variants are **configurable properties** that control component appearance:
```typescript
const ButtonCls = contract()
.variant("size", ["sm", "md", "lg"]) // String variants
.variant("tone", ["default", "primary", "danger"])
.bool("disabled") // Boolean variants
.bool("loading")
.slots(["root"])
.def()
.match("size", "lg", { // Use variants in rules
root: { class: ["px-6", "py-3"] }
})
.match("disabled", true, {
root: { class: ["opacity-50"] }
})
.cls();
```
### ๐ฏ Rules
Rules define **conditional styling** based on variant combinations:
```typescript
const ButtonCls = contract()
.variant("size", ["sm", "md", "lg"])
.variant("tone", ["default", "primary"])
.bool("disabled")
.slots(["root"])
.def()
// Simple rule
.match("size", "lg", {
root: { class: ["px-6", "py-3"] }
})
// Complex rule with multiple conditions
.rule({ size: "lg", tone: "primary" }, {
root: { class: ["px-8", "py-4"], token: ["color.bg.primary"] }
})
// Boolean rules
.match("disabled", true, {
root: { class: ["opacity-50", "cursor-not-allowed"] }
})
.cls();
```
### ๐ฏ Type-Safety Magic โจ
CLS's contract builder is **incredibly clever** - it tracks what you've defined in your contract and **forces you to implement everything properly**. Here's how it works:
#### **Token Enforcement** ๐ท๏ธ
```typescript
// โ
Define tokens in contract
const ButtonCls = contract()
.tokens(["color.bg.primary", "color.text.primary"]) // โ Declare tokens
.def()
.token({ // โ MUST implement all declared tokens
"color.bg.primary": { class: ["bg-blue-600"] },
"color.text.primary": { class: ["text-white"] },
// Missing "color.bg.primary"? TypeScript will SCREAM! ๐จ
})
.cls(); // โ
Only works when all tokens are implemented
```
**What happens if you miss a token?**
```typescript
// โ This will cause a TypeScript error!
const BadButtonCls = contract()
.tokens(["color.bg.primary", "color.text.primary"])
.def()
.token({
"color.bg.primary": { class: ["bg-blue-600"] },
// Missing "color.text.primary" - TypeScript error!
})
.cls(); // Error: "Tokens are defined on a contract, but you've not called token() definition method"
```
#### **Default Enforcement** โ๏ธ
```typescript
// โ
Define variants in contract
const ButtonCls = contract()
.variant("size", ["sm", "md", "lg"]) // โ Declare variants
.def()
.defaults({ // โ MUST provide defaults for all variants
size: "md",
// Missing size default? TypeScript will SCREAM! ๐จ
})
.cls(); // โ
Only works when all defaults are provided
```
**What happens if you miss a default?**
```typescript
// โ This will cause a TypeScript error!
const BadButtonCls = contract()
.variant("size", ["sm", "md", "lg"])
.def()
// Missing .defaults() call - TypeScript error!
.cls(); // Error: "Variants are defined on a contract, but you've not called defaults() definition method"
```
#### **Slot Flexibility** ๐ช
```typescript
// โ
Define slots in contract
const ButtonCls = contract()
.slots(["root", "label"]) // โ Declare available slots
.def()
.root({ // โ Style only the slots you need
root: { class: ["px-4", "py-2"] },
label: { class: ["font-medium"] },
// You're free to style any or all declared slots! ๐จ
})
.cls();
```
**Flexibility at its finest:**
```typescript
// โ
Style only some slots
const MinimalButton = contract()
.slots(["root", "label", "icon"])
.def()
.root({
root: { class: ["px-4", "py-2"] },
// label and icon are optional - no TypeScript errors!
})
.cls();
// โ
Style slots in different rules
const DynamicButton = contract()
.slots(["root", "label", "icon"])
.variant("size", ["sm", "md"])
.def()
.root({
root: { class: ["px-4", "py-2"] },
})
.rule({ size: "lg" }, {
icon: { class: ["w-6", "h-6"] }, // Add icon styling only for large buttons
})
.cls();
```
#### **Variant Type Safety** ๐ญ
```typescript
// โ
TypeScript knows exactly what variants are valid
const ButtonCls = contract()
.variant("size", ["sm", "md", "lg"])
.bool("disabled")
.def()
.rule({ size: "lg" }, { root: { class: ["px-6"] } }) // โ
Valid
.rule({ size: "xl" }, { root: { class: ["px-8"] } }) // โ TypeScript error! "xl" not in ["sm", "md", "lg"]
.rule({ disabled: true }, { root: { class: ["opacity-50"] } }) // โ
Valid
.cls();
```
### **The Magic Behind the Scenes** ๐งโโ๏ธ
CLS uses **advanced TypeScript features** to enforce correctness:
1. **Conditional Types** - Checks if tokens/variants exist in contract
2. **Template Literal Types** - Creates precise error messages
3. **Generic Constraints** - Ensures type safety throughout the chain
4. **Type-Level Flags** - Tracks completion state (hasToken, hasDefaults)
```typescript
// This is what CLS does under the hood (simplified):
type Builder<TContract, TState> =
// If contract has tokens but you haven't called .token()
Check.If<Token.Has<TContract>, TState["hasToken"]> extends false
? { cls(error: "Tokens are defined but you've not called token()"): never }
: {}
// If contract has variants but you haven't called .defaults()
& Check.If<Variant.With<TContract>, TState["hasDefaults"]> extends false
? { cls(error: "Variants are defined but you've not called defaults()"): never }
: {}
// Only when everything is properly implemented
& { cls(): Cls.Type<TContract> }
```
### **Why This Matters** ๐ฏ
- **๐ No More Runtime Surprises** - Catch errors at compile time, not in production
- **๐ง Better Developer Experience** - TypeScript guides you to correct usage
- **๐ Guaranteed Completeness** - Can't forget to implement required parts
- **๐ Self-Documenting** - Contract serves as both interface and documentation
- **๐ Refactoring Safety** - Change contract, TypeScript tells you what to update
### Basic Contract Builder Methods
```typescript
import { contract } from '@use-pico/cls';
const ButtonCls = contract()
.tokens(["color.bg.primary", "color.text.primary"]) // Add tokens
.slots(["root", "label"]) // Add slots
.variant("size", ["sm", "md", "lg"]) // Add string variants
.bool("disabled") // Add boolean variants
.def() // Start definition phase
.token({ // Define token values
"color.bg.primary": { class: ["bg-blue-600"] },
"color.text.primary": { class: ["text-white"] },
})
.root({ // Define base styling
root: { class: ["px-4", "py-2", "rounded"] },
label: { class: ["font-medium"] },
})
.rule({ size: "lg" }, { root: { class: ["px-6", "py-3"] } }) // Add rules
.defaults({ size: "md", disabled: false }) // Set defaults
.cls(); // Create instance
```
### Low-Level API - Advanced Usage
> **โ ๏ธ Advanced Usage Warning:** The low-level `cls()` function is available but **quite complicated** and should generally be avoided in favor of the Contract Builder API. It's included here for completeness, but most developers should stick with the `contract()` approach.
#### **1. Low-Level `cls()` Function**
```typescript
import { cls } from '@use-pico/cls';
// Call cls() with everything embedded for maximum type safety
const ButtonCls = cls(
// Contract (structure) - embedded inline
{
tokens: ["color.bg.primary"],
slot: ["root"],
variant: { size: ["sm", "md"] }
},
// Definition (styling) - embedded inline
{
token: {
"color.bg.primary": { class: ["bg-blue-600"] }
},
rules: [
{
match: {}, // No conditions (root rule)
slot: {
root: {
class: ["px-4", "py-2"],
token: ["color.bg.primary"]
}
}
}
],
defaults: { size: "md" }
}
);
```
#### **2. Runtime Overrides (Single Tweak)**
```typescript
// โ
Runtime overrides using single tweak object (undefined values are cleaned up)
const { slots, variant } = ButtonCls.create({
variant: { size: "lg" },
slot: {
root: { class: ["shadow-lg"] }
},
token: {
"color.bg.primary": { class: ["bg-indigo-600"] } // Override token value
}
});
```
## ๐งฌ Inheritance
CLS makes inheritance **simple and type-safe**:
```typescript
// Parent contract
const BaseButtonCls = contract()
.tokens(["color.bg", "color.text"])
.slots(["root"])
.variant("size", ["sm", "md", "lg"])
.def()
.token({
"color.bg": { class: ["bg-gray-100"] },
"color.text": { class: ["text-gray-900"] },
})
.root({ root: { token: ["color.bg", "color.text"], class: ["border", "rounded"] } })
.defaults({ size: "md" })
.cls();
// Child contract inheriting from parent
const PrimaryButtonCls = contract(BaseButtonCls.contract)
.tokens(["color.bg.primary"]) // Add new tokens
.bool("disabled") // Add new variants
.def()
.token({
"color.bg.primary": { class: ["bg-blue-600"] },
"color.bg": { class: ["bg-white"] }, // Override parent token
})
.rule({ disabled: true }, { root: { class: ["opacity-50"] } })
.defaults({ size: "lg", disabled: false }) // Override parent defaults
.cls();
```
## โ๏ธ React Integration
CLS provides **first-class React integration** with hooks, context, and HOCs. Here's a complete, real-world example:
### Complete Button Component Example
```tsx
import { type Cls, useCls, withCls } from '@use-pico/cls';
import type { ButtonHTMLAttributes, FC, Ref } from 'react';
// Available type exports for advanced usage:
// import type { Utils } from '@use-pico/cls'; // Utility type helpers
// 1. Define your CLS instance (simplified version)
const ButtonCls = contract()
.slots(["wrapper", "root"])
.bool("disabled")
.variant("size", ["sm", "md", "lg"])
.variant("tone", ["primary", "secondary", "danger"])
.def()
.root({
wrapper: { class: ["Button-wrapper"] },
root: {
class: ["Button-root", "flex", "items-center", "gap-2", "transition-all"],
token: ["scale.default", "border.default"]
}
})
.rule({ size: "sm" }, { root: { token: ["size.sm"] } })
.rule({ size: "md" }, { root: { token: ["size.md"] } })
.rule({ size: "lg" }, { root: { token: ["size.lg"] } })
.rule({ tone: "primary" }, { root: { token: ["tone.primary"] } })
.rule({ tone: "secondary" }, { root: { token: ["tone.secondary"] } })
.rule({ tone: "danger" }, { root: { token: ["tone.danger"] } })
.rule({ disabled: true }, {
wrapper: { class: ["cursor-not-allowed"] },
root: { token: ["disabled"] }
})
.defaults({ size: "md", tone: "primary", disabled: false })
.cls();
// 2. Export the type and namespace for TypeScript integration
export type ButtonCls = typeof ButtonCls;
export namespace ButtonCls {
export type Props<P = unknown> = Cls.Props<ButtonCls, P>;
}
// 3. Define component props with CLS integration
export namespace Button {
export interface Props extends ButtonCls.Props<ButtonHTMLAttributes<HTMLButtonElement>> {
wrapperRef?: Ref<HTMLDivElement>;
buttonRef?: Ref<HTMLButtonElement>;
loading?: boolean;
// CLS variant props with proper typing
size?: Cls.VariantOf<ButtonCls, "size">;
tone?: Cls.VariantOf<ButtonCls, "tone">;
}
}
// 4. Base component with full CLS integration
export const BaseButton: FC<Button.Props> = ({
wrapperRef,
buttonRef,
loading,
size,
tone,
cls = ButtonCls, // Allow CLS override
tweak, // User customization
disabled,
children,
...props
}) => {
// 5. useCls with internal and user configs
const { slots, variant } = useCls(cls,
{ // Internal config (lower precedence)
variant: {
disabled: disabled || loading,
size,
tone,
},
},
tweak // User tweak (highest precedence)
);
// `variant` contains the resolved variant values from both component props and user tweaks
// This gives you access to the final resolved state for component logic if needed
// console.log(variant.size); // "lg" (from props or tweak override)
// console.log(variant.disabled); // true/false (component logic or tweak override)
// console.log(variant.tone); // "primary" (from props or tweak override)
return (
<div
ref={wrapperRef}
className={slots.wrapper()}
>
<button
ref={buttonRef}
className={slots.root()}
disabled={disabled || loading}
{...props}
>
{loading && <span>โณ</span>}
{children}
</button>
</div>
);
};
// 6. Export with withCls HOC for external CLS access
export const Button = withCls(BaseButton, ButtonCls);
```
### Key React Integration Features
#### **1. Props Extension Pattern** ๐
```tsx
// Extend CLS props with native HTML attributes
export interface Props extends ButtonCls.Props<ButtonHTMLAttributes<HTMLButtonElement>> {
// Custom props
loading?: boolean;
// CLS variants with proper typing
size?: Cls.VariantOf<ButtonCls, "size">;
tone?: Cls.VariantOf<ButtonCls, "tone">;
}
```
#### **2. Nested Array Support in Tweak Prop** ๐ฏ
The `tweak` prop in `Cls.Props` supports **nested arrays** for complex tweak combinations:
```tsx
// Single tweak object
tweak?: { variant: { size: "lg" }, slot: { root: { class: ["custom"] } } }
// Array of tweaks (last takes precedence)
tweak?: [
{ variant: { size: "sm" } },
{ slot: { root: { class: ["override"] } } }
]
// Nested arrays with undefined values (automatically filtered)
tweak?: [
{ variant: { size: "md" } },
[
undefined,
{ slot: { root: { class: ["nested"] } } },
undefined
],
{ variant: { tone: "primary" } }
]
// Deep nesting (up to 10 levels supported)
tweak?: [
{ variant: { size: "lg" } },
[
[
{ slot: { root: { class: ["deep-nested"] } } }
]
]
]
```
**Key Features:**
- **๐ Automatic Flattening**: Nested arrays are automatically flattened up to 10 levels deep
- **๐งน Undefined Filtering**: `undefined` values are automatically removed
- **๐ Precedence Order**: Later tweaks override earlier ones (left to right)
- **๐ฏ Type Safety**: Full TypeScript support for all nested structures
**Practical Example - Complex Tweak Composition:**
```tsx
const MyButton = ({ size, tone, disabled, loading, userTweak }) => {
const { slots, variant } = useCls(ButtonCls, {
// Complex nested tweak structure
tweak: [
// Base component logic
{ variant: { size, tone, disabled: disabled || loading } },
// Conditional styling based on state
loading ? { slot: { root: { class: ["loading"] } } } : undefined,
// Nested array for complex overrides
[
{ slot: { root: { class: ["base-override"] } } },
disabled ? { slot: { root: { class: ["disabled-override"] } } } : undefined,
],
// User customization (highest precedence)
userTweak
]
});
return <button className={slots.root()}>Button</button>;
};
```
#### **3. useCls with Multiple Tweaks** โ๏ธ
```tsx
const { slots, variant } = useCls(
cls, // CLS instance (can be overridden)
{ // Internal config (component logic) - LOWEST PRECEDENCE
variant: {
disabled: disabled || loading, // Component-controlled logic
},
},
{ // Props config (from component props) - MEDIUM PRECEDENCE
variant: { size, tone }
},
tweak // User config (from tweak prop) - HIGHEST PRECEDENCE
);
```
> **๐ก Precedence Rule:** Tweaks are processed in order, with **last tweak taking precedence** over earlier ones. All tweaks are cleaned up (undefined values removed), so you can safely pass partial objects without affecting the final result.
#### **3. withCls HOC** ๐ญ
```tsx
// Enables external CLS access
export const Button = withCls(BaseButton, ButtonCls);
// Now you can access:
// Button.cls - The full CLS instance
// Button.contract - The contract (structure definition)
// Button.definition - The definition (styling values)
// Button["~slots"] - Internal slots kit (advanced usage)
```
**Advanced withCls Usage:**
```tsx
// Access contract for extending
const CustomButton = contract(Button.contract)
.variant("style", ["outline", "ghost"])
.def()
.rule({ style: "outline" }, { root: { class: ["border-2"] } })
.cls();
// Access definition for inspection
console.log(Button.definition.defaults); // { size: "md", tone: "primary" }
console.log(Button.definition.rules); // All styling rules
```
### Advanced Usage Examples
#### **Tweak Precedence Pattern** ๐ฏ
```tsx
// Complete example showing tweak precedence: user > props > component
interface ButtonProps {
size?: Cls.VariantOf<ButtonCls, "size">;
tone?: Cls.VariantOf<ButtonCls, "tone">;
disabled?: boolean;
loading?: boolean;
tweak?: Cls.Props<ButtonCls>["tweak"]; // User customization
}
const Button: FC<ButtonProps> = ({
size,
tone,
disabled,
loading,
tweak, // User tweak - HIGHEST PRECEDENCE
children
}) => {
const { slots, variant } = useCls(ButtonCls,
{ // 1. Component logic (lowest precedence)
variant: {
disabled: disabled || loading
}
},
{ // 2. Props from component (medium precedence)
variant: { size, tone }
},
tweak // 3. User tweak (highest precedence)
);
return (
<button className={slots.root()}>
{variant.loading ? "Loading..." : children}
</button>
);
};
// Usage examples:
<Button
size="md"
tone="primary"
tweak={{ variant: { size: "lg" } }} // User overrides size to "lg"
>
Button Text
</Button>
// Result: size="lg" (user tweak wins), tone="primary" (from props)
```
#### **Custom CLS Instance**
```tsx
// Create custom button variant
const CustomButtonCls = contract(ButtonCls.contract)
.variant("style", ["outline", "ghost"])
.def()
.rule({ style: "outline" }, { root: { class: ["border-2", "bg-transparent"] } })
.rule({ style: "ghost" }, { root: { class: ["bg-transparent", "hover:bg-gray-100"] } })
.cls();
// Use custom CLS with type safety
<Button cls={ButtonCls.use(CustomButtonCls)} style="outline" tone="danger">
Custom Button
</Button>
```
> **๐ฏ Type Safety Trick:** `ButtonCls.use(CustomButtonCls)` ensures **type compatibility** - `CustomButtonCls` *must* extend `ButtonCls` or TypeScript will error! This prevents incompatible CLS instances from being passed.
#### **Runtime Customization**
```tsx
// User customization via tweak prop
<Button
size="lg"
tone="primary"
tweak={{
slot: what.slot({
root: { class: ["shadow-lg", "hover:shadow-xl"] }
}),
variant: { tone: "secondary" } // Override tone
})}
>
Customized Button
</Button>
```
#### **Tweak Utility for Advanced Merging**
```tsx
// Manual tweak merging for advanced scenarios
const MyButton = ({ userTweak, size, tone, disabled }) => {
const finalTweak = ButtonCls.tweak(
{ variant: { disabled } }, // Component logic (lowest precedence)
{ variant: { size, tone } }, // Props (medium precedence)
userTweak // User customization (highest precedence)
);
const { slots, variant } = ButtonCls.create(finalTweak);
return <button className={slots.root()}>Button</button>;
};
// Alternative: Direct usage (simplest)
const SimpleButton = ({ userTweak }) => {
const { slots, variant } = ButtonCls.create(userTweak);
return <button className={slots.root()}>Button</button>;
};
```
#### **Context Integration**
```tsx
import { TokenContext, VariantProvider } from '@use-pico/cls';
const App = () => (
<TokenContext value={ThemeCls}>
<div>
{/* All buttons inherit theme tokens */}
<Button tone="primary">Themed Button</Button>
<Button tone="secondary">Another Themed Button</Button>
{/* Scope variant overrides to a subtree with automatic type inference */}
<VariantProvider cls={ButtonCls} variant={{ size: "lg" }}>
<div>
{/* All buttons in this subtree will be large */}
<Button tone="primary">Large Themed Button</Button>
<Button tone="secondary">Another Large Themed Button</Button>
{/* Nested VariantProvider with inheritance - merges parent variants */}
<VariantProvider cls={ButtonCls} variant={{ tone: "danger" }} inherit>
<div>
{/* These buttons will be large AND danger (inherited size + new tone) */}
<Button>Large Danger Button</Button>
<Button>Another Large Danger Button</Button>
</div>
</VariantProvider>
</div>
</VariantProvider>
</div>
</TokenContext>
);
```
> **๐๏ธ Design System Compatibility:** If you have an external Design System (e.g., `DesignCls` from which `ThemeCls` extends), use `DesignCls.use(ThemeCls)` in the provider to ensure **design system compatibility**. This guarantees everything matches and runs as expected across your entire application.
### Available React Hooks
#### **useCls** - The Main Hook ๐ฏ
```tsx
const { slots, variant } = useCls(ButtonCls, ...tweaks);
```
**`useCls` is the main hook** for CLS in React components. It returns both **slots** and **variants** for maximum flexibility:
```tsx
// Simple usage - single tweak (automatically subscribes to TokenContext and VariantContext)
const { slots, variant } = useCls(ButtonCls, tweak);
// Multiple tweaks with precedence (last takes precedence)
const { slots, variant } = useCls(ButtonCls,
internalTweak, // Lowest precedence
propsTweak, // Medium precedence
userTweak // Highest precedence
);
// Use slots for styling
<button className={slots.root()}>Button</button>
// Individual slots also support tweaks!
<button className={slots.root(
{ variant: { size: "lg" } }, // Override size for this specific slot
{ slot: { root: { class: ["shadow-lg"] } } } // Add custom classes
)}>Button</button>
// Use variants for component logic
console.log(variant.size); // "lg"
console.log(variant.disabled); // true/false
console.log(variant.tone); // resolved from theme or props
```
**Key Features:**
- โ
**Automatically connected to TokenContext** - Global theme inheritance works seamlessly
- โ
**Automatically connected to VariantContext** - Scoped variant overrides work automatically
- โ
**Performance optimized** - Minimal overhead for common use cases
- โ
**Type-safe** - Full TypeScript support with proper inference
- โ
**Complete API** - Both slots and variants in one hook
- โ
**Single source of truth** - Access resolved variant values when needed
**When to Use Variants:**
- ๐ฏ **Component logic** that needs to access resolved variant values
- ๐ฏ **Conditional rendering** based on variant combinations
- ๐ฏ **Debugging** - Inspect what variants are actually applied
- ๐ฏ **Analytics** - Track which variant combinations users interact with
> **๐ก Pro Tip:** This is the hook you'll use **100% of the time**. It provides everything you need - slots for styling and variants for component logic - all in one convenient, type-safe package!
#### **useTokenContext** - Access Token Context ๐
```tsx
const context = useTokenContext<ButtonCls>();
```
**Low-level hook** for accessing the current CLS context:
```tsx
// Access current theme
const themeCls = useTokenContext();
// With type safety
const buttonTheme = useTokenContext<ButtonCls>();
```
**When to Use:**
- ๐ฏ **Custom hooks** that need theme access
- ๐ฏ **Theme switching logic**
- ๐ฏ **Advanced composition patterns**
> **๐ง Advanced Usage:** This is a low-level hook. Most components should use `useCls` which automatically connects to context.
#### **useVariantContext** - Access Variant Context ๐๏ธ
```tsx
const variantContext = useVariantContext();
```
**Hook** for accessing the current VariantContext values:
```tsx
// Access current variant context
const variants = useVariantContext();
```
**When to Use:**
- ๐ฏ **Custom hooks** that need variant access
- ๐ฏ **Conditional logic** based on variant values
- ๐ฏ **Advanced composition patterns**
> **๐ง Advanced Usage:** This is a low-level hook. Most components should use `useCls` which automatically integrates with VariantContext.
#### **VariantProvider** - Scoped Variant Context ๐๏ธ
**React component** for providing type-safe variant overrides to a subtree:
```tsx
<VariantProvider cls={ButtonCls} variant={{ size: "lg" }} inherit>
<YourComponents />
</VariantProvider>
```
**Props:**
- **`cls`** - CLS instance (used for type inference)
- **`variant`** - Variant object with overrides (not whole tweak)
- **`inherit`** - Whether to merge with parent VariantContext (default: `false`)
**Inheritance Behavior:**
- **`inherit={false}`** (default) - Replaces parent variants completely
- **`inherit={true}`** - Merges with parent variants, lower VariantProvider takes precedence
```tsx
// Parent provides size: "lg"
<VariantProvider cls={ButtonCls} variant={{ size: "lg" }}>
{/* Child replaces parent completely */}
<VariantProvider cls={ButtonCls} variant={{ tone: "danger" }}>
{/* Result: { tone: "danger" } - size is lost */}
</VariantProvider>
{/* Child inherits and merges with parent */}
<VariantProvider cls={ButtonCls} variant={{ tone: "danger" }} inherit>
{/* Result: { size: "lg", tone: "danger" } - both preserved */}
</VariantProvider>
{/* Reset all variants from parent - provide empty variants and disable inheritance */}
<VariantProvider cls={ButtonCls} variant={{}}>
{/* Result: {} - no variants from parent or child */}
</VariantProvider>
</VariantProvider>
```
**When to Use:**
- ๐ฏ **Scoped variant overrides** for specific UI sections
- ๐ฏ **Nested theming** with inheritance
- ๐ฏ **Conditional styling** based on variant context
- ๐ฏ **Resetting variants** - Use `variant={{}}` to clear all parent variants
**Common Patterns:**
```tsx
// Override specific variants
<VariantProvider cls={ButtonCls} variant={{ size: "lg" }}>
<Button>Large Button</Button>
</VariantProvider>
// Inherit and extend parent variants
<VariantProvider cls={ButtonCls} variant={{ tone: "danger" }} inherit>
<Button>Danger Button with inherited size</Button>
</VariantProvider>
// Reset all variants (clear parent context)
<VariantProvider cls={ButtonCls} variant={{}}>
<Button>Button with default variants only</Button>
</VariantProvider>
```
#### **wrap** - Type-Safe VariantProvider Factory ๐ญ
**Utility function** for creating type-safe `VariantProvider` components:
```tsx
import { wrap } from '@use-pico/cls';
// Create a type-safe VariantProvider factory for a specific CLS
const ButtonWrapper = wrap(ButtonCls);
// Use it without needing to pass cls prop
<ButtonWrapper.VariantProvider variant={{ size: "lg" }}>
<YourComponents />
</ButtonWrapper.VariantProvider>
```
**Benefits:**
- ๐ฏ **Type safety** - VariantProvider is pre-configured with the correct CLS type
- ๐ฏ **Cleaner syntax** - No need to pass `cls` prop every time
- ๐ฏ **Reusable** - Create once, use everywhere with the same CLS
**When to Use:**
- ๐ฏ **Component libraries** - Pre-configure VariantProviders for your components
- ๐ฏ **Design systems** - Create typed providers for consistent usage
- ๐ฏ **Large applications** - Reduce boilerplate when using the same CLS repeatedly
#### **useClsMemo** - Memoized CLS Hook ๐
```tsx
const { slots, variant } = useClsMemo(ButtonCls, tweaks, deps);
```
**Performance-optimized version** of `useCls` that memoizes both slots and variants using `useMemo`:
```tsx
// Simple usage - single tweak with memoization
const { slots, variant } = useClsMemo(ButtonCls, tweak, [tweak]);
// Array of tweaks with memoization
const MyButton = ({ size, tone, disabled, loading, tweak }) => {
const { slots, variant } = useClsMemo(
ButtonCls,
[ // Array of tweaks (last takes precedence)
{ // Component logic - LOWEST PRECEDENCE
variant: {
disabled: disabled || loading // Component-controlled logic
}
},
{ // Props from component - MEDIUM PRECEDENCE
variant: { size, tone }
},
tweak // User customization from props - HIGHEST PRECEDENCE
],
[size, tone, disabled, loading, tweak] // Only recompute when these change
);
// Use slots for styling
// Use variants for component logic
console.log(variant.size); // "lg"
console.log(variant.disabled); // true/false
console.log(variant.tone); // resolved from theme or props
return <button className={slots.root()}>Button</button>;
};
```
**When to Use `useClsMemo`:**
- ๐ฏ **Performance-critical components** with stable props
- ๐ฏ **Large component trees** where memoization prevents cascading re-renders
- ๐ฏ **Components with expensive CLS computations**
- ๐ฏ **Analytics or logging** based on variant combinations
- ๐ฏ **Conditional rendering** based on resolved variant states
> **โ ๏ธ Important Dependency Note:** `useClsMemo` is **dependency-driven**. If a user provides a dynamic `tweak` prop to a component, the memoized hook won't automatically detect changes in the tweak function's behavior. You must include all relevant dependencies in the `deps` array, or the hook will use stale values. For dynamic user tweaks, consider using the non-memoized version (`useCls`) instead.
**Practical Example - Performance Optimization:**
```tsx
// Performance-critical button with memoization
const OptimizedButton = ({ size, tone, disabled, loading, tweak }) => {
const { slots, variant } = useClsMemo(
ButtonCls,
[
{ // Internal config (lower precedence)
variant: {
size,
tone,
disabled: disabled || loading
}
},
tweak // User tweak (highest precedence)
],
[size, tone, disabled, loading, tweak] // All dependencies included
);
return (
<button className={slots.root()}>
{variant.loading ? "Loading..." : "Click me"}
</button>
);
};
```
## ๐ฏ Advanced Features
### Runtime Overrides
```typescript
// Override tokens at creation time (variadic tweak parameters)
const { slots, variant } = ButtonCls.create({
variant: { size: "lg" },
token: {
// Runtime override of token values
"color.bg.primary": { class: ["bg-indigo-600"] }
}
});
// Multiple tweaks with precedence
const { slots, variant } = ButtonCls.create(
internalTweak, // Lowest precedence
propsTweak, // Medium precedence
userTweak // Highest precedence
);
```
### Individual Slot Tweaks
```typescript
// Individual slots support variadic tweaks too!
const { slots, variant } = ButtonCls.create();
// Basic slot usage
<button className={slots.root()}>Button</button>
// Slot with variant override
<button className={slots.root({ variant: { size: "lg" } })}>Large Button</button>
// Slot with multiple tweaks
<button className={slots.root(
{ variant: { size: "lg" } }, // Override size
{ slot: { root: { class: ["shadow-lg"] } } } // Add custom classes
)}>Custom Button</button>
// Slot with token override
<button className={slots.root({
token: { "color.bg.primary": { class: ["bg-purple-600"] } }
})}>Purple Button</button>
```
### Override and Clear Flags
```typescript
// Use the override flag to explicitly override previous tweaks
const { slots, variant } = ButtonCls.create(
{ variant: { size: "md" } }, // This will be overridden
{
override: true, // Override flag
variant: { size: "lg" } // This will override the previous size
}
);
// Use the clear flag to completely reset and start fresh
const { slots, variant } = ButtonCls.create(
{ variant: { size: "md" } }, // This will be completely ignored
{ slot: { root: { class: ["px-4"] } } }, // This will be completely ignored
{
clear: true, // Clear flag - kills all previous tweaks
variant: { size: "lg" } // Only this will be applied
}
);
// Override flag works with slots too
<button className={slots.root(
{ slot: { root: { class: ["px-4"] } } }, // This will be overridden
{
override: true, // Override flag
slot: { root: { class: ["px-8"] } } // This will override the previous padding
}
)}>Button</button>
// Clear flag works with slots too
<button className={slots.root(
{ slot: { root: { class: ["px-4"] } } }, // This will be completely ignored
{ slot: { root: { class: ["py-2"] } } }, // This will be completely ignored
{
clear: true, // Clear flag - kills all previous tweaks
slot: { root: { class: ["px-8", "py-4"] } } // Only this will