Why Make Another CSS Framework?
As you may know, there are many CSS frameworks (hundreds of them, and a lot of them are not maintained today). Everybody can choose one that suits their work style or project requirements. So why make another one? It is certainly not because we can do it better but because we want to do it our way. We want to be in control and make decisions.
It Is Opinionated
Each system is opinionated but on a different level; this is valid for Spruce too. We don’t want to vote for (strictly) any particular solution (because there is always more than one), but we will show you what we think is the best for us (and maybe for you too). We don’t believe there is a good or bad solution, but we can learn from any of them.
Coding Style Guide and Practices
Where it is possible, we use elements and/or attributes to style elements, but it is still a class-based framework.
@use 'sprucecss/scss/spruce' as *;
.accordion-list {
@include layout-stack('s');
}
.accordion-card {
$this: &;
background-color: color('surface');
border: config('border-width-base', $display) solid color('border');
border-radius: radius('lg');
&__title {
font-size: font-size('h4');
margin-block: 0;
padding: spacer('m');
&:has(#{$this}__toggle) {
padding: 0;
}
}
&__toggle {
@include clear-btn;
align-items: center;
display: flex;
gap: spacer('m');
inline-size: 100%;
justify-content: space-between;
padding: spacer('m');
text-align: start;
&:hover {
color: color('primary');
}
&:focus-visible {
outline: none;
#{$this}__icon {
@include short-ring('button', 'primary');
}
}
&[aria-expanded='true'] {
svg {
rotate: 90deg;
}
.horizontal-line {
opacity: 0;
}
}
}
&__icon {
--size: 1.75rem;
align-items: center;
background-color: color('primary-background', 'btn');
block-size: var(--size);
border-radius: radius('sm');
color: color('primary-foreground', 'btn');
display: flex;
flex-shrink: 0;
inline-size: var(--size);
justify-content: center;
svg {
--size: 1.25rem;
@include transition;
block-size: var(--size);
inline-size: var(--size);
}
.horizontal-line {
@include transition;
}
}
&__content {
@include transition($property: 'grid-template-rows, padding-block-end');
display: grid;
grid-template-rows: 1fr;
padding-block-end: spacer('m');
padding-inline: spacer('m');
&[inert] {
grid-template-rows: 0fr;
padding-block-end: 0;
}
> div {
overflow: hidden;
@include layout-stack('xs');
* + h2,
* + h3,
* + h4,
* + h5 {
margin-block-start: spacer('s');
}
}
}
}
@include motion-reduce {
.accordion-card__content,
.accordion-card__icon svg {
transition: none;
}
}
<div class="accordion-list" data-accordion>
<div class="accordion-card">
<h3 class="accordion-card__title">
Why Make Another CSS Framework?
</h3>
<div class="accordion-card__content">
<div>
<p>As you may know, there are many CSS frameworks (hundreds of them, and a lot of them are not maintained today). Everybody can choose one that suits their work style or project requirements. So why make another one? It is certainly not because we can do it better but because we want to do it our way. We want to be in control and make decisions.</p>
</div>
</div>
</div>
<div class="accordion-card">
<h3 class="accordion-card__title">
It Is Opinionated
</h3>
<div class="accordion-card__content">
<div>
<p>Each system is opinionated but on a different level; this is valid for Spruce too. We don’t want to vote for (strictly) any particular solution (because there is always more than one), but we will show you what we think is the best for us (and maybe for you too). We don’t believe there is a good or bad solution, but we can learn from any of them.</p>
</div>
</div>
</div>
<div class="accordion-card">
<h3 class="accordion-card__title">
Coding Style Guide and Practices
</h3>
<div class="accordion-card__content">
<div>
<p>Where it is possible, we use elements and/or attributes to style elements, but it is still a class-based framework.</p>
</div>
</div>
</div>
</div>
(() => {
let count = 0;
document.querySelectorAll('[data-accordion]').forEach((list) => {
const multiple = list.dataset.accordion === 'multiple';
const cards = [...list.querySelectorAll('.accordion-card')];
const toggle = (card, open) => {
card.querySelector('.accordion-card__toggle').setAttribute('aria-expanded', String(open));
card.querySelector('.accordion-card__content').inert = !open;
};
cards.forEach((card) => {
const heading = card.querySelector('.accordion-card__title');
const content = card.querySelector('.accordion-card__content');
const button = document.createElement('button');
count += 1;
content.id ||= `accordion-content-${count}`;
button.className = 'accordion-card__toggle';
button.type = 'button';
button.setAttribute('aria-controls', content.id);
button.innerHTML = '<span class="accordion-card__label"></span><span class="accordion-card__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line class="vertical-line" x1="12" y1="5" x2="12" y2="19"></line><line class="horizontal-line" x1="5" y1="12" x2="19" y2="12"></line></svg></span>';
button.querySelector('.accordion-card__label').textContent = heading.textContent.trim();
heading.replaceChildren(button);
toggle(card, window.location.hash === `#${content.id}`);
button.addEventListener('click', () => {
const open = button.getAttribute('aria-expanded') !== 'true';
if (open && !multiple) {
cards.filter((other) => other !== card).forEach((other) => toggle(other, false));
}
toggle(card, open);
});
});
});
})();