Styling Components in Coherent.js
This comprehensive guide covers all aspects of styling components in Coherent.js, from basic CSS classes to external CSS files, advanced theming systems and CSS-in-JS patterns.
🎨 Basic Styling Approaches
1. CSS Classes
The most straightforward way to style components:
const Button = ({ variant = 'primary', size = 'medium', children }) => ({
button: {
className: `btn btn--${variant} btn--${size}`,
children: Array.isArray(children) ? children : [children]
}
});
// Corresponding CSS
const styles = `
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
}
.btn--primary {
background-color: #007bff;
color: white;
}
.btn--primary:hover {
background-color: #0056b3;
}
.btn--secondary {
background-color: #6c757d;
color: white;
}
.btn--small {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.btn--large {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
`;
2. Inline Styles
For dynamic or component-specific styling:
const ProgressBar = ({ progress = 0, color = '#007bff', height = '20px' }) => ({
div: {
style: `
width: 100%;
height: ${height};
background-color: #e9ecef;
border-radius: 0.25rem;
overflow: hidden;
`,
children: [
{
div: {
style: `
width: ${Math.min(Math.max(progress, 0), 100)}%;
height: 100%;
background-color: ${color};
transition: width 0.3s ease;
`
}
}
]
}
});
// Usage
const progressBar = ProgressBar({
progress: 75,
color: '#28a745',
height: '10px'
});
3. Conditional Styling
Apply styles based on component state or props:
const Alert = ({ type = 'info', message, dismissible = false, onDismiss }) => {
const alertStyles = {
info: 'alert-info',
success: 'alert-success',
warning: 'alert-warning',
error: 'alert-error'
};
return {
div: {
className: `alert ${alertStyles[type]} ${dismissible ? 'alert-dismissible' : ''}`,
role: 'alert',
children: [
{ span: { text: message } },
dismissible ? {
button: {
className: 'alert-close',
'aria-label': 'Close',
onclick: onDismiss,
children: [{ span: { text: '×' } }]
}
} : null
].filter(Boolean)
}
};
};
📁 External CSS Files
Coherent.js supports loading CSS files separately from your JavaScript code, making it easy to organize styles in dedicated files.
Loading CSS Files
Use the cssFiles option in render functions to automatically load and inject CSS files:
import { render } from 'coherent';
const App = () => ({
div: {
className: 'app-container',
children: [
{ h1: { className: 'app-title', text: 'My Application' } },
{ p: { className: 'app-description', text: 'Welcome to my app!' } }
]
}
});
// Automatically load CSS files
const html = await render(App(), {
cssFiles: [
'./styles/main.css',
'./styles/components.css',
'./styles/themes/default.css'
]
});
CSS File Organization
Organize your CSS files by feature or component:
/styles/
├── main.css // Global styles
├── components/
│ ├── button.css // Button component styles
│ ├── form.css // Form component styles
│ └── navigation.css // Navigation styles
├── themes/
│ ├── light.css // Light theme
│ └── dark.css // Dark theme
└── utilities/
├── spacing.css // Spacing utilities
└── typography.css // Typography utilities
Example styles/components/button.css:
.btn {
display: inline-block;
padding: 0.375rem 0.75rem;
margin-bottom: 0;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
text-align: center;
white-space: nowrap;
vertical-align: middle;
cursor: pointer;
user-select: none;
border: 1px solid transparent;
border-radius: 0.25rem;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.btn-primary {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
.btn-primary:hover {
color: #fff;
background-color: #0056b3;
border-color: #004085;
}
.btn-secondary {
color: #fff;
background-color: #6c757d;
border-color: #6c757d;
}
.btn-large {
padding: 0.5rem 1rem;
font-size: 1.25rem;
border-radius: 0.3rem;
}
CSS Links and Inline Styles
You can also use external CSS links and inline styles:
const html = await render(App(), {
// External CDN stylesheets
cssLinks: [
'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css',
'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'
],
// Inline CSS for quick overrides
cssInline: `
.custom-override {
color: #333;
font-family: 'Inter', sans-serif;
}
`,
// Local CSS files
cssFiles: ['./styles/custom.css']
});
CSS Minification
Enable CSS minification for production builds:
const html = await render(App(), {
cssFiles: ['./styles/main.css'],
cssMinify: process.env.NODE_ENV === 'production'
});
Working with CSS Modules
For CSS Modules support, use the CSS file loading with scoped class names:
// styles.module.css
/*
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.title {
font-size: 2.5rem;
color: #333;
margin-bottom: 1rem;
}
*/
import styles from './styles.module.css';
const Component = () => ({
div: {
className: styles.container,
children: [
{ h1: { className: styles.title, text: 'Title' } }
]
}
});
🎭 CSS-in-JS Patterns
Style Object Creation
const createStyles = (theme) => ({
container: {
display: 'flex',
flexDirection: 'column',
padding: theme.spacing.medium,
backgroundColor: theme.colors.background,
borderRadius: theme.borderRadius,
boxShadow: theme.shadows.medium
},
header: {
fontSize: theme.fonts.sizes.large,
fontWeight: theme.fonts.weights.bold,
color: theme.colors.primary,
marginBottom: theme.spacing.small
},
body: {
fontSize: theme.fonts.sizes.medium,
lineHeight: theme.fonts.lineHeights.normal,
color: theme.colors.text
},
footer: {
display: 'flex',
justifyContent: 'flex-end',
gap: theme.spacing.small,
marginTop: theme.spacing.medium,
paddingTop: theme.spacing.small,
borderTop: `1px solid ${theme.colors.border}`
}
});
// Convert style object to CSS string
const stylesToString = (styles) => {
return Object.entries(styles)
.map(([property, value]) => {
const cssProperty = property.replace(/([A-Z])/g, '-$1').toLowerCase();
return `${cssProperty}: ${value}`;
})
.join('; ');
};
const Card = ({ title, content, actions, theme }) => {
const styles = createStyles(theme);
return {
div: {
style: stylesToString(styles.container),
children: [
title ? {
h2: {
style: stylesToString(styles.header),
text: title
}
} : null,
content ? {
div: {
style: stylesToString(styles.body),
children: Array.isArray(content) ? content : [content]
}
} : null,
actions ? {
div: {
style: stylesToString(styles.footer),
children: Array.isArray(actions) ? actions : [actions]
}
} : null
].filter(Boolean)
}
};
};
Dynamic Style Generation
const generateButtonStyles = ({ variant, size, disabled, rounded }) => {
const baseStyles = {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
border: 'none',
cursor: disabled ? 'not-allowed' : 'pointer',
transition: 'all 0.2s ease',
fontWeight: '500',
textDecoration: 'none'
};
// Size variations
const sizeStyles = {
small: {
padding: '0.25rem 0.75rem',
fontSize: '0.875rem'
},
medium: {
padding: '0.5rem 1rem',
fontSize: '1rem'
},
large: {
padding: '0.75rem 1.5rem',
fontSize: '1.125rem'
}
};
// Variant styles
const variantStyles = {
primary: {
backgroundColor: disabled ? '#6c757d' : '#007bff',
color: 'white'
},
secondary: {
backgroundColor: disabled ? '#e9ecef' : 'transparent',
color: disabled ? '#6c757d' : '#007bff',
border: `1px solid ${disabled ? '#e9ecef' : '#007bff'}`
},
success: {
backgroundColor: disabled ? '#6c757d' : '#28a745',
color: 'white'
}
};
return {
...baseStyles,
...sizeStyles[size],
...variantStyles[variant],
borderRadius: rounded ? '2rem' : '0.25rem',
opacity: disabled ? 0.6 : 1
};
};
const DynamicButton = (props) => {
const {
children,
variant = 'primary',
size = 'medium',
disabled = false,
rounded = false,
...restProps
} = props;
const styles = generateButtonStyles({ variant, size, disabled, rounded });
return {
button: {
style: stylesToString(styles),
disabled,
...restProps,
children: Array.isArray(children) ? children : [children]
}
};
};
🌈 Theming System
Theme Definition
const createTheme = (overrides = {}) => {
const baseTheme = {
colors: {
primary: '#007bff',
secondary: '#6c757d',
success: '#28a745',
warning: '#ffc107',
error: '#dc3545',
info: '#17a2b8',
light: '#f8f9fa',
dark: '#343a40',
white: '#ffffff',
black: '#000000',
// Semantic colors
background: '#ffffff',
surface: '#f8f9fa',
text: '#212529',
textSecondary: '#6c757d',
border: '#dee2e6',
shadow: 'rgba(0, 0, 0, 0.1)'
},
spacing: {
xs: '0.25rem',
small: '0.5rem',
medium: '1rem',
large: '1.5rem',
xl: '2rem',
xxl: '3rem'
},
fonts: {
family: {
body: 'system-ui, -apple-system, sans-serif',
heading: 'system-ui, -apple-system, sans-serif',
mono: 'SFMono-Regular, Consolas, monospace'
},
sizes: {
xs: '0.75rem',
small: '0.875rem',
medium: '1rem',
large: '1.25rem',
xl: '1.5rem',
xxl: '2rem'
},
weights: {
light: '300',
normal: '400',
medium: '500',
bold: '700'
},
lineHeights: {
tight: 1.2,
normal: 1.5,
relaxed: 1.75
}
},
shadows: {
small: '0 1px 3px rgba(0, 0, 0, 0.1)',
medium: '0 4px 6px rgba(0, 0, 0, 0.1)',
large: '0 10px 25px rgba(0, 0, 0, 0.1)'
},
borderRadius: '0.25rem',
breakpoints: {
mobile: '480px',
tablet: '768px',
desktop: '1024px',
wide: '1200px'
},
transitions: {
fast: '0.1s ease',
normal: '0.2s ease',
slow: '0.3s ease'
}
};
// Deep merge with overrides
return deepMerge(baseTheme, overrides);
};
// Dark theme variant
const darkTheme = createTheme({
colors: {
background: '#1a1a1a',
surface: '#2d2d2d',
text: '#ffffff',
textSecondary: '#a0a0a0',
border: '#404040'
}
});
// High contrast theme
const highContrastTheme = createTheme({
colors: {
primary: '#0000ff',
text: '#000000',
background: '#ffffff',
border: '#000000'
}
});
Theme Provider
const ThemeProvider = withState({
currentTheme: 'light',
themes: {
light: createTheme(),
dark: darkTheme,
highContrast: highContrastTheme
}
})(({ state, stateUtils, children }) => {
const { setState } = stateUtils;
const setTheme = (themeName) => {
setState({ currentTheme: themeName });
// Apply theme to document
if (typeof document !== 'undefined') {
document.documentElement.setAttribute('data-theme', themeName);
}
};
const theme = state.themes[state.currentTheme];
// Provide theme context to children
const enhanceChild = (child) => {
if (typeof child === 'function') {
return child({ theme, setTheme });
}
return child;
};
return {
div: {
'data-theme': state.currentTheme,
style: `
--color-primary: ${theme.colors.primary};
--color-background: ${theme.colors.background};
--color-text: ${theme.colors.text};
--spacing-medium: ${theme.spacing.medium};
--border-radius: ${theme.borderRadius};
`,
children: Array.isArray(children)
? children.map(enhanceChild)
: [enhanceChild(children)]
}
};
});
Theme-Aware Components
const ThemedCard = ({ theme, title, content, variant = 'default' }) => {
const cardStyles = {
default: {
backgroundColor: theme.colors.surface,
borderColor: theme.colors.border
},
primary: {
backgroundColor: theme.colors.primary,
borderColor: theme.colors.primary,
color: theme.colors.white
},
success: {
backgroundColor: theme.colors.success,
borderColor: theme.colors.success,
color: theme.colors.white
}
};
const selectedStyles = cardStyles[variant];
return {
div: {
style: `
background-color: ${selectedStyles.backgroundColor};
border: 1px solid ${selectedStyles.borderColor};
border-radius: ${theme.borderRadius};
padding: ${theme.spacing.medium};
box-shadow: ${theme.shadows.medium};
color: ${selectedStyles.color || theme.colors.text};
font-family: ${theme.fonts.family.body};
`,
children: [
title ? {
h3: {
style: `
margin: 0 0 ${theme.spacing.small} 0;
font-size: ${theme.fonts.sizes.large};
font-weight: ${theme.fonts.weights.bold};
`,
text: title
}
} : null,
content ? {
div: {
style: `
font-size: ${theme.fonts.sizes.medium};
line-height: ${theme.fonts.lineHeights.normal};
`,
children: Array.isArray(content) ? content : [content]
}
} : null
].filter(Boolean)
}
};
};
📱 Responsive Styling
Media Query Utilities
const mediaQueries = (theme) => ({
mobile: `@media (max-width: ${theme.breakpoints.mobile})`,
tablet: `@media (max-width: ${theme.breakpoints.tablet})`,
desktop: `@media (min-width: ${theme.breakpoints.desktop})`,
wide: `@media (min-width: ${theme.breakpoints.wide})`
});
const ResponsiveGrid = ({ theme, items, columns = { mobile: 1, tablet: 2, desktop: 3 } }) => {
const generateGridStyles = () => {
const mq = mediaQueries(theme);
return `
display: grid;
gap: ${theme.spacing.medium};
grid-template-columns: repeat(${columns.desktop}, 1fr);
${mq.tablet} {
grid-template-columns: repeat(${columns.tablet}, 1fr);
}
${mq.mobile} {
grid-template-columns: repeat(${columns.mobile}, 1fr);
}
`;
};
return {
div: {
style: generateGridStyles(),
children: items.map((item, index) => ({
div: {
key: index,
style: `
background: ${theme.colors.surface};
padding: ${theme.spacing.medium};
border-radius: ${theme.borderRadius};
border: 1px solid ${theme.colors.border};
`,
children: [item]
}
}))
}
};
};
Responsive Typography
const ResponsiveText = ({
theme,
children,
variant = 'body',
responsive = true
}) => {
const typographyStyles = {
h1: {
fontSize: responsive ?
`clamp(${theme.fonts.sizes.xl}, 5vw, ${theme.fonts.sizes.xxl})` :
theme.fonts.sizes.xxl,
fontWeight: theme.fonts.weights.bold,
lineHeight: theme.fonts.lineHeights.tight
},
h2: {
fontSize: responsive ?
`clamp(${theme.fonts.sizes.large}, 4vw, ${theme.fonts.sizes.xl})` :
theme.fonts.sizes.xl,
fontWeight: theme.fonts.weights.bold,
lineHeight: theme.fonts.lineHeights.tight
},
body: {
fontSize: theme.fonts.sizes.medium,
fontWeight: theme.fonts.weights.normal,
lineHeight: theme.fonts.lineHeights.normal
},
small: {
fontSize: theme.fonts.sizes.small,
fontWeight: theme.fonts.weights.normal,
lineHeight: theme.fonts.lineHeights.normal
}
};
const styles = typographyStyles[variant];
return {
span: {
style: stylesToString({
...styles,
fontFamily: theme.fonts.family.body,
color: theme.colors.text
}),
children: Array.isArray(children) ? children : [children]
}
};
};
🎯 Animation and Transitions
CSS Transitions
const AnimatedButton = ({ theme, children, loading = false, ...props }) => {
const buttonStyles = {
padding: theme.spacing.medium,
backgroundColor: loading ? theme.colors.secondary : theme.colors.primary,
color: theme.colors.white,
border: 'none',
borderRadius: theme.borderRadius,
cursor: loading ? 'wait' : 'pointer',
transition: `all ${theme.transitions.normal}`,
transform: loading ? 'scale(0.98)' : 'scale(1)',
opacity: loading ? 0.8 : 1,
position: 'relative',
overflow: 'hidden'
};
return {
button: {
style: stylesToString(buttonStyles),
disabled: loading,
...props,
children: [
loading ? {
span: {
style: `
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
`
}
} : null,
{
span: {
style: `opacity: ${loading ? 0 : 1}; transition: opacity ${theme.transitions.fast};`,
children: Array.isArray(children) ? children : [children]
}
}
].filter(Boolean)
}
};
};
CSS Animations
const generateKeyframes = () => `
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideIn {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
`;
const AnimatedCard = ({ theme, animation = 'fadeIn', delay = 0, children }) => {
const animationStyles = {
fadeIn: `fadeIn 0.5s ease ${delay}s both`,
slideIn: `slideIn 0.3s ease ${delay}s both`,
pulse: `pulse 2s ease-in-out ${delay}s infinite`
};
return {
div: {
style: `
animation: ${animationStyles[animation]};
background: ${theme.colors.surface};
padding: ${theme.spacing.medium};
border-radius: ${theme.borderRadius};
border: 1px solid ${theme.colors.border};
`,
children: Array.isArray(children) ? children : [children]
}
};
};
🛠️ Utility Functions
Style Merging
const mergeStyles = (...styleSets) => {
return styleSets.reduce((merged, styles) => {
if (!styles) return merged;
if (typeof styles === 'string') {
return `${merged}; ${styles}`;
}
if (typeof styles === 'object') {
return { ...merged, ...styles };
}
return merged;
}, {});
};
// Usage
const CombinedButton = ({ theme, variant, size, customStyles, ...props }) => {
const baseStyles = generateButtonStyles({ variant, size });
const finalStyles = mergeStyles(baseStyles, customStyles);
return {
button: {
style: typeof finalStyles === 'object' ?
stylesToString(finalStyles) :
finalStyles,
...props
}
};
};
CSS Variable Generation
const generateCSSVariables = (theme, prefix = '--') => {
const flatten = (obj, parentKey = '') => {
let result = {};
for (const [key, value] of Object.entries(obj)) {
const newKey = parentKey ? `${parentKey}-${key}` : key;
if (typeof value === 'object' && value !== null) {
result = { ...result, ...flatten(value, newKey) };
} else {
result[`${prefix}${newKey}`] = value;
}
}
return result;
};
return flatten(theme);
};
const ThemeVariables = ({ theme }) => {
const variables = generateCSSVariables(theme);
const cssVariables = Object.entries(variables)
.map(([key, value]) => `${key}: ${value}`)
.join('; ');
return {
style: {
textContent: `:root { ${cssVariables} }`
}
};
};
📚 Best Practices
1. Consistent Design System
// ✅ Good - Use design tokens
const Button = ({ theme, variant }) => ({
button: {
padding: theme.spacing.medium,
fontSize: theme.fonts.sizes.medium,
borderRadius: theme.borderRadius,
backgroundColor: theme.colors[variant]
}
});
// ❌ Avoid - Magic numbers
const Button = ({ variant }) => ({
button: {
padding: '12px 24px',
fontSize: '14px',
borderRadius: '4px',
backgroundColor: variant === 'primary' ? '#007bff' : '#6c757d'
}
});
2. Performance Optimization
// ✅ Good - Reuse style objects
const buttonStyles = {
base: { padding: '0.5rem 1rem', border: 'none' },
primary: { backgroundColor: '#007bff', color: 'white' }
};
// ❌ Avoid - Recreating styles
const Button = () => ({
button: {
style: stylesToString({
padding: '0.5rem 1rem', // Recreated every render
border: 'none'
})
}
});
3. Accessibility
const AccessibleButton = ({ theme, children, ...props }) => ({
button: {
style: `
background: ${theme.colors.primary};
color: ${theme.colors.white};
border: 2px solid transparent;
padding: ${theme.spacing.medium};
font-size: ${theme.fonts.sizes.medium};
border-radius: ${theme.borderRadius};
cursor: pointer;
transition: all 0.2s ease;
`,
// Focus styles for accessibility
'data-focus-styles': `
outline: 2px solid ${theme.colors.primary};
outline-offset: 2px;
`,
...props,
children
}
});
4. Maintainable CSS
// ✅ Good - Semantic naming
const semanticStyles = {
cardContainer: { /* styles */ },
cardHeader: { /* styles */ },
cardBody: { /* styles */ }
};
// ❌ Avoid - Presentational naming
const presentationalStyles = {
blueBox: { /* styles */ },
bigText: { /* styles */ },
redBorder: { /* styles */ }
};
This comprehensive styling guide provides all the tools and patterns needed to create beautiful, maintainable, and accessible designs in Coherent.js applications.