Patterns: classnames
Conditional class logic via the
classnameslibrary. Load via thepatterns-stylingcontext group.
Always use the classnames library for conditional class logic. Import as classNames (capital N) for consistency with the rest of the codebase:
import classNames from 'classnames';
Correct patterns
// Object syntax (dominant pattern - use this)
className={classNames('base-class fw-bold', {
'd-none': isHidden,
'text-primary': isActive,
'opacity-50': isDisabled,
})}
// BEM modifier via computed key
className={classNames(baseClassName, {
[`${baseClassName}--active`]: isActive,
[`${baseClassName}--collapsed`]: !isSidebarOpen,
})}
// Merging external className prop
className={classNames('internal-class', className)}
// Condition-only (no base string)
className={classNames({ 'mb-3': showMessage })}
// Stored in variable for reuse across multiple elements
const cardClassName = classNames('border-radius-lg bordered', {
'border-color-primary': isSelected,
'bg-primary-light': isHighlighted,
});
Anti-patterns (do NOT use)
// BAD: template literal with ternary
className={`base ${isActive ? 'active' : ''}`}
// FIX: classNames('base', { 'active': isActive })
// BAD: string concatenation
className={'base' + (isActive ? ' active' : '')}
// FIX: classNames('base', { 'active': isActive })
// BAD: ternary for entire className
className={isActive ? 'class-a' : 'class-b'}
// FIX: classNames({ 'class-a': isActive, 'class-b': !isActive })
// BAD: boolean coercion in template literal
// (renders the literal string "false" as a class name when the condition is falsy)
className={`${isCreateMode && 'opacity-0'} text-secondary`}
// FIX: classNames('text-secondary', { 'opacity-0': isCreateMode })
// BAD: ternary producing empty string class
className={condition ? 'mb-2' : ''}
// FIX: classNames({ 'mb-2': condition })
Current inventory of files still using template-literal/ternary class logic is tracked in the hbf-console style migration task.