Appearance
SCSS
Altruistiq styleguide for writing maintainable and durable SCSS.
CSS module convention: BEM
Note
- Make sure to add the
.scss
extension to sass files or compilation will fail. - Partial files should be named starting with an underscore, e.g.
_variables.scss
Selectors
Selectors should be as specific to the task as possible. In the context of Vue, scoped styles have allowed much more freedom in this area and that freedom often leads to less readable and less manageable CSS. In most cases it should be possible to write module-scoped styles without relying upon the framework’s component scoping.
If we need to add styles to an element in our component then our instinct should be to add a class to that element so that we can select it. Descendent selectors are risky and can have unintended consequences especially when used to target element tags. There are good situations that require their use but those will usually be when you are overriding a 3rd party component’s styles, and for those scenarios there are mitigation options to alleviate as much of the risk as possible.
The extra benefit of being explicit with our classes is that if someone does need to override our styles, they know exactly which element to target. It’s also possible for that developer to make those conditional overrides in your component so that if you need to know what is styling “component__element” then you just go to the component file and find its style declaration. No need to do a project-wide search.
Our guiding principle should be to avoid situations where another developer says: “why is this element receiving these styles?”.
Examples:
❌ BAD
html
<div class="sidenav">
<ul class="items">
<li class="item">Content</li>
<li class="item">Content</li>
<li class="item disabled">Content</li>
</ul>
</div>
<div class="sidenav">
<ul class="items">
<li class="item">Content</li>
<li class="item">Content</li>
<li class="item disabled">Content</li>
</ul>
</div>
scss
$module: "sidenav";
.#{$module} {
// Very possible to have a second ul which would inherit the
// same styling by default
ul {
margin-bottom: 30px;
.item {
font-size: 16px;
padding: 10px;
&.disabled {
opacity: 0.3;
cursor: not-allowed;
}
}
}
}
$module: "sidenav";
.#{$module} {
// Very possible to have a second ul which would inherit the
// same styling by default
ul {
margin-bottom: 30px;
.item {
font-size: 16px;
padding: 10px;
&.disabled {
opacity: 0.3;
cursor: not-allowed;
}
}
}
}
✅ GOOD
html
<div class="sidenav">
<ol class="sidenav__items">
<li class="sidenav__item">Content</li>
<li class="sidenav__item">Content</li>
<li class="sidenav__item disabled">Content</li>
</ol>
</div>
<div class="sidenav">
<ol class="sidenav__items">
<li class="sidenav__item">Content</li>
<li class="sidenav__item">Content</li>
<li class="sidenav__item disabled">Content</li>
</ol>
</div>
scss
$module: "sidenav";
.#{$module} {
&__items {
margin-bottom: 30px;
}
&__item {
font-size: 16px;
padding: 10px;
&.disabled {
opacity: 0.3;
cursor: not-allowed;
}
}
}
$module: "sidenav";
.#{$module} {
&__items {
margin-bottom: 30px;
}
&__item {
font-size: 16px;
padding: 10px;
&.disabled {
opacity: 0.3;
cursor: not-allowed;
}
}
}
Mitigating potential problems with 3rd party components
scss
$module: "sidenav";
.#{$module} {
&__items {
margin-bottom: 30px;
}
&__item {
font-size: 16px;
padding: 10px;
&.disabled {
opacity: 0.3;
cursor: not-allowed;
}
// If we do need to override something we don't control then make
// the descendent selector as shallow as possible to avoid unexpected
// consequences further down the DOM tree
> .o-title {
font-size: 16px;
}
}
}
$module: "sidenav";
.#{$module} {
&__items {
margin-bottom: 30px;
}
&__item {
font-size: 16px;
padding: 10px;
&.disabled {
opacity: 0.3;
cursor: not-allowed;
}
// If we do need to override something we don't control then make
// the descendent selector as shallow as possible to avoid unexpected
// consequences further down the DOM tree
> .o-title {
font-size: 16px;
}
}
}
Nesting
We aim to restrict ourselves to minimal levels of nesting because it can cause readability issues for other developers, and it helps us avoid specificity wars with nested descendent selectors. See the examples below and see which of them is easiest to reason about what the component will look like.
❌ BAD
scss
$module: "inbox";
.#{$module} {
background-color: white;
li {
display: flex;
flex-flow: row nowrap;
align-items: center;
padding: 8px 16px;
background-color: grey;
color: white;
@media (max-width: $mobile) {
font-size: 14px;
padding: 4px 8px;
.item-checkbox {
margin-right: 8px;
}
}
.item-checkbox {
margin-right: 16px;
}
&.urgent {
font-weight: 700;
.item-icon {
color: red;
}
}
&.selected {
background-color: darkgrey;
}
p {
font-size: 15px;
}
i {
margin-left: auto;
}
}
}
$module: "inbox";
.#{$module} {
background-color: white;
li {
display: flex;
flex-flow: row nowrap;
align-items: center;
padding: 8px 16px;
background-color: grey;
color: white;
@media (max-width: $mobile) {
font-size: 14px;
padding: 4px 8px;
.item-checkbox {
margin-right: 8px;
}
}
.item-checkbox {
margin-right: 16px;
}
&.urgent {
font-weight: 700;
.item-icon {
color: red;
}
}
&.selected {
background-color: darkgrey;
}
p {
font-size: 15px;
}
i {
margin-left: auto;
}
}
}
✅ GOOD
scss
$module: "inbox";
.#{$module} {
background-color: white;
&__item {
display: flex;
flex-flow: row nowrap;
align-items: center;
font-size: 15px;
padding: 8px 16px;
background-color: grey;
color: white;
margin-bottom: 4px;
@media (max-width: $mobile) {
font-size: 14px;
padding: 4px 8px;
}
&.urgent {
font-weight: 700;
}
&.selected {
background-color: darkgrey;
}
}
&__item-checkbox {
margin-right: 16px;
@media (max-width: $mobile) {
margin-right: 8px;
}
}
&__item-copy {
font-size: 15px;
}
&__icon {
margin-left: auto;
.#{$module}__item.urgent & {
color: red;
}
}
}
$module: "inbox";
.#{$module} {
background-color: white;
&__item {
display: flex;
flex-flow: row nowrap;
align-items: center;
font-size: 15px;
padding: 8px 16px;
background-color: grey;
color: white;
margin-bottom: 4px;
@media (max-width: $mobile) {
font-size: 14px;
padding: 4px 8px;
}
&.urgent {
font-weight: 700;
}
&.selected {
background-color: darkgrey;
}
}
&__item-checkbox {
margin-right: 16px;
@media (max-width: $mobile) {
margin-right: 8px;
}
}
&__item-copy {
font-size: 15px;
}
&__icon {
margin-left: auto;
.#{$module}__item.urgent & {
color: red;
}
}
}
Module Naming
The name of the module should represent what the component does rather than where the component is used. We might have a new component appear in a design for a new section but if we ignore where it lives and focus on what it tries to accomplish we can make sure we name the module in a way that allows it to be reused without confusing people.
We also leverage the $module pattern seen below to save ourselves a little bit of time if we ever need to change the name of the module in the future. It could be part of the pull request review or it could be an update that enables that component to work in a different way that requires a name change.
If the module really is unique to its location and will never be re-used for some reason then that is a valid case for choosing a location-relevant module name. But we should still follow the #{$module} pattern just in case we change our minds and want to save ourselves a few precious seconds.
❌ BAD
scss
.targets-card {
&__title {
.targets-card.disabled & {
opacity: 0.4;
}
}
}
.targets-card {
&__title {
.targets-card.disabled & {
opacity: 0.4;
}
}
}
✅ GOOD
scss
$module: "card";
.#{$module} {
&__title {
.#{$module}.disabled & {
opacity: 0.4;
}
}
}
$module: "card";
.#{$module} {
&__title {
.#{$module}.disabled & {
opacity: 0.4;
}
}
}
General rules
Only one module per component. You should never have more than one root to your component’s styles. If you need to, it probably means you need to make another component.
If you prefer sticking strictly to the blockelement—modifier pattern rather than leveraging class modifiers like blockelement.modifier then just make sure that your markup isn’t suffering too badly for it. In a lot of cases —modifier is superior but it can also bloat your markup if your component name is quite long and you’re dealing with a lot of conditional modifiers. e.g.
html<section class="just-an-example"> <div class="just-an-example__child" :class="{ 'just-an-example__child--active': someBoolean, 'just-an-example__child--disabled': someOtherBoolean, 'just-an-example__child--highlight': someThirdBoolean }" ></div> </section>
<section class="just-an-example"> <div class="just-an-example__child" :class="{ 'just-an-example__child--active': someBoolean, 'just-an-example__child--disabled': someOtherBoolean, 'just-an-example__child--highlight': someThirdBoolean }" ></div> </section>
If you end up in a situation where you’re writing .block__element-elementchild patterns a lot, you might want to consider splitting them into two components. This can happen a lot with things like lists which can end up like:
html<nav class="sidenav"> <ul class="sidenav__items"> <li> <div class="sidenav__item"> <span class="sidenav__item-name">Name</span> <svg class="sidenav__item-icon"></svg> </div> </li> </ul> </nav>
<nav class="sidenav"> <ul class="sidenav__items"> <li> <div class="sidenav__item"> <span class="sidenav__item-name">Name</span> <svg class="sidenav__item-icon"></svg> </div> </li> </ul> </nav>
This is a simplistic example but you can see how this markup could be written like this instead:
html<!-- sidenav component --> <nav class="sidenav"> <ul class="sidenav__items"> <li> <sidenav-component /> </li> </ul> </nav> <!-- sidenav-item component --> <div class="sidenav-item"> <span class="sidenav-item__name">Name</span> <svg class="sidenav-item__icon"></svg> </div>
<!-- sidenav component --> <nav class="sidenav"> <ul class="sidenav__items"> <li> <sidenav-component /> </li> </ul> </nav> <!-- sidenav-item component --> <div class="sidenav-item"> <span class="sidenav-item__name">Name</span> <svg class="sidenav-item__icon"></svg> </div>
The styling and the logic for the items is not really relevant to the sidenav component, they just happen to live in the same place. Separating things like that can often make more sense and avoid the sidenav component doing too much and becoming confusing.