How I Track Down a “Vanishing CSS” Bug (Built a Neat Little Form in the Process)

I had one of those pull‑your‑hair‑out afternoons where the simplest page refused to pick up any of its CSS. Chrome DevTools was showing HTML attributes inside the Styles panel id, type, even min as if they were CSS properties. That was my first clue that the browser had slipped into “attribute‑eating” mode.

Error Code:

<div class="addownsong">
<span class="hidesongadder"><i class="fa fa-times"></i></span>
<h4>Artiest</h4>
<input id="artist-name" type="text" style="background-image: url(&quot;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABHklEQVQ4EaVTO26DQBD1ohQWaS2lg9JybZ+AK7hNwx2oIoVf4UPQ0Lj1FdKktevIpel8AKNUkDcWMxpgSaIEaTVv3sx7uztiTdu2s/98DywOw3Dued4Who/M2aIx5lZV1aEsy0+qiwHELyi+Ytl0PQ69SxAxkWIA4RMRTdNsKE59juMcuZd6xIAFeZ6fGCdJ8kY4y7KAuTRNGd7jyEBXsdOPE3a0QGPsniOnnYMO67LgSQN9T41F2QGrQRRFCwyzoIF2qyBuKKbcOgPXdVeY9rMWgNsjf9ccYesJhk3f5dYT1HX9gR0LLQR30TnjkUEcx2uIuS4RnI+aj6sJR0AM8AaumPaM/rRehyWhXqbFAA9kh3/8/NvHxAYGAsZ/il8IalkCLBfNVAAAAABJRU5ErkJggg==&quot;); background-repeat: no-repeat; background-attachment: scroll; background-size: 16px 18px; background-position: 98% 50%; cursor: auto;">

<h4>Titel</h4>
<input id="track-name" type="text">

<div class="minutes">
    <h4>Minuten</h4>
    <input id="track-minutes" type="text" min="0" max="59">
</div>

<div class="timedivider">
    <h4>:</h4>
</div>

<div class="seconds">
    <h4>Seconden</h4>
    <input id="track-seconds" type="text" min="0" max="59">
</div>

<p><strong>Wil je een scanbare Spotify code toevoegen aan je topplaatje? Gebruik onderstaande zoekbalk om het juiste nummer te vinden!</strong></p>
<input style="text-align: center;border-color:#df0d84" id="songsearch2" type="text" placeholder="Vind je favoriete nummer op basis van titel">
<div id="songsearch2-results"></div>
<div id="selected-spotify-code"></div>
<input hidden="" id="uploaded-image">
<div class="dropzone dropzonebox dz-clickable" id="my-awesome-dropzone"><div class="dz-message" data-dz-message=""><span>Sleep een foto hierheen of klik om te uploaden</span></div></div>
<div style="color:#ff0000;margin-top:10px;"><center><strong>Let op: voor het beste resultaat, voeg je een vierkante foto toe</strong></center></div>
<div style="background-color:#ddd;color:#545454;border-color:#ddd;" class="createButton placeholderButton">Voeg eerst alle gegevens toe</div>
<div style="display: none;" class="createButton addCreation">Voeg je creatie toe!</div>
</div>

Fix in Principle

  1. Close the quote right after the Data URI.
  2. Or (cleaner) put the background in a CSS class:
.input-with-icon {
background: url(icon-search.svg) no-repeat 98% 50% / 16px 18px;
}
  1. Remove the whole style="…" from the HTML.

Result: the parser’s state machine is back on rails, the rest of the document is read normally, and external styles cascade as expected.

What Actually Happened

I’d pasted a huge Base‑64 icon straight into an inline style on the very first <input>. Somewhere in that 300‑character blur I forgot to close the opening quote after url("…) and the browser happily chewed through everything that followed, treating it as more CSS.

<!-- culprit (simplified) -->
<input
id="artist-name"
type="text"
style="background-image:url("data:image/png;base64,… <!-- 🔴 quote never closes -->
background-repeat:no-repeat;">

Once that quote goes missing, the parser can’t tell where the attribute ends, so every semicolon or angle‑bracket turns into garbage. From that point on no rule lands anywhere and DevTools starts listing HTML attributes as if they were CSS declarations.

Fix: close the quote or, better, get the Base‑64 monster out of the markup and into a stylesheet.

A Clean, Working Demo

Below is the trimmed‑down project I ended up with after the fix. It’s a tiny “add your own song” form with live validation and a dropzone.

index.html

<link rel="stylesheet" href="styles.css" />
<script defer src="app.js"></script>

<div class="add-own-song">
<button class="hide-song-adder" aria-label="Close form">
<i class="fa fa-times"></i>
</button>

<label>
<span>Artiest</span>
<input id="artist-name" type="text" class="input-with-icon" />
</label>

<label>
<span>Titel</span>
<input id="track-name" type="text" />
</label>

<div class="time-group">
<label>
<span>Minuten</span>
<input id="track-minutes" type="number" min="0" max="59" />
</label>

<span class="time-divider">:</span>

<label>
<span>Seconden</span>
<input id="track-seconds" type="number" min="0" max="59" />
</label>
</div>

<p class="tip">
<strong>
Wil je een scanbare Spotify‑code toevoegen aan je topplaatje?<br />
Gebruik onderstaande zoekbalk om het juiste nummer te vinden!
</strong>
</p>

<input id="song-search" type="text"
placeholder="Vind je favoriete nummer op basis van titel" />

<div id="song-search-results"></div>
<div id="selected-spotify-code"></div>

<input hidden id="uploaded-image" />

<div id="dropzone" class="dropzone">
Sleep een foto hierheen of klik om te uploaden
</div>

<div class="notice">
<strong>Let op: voor het beste resultaat, voeg je een vierkante foto toe</strong>
</div>

<button class="create-btn" disabled>Voeg je creatie toe!</button>
</div>

styles.css

body { font-family: system-ui, sans-serif; margin: 2rem; }

.add-own-song { max-width: 32rem; margin: auto; }

label { display: block; margin: 1rem 0 .25rem; }

.input-with-icon {
background: url(icon-search.svg) no-repeat 98% 50% / 16px 18px;
padding-right: 2.5rem;
}

.time-group { display: flex; align-items: flex-end; gap: .5rem; }
.time-divider { font-size: 1.5rem; line-height: 2.3rem; }

.notice { color: #ff0000; margin: .75rem 0; text-align: center; }
.create-btn[disabled] { background:#ddd; color:#545454; cursor:not-allowed; }
.create-btn:not([disabled]) { background:#df0d84; color:#fff; cursor:pointer; }

app.js

const fields   = [...document.querySelectorAll(
'#artist-name,#track-name,#track-minutes,#track-seconds'
)];
const addBtn = document.querySelector('.create-btn');
const minutes = document.getElementById('track-minutes');
const seconds = document.getElementById('track-seconds');

/* Live validation */
function validate () {
const filled = fields.every(f => f.value.trim() !== '');
const inRange = (+minutes.value < 60) && (+seconds.value < 60);
addBtn.disabled = !(filled && inRange);
}
fields.forEach(f => f.addEventListener('input', validate));

/* Auto‑jump minutes → seconds */
minutes.addEventListener('input', e => {
if (e.target.value.length >= 2) seconds.focus();
});

/* Demo submit */
addBtn.addEventListener('click', () => {
alert('🎉 Creatie toegevoegd!');
document.querySelector('.add-own-song').reset?.();
addBtn.disabled = true;
});

Want to Take It Further

ChallengeTry This
Custom error hintsShow a tiny red note under each field when it’s empty or “60+”.
Drag‑and‑drop previewRender a thumbnail in #dropzone the moment a file lands.
Spotify auto‑completeHit the Spotify Web API while the user types in #song-search.
Keyboard‑only flowAdd tabindex and ARIA roles so everything works without a mouse.
Jest unit testsFeed dummy values into validate() and assert the button state.

Take‑away Rules to Avoid “Attribute‑Eating” Bugs

  1. Never inline huge Data URIs in HTML huge unreadable strings are quote‑mismatch magnets.
  2. Use Prettier/ESLint/HTMLHint their lexers will spot orphaned quotes before the browser does.
  3. Keep one concern per file
    • HTML = structure
    • CSS = presentation
    • JS = behaviour
      and your diff/PR reviews will catch typos immediately.
  4. Let the browser fail loudly in DevTools, collapse external styles; if you still see type: text, you know where to look.

Final Thought

I’m always amazed at how one missing quote can nuke an entire stylesheet. Since this little incident, I keep heavy assets (images, Base‑64 blobs, complex gradients) out of my markup and inside dedicated CSS files. My HTML stays tidy, DevTools stays sane, and the odds of me chasing a ghost bug at 4 PM drop to almost zero. If this post spares you that headache even once, it’s done its job.

Related blog posts