@zohodesk/components
Version:
Dot UI is a customizable React component library built to deliver a clean, accessible, and developer-friendly UI experience. It offers a growing set of reusable components designed to align with modern design systems and streamline application development
416 lines (314 loc) • 18 kB
Markdown
# Color Helpers
Tailwind-inspired color utility classes and a `colorHelper` for DOT components.
---
## File Structure
```
colorHelpers/
├── background/
│ └── backgroundColor.module.css # Background color utilities
├── text/
│ └── textColor.module.css # Text color utilities
├── border/
│ └── borderColor.module.css # Border color utilities
├── constants/
│ └── index.js # PALETTE_CONFIG (palette → tone mapping)
├── colorHelper.js # getPaletteClasses + getBgClass, getTextClass, getBorderClass
└── paletteUtilities.README.md # This file
```
---
## Available Colors
All three CSS files share the same 7 color tones:
| Tone | Token Family | Use Case |
|------|--------------|----------|
| **default** | `--zdt_v1_palette_default_*` | Default/neutral actions |
| **grey** | `--zdt_v1_palette_grey_*` | Secondary, muted elements |
| **blue** | `--zdt_v1_palette_blue_*` | Information, primary actions |
| **green** | `--zdt_v1_palette_green_*` | Success, positive status |
| **orange** | `--zdt_v1_palette_orange_*` | Warning, caution |
| **red** | `--zdt_v1_palette_red_*` | Error, danger, destructive actions |
| **yellow** | `--zdt_v1_palette_yellow_*` | Alerts, highlights |
---
## Available Utilities
### Background (`background/backgroundColor.module.css`)
Per color (`default`, `grey`, `blue`, `green`, `orange`, `red`, `yellow`):
| Class Pattern | Description |
|---------------|-------------|
| `.bg-{color}` | Base background |
| `.bg-{color}-light` | Light background variant |
| `.hover\:bg-{color}:hover` | Hover state |
| `.hover\:bg-{color}-light:hover` | Light hover state |
| `.focus\:bg-{color}:focus` | Focus state |
| `.focus\:bg-{color}-light:focus` | Light focus state |
| `.active\:bg-{color}:active` | Active state |
| `.active\:bg-{color}-light:active` | Light active state |
| `.selected\:bg-{color}` | Selected state |
| `.selected\:bg-{color}-light` | Light selected state |
**Utility classes:** `.bg-transparent`, `.bg-white`, `.bg-black`
### Text (`text/textColor.module.css`)
Per color (`default`, `grey`, `blue`, `green`, `orange`, `red`, `yellow`):
| Class Pattern | Description |
|---------------|-------------|
| `.text-{color}` | Base text color |
| `.hover\:text-{color}:hover` | Hover state |
| `.focus\:text-{color}:focus` | Focus state |
| `.active\:text-{color}:active` | Active state |
| `.selected\:text-{color}` | Selected state |
**Special:** `.text-brand-filled` (+ hover/focus/active/selected) — uses `--zdt_v1_palette_default_filled_text` for filled default backgrounds.
**Utility classes:** `.text-transparent`, `.text-white`, `.text-black`
> **Note:** Text has no `-light` variant. The `colorHelper` switches to `filledTextTone` (e.g., `white`) when the background is filled.
### Border (`border/borderColor.module.css`)
Per color (`default`, `grey`, `blue`, `green`, `orange`, `red`, `yellow`):
| Class Pattern | Description |
|---------------|-------------|
| `.border-{color}` | Base border |
| `.border-{color}-light` | Light border variant (resting state only) |
| `.hover\:border-{color}:hover` | Hover state |
| `.focus\:border-{color}:focus` | Focus state |
| `.active\:border-{color}:active` | Active state |
| `.selected\:border-{color}` | Selected state |
**Utility classes:** `.border-transparent`, `.border-white`, `.border-black`
> **Note:** Border state classes only use the base tone — no `-light` state variants. The `-light` class is available for the resting state only.
---
## Direct CSS Usage (Examples)
Import only the CSS modules you need and compose classes directly.
### Example 1: Status Tag (text + background)
```javascript
import bgStyle from './background/backgroundColor.module.css';
import textStyle from './text/textColor.module.css';
function StatusTag({ status }) {
const className = compileClassNames({
[bgStyle['bg-green-light']]: status === 'success',
[bgStyle['bg-red-light']]: status === 'error',
[bgStyle['bg-orange-light']]: status === 'warning',
[textStyle['text-green']]: status === 'success',
[textStyle['text-red']]: status === 'error',
[textStyle['text-orange']]: status === 'warning',
});
return <span className={className}>{status}</span>;
}
```
### Example 2: Bordered Card (border + background)
```javascript
import bgStyle from './background/backgroundColor.module.css';
import borderStyle from './border/borderColor.module.css';
function Card({ children, variant }) {
const className = compileClassNames({
[bgStyle['bg-blue-light']]: variant === 'info',
[bgStyle['bg-red-light']]: variant === 'error',
[borderStyle['border-blue']]: variant === 'info',
[borderStyle['border-red']]: variant === 'error',
});
return <div className={className}>{children}</div>;
}
```
### Example 3: Interactive Button (text + background + border + hover)
```javascript
import bgStyle from './background/backgroundColor.module.css';
import textStyle from './text/textColor.module.css';
import borderStyle from './border/borderColor.module.css';
function Button({ label, variant }) {
// Primary: blue bg, white text, blue border
const primaryClasses = compileClassNames({
[bgStyle['bg-blue']]: true,
[bgStyle['hover:bg-blue']]: true,
[textStyle['text-white']]: true,
[borderStyle['border-blue']]: true,
[borderStyle['hover:border-blue']]: true,
});
// Danger: red bg, white text, red border
const dangerClasses = compileClassNames({
[bgStyle['bg-red']]: true,
[bgStyle['hover:bg-red']]: true,
[textStyle['text-white']]: true,
[borderStyle['border-red']]: true,
[borderStyle['hover:border-red']]: true,
});
return <button className={variant === 'danger' ? dangerClasses : primaryClasses}>{label}</button>;
}
```
---
## Using `colorHelper.js`
### Named Exports
Three utility functions return individual CSS module classes:
```javascript
import { getBgClass, getTextClass, getBorderClass } from './colorHelper';
```
#### `getBgClass({ state?, tone, isLighter? })`
Returns a background CSS class from `backgroundColor.module.css`.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `tone` | `string` | Yes | Color tone (`default`, `grey`, `blue`, `green`, `orange`, `red`, `yellow`, `white`, `black`) |
| `state` | `string` | No | State prefix (`hover`, `focus`, `active`, `selected`). Omit for base class. |
| `isLighter` | `boolean` | No | `true` → appends `-light` to the tone |
**Generated key pattern:** `[state:]bg-{tone}[-light]`
```javascript
getBgClass({ tone: 'blue' }) // → paletteBgStyle['bg-blue']
getBgClass({ tone: 'blue', isLighter: true }) // → paletteBgStyle['bg-blue-light']
getBgClass({ state: 'hover', tone: 'red' }) // → paletteBgStyle['hover:bg-red']
getBgClass({ state: 'hover', tone: 'red', isLighter: true }) // → paletteBgStyle['hover:bg-red-light']
```
#### `getTextClass({ state?, textTone })`
Returns a text CSS class from `textColor.module.css`.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `textTone` | `string` | Yes | Text tone (`brand`, `grey`, `blue`, `green`, `orange`, `red`, `yellow`, `white`, `black`, `brand-filled`) |
| `state` | `string` | No | State prefix (`hover`, `focus`, `active`, `selected`). Omit for base class. |
> **Note:** `textTone` has no `isLighter` equivalent. Use `filledTextTone` (e.g. `white`) directly when needed.
**Generated key pattern:** `[state:]text-{textTone}`
```javascript
getTextClass({ textTone: 'green' }) // → paletteTextStyle['text-green']
getTextClass({ state: 'hover', textTone: 'white' }) // → paletteTextStyle['hover:text-white']
getTextClass({ textTone: 'brand-filled' }) // → paletteTextStyle['text-brand-filled']
```
#### `getBorderClass({ state?, tone, isLighter? })`
Returns a border CSS class from `borderColor.module.css`.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `tone` | `string` | Yes | Color tone (`default`, `grey`, `blue`, `green`, `orange`, `red`, `yellow`) |
| `state` | `string` | No | State prefix (`hover`, `focus`, `active`, `selected`). Omit for base class. |
| `isLighter` | `boolean` | No | `true` → appends `-light` to the tone (resting state only by convention) |
**Generated key pattern:** `[state:]border-{tone}[-light]`
```javascript
getBorderClass({ tone: 'blue' }) // → paletteBorderStyle['border-blue']
getBorderClass({ tone: 'blue', isLighter: true }) // → paletteBorderStyle['border-blue-light']
getBorderClass({ state: 'hover', tone: 'red' }) // → paletteBorderStyle['hover:border-red']
```
#### Internal Helpers (not exported)
- **`toTone(tone, isLighter)`** — returns `'{tone}-light'` if `isLighter` is truthy, else `'{tone}'`
- **`withState(state, key)`** — returns `'{state}:{key}'` if `state` is truthy, else `'{key}'`
---
### Default Export: `getPaletteClasses`
For components accepting `palette`, `bgAppearance`, `borderAppearance`, and `paletteShade` props.
```javascript
import getPaletteClasses from './colorHelper';
```
#### Palette → Tone Mapping (from `PALETTE_CONFIG`)
| Palette | Tone (bg/border) | Text Tone | Filled Text Tone |
|---------|-------------------|-----------|-------------------|
| `default` | `brand` | `brand` | `brand-filled` |
| `primary` | `blue` | `blue` | `white` |
| `secondary` | `grey` | `grey` | `white` |
| `danger` | `red` | `red` | `white` |
| `success` | `green` | `green` | `white` |
Unknown palettes return `DUMMY_OBJECT` (empty frozen object).
#### Parameters
| Parameter | Values | Effect |
|-----------|--------|--------|
| `palette` | `default` \| `primary` \| `secondary` \| `danger` \| `success` | Selects the color tone config from `PALETTE_CONFIG` |
| `bgAppearance` | `'none'` \| `'default'` \| other | `'default'` → base bg + state bg; `'none'` → no bg, no state bg, no filled text logic; other → no base bg but state bg + filled text logic |
| `isSelected` | `boolean` | `true` → folds selected-state classes into `bg`, `text`, and `border` (replaces their resting values with the `selected` state class) |
| `borderAppearance` | `'none'` \| `'onHover'` \| other | `'none'` → transparent base, no state borders; `'onHover'` → transparent base, with state borders; other → visible base + state borders |
| `paletteShade` | `'lighter'` \| other | `'lighter'` → `-light` variant for bg and base border; also keeps text as `textTone` instead of `filledTextTone` |
#### Text Tone Logic
The text color depends on both `bgAppearance` and `paletteShade`:
| Condition | Base text (`text`) | State text (`hoverText`, etc.) |
|---|---|---|
| `bgAppearance: 'default'` + default shade | `filledTextTone` (e.g. `white`) | `filledTextTone` |
| `bgAppearance: 'default'` + `'lighter'` shade | `textTone` (e.g. `blue`) | `textTone` |
| `bgAppearance:` other (not `'none'`) + default shade | `textTone` | `filledTextTone` |
| `bgAppearance:` other (not `'none'`) + `'lighter'` shade | `textTone` | `textTone` |
| `bgAppearance: 'none'` | `textTone` | `textTone` |
> **Key insight:** `baseTextTone = isFilledBg && !isLighter ? filledTextTone : textTone`. `stateTextTone = hasBgState && !isLighter ? filledTextTone : textTone`. This means state text can differ from base text when `bgAppearance` is not `'default'` but also not `'none'`.
#### Return Value
```javascript
{
bg, // isSelected → selected bg (if bgAppearance !== 'none'); else base bg (only if bgAppearance === 'default')
hoverBg, // getBgClass({ state: 'hover', tone, isLighter }) — if bgAppearance !== 'none'
focusBg, // getBgClass({ state: 'focus', tone, isLighter }) — if bgAppearance !== 'none'
activeBg, // getBgClass({ state: 'active', tone, isLighter }) — if bgAppearance !== 'none'
text, // isSelected → selected text class; else getTextClass({ textTone: baseTextTone })
hoverText, // getTextClass({ state: 'hover', textTone: stateTextTone })
focusText, // getTextClass({ state: 'focus', textTone: stateTextTone })
activeText, // getTextClass({ state: 'active', textTone: stateTextTone })
border, // isSelected → selected border (if borderAppearance !== 'none'); else resting border
hoverBorder, // getBorderClass({ state: 'hover', tone }) — if borderAppearance !== 'none'
focusBorder, // getBorderClass({ state: 'focus', tone }) — if borderAppearance !== 'none'
activeBorder // getBorderClass({ state: 'active', tone }) — if borderAppearance !== 'none'
}
```
> **Note:** State border classes always use the base tone (no `isLighter`), even when the resting border uses `-light`.
#### Example: Filled Primary Button
```javascript
const classes = getPaletteClasses({
palette: 'primary',
bgAppearance: 'default',
borderAppearance: 'visible',
paletteShade: 'default',
});
// classes.bg → 'bg-blue' (bgAppearance: 'default' + default shade)
// classes.hoverBg → 'hover:bg-blue'
// classes.text → 'text-white' (filledTextTone — bgAppearance: 'default' + not lighter)
// classes.hoverText → 'hover:text-white' (filledTextTone — hasBgState + not lighter)
// classes.border → 'border-blue' (visible appearance)
// classes.hoverBorder → 'hover:border-blue'
```
#### Example: Light Danger Tag
```javascript
const classes = getPaletteClasses({
palette: 'danger',
bgAppearance: 'default',
borderAppearance: 'none',
paletteShade: 'lighter',
});
// classes.bg → 'bg-red-light' (bgAppearance: 'default' + lighter shade)
// classes.hoverBg → 'hover:bg-red-light'
// classes.text → 'text-red' (textTone — lighter shade overrides filledTextTone)
// classes.hoverText → 'hover:text-red' (textTone — lighter shade)
// classes.border → 'border-transparent' (borderAppearance: 'none')
// classes.hoverBorder → undefined (no state borders with 'none')
```
#### Example: Ghost Secondary Button
```javascript
const classes = getPaletteClasses({
palette: 'secondary',
bgAppearance: 'none',
borderAppearance: 'onHover',
paletteShade: 'default',
});
// classes.bg → undefined (not filled)
// classes.hoverBg → undefined (bgAppearance: 'none')
// classes.text → 'text-grey' (textTone — not filled)
// classes.hoverText → 'hover:text-grey' (textTone — bgAppearance 'none' → no filled text)
// classes.border → 'border-transparent' (onHover → transparent base)
// classes.hoverBorder → 'hover:border-grey' (onHover → state borders present)
```
#### Example: Hover-fill Button (bgAppearance: non-standard value)
```javascript
const classes = getPaletteClasses({
palette: 'primary',
bgAppearance: 'hover-fill', // any value that isn't 'none' or 'default'
borderAppearance: 'visible',
paletteShade: 'default',
});
// classes.bg → undefined (not 'default' → no base bg)
// classes.hoverBg → 'hover:bg-blue' (not 'none' → state bg present)
// classes.text → 'text-blue' (baseTextTone — not filled, so textTone)
// classes.hoverText → 'hover:text-white' (stateTextTone — hasBgState + not lighter → filledTextTone!)
// classes.border → 'border-blue' (visible)
// classes.hoverBorder → 'hover:border-blue'
```
> This shows a key behavior: the base text is the normal tone, but **state text switches to `filledTextTone`** because the hover/state background will be filled (dark).
> **Note on `isSelected`:** Pass `isSelected: true` to fold the selected state into `bg`, `text`, and `border`. Instead of the resting class, each returns its `selected:` variant class. There are no separate `selectedBg`/`selectedText`/`selectedBorder` return keys.
---
## Behavior Summary
| Property | Lighter shade (`paletteShade: 'lighter'`) | Default shade |
|----------|-------------------------------------------|---------------|
| **Background** | Uses `-light` variant (e.g. `bg-blue-light`) | Uses base (e.g. `bg-blue`) |
| **Base text** | Always `textTone` (e.g. `text-blue`) | `filledTextTone` when filled (e.g. `text-white`), else `textTone` |
| **State text** | Always `textTone` (e.g. `hover:text-blue`) | `filledTextTone` when `bgAppearance !== 'none'` (e.g. `hover:text-white`), else `textTone` |
| **Border (base)** | Uses `-light` variant if not transparent (e.g. `border-blue-light`) | Uses base if not transparent (e.g. `border-blue`) |
| **Border (states)** | Always base tone (e.g. `hover:border-blue`) | Always base tone |
---
## FAQ
**Q: Why do text classes have no `-light` variant?**
A: Text on a light background should stay the base color for readability. When the background is filled (dark), text switches to a contrasting tone (e.g. `white`) instead.
**Q: Why do border state classes not use `-light`?**
A: Border hover/focus/active/selected states always use the full-intensity base tone for visibility, even when the resting border uses a lighter shade.
**Q: What happens if I pass an unknown palette to `getPaletteClasses`?**
A: It returns `DUMMY_OBJECT` (an empty frozen object), so no classes are applied.
**Q: Can I use CSS classes directly without `getPaletteClasses`?**
A: Yes. The CSS modules are standalone. `getPaletteClasses` is a convenience for components that accept palette/appearance/shade props.
**Q: When does state text differ from base text?**
A: When `bgAppearance` is neither `'none'` nor `'filled'` with default (non-lighter) shade. Base text uses `textTone` (colored), but state text uses `filledTextTone` (e.g. `white`) — because the hover/state will show a filled background.
---
**Last Updated:** March 2026