Doh.js provides several approaches to managing CSS:
Doh.css() function: Dynamically create and manage global stylesheet elementsthis.css): Element-specific styling using property proxiesthis.classes): Class-based styling using DOM class manipulationThis document covers all these approaches, with guidance on when to use each technique.
Doh.css() Function// Add CSS rules to the default cached style element
Doh.css(`
.custom-box {
background-color: #f0f0f0;
border: 1px solid #ccc;
padding: 20px;
margin: 10px;
border-radius: 5px;
}
`);
The Doh.css() function uses an internal cache to manage style elements. When called without a specific style element parameter, it will:
This minimizes the number of style elements created in the document.
While the default behavior uses a cached style element, you can still create separate style elements when needed:
// Create a new, separate style element
const themeStyles = document.createElement('style');
themeStyles.setAttribute('type', 'text/css');
document.head.appendChild(themeStyles);
// Use the custom style element with Doh.css
Doh.css(`
:root {
--primary-color: blue;
--secondary-color: navy;
}
`, themeStyles);
// Get a reference to an existing style element
const myStyleElement = document.querySelector('#my-styles');
// Append more styles to it
Doh.css(`
.theme-button { background-color: blue; }
`, myStyleElement);
/**
* Creates or updates a style element with the provided CSS content
* @param {string} cssContent - CSS rules to include in the style tag
* @param {HTMLStyleElement} [styleElement] - Optional existing style element to update
* @returns {HTMLStyleElement} - The created or updated style element
*/
Doh lets you define CSS styles at the pattern level, where they are automatically:
Pattern('StyledCard', 'html', {
// These CSS properties will be converted to a class
css: {
backgroundColor: 'white',
borderRadius: 8, // Numeric values are automatically converted to 'px'
padding: 20, // This becomes '20px' in the CSS
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
margin: '10px 0'
},
// Raw CSS string is also supported and merged with css object
style: 'display: flex; flex-direction: column;'
});
// Create an instance - it will automatically have the pattern's styling
let card = New('StyledCard', {
html: 'This card has pre-defined styling from its pattern'
});
When you define a pattern with CSS properties:
initial_classes arrayinitial_css and initial_style propertiesthis.css)For element-specific styling, the html pattern provides a css proxy that manages inline styles on the element.
let element = New('html', {
html_phase: function() {
// Set individual properties
this.css.color = 'navy';
this.css.fontSize = '16px';
// Get a property
console.log(this.css.color);
// Remove a property
this.css.color = '';
// Batch update
this.css({
backgroundColor: 'lightyellow',
border: '1px solid gray',
padding: '5px'
});
// Vendor prefixes
this.css.webkitTransform = 'rotate(45deg)';
// Get computed style
console.log(this.css.display);
}
});
css() methodthis.classes)Doh's html pattern also includes a proxy for managing CSS classes. The classes proxy provides array-like functionality while maintaining synchronization with the DOM's classList.
let element = New('html', {
html_phase: function() {
// Add classes
this.classes.push('highlight');
this.classes('important', 'large');
// Remove a class
delete this.classes.important;
// Check if a class exists
if ('highlight' in this.classes) {
console.log('Element is highlighted');
}
// Iterate over classes
for (let className of this.classes) {
console.log(className);
}
// Get class count
console.log(this.classes.length);
// Convert to string
console.log(this.classes.toString());
}
});
The classes proxy supports all standard array methods. Important: Following Doh's proxy array philosophy, methods that return arrays will mutate the DOM and return the result. If you need non-mutating behavior, copy to a local array first.
These methods modify the classes in the DOM and return the result:
push(...items) - Add classes to the end, returns new lengthunshift(...items) - Add classes to the beginning, returns new lengthpop() - Remove and return the last classshift() - Remove and return the first classsplice(start, deleteCount, ...items) - Remove/replace classes, returns array of removed classessort(compareFn?) - Sort classes alphabetically, returns sorted arrayreverse() - Reverse the order of classes, returns reversed arrayfilter(callback) - Filter classes matching condition AND update DOM to only have filtered classesmap(callback) - Transform class names AND update DOM with transformed classesslice(start?, end?) - Slice classes AND update DOM to only have sliced classesconcat(...arrays) - Combine with other arrays AND update DOM with concatenated resultreduce(callback, initialValue?) - Reduce classes; if result is an array, updates DOM with resultreduceRight(callback, initialValue?) - Reduce from right; if result is an array, updates DOM with resultfill(value, start?, end?) - Fill classes with a value (rarely used for classes)copyWithin(target, start?, end?) - Copy classes within the array (rarely used for classes)These methods don't modify the DOM, they only return query results:
find(callback) - Find first matching classfindIndex(callback) - Find index of first matching classsome(callback) - Test if any class matches conditionevery(callback) - Test if all classes match conditionincludes(value) - Check if class existsindexOf(value, fromIndex?) - Find index of classlastIndexOf(value, fromIndex?) - Find last index of classjoin(separator?) - Join classes into stringforEach(callback) - Iterate over classesentries() - Return iterator of [index, value] pairskeys() - Return iterator of indicesvalues() - Return iterator of class valueslet element = New('html', {
html_phase: function() {
// Add multiple classes
this.classes.push('card', 'shadow', 'rounded', 'theme-light', 'theme-blue');
// Filter classes - THIS MODIFIES THE DOM to only keep matching classes
// After this, element only has classes starting with 'theme-'
const themeClasses = this.classes.filter(cls => cls.startsWith('theme-'));
// Check if any class matches (doesn't mutate)
if (this.classes.some(cls => cls.includes('active'))) {
console.log('Has active class');
}
// Find a specific class (doesn't mutate)
const primaryClass = this.classes.find(cls => cls.startsWith('primary-'));
// Sort classes alphabetically (mutates DOM)
this.classes.sort();
// Reverse class order (mutates DOM)
this.classes.reverse();
// Remove classes using splice (mutates DOM)
this.classes.splice(0, 2); // Remove first 2 classes
// Map classes - THIS MODIFIES THE DOM with transformed class names
// After this, all classes are prefixed with 'ui-'
const prefixedClasses = this.classes.map(cls => `ui-${cls}`);
// Slice classes - THIS MODIFIES THE DOM to only keep sliced classes
// After this, element only has first 3 classes
this.classes.slice(0, 3);
// Join classes with custom separator (doesn't mutate)
const classString = this.classes.join(' | ');
// For non-mutating operations, copy to local array first
const localCopy = Array.from(this.classes);
const filtered = localCopy.filter(cls => cls.startsWith('ui-'));
// DOM is unchanged, filtered array is separate
}
});
Important Note: Methods like filter(), map(), slice(), and concat() that normally return new arrays will mutate the DOM classes in Doh's proxy system. This is intentional - the classes proxy is an "operation system" that modifies the live DOM. If you need non-mutating behavior, copy to a local array first using Array.from(this.classes) or [...this.classes].
You can observe and react to changes in CSS properties using Doh's data binding system.
let myElement = New('html', {
css: { color: 'blue' }
});
// Observe changes to the element's color style
Doh.observe(myElement.css, 'color', (target, prop, newValue, oldValue) => {
console.log(`Color changed from ${oldValue} to ${newValue}`);
});
// Later, changing the proxy property triggers the observer AND updates the DOM
myElement.css.color = 'red'; // Logs: "Color changed from blue to red"
Immediate reads after writes
await Doh.microtask();
See /docs/core/data-binding#timing-and-microtasks.
Doh.css() when:this.css) when:this.classes) when:// 1. Define global theme styles
Doh.css(`
:root {
--primary-color: #3498db;
--secondary-color: #2980b9;
--text-color: #333;
--background-color: #f4f4f4;
}
body {
background-color: var(--background-color);
color: var(--text-color);
font-family: sans-serif;
}
.theme-button {
background-color: var(--primary-color);
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
}
.theme-button:hover {
background-color: var(--secondary-color);
}
`);
// 2. Define a styled component with pattern-level CSS
Pattern('Card', 'html', {
css: {
backgroundColor: 'white',
borderRadius: 8,
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
padding: 20,
margin: '20px 0'
}
});
// 3. Create a component that combines pattern-level styling,
// classes proxy, and css proxy
Pattern('DynamicCard', 'Card', {
init: function(options) {
this.options = options || {};
this.expanded = false;
},
html_phase: function() {
// Add a theme class using classes proxy
this.classes.push('theme-card');
// Set dynamic styles using css proxy
if (this.options.height) {
this.css.height = this.options.height;
}
// Create content
this.e.html(`
<h3>${this.options.title || 'Card Title'}</h3>
<div class="card-content">${this.options.content || ''}</div>
<button class="theme-button">Toggle</button>
`);
// Add event handling
this.e.find('button').on('click', () => {
this.expanded = !this.expanded;
this.css.height = this.expanded ? 'auto' : this.options.height;
// Toggle classes
if (this.expanded) {
this.classes.push('expanded');
} else {
delete this.classes.expanded;
}
});
}
});
// 4. Create a new style element for a specific feature
const darkModeStyles = document.createElement('style');
document.head.appendChild(darkModeStyles);
// 5. Change theme at runtime
function updateTheme(darkMode) {
if (darkMode) {
Doh.css(`
:root {
--primary-color: #2c3e50;
--secondary-color: #1a252f;
--text-color: #ecf0f1;
--background-color: #34495e;
}
`, darkModeStyles);
} else {
// Clear dark mode styles
darkModeStyles.textContent = '';
}
}