I'm wondering if a vanilla-approach (without helpers libraries) is still a valid choice...
I'm not sure what you mean here, can you explain. Would 'vanilla approach' mean using a CSS Stylesheet with CSS Selectors for some or most of these styles? I think you should try to limit the CSS-in-JS to only the properties or values that depend on JavaScript.
I obliviously can add a className to each of my components but I'm trying to adopt CSS-in-JS best practice in order to use all the goodies of let JS managing my styles and to have a scoped CSS for each component.
What goodies are those? I do stuff with style scoping all the time, but never via inline styles. Scoped styles are something I do with JS-in-CSS (as opposed to CSS-in-JS) but usually it's for element queries, container queries, or using the properties of the elements I'm styling to inform the CSS I'm writing. When I write scoped styles I'm less concerned with getting my CSS to apply to elements as I am using JavaScript to help me figure out how to write the values in the CSS I'm applying.
Is there a particular set of tools you're using right now. Scoped styles should be as easy as this:
<div class="widget">
<h2>Widget Title</h2>
</div>
<div style="width: 75%">
<div class="widget">
<h2>Widget Title</h2>
</div>
</div>
<div style="width: 50%">
<div class="widget">
<h2>Widget Title</h2>
</div>
</div>
<script>
window.addEventListener('load', JSinCSS)
window.addEventListener('resize', JSinCSS)
window.addEventListener('input', JSinCSS)
window.addEventListener('click', JSinCSS)
function JSinCSS() {
var tag = document.querySelector('#JSinCSS')
if (!tag) {
tag = document.createElement('style')
tag.id = 'JSinCSS'
document.head.appendChild(tag)
}
tag.innerHTML = `
/* Global CSS goes anywhere */
body {
background: tan;
}
/* Evaluating global JS in CSS */
body:before {
content: '${innerWidth} x ${innerHeight}'
}
/* Running JS from CSS using functions */
${scopedStyle('.widget', `
$this {
border: 1px solid red;
}
`)}
`
}
// A simple style-scoping plugin
function scopedStyle(selector, stylesheet) {
// Find all elements in document matching selector
var tag = document.querySelectorAll(selector)
var style = ''
var count = 0
// For each element matching selector
for (var i=0; i<tag.length; i++) {
// Make a unique identifier
var attr = btoa(selector).replace(/=/g, '')
var element = '[data-' + attr + '="' + count + '"]'
// Mark the element with the unique identifier
tag[i].setAttribute('data-' + attr, count)
// Replace `$this` with that same identifier in CSS
var css = stylesheet.replace(/\$this/g, element)
// Add a duplicate of the scoped CSS rule to the stylesheet we're returning
style += css
count++
}
// Return all scoped CSS to JS-in-CSS stylesheet
return style
}
</script>
That's a working JS-in-CSS function hooked up to 4 global events the reprocesses a CSS stylesheet that can be augmented with anything that JavaScript knows.
The way the CSS reprocessor works is is creates a <style> tag in the document, and every time the events it's watching are triggered, it recalculates the contents of that <style> tag.
In order to 'scope' the application of a style to each element that matches a selector (individually) all that was required was to count each element and assign them a unique identifier (if you're using JS to build your app maybe you can do the 'unique naming' part as you render your HTML in JS…) and then for each matching element I also took the supplied Stylesheet with a special placeholder word, here I've used $this and I will add a copy of the held stylesheet, but replacing $this with the same unique identifier for the element we want the rule to apply to.
$this {
border: 1px solid red;
}
By the time you have looped through all of the elements matching your selector and added the rules scoped to each of their identifiers, our scopedStyle() plugin is going to be returning a string of CSS text like this:
[data-LndpZGdldA="0"] {
border: 1px solid red;
}
[data-LndpZGdldA="1"] {
border: 1px solid red;
}
[data-LndpZGdldA="2"] {
border: 1px solid red;
}
So if we think back to the original stylesheet our JSinCSS() plugin was holding, this string is going to end up at the bottom of that, below the global CSS example and the global JS example.
body {
background: tan;
}
body:before {
content: '1248 x 238'
}
[data-LndpZGdldA="0"] {
border: 1px solid red;
}
[data-LndpZGdldA="1"] {
border: 1px solid red;
}
[data-LndpZGdldA="2"] {
border: 1px solid red;
}
And here's the code above running in the browser. We can see the data-LndpZGdldA attributes on the class=widget elements at the bottom, and the <style id=JSinCSS> that's being populated with the current state of our rendered CSS stylesheet. Success!

So that's a little overview of how you can implement style scoping from scratch in JS, and the parts required to make it work:
Hopefully you can figure out how to apply the same idea to your HTML here. You want to be working with CSS stylesheets and taking advantage of CSS selectors, but also taking advantage of JavaScript's smarts and everything it knows too!