Skip to main content

Tutorial

This is a basic overview that is organized so that it can be read from start to end and so that the reader understands the fundamental principles and basic ways of using the framework.

We offer 4 ways to explain our methodology and framework:

  • Cheat Sheet - takes 3 minutes - Select what you want to do and find out how to do it in SPOT CSS framework.

  • Quick start guide - takes 15 minutes - The quickest way to see the way style is written in this framework, without explanation of why it is so and what are the advantages of such an approach.

  • Tutorial (current document) - takes 1 hour - A basic overview that is organized so that it can be read from start to end and so that the reader understands the fundamental principles and basic ways of using the framework.

  • Documentation - takes hours - Comprehensive documentation of everything, organized by mixins and framework entities, so you can quickly find exactly what you want to learn more about.

Choose the one that fits your current needs best.

Intro

The problem

Why is CSS a pain?

  • Because it's global
    Each declaration can affect anything in the document.

  • Changes come in from anywhere
    Each declaration can affect anything from any line and any file.

  • Rules can affect unwanted targets
    Each declaration can affect more than the intended target.

  • It can be done in infinite ways
    The points above make any task doable in a millions ways.

Solution

The goals of this methodology:

  • develop with simplicity, clarity and definiteness
    Avoid unpredictable dependencies and scattered information in many files.

  • maintain with confidence without side-effects
    Watch out for unwanted outcomes, avoid uncertain results.

  • scaling project and team
    Do not slow down as the project grows. Keep developers substitutable.

  • prevent resignation from CSS entropy
    Do not let your project fall into chaos where the only solution is to start over.

Main concept

SPOT CSS methodology and framework is all about architecture!

The main objective was to design rules that will unify the resulting code into a single possible form, no matter who writes it.

The solution is to have all rules for specific element in one and only SINGLE PLACE.

Hence the name SPOT CSS as the abbreviation of "Single Place of Truth".

3 Rules

There are only 3 simple rules to follow. And SASS compiler will let you know if you break them.

#1 One entity in one place

The most important rule is ONE ENTITY IN ONE PLACE, hence the name "Single Place Of Truth".

This applies to any entity, such as a component, element, or mutation.

You will find any entity always in one contiguous block.

#2 Fixed semantic order

The second rule is FIXED SEMANTIC ORDER of all entities, that means all mixins follows a system when and where they can be used.

This applies to mixins, such as a component, element, register, state, variant, context, etc.

The second rule is FIXED SEMANTIC ORDER of all entities:

A component contains:

  1. register - the very first section, used when component has mutations or sub elements, or can be omitted when component is in mode draft.
  2. default - the default style section of component or sub element. It can be omitted when current element has no mutations, just single fixed style.
  3. responsive - mutations for breakpoints, media queries, or special responsive contexts.
  4. state - section of all states of current component/element.
  5. variant - section of all variants of current component/element.
  6. browser - section of all browser exceptions of current component/element.
  7. context - section of all contexts of current component/element.
  8. pseudo-element - repeat entities with order 2. - 6.
  9. element - repeat entities with order 2. - 6.

When this prescripted order is not followed, SASS parser will let you know that you break the 2nd rule.

#3 Follow HTML structure

The third and last rule is FOLLOW HTML STRUCTURE to find everything very quickly.

<button>
<span class="icon">
<img src="{{ buttonImage }}" />
</span>
<span class="text">
button name
</span>
</button>
+mode('draft')

+component('button')
display: inline-block

+____________________________
+element('.icon')
vertical-align: middle

+____________________________
+element('img')
width: 100%

+____________________________
+element('.text')
font-size: 1.2em

If your component has sub elements, a SASS/SCSS code have to reflect exact same element structure.

Why SASS syntax (not SCSS)?

Because you don't need to write @include but only +. And in this framework, you will do it very often. It's more simple and more readable.

Of course there are other differences: tabs instead curly brackets { } and no semicolons ;. But the main reason is omiting the @include keyword.

@include lorem() {
property: value;

@include ipsum('dolor') {
property: value;

@include sit('amet') {
@include consectetur {
property: value;
}
}
}
}
+lorem()
property: value

+ipsum('dolor')
property: value

+sit('amet')
+consectetur
property: value

But when you don't like SASS syntax (or just not used to), you can easily write SCSS. Therefore, all examples are also given in this syntax. It's a matter of preference (but also habit).

Installation

Install spotcss in your project:

$ npm i --save-dev spotcss

Import spotcss framework in your style:

@import "~spotcss"

Component

Component is a very base entity in this component-based methodology and framework. It can be any simple/single element, or any block of the website (structure of elements).

+component('button')
display: inline-block
button {
display: inline-block;
}

You can find more about this topic in the documentation of the mixin +component.

Mutations

When one entity has more appearances, we call it a MUTATION.

If element has only one appearance, we call it element without mutations. If element has more appearances, it has default appearance and other mutations.

Default

When component has more mutations, the "default" mutation must be declared and should go first.

+mode('draft')

+component('button')
+default
display: inline-block
background-color: grey

+variant('.primary')
background-color: green



button {
display: inline-block;
background-color: grey;
}
button.primary {
background-color: green;
}

The mixins used in the code above: +mode, +component, +default, +variant.

You can find more about this topic in the documentation of the mixin +default.

Variant

Variant is the basic type of the mutation. It defines other than default appearance of the component.

+mode('draft')

+component('button')
+default
display: inline-block
background-color: grey

+variant('.primary')
background-color: green



button {
display: inline-block;
background-color: grey;
}
button.primary {
background-color: green;
}

The same principle applies to sub-elements:

+mode('draft')

+component('button')
+default
display: inline-block
background-color: grey

+variant('.primary')
background-color: green

+element('.text')
+default
font-weight: 1.2em

+variant('.primary')
font-weight: 1.6em



button {
display: inline-block;
background-color: grey;
}
button.primary {
background-color: green;
}

button .text {
font-weight: 1.2em;
}
button.primary .text {
font-weight: 1.6em;
}

Important: note that the class/variant .primary was applied to the root element of the component in both cases (button.primary .text).

The mixins used in the code above: +mode, +component, +default, +variant.

You can find more about this topic in the documentation of the mixin +variant.

State

Mixin +state works exactly the same as mixin +variant. The only difference is in semantics. But it can be combined together too.

We call variant a mutation of component which is permanent and usually does not change during the lifetime of a particular component.

Typical +variant examples are following: .primary, .secondary, .rounded, .borderless, .tiny, .small, .large, .heavy, .wide, .full-width, .inline, .opaque, .no-wrap.

A state is something (in SPOT CSS methodology) that is changing during the lifetime of a particular component and can be often animated (with transition).

Typical +state examples are following: :hover, :visited, :active, :focus, :disabled, :checked, .collapsed, .expanded, .selected, .inactive.

The rule of thumb for using a +state mixin instead of +variant is mostly for cases of common pseudo-classes ("user action pseudo-classes" and "form input pseudo-classes") and mutations where transitions are usually implemented.

+mode('draft')

+component('button')
+default
display: inline-block
background-color: grey

+state(':hover')
background-color: yellow



button {
display: inline-block;
background-color: grey;
}
button:hover {
background-color: yellow;
}

The mixins used in the code above: +mode, +component, +default, +state.


And you can also mix multiple mutations together:

+mode('draft')

+component('button')
+default
display: inline-block
background-color: grey

+state(':hover')
background-color: yellow

+variant('.primary')
+default
background-color: green

+state(':hover')
background-color: white



button {
display: inline-block;
background-color: grey;
}
button:hover {
background-color: yellow;
}

button.primary {
background-color: green;
}
button.primary:hover {
background-color: white;
}

You can find more about this topic in the documentation of the mixin +state.

Context

+mode('draft')

+component('button')
+default
display: inline-block
background-color: grey

+context('ui-modal')
display: block



button {
display: inline-block;
background-color: grey;
}
ui-modal button {
display: block;
}

The mixins used in the code above: +mode, +component, +default, +context.

You can find more about this topic in the documentation of the mixin +context.

Responsive

Mixin +responsive works similar to mixin +context. The main difference is in semantics and that you can use media queries.

+mode('draft')

+component('button')
+default
display: inline-block
background-color: grey

+responsive('html.mobile-portrait')
display: block

+responsive('@media (max-width: 360px)')
display: flex



button {
display: inline-block;
background-color: grey;
}
html.mobile-portrait button {
display: block;
}
@media (max-width: 360px) {
button {
display: flex;
}
}

The mixins used in the code above: +mode, +component, +default, +responsive.

You can find more about this topic in the documentation of the mixin +responsive.

Sub-elements

A component can be composed of several elements, and the following mixins help us define them.

Element

The mixin element is used to define the internal elements of the component and must be mentioned in the register section and then in the same order and in the same depth in the component code itself.

+component('button')
+register
+element('.icon')
+element('> img')
+element('.text')

display: inline-block

+____________________________
+element('.icon')
vertical-align: middle

+____________________________
+element('> img')
width: 100%

+____________________________
+element('.text')
font-size: 1.2em

The mixins used in the code above: +component, +register, +element.

You can find more about this topic in the documentation of the mixin +element.

Pseudo-element

Even pseudo-elements are considered separate as elements and, just like an element, they must be registered in the register section and then in the same order and in the same depth in the component code.

The pseudo-element must be placed before the elements and it is not necessary to use the prefix :: in the selector (but you can). You can use all pseudo-element selectors such as before, after, first-letter, first-line, placeholder, selection...

+component('button')
+register
+pseudo-element('before')
+element('.text')

display: inline-block

+____________________________
+pseudo-element('before')
vertical-align: middle

+____________________________
+element('.text')
font-size: 1.2em

The mixins used in the code above: *+component, +register, +pseudo-element, +element**.

You can find more about this topic in the documentation of the mixin +pseudo-element.

Separator

Each element must be preceded by a separator. It's a mixin of underscores (as many as you want) and the parser will yell at you with warning if you forget it. It is used for visual perception where the start and the end of the CSS code of every element are, and it proved to be a very useful (mandatory) mixin.

Note:

  • it is never used in register part
  • it must be used before pseudo-elements too (it is also an element from styling perspective there for code organizing perspective)
  • for this specific element, there is no other place where any other CSS for this element is written out of its "separator borderlines" = single place of truth
+component('button')
+register
+element('.icon')
+pseudo-element('after')
+element('.text')

display: inline-block

+____________________________
+element('.icon')
vertical-align: middle

+____________________________
+pseudo-element('after')
width: 100%

+____________________________
+element('.text')
font-size: 1.2em

Register

Register is the first mixin that follows right after the +component mixin (inside).

It does not always have to be used. If the component has no sub-elements or mutations, this mixin can be omitted.

Its purpose is to let the component know what sub-elements it contains and which elements are bound to which mutations.

In addition, it serves for doublechecking the code written below and also for a quick overview of what the component supports/implements.

+component('button')
+register
+element('.icon')
+element('img')
+element('.text')

display: inline-block
margin: 1em
font-size: 1.25em

+____________________________
+element('.icon')
...

The mixins used in the code above: +component, +register, +element.

As you can see in example above, the CSS code of the root element of the component follows right after the register section.

In some cases it could be omitted with +mode('draft') and you can find more about this topic in the documentation of the mixin +register.

Element structure in register

In the register section we have to use the same element mixins: +element and +pseudo-element as in the code later. You just need to describe the element structure of the whole component.

+component('button')
+register
+element('.icon')
+element('img')
+element('.text')
+pseudo-element('after')

// ... CSS CODE ..

+____________________________
+element('.icon')

...
<button>
<span class="icon">
<img src="{{ iconUrl }}" />
</span>
<span class="text">
{{ buttonName }}
</span>
</button>

The mixins used in the code above: +component, +register, +element, +pseudo-element.

The parser will yell at you with warning if you forget to register any element you will use later in component. Same when you register something and forget to implement later in code.

Note that you don't use the separators in the register section.

Mutations in register

After we have registered the element structure of the component, we can register the mutations of the component and its sub-elements.

+component('button')
+register
+variant('.primary')
+variant('.secondary')
+variant('.large')
+variant('.disabled')
+context('.modal-window')
+element('.icon')
+element('img')
+state(':hover')
+element('.text')
+pseudo-element('after')

...

There are 3 types of places where the mutations can be registered:

+component('button')
+register
[A section]
+element('.icon')
[B section]
+element('img')
[B section]
+element('.text')
[B section]
+pseudo-element('after')
[C section - NOT ALLOWED]
...
  • A - root element of the component (right after/inside the +register mixin) - if you register states/variants here, it's expected that root element of this component should match this selectors. There is an exception that this is the only place where can be the context-like mutations registered. That's because +context/+responsive are applied always to outside context of the component, so registering it to some sub-elements does not make sense.

  • B - sub-elements of the component (right after/inside the +element mixin) - if component has states and variants on inside sub-elements too, as we said in "A section" above, here can be registered states/variants only (not the context-like mutations).

  • C - inside the +pseudo-elements - it's not allowed because pseudo-element technicaly cannot have class name, id, or :hover state itself. Only some of his parent elements (in most cases his direct parent element) has these states/variants.

Only registered mutations can be used later in code (with exception of mode draft).

Very important to understand:

+component('button')
+register
+variant('.primary')
+element('.icon')
+element('img')

+default
// code

+variant('.primary')
// code

+____________________
+element('.icon')
// code

+____________________
+element('img')
+default
// code

+variant('.primary')
// code

...






button {
/* code */
}
button.primary {
/* code */
}

button .icon {
/* code */
}

button img {
/* code */
}

button.primary img {
/* code */
}

Notice in example above that variant .primary is registered only in root element of the component, because now the framework knows where should be the class expected, but now you can use variant .primary in any sub-element of the component. You are just saying in each sub-element individualy if (and if yes how) the particular element looks in variant .primary which means when root element has this class.

So do not do this:

+component('button')
+register
+variant('.primary')
+element('.icon')
+element('img')
+variant('.primary')

+default
// code

+variant('.primary')
// code

+____________________
+element('.icon')
// code

+____________________
+element('img')
+default
// code

+variant('.primary')
// code

...







button {
/* code */
}
button.primary {
/* code */
}

button .icon {
/* code */
}

button img {
/* code */
}

button img.primary {
/* code */
}

This is wrong if <img> doesn't have class .primary ever. Don't register variants on every sub-element where you want to style the difference, but you just register it on element where the mutation is defined in HTML.

Aliases in register

Every +element, +pseudo-element, or mutation can be registered with alias and this way it can have more semantic and readable name.

Alias has a simple rule - it must be enclosed in curly brackets: '{alias name}' and it can contain any characters except those curly bracket (inside). The second rule is - it must be unique alias in current component.

See following examples:

+component('{button}', 'button.btn')
+register
+state('{focused}', ':focus', '.focused')
+variant('{highlighted}', '.go01-flushed')
+pseudo-element('{caret}', 'after')

+default
// code

+state('{focused}')
// code

+__________________________
+pseudo-element('{caret}')
+default
// code

+variant('{highlighted}')
// code

...

You can see usage of the alias in the example above. You don't need to set alias for every sub-element or mutation. But there are cases when it is very useful:

  • when the class name (or any other selector) is not understable/semantic:
    • +variant('{highlighted}', '.go01-flushed')

  • when the class name (or any other selector) is too long:
    • +state('{faded}', '.outdated.inactive:not(.checked):last-child')

  • when more selectors have the same visual result:
    • +state('{focused}', ':focus', '.focused')

  • when sub-element have not clear meaning:
    • +element('{badge}', '> span')

  • when pseudo-element have not clear meaning:
    • +pseudo-element('{horizontal separator}', 'before')

  • when component have too long selector or with not clear meaning:
    • +component('{button}', 'button.btn')
      (this usecase is useful mainly in our special SPOT Editor)

The result should be more clear and readable code.

Exclusive mutations in register

The register part can be used in even smarter way. You can group exclusive mutations together.

Exclusive mutations are those which it makes no sense to combine them. For example:

+component('{button}', 'button.btn')
+register
+state(':hover')
+state(':focus')
+variant('.primary')
+variant('.secondary')
+variant('.tercialy')
+variant('.small')
+variant('.medium')
+variant('.large')
+context('.modal-widow')

...

The better way to write this is:

+component('{button}', 'button.btn')
+register
+state(':hover')
+state(':focus')
+variant('{priority}', '.primary', '.secondary', '.tercialy')
+variant('{size}', '.small', '.medium', '.large')
+context('.modal-widow')

...

The result is the same - you can use all variants later the same way, but the register block is much cleaner this way.

Another option to register more exclusive variants - via placeholder ?:

+component('{button}', 'button.btn')
+register
+variant('[type="?"]', 'text', 'number', 'password')
+variant('.size-?', 'tiny', 'small', 'medium', 'large')
...

You can use a placeholder char ? in first param and then list all possible values. But later in code, you have to use the whole form, e.g.:

  ...

+default
// code

+variant('[type="number"]')
// code

+variant('.size-large')
// code

...

Mode

Mode is a way to quickly write code as a draft without following strict rules and later finish it properly.

Mode has to be set before (each) component because it's applied only for the following component.

Default mode is strict and it doesn't need to be declared (because it's default).

+mode('draft')

+component('button')

...

If you set a mode draft then:

Warning: many important functions cannot be used without the register section, so using the draft mode has its limitations.

We also use the mode draft as the default mode in many examples on this site for having those code examples more simple.

Link to the documentation but there is nothing more about it: +mode.

Specificity

In SPOT CSS system is problem of selector specificity reduced only for single element. That's because all selectors for any element are in single block and nothing else is addressing this element (if you follow the SPOT principle). So specificity must be resolved only for a few element selectors, as you can see in example below.

See the example below:

  ...

+____________________________
+element('.icon')
+default
display: inline-block
color: #3432A1

+state(':hover')
vertical-align: middle
margin: 0.2em

+state('.pressed')
width: 100%
height: auto

+variant('.primary')
+default
font-size: 1.2em
display: inline-block

+state(':hover')
vertical-align: middle
margin: 0.2em

+____________________________
...

Challenge: What if we need to have +state('.pressed') stronger than +variant('.primary')+state(':hover')? Moving it in the code at the end will not help. Selector .primary:hover will be always stronger than just .primary. For this case we have the following options to help us:

+add-element-specificity
+add-class-specificity
+add-id-specificity
+add-specificity
+override
+override-mutation

The solution of the problem above could be for example:

  ...

+____________________________
+element('.icon')
+default
display: inline-block
color: #3432A1

+state(':hover')
vertical-align: middle
margin: 0.2em

+state('.pressed')
+override-mutation('.primary:hover')
width: 100%
height: auto

+variant('.primary')
+default
font-size: 1.2em
display: inline-block

+state(':hover')
vertical-align: middle
margin: 0.2em

+____________________________
...

The mixins used in the code above: +element, +default, +state, +override-mutation.

Only-for

There is no need to register tree-structural pseudo-classes. Just use the mixin +only-for.

+______________________________
+element('p')
+default
margin: 10px 0

+only-for(':first-child')
margin: 0


p {
margin: 10px 0;
}

p:first-child {
margin: 0
}

Or you can use the mixin +only-for for reusing the generic code.

Example of grouped mutations:

+register
+variant('.lorem')
+variant('.ipsum')

...

+variant('.lorem', '.ipsum')
+default
overflow: hidden
margin: 1em

+only-for('.lorem')
display: block

+only-for('.ipsum')
display: flex







.lorem, .ipsum {
overflow: hidden;
margin: 1em;
}

.lorem {
display: block;
}
.ipsum {
display: flex;
}


Example of exclusive mutations:

+register
+state('{collapsible}', '.expanded', '.collapsed')

...

+state('{collapsible}')
+default
overflow: hidden
margin: 1em

+only-for('.expanded')
display: block

+only-for('.collapsed')
display: flex






.expanded, .collapsed {
overflow: hidden;
margin: 1em;
}

.expanded {
display: block;
}
.collapsed {
display: flex;
}


Example of elements:

+____________________________________
+element('th', 'td')
+default
overflow: hidden
margin: 1em

+only-for('th')
color: white

+only-for('td')
color: grey


th, td {
overflow: hidden;
margin: 1em;
}

th {
color: white;
}
td {
color: grey;
}


Example of pseudo-elements:

+____________________________________
+pseudo-element('before', 'after')
+default
overflow: hidden
margin: 1em

+only-for('::before')
color: white

+only-for('::after')
color: grey


my-button::before, my-button::after {
overflow: hidden;
margin: 1em;
}

my-button::before {
color: white;
}
my-button::after {
color: grey;
}

You can find more about this topic in the documentation of the mixin +only-for.

These were the basics...

We have shown the basics and a quick overview of what is enough in most cases.

If you want to look deeper, it's time to check out the full documentation.