JavaScript Etch-A-Sketch implementation from The Odin Project I'm working through the curriculum at The Odin Project. I recently completed the Etch-A-Sketch project and am looking for some help/feedback regarding my code. Specifically, I feel that the functions in my JavaScript contain too much repeat code. But, I'm having trouble understanding how to refactor the functions without breaking them.

Github HTML Preview

Full project code

const gridContainer = document.getElementById('gridContainer');


    function createGrid(size){
        let totalSquares = size * size;
        for (let i = 0; i &lt; totalSquares; i++){
            const newSquare = document.createElement('div');
            newSquare.classList.add('newSquare');
            gridContainer.appendChild(newSquare);
            newSquare.addEventListener('mouseover', function(e){
            newSquare.style.backgroundColor = 'black';
            });    

        }
        document.documentElement.style.setProperty("--rowNum", size);
        document.documentElement.style.setProperty("--colNum", size);
    }

    function pencilGrid(size){
        let totalSquares = size * size;
        for (let i = 0; i &lt; totalSquares; i++){
            const newSquare = document.createElement('div');
            newSquare.classList.add('newSquare');
            gridContainer.appendChild(newSquare);
            newSquare.addEventListener('mouseover', function(e){
            newSquare.style.backgroundColor = 'black';
            newSquare.style.opacity -= '-0.1';
            });    

        }
        document.documentElement.style.setProperty("--rowNum", size);
        document.documentElement.style.setProperty("--colNum", size);
    }

    function colorfulGrid(size){
        let totalSquares = size * size;
        for (let i = 0; i &lt; totalSquares; i++){
            const newSquare = document.createElement('div');
            newSquare.classList.add('newSquare');
            gridContainer.appendChild(newSquare);
            newSquare.addEventListener('mouseover', function(e){
                let randomColor = Math.floor(Math.random()*16777215).toString(16);
            newSquare.style.backgroundColor = randomColor;
            newSquare.style.border = '0px';
            });    

        }
        document.documentElement.style.setProperty("--rowNum", size);
        document.documentElement.style.setProperty("--colNum", size);
    }

    function clearGrid(){
        while (gridContainer.firstChild){
            gridContainer.removeChild(gridContainer.firstChild);
        }
    }


    createGrid(16);




    const newGridButton = document.getElementById('newGridButton');
    newGridButton.addEventListener('click', function(e){
        clearGrid();
        let newSize = prompt('New Grid Size:');
        createGrid(newSize);

    });

    const pencilButton = document.getElementById('pencilButton');
    pencilButton.addEventListener('click', function(e){
        clearGrid();
        let newSize = prompt('New Grid Size')
        pencilGrid(newSize);
    });

    const colorfulButton = document.getElementById('colorfulButton');
    colorfulButton.addEventListener('click', function(e){
        clearGrid();
        let newSize = prompt('New Grid Size')
        colorfulGrid(newSize);
    });
Bug
There is a small chance  (about 1 in 16) that the random color function will generate an bad color as you don't check for leading zeros. See code below for an alternative.
DRYing out code (Don't Repeat Yourself)
Yes there is way to much repeated code.
Repeated code is not only a pain to create, it is a source of bugs when it comes time to make changes.
Differences as arguments.
To reduce repetition we use functions to wrap up code that has only slight differences, passing the differences as arguments.
For example you have the 3 button event handlers with 3 sections of almost identical code.

const newGridButton = document.getElementById('newGridButton');
newGridButton.addEventListener('click', function(e){
    clearGrid();
    let newSize = prompt('New Grid Size:');
    createGrid(newSize);
});

const pencilButton = document.getElementById('pencilButton');
pencilButton.addEventListener('click', function(e){
    clearGrid();
    let newSize = prompt('New Grid Size')
    pencilGrid(newSize);
});

const colorfulButton = document.getElementById('colorfulButton');
colorfulButton.addEventListener('click', function(e){
    clearGrid();
    let newSize = prompt('New Grid Size')
    colorfulGrid(newSize);
});


The only difference in each is the Id of the button and the function called at the end. Thus we can simply wrap one of those sections in a function passing the Id and the setup function as arguments.
function setUpButton(buttonId, createGrid) {
    const button = document.getElementById(buttonId);
    button.addEventListener('click', function(e){
        clearGrid();
        let newSize = prompt('New Grid Size')
        createGrid(newSize);
    });
}

Reduce code noise
Shorten it little by removing code noise,

The prompt can move into the function call.
The clearGrid can move to the createGrid function
We can use direct DOM reference for the button element when we call the function, so we don't need document.getElementById
Use an arrow function for the event, and we don't need to event argument.

Thus we get the whole thing done in 6 lines.
function setUpButton(button, createGrid) {
    button.addEventListener('click', () =&gt; createGrid(prompt('New Grid Size')))
}
setUpButton(newGridButton, createGrid);
setUpButton(pencilButton, pencilGrid);
setUpButton(colorfulButton, colorfulGrid);

18 lines down to 6.
Using names to reference functions
Looking at the create grid function the only difference is the mouse event, all the rest is identical. Create the mouse event functions separately. We need a reference to the newSquare, that can be found in the event as event.target
To make accessing the functions easier we can add them to an object and use their names in the create grid function.
const draw = {
    colorful(e) {
        e.target.style.background = (Math.random()*0xFFFFFF|0).toString(16).padStart(7,&quot;#000000&quot;);
        e.target.style.border = '0px';
    },
    pencil(e) {
        e.target.style.background = 'black';
        e.target.style.opacity += 0.1;
    },
    create(e) { e.target.style.background = 'black' }
}

and modify the create grid function to take the name of the draw function as well as the size. Also move the clear grid function into this function.
The end result
Putting it all together we get it all done in about half the code.
const drawing = {
    colorful(e) {
        e.target.style.background = (Math.random()*0xFFFFFF|0).toString(16).padStart(7,&quot;#000000&quot;);
        e.target.style.border = &quot;0px&quot;;
    },
    pencil(e) {
        e.target.style.backgroundColor = 'black';
        e.target.style.opacity += 0.1;
    },
    black(e) { e.target.style.backgroundColor = 'black' },
};
setUpButton(newGridButton, &quot;black&quot;);
setUpButton(pencilButton, &quot;pencil&quot;&quot;);
setUpButton(colorfulButton, &quot;colorful&quot;);   
createGrid(&quot;black&quot;);

function setUpButton(button, name) {
    button.addEventListener('click', () =&gt; createGrid(name, prompt('New Grid Size')))
}

function createGrid(name, size = 16){
    var count = size ** 2;
    while (gridContainer.firstChild) { gridContainer.removeChild(gridContainer.firstChild) }
    while (count--) {
        const cell = Object.assign(document.createElement(&quot;div&quot;), {className : &quot;newSquare&quot;});
        gridContainer.appendChild(cell);
        cell.addEventListener(&quot;mouseover&quot;, drawing[name]);  
    }
    [&quot;--rowNum&quot;,&quot;--colNum&quot;].forEach(p =&gt; document.documentElement.style.setProperty(p, size));
} 

I used direct element reference for gridContainer, newGridButton, colorfulButton, and pencilButton so you must ensure that they are unique ids on the page.