Overview
Colourized DevTools Console logs
javascript
Written: Dec-2025

Use the following function to generate a reader friendly version of a string html:

js
function getFormatHTML(html) {
    const indentChar = '  ';
    let level = 0;

    // Split every tag onto its own line
    html = html.replace(/(<\/?[^>]+>)/g, '\n$1\n');

    const lines = html.split('\n').map(line => line.trim()).filter(line => line.length);

    const formatted = lines.map(line => {
        if (line.match(/^<\/\w/)) level = Math.max(level - 1, 0); // closing tag
        const indentedLine = indentChar.repeat(level) + line;
        if (line.match(/^<\w+[^>]*?>$/) && !line.endsWith('/>')) level++; // opening tag
        return indentedLine;
    });

    return formatted.join('\n');
}

js ::  Example 1
console.log(getFormatHTML('<p><b>Simples</b></p>'));

js ::  Example 2
let html = '<p>';
{
  html += 'Lorem ipsum dolor sit amet, <br/>consectetur adipiscing elit, ';
  html += '<i>';
  {
    html += 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ';
  }
  html += '</i>';
  html += 'Ut enim ad minim veniam, ';
  html += '<i>';
  {
    html += 'quis nostrud exercitation ullamco ';
  }
  html += '</i>';
  html += '<b>';
  {
    html += 'laboris nisi ut aliquip ex';
  }
  html += '</b>';
  html += 'ea commodo consequat. Duis aute irure dolor in ';
  html += 'reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ';
}
html += '</p>';
html += '<p>';
{
  html += 'Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.';
}
html += '</p>';

console.log(getFormatHTML(html));

Single level block collapsed

To make the whole html element show on a single row when there's only one level - use the following version:

js
function getFormatHTML(html) {
    const indentChar = '  ';
    let level = 0;

    // Split every tag onto its own line
    html = html.replace(/(<\/?[^>]+>)/g, '\n$1\n');

    const lines = html.split('\n').map(line => line.trim()).filter(line => line.length);
    const formatted = [];

    for (let i = 0; i < lines.length; i++) {
        let line = lines[i];

        const tagNameMatch = line.match(/^<(\w+)/);
        const tagName = tagNameMatch ? tagNameMatch[1] : null;

        // Inline element: <tag ...>text</tag> with no nested tags
        if (
            tagName &&
            i + 2 < lines.length &&
            !lines[i + 1].startsWith('<') &&
            !lines[i + 1].includes('<') &&
            lines[i + 2].startsWith(</${tagName})
        ) {
            formatted.push(indentChar.repeat(level) + line + lines[i + 1] + lines[i + 2]);
            i += 2;
            continue;
        }

        // Closing tag reduces indent
        if (line.match(/^<\/\w/)) level = Math.max(level - 1, 0);

        formatted.push(indentChar.repeat(level) + line);

        // Opening tag increases indent (except self-closing)
        if (line.match(/^<\w+[^>]*?>$/) && !line.endsWith('/>')) {
            level++;
        }
    }

    return formatted.join('\n');
}

Colourized DevTools Console logs

To colourize the output in console.logs, (note, this will present attribute pairs on the same line if it's a short list, otherwise it will break them into separate lines - but assumes the html is well formed and made up of a="b" pairs).

Use maxAttrLength to break long lines down into short blocks if desired

Use ignoreAttFg to show or hide attributes which you may consider as noise in your tracing.

js

function logFormatHTML(html, maxAttrLength = 60, ignoreAttFg = true) {
    const colorTag = '#71c471';
    const colorText = '#e1f2e1';

    const formatted = getFormatHTML(html);
    const lines = formatted.split('\n');

    const logParts = [];
    const styles = [];

    lines.forEach(line => {
        const match = line.match(/^(\s*)(.*)/);
        const indent = match[1] || '';
        let content = match[2] || '';

        // indentation
        logParts.push('%c' + indent);
        styles.push('');

        // Handle long tags with attributes
        if (content.startsWith('<') && content.endsWith('>') && content.length > maxAttrLength) {
            const tagMatch = content.match(/^<(\w+)(.*?)>(.*)$/);
            if (tagMatch) {
                const tagName = tagMatch[1];
                let attrStr = tagMatch[2].trim();
                let innerText = tagMatch[3] || '';

                const attrs = attrStr.match(/[\w-]+="[^"]*"/g) || [];

                // ignoreAttFg logic
                let tagLine;
                if (ignoreAttFg && attrs.length > 1) {
                    tagLine = <${tagName} ${attrs[0]} ...>;
                } else if (attrs.length > 0) {
                    tagLine = <${tagName} ${attrs.join(' ')}>;
                } else {
                    tagLine = <${tagName}>;
                }

                // log opening tag
                logParts.push('%c' + tagLine);
                styles.push(background: ${colorTag}; color:white; padding:0 2px; margin:1px;);

                // log inner text and closing tag separately
                if (innerText) {
                    const innerMatch = innerText.match(/^(.*?)(<\/\w+>)$/);
                    if (innerMatch) {
                        const textOnly = innerMatch[1];
                        const closingTag = innerMatch[2];

                        if (textOnly) {
                            logParts.push('%c' + textOnly);
                            styles.push(background: ${colorText}; color:black; padding:0 2px; margin:1px;);
                        }

                        logParts.push('%c' + closingTag);
                        styles.push(background: ${colorTag}; color:white; padding:0 2px; margin:1px;);
                    } else {
                        // just inner text without closing tag
                        logParts.push('%c' + innerText);
                        styles.push(background: ${colorText}; color:black; padding:0 2px; margin:1px;);
                    }
                }

                logParts.push('%c\n');
                styles.push('');
                return;
            }
        }

        // Split remaining content into tags vs text
        const tagRegex = /(<[^>]+>)/g;
        let lastIndex = 0;
        let m;
        while ((m = tagRegex.exec(content)) !== null) {
            const index = m.index;

            if (index > lastIndex) {
                const text = content.slice(lastIndex, index);
                logParts.push('%c' + text);
                styles.push(background: ${colorText}; color:black; padding:0 2px; margin:1px;);
            }

            logParts.push('%c' + m[0]);
            styles.push(background: ${colorTag}; color:white; padding:0 2px; margin:1px;);

            lastIndex = index + m[0].length;
        }

        if (lastIndex < content.length) {
            const text = content.slice(lastIndex);
            logParts.push('%c' + text);
            styles.push(background: ${colorText}; color:black; padding:0 2px; margin:1px;);
        }

        logParts.push('%c\n');
        styles.push('');
    });

    console.log(logParts.join(''), ...styles);
}

square