A minimal utility (~40 bytes) for conditionally joining class names. A lightweight alternative to clsx or classnames.
Function Signature
function cx(
...args: (string | false | null | undefined)[]
): string;
Parameters
args
(string | false | null | undefined)[]
required
Class names to join. Falsy values (false, null, undefined) are automatically filtered out
Returns
Space-separated class names string
Usage
Basic Joining
Join multiple class names:
import { cx } from '@alex.radulescu/styled-static';
cx('base', 'active') // → 'base active'
cx('a', 'b', 'c') // → 'a b c'
Conditional Classes
Use boolean conditions to toggle classes:
const Button = styled.button`...`;
<Button className={cx(
'btn',
isActive && 'active',
isDisabled && 'disabled'
)}>
Click me
</Button>
// When isActive=true, isDisabled=false:
// → className="btn active"
With css Helper
Combine with the css helper for scoped styles:
import { css, cx } from '@alex.radulescu/styled-static';
const activeClass = css`
outline: 2px solid blue;
background: #eff6ff;
`;
const errorClass = css`
border: 1px solid red;
`;
<Button className={cx(
isActive && activeClass,
hasError && errorClass
)}>
Dynamic styling
</Button>
Falsy Value Filtering
All falsy values are automatically filtered:
cx('a', null, 'b') // → 'a b'
cx('a', undefined, 'b') // → 'a b'
cx('a', false, 'b') // → 'a b'
cx('a', '', 'b') // → 'a b' (empty strings filtered)
With Styled Components
Mix styled component class names:
const Button = styled.button`...`;
const Card = styled.div`...`;
<div className={cx(Button.className, Card.className, 'custom')}>
Combined styles
</div>
Real-World Examples
Handle multiple input states:
const Input = styled.input`...`;
const focusClass = css`
outline: 2px solid blue;
`;
const errorClass = css`
border-color: red;
`;
function FormInput({ isFocused, hasError, isDisabled }) {
return (
<Input
className={cx(
isFocused && focusClass,
hasError && errorClass,
isDisabled && 'opacity-50'
)}
/>
);
}
Navigation Links
Style active navigation items:
const NavLink = styled.a`
padding: 0.5rem 1rem;
color: #64748b;
`;
const activeClass = css`
color: #3b82f6;
font-weight: 600;
`;
function Nav({ currentPath }) {
return (
<nav>
<NavLink
href="/home"
className={cx(currentPath === '/home' && activeClass)}
>
Home
</NavLink>
<NavLink
href="/about"
className={cx(currentPath === '/about' && activeClass)}
>
About
</NavLink>
</nav>
);
}
Create variant styles manually:
const Button = styled.button`
padding: 0.5rem 1rem;
`;
const primaryClass = css`
background: blue;
color: white;
`;
const largeClass = css`
padding: 1rem 2rem;
font-size: 1.25rem;
`;
function VariantButton({ variant, size }) {
return (
<Button
className={cx(
variant === 'primary' && primaryClass,
size === 'large' && largeClass
)}
/>
);
}
Responsive Classes
Combine with Tailwind CSS utility classes:
const Card = styled.div`
padding: 1rem;
`;
function ResponsiveCard({ isHighlighted }) {
return (
<Card
className={cx(
'rounded-lg',
'shadow-md',
'hover:shadow-lg',
'md:p-6',
'lg:p-8',
isHighlighted && 'ring-2 ring-blue-500'
)}
>
Content
</Card>
);
}
Implementation
The entire implementation is ~40 bytes:
export function cx(...args: (string | false | null | undefined)[]): string {
let result = "";
for (const a of args) {
if (a) result = result ? `${result} ${a}` : a;
}
return result;
}
Why Not clsx?
The cx utility is intentionally minimal:
- Size: ~40 bytes vs ~300+ bytes for
clsx
- Simplicity: Flat arguments only (no nested arrays/objects)
- Zero dependencies: No external packages needed
If you need nested arrays or object syntax, use clsx instead:
// cx doesn't support:
cx({ active: true, disabled: false }) // ❌
cx(['a', 'b', ['c', 'd']]) // ❌
// But clsx does:
import clsx from 'clsx';
clsx({ active: true, disabled: false }) // ✅
clsx(['a', 'b', ['c', 'd']]) // ✅
See Also
- css - Get a scoped class name
- styled - Create styled components
- cssVariants - Create variant functions that return class strings