Primary Design System

Overview Code

Color

Key elements and active state

Short body text and titles

Long body text, neutral and stable elements

Disabled text

Subtitles, placeholders and disabled state

Highest importance elements (danger, error, important word...)

Warning

Valid

For the concern of contrast, text can be turned to white as long as the background keep the suitable color.

Color

Import Library

          
@import 'utils/variables';
          
        

Variables

Key elements and active state

$c-key

Short body text and titles

$c-title

Long body text, neutral and stable elements

$c-neutral

Disabled text

$c-disabled

Subtitles, placeholders and disabled state

$c-secondary

Highest importance elements (danger, error, important word...)

$c-danger

Warning

$c-warning

Valid

$c-valid

Grid

7 columns grid

Cell

Height = 1.5em (constant)

Width = (Screen width - 10% screen width) / 7
   = 90vw/7

More than 12 columns is unescessary. Avoid using even number of columns, it makes the composition flat and static.

Example

24px

Other areas can be added, such as side bar, nav bar… But the main area should always be split in 7 columns.

Spacing

Use extra spacing as margin or padding for text elements and icons. For larger vertical spacing, use font-size values.

Simple extra spacing

0.313 x 0.313em

Double extra spacing

0.625 x 0.625em

Triple extra spacing

0.938 x 0.938em

Quadruple extra spacing

1.250 x 1.250em

Quintuple extra spacing

1.563 x 1.563em

Example

See more

Subtitle

Lorem ipsum dolor sit amet, adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet.

Elements with few space between each other tend to be grouped together.

Grid

Import Library

          
@import 'components/grid';
          
        

Classes

For a basic split in 7 grid, put this class on your grid container :

.grid-container

For a laid out grid, use these classes :

Establishes a laid out grid

.grid-container--areas

Establishes a zone in the nav bar area

.grid-area--nav

Establishes a zone in the side bar area

.grid-area--side

Establishes a zone in the side bar area

.grid-area--side

Establishes a zone rigth after the nav bar, with the same dimensions minus the side bar.

.grid-area--switcher

Establishes a zone with the space remaining.

.grid-area--main

Iconography

16x16px

chevron_up
chevron_down
arrow_left
arrow_right
arrow_up
arrow_down
cross
plus
edit
bin
copy
info
cart
warning
download
upload

Typography

Font

Soleil

Wolfgang Homola

Hierarchy

Line height

Font size

Header

Standard

1,25

121px

7.5625em

Display

1,25

81px

5.0625em

Title 1

1,5

54px

3.375em

Title 2

1,5

36px

2.25em

Subtitle

1,5

24px

1.5em

Lorem ipsum dolor sit amet, adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet

1,5

16px

1em

Lorem ipsum dolor sit amet, adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet

1,5

14px

.875em

Lorem ipsum dolor sit amet, adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet

Typography

Import Library

          
@import 'utils/variables';
          
        

Variables

Display

$size-text-peta

Title1

$size-text-tera

Title2

$size-text-giga

Subtitle

$size-text-mega

Description

$size-text-kilo

Body

$size-text

Small text

$size-text-micro

Voice and tone

Voice

Primary voice is short and to the point. It uses a basic but precise vocabulary. It can use deadpan humor sparingly and in proper context.

Tone

Primary Design System is meant to be used in an online shop context. Knowing this, the tone need to deal with three specific situations :

Products presentation :

Be conversationnal, use humor, convince the user, overuse adjectives.

Technical specification :

Avoid sentences, instead use keywords. Be objective and brief.

Payment form :

Be the clearest possible, use precise expressions. Don’t be to short but don’t lose your user into unnecessary long description. Avoid humor.

Buttons and links

Text in button should be left-aligned. If words are not enough, put an icon on the right of the text.

Call To Action

Use it for actions that require most attention from the user.

70px width min.
Buy Buy Disabled

Secondary button

Use it for secondary actions.

70px width min.
More info More info Disabled

Ghost button

Use it for actions that require less attention than anything else on the page. Usually paired with the cta.

70px width min.
Cancel Cancel Disabled

Danger button

Use it for actions that can cause damage to the user’s data.

70px width min.
Delete Delete Disabled

Warning button

Use it when something requires the user's attention before clicking the button.

70px width min.
Continue Continue Disabled

Link

I'm a link

Buttons and links

Import Library

          
@import 'components/button';
          
        

Classes

Button

.btn

Cta button

.btn .btn--cta

Secondary button

.btn .btn--secondary

Ghost button

.btn .btn--ghost

Danger button

.btn .btn--cta .btn--danger

Warning button

.btn .btn--cta .btn--warning

Disabled button

.btn .btn--disabled

Disabled border button

.btn .btn-brdr--disabled

Disabled filled button

.btn .btn-bg--disabled

Form

Ideally, the form is contained in the screen height with no scroll. If the content can’t fit into the screen, make it in several pages and add a progress bar on top.

Normal field

Submit

Mobile number field

+
    Submit

    Card number field

    Submit

    Compulsory field

    The form will not be submitted if the field is not completed.

    Your name is required.

    Submit

    Formatted field

    The form will not be submitted if the field is not correct.

    Invalid syntax. Email address should contain a single @ and at least one dot.

    Submit

    Formatted field with an error margin

    The form will warn the user that the field seems incorrect before being submitted but will not prevent the submission.

    This serial number seems incorrect.

    Submit

    Form

    Import Library

              
    @import 'components/form';
              
            

    Classes

    Form

    .form

    Input

    .field .field--up

    Label

    .placeholder

    Digit only input

    .field--number

    Phone code input

    .field--area-code

    Chevron for suggestions

    .field--chev

    Phone code suggestions

    .field--num__area

    Phone number input

    .field--num

    Card number input

    .field--card

    Required input

    .required

    Error message

    .error-message .disappear

    Warning message

    .warning-message .disappear

    Formatted input with NO error margin

    .formatted .error

    Formatted input with error margin

    .formatted .warning

    Submit button

    .btn-submit

    Javascript

    Add this block to your code to make your inputs work.

              
    const fields = document.querySelectorAll(".field");
    
    fields.forEach(el => {
      el.addEventListener("input", e => {
         if (el.value.length != 0 && el.parentElement.querySelector(".placeholder")) {
          el.parentElement.querySelector(".placeholder").classList.add("placeholder--up");
         } else if (el.parentElement.querySelector(".placeholder")) {
          el.parentElement.querySelector(".placeholder").classList.remove("placeholder--up");
         }
      });
    });
              
            

    If your website contains digit only inputs, add this block to your code :

              
    const fieldNumber = document.querySelectorAll(".field--number");
    
    fieldNumber.forEach(el => {
      el.addEventListener("input", e => {
         el.checkValidity();
      });
      el.addEventListener("invalid", e => {
        setTimeout(function(){
          el.value = el.value.substring(0, el.value.length - 1);
        }, 100);
      });
    });
              
            

    If your website contains phone number inputs, add this block to your code :

              
    const fieldNumArea = document.querySelector(".field--num__area");
    let countriesData = "https://restcountries.eu/rest/v2/region/europe?fields=name;callingCodes;flag;demonym";
    const fieldCodeArea = document.querySelector(".field--area-code");
    const fieldNum = document.querySelector(".field--num");
    let fieldNumAreaLi = [];
    let fieldNumAreaCode = [];
    
    let countries = [];
    let countriesArea = [];
    let flag = [];
    let demonym = [];
    
    fetch(countriesData)
    .then(response => {
      return response.json();
    })
    .then((json) => {
      for (var i = 0; i < json.length; i++) {
        countries.push(json[i].name);
        flag.push(json[i].flag);
        countriesArea.push(json[i].callingCodes);
        demonym.push(json[i].demonym);
        let li = document.createElement("li");
        li.innerHTML = "<a class='field--num__area__link'><img class='field--num__area__flag' src='" + flag[i] + "' alt='" + demonym[i] + " flag.'>" + countries[i] + "<span class='field--num__area__code'>+" + countriesArea[i] + "</span></a>";
        li.classList.add("field--num__area__li");
        fieldNumArea.appendChild(li);
        fieldNumAreaLi.push(li);
        fieldNumAreaCode.push(li.children[0].children[1]);
      }
      for (var i = 0; i < fieldNumAreaLi.length; i++) {
        fieldNumAreaLi[i].addEventListener("click", e => {
          let code = e.currentTarget.children[0].children[1].innerHTML;
          fieldCodeArea.value = code.substring(1, code.length);
          fieldNum.focus();
        });
      }
    })
    .catch((error) => {
        console.error("Ça fonctionne pas.");
        console.log(error);
        errorMessage.classList.remove("disappear");
    });
    
    const fieldChevron = document.querySelector(".field--chev");
    
    fieldCodeArea.addEventListener("click", e => {
      fieldChevron.classList.add("field--chev--open");
      fieldNumArea.classList.add("appear");
    });
    
    fieldChevron.addEventListener("click", e => {
      fieldChevron.classList.toggle("field--chev--open");
      fieldNumArea.classList.toggle("appear");
      if (fieldChevron.classList.value === "field--chev field--chev--open") {
        fieldCodeArea.focus();
      } else {
        fieldCodeArea.blur();
        fieldNum.focus();
      }
    });
    
    fieldCodeArea.addEventListener("blur", e => {
      document.body.addEventListener('mouseup', e => {
        fieldChevron.classList.remove("field--chev--open");
        fieldNumArea.classList.remove("appear"); 
      });
    });
    
    fieldCodeArea.addEventListener("input", e => {
      for (var i = 0; i < fieldNumAreaCode.length; i++) {
        if (fieldNumAreaCode[i].innerHTML.indexOf(fieldCodeArea.value) === -1) {
          fieldNumAreaLi[i].classList.add("disappear");
        } else {
          fieldNumAreaLi[i].classList.remove("disappear");
        }
      }
    });
              
            

    Credits : this block uses https://restcountries.eu API.

    If your website contains card number inputs, add this block to your code :

              
    const fieldCard = document.querySelectorAll(".field--card");
    
    fieldCard.forEach(el => {
      el.addEventListener("input", e => {
        if (el !== fieldCard[fieldCard.length - 1]) {
          if (el.value.length === 4) {
            el.nextElementSibling.focus();
          }
        } 
        el.addEventListener("keydown", e => {
          if (el !== fieldCard[0]) {
            if (el.value.length === 0) {
              if (e.which === 8) {
                el.previousElementSibling.focus();
              }
            }
          }
        });
      });
    });
              
            

    If your website contains formatted inputs with NO error margin, add this block to your code :

              
    const errorField = document.querySelectorAll(".error");
    
    errorField.forEach(el => {
      el.addEventListener("input", e => {
         el.checkValidity();
         if (el.checkValidity() === false) {
            el.parentElement.lastElementChild.classList.add("btn--disabled", "btn-bg--disabled");
            el.classList.add("invalid");
            el.parentElement.querySelector(".error-message").classList.remove("disappear");
          } else if (el.checkValidity() === true) {
            el.parentElement.lastElementChild.classList.remove("btn--disabled", "btn-bg--disabled");
            el.classList.remove("invalid");
            el.parentElement.querySelector(".error-message").classList.add("disappear");
          }
      });
    });
              
            

    If your website contains formatted inputs with error margin, add this block to your code :

              
    const warningField = document.querySelectorAll(".warning");
    
    warningField.forEach(el => {
      el.addEventListener("input", e => {
        if (el.value.length != 0) {
          el.addEventListener("blur", e => {
            el.checkValidity();
           if (el.checkValidity() === false) {
              el.parentElement.lastElementChild.classList.add("btn--warning");
              el.classList.add("unexpected");
              el.parentElement.querySelector(".warning-message").classList.remove("disappear");
              el.parentElement.lastElementChild.innerHTML = "Submit anyway";
            } else if (el.checkValidity() === true) {
              el.parentElement.lastElementChild.classList.remove("btn--warning");
              el.classList.remove("unexpected");
              el.parentElement.querySelector(".warning-message").classList.add("disappear");
              el.parentElement.lastElementChild.innerHTML = "Submit";
            }
          });
        }
      });
    });
              
            

    Progress bar

    Display steps from left to right.

    Progress bar

    Progress bar with steps

    Space between notches should be proportionnal to the estimated time to complete the step task.

      Progress bar

      Import Library

                
        @import 'components/progress';
                
              

      Classes

      Progress bar

      .progress-bar

      Progress bar notched

      .progress-bar--notched

      Progress bar base

      .progress-bar__base

      Progress bar flow

      .progress-bar__flow

      Notches

      .progress-bar__notches

      Javascript

      Enter the estimated time for each step in second inside the "steps" array.

      In this case, there are 5 steps each with a duration of 180, 300, 120, 600 and 360 seconds.

                
        let steps = [180, 300, 120, 600, 360];
                
              

      Then add this block of code to make your progress bar work.

                
        const progressBar = document.querySelectorAll(".progress-bar");
        const progressBarNotched = document.querySelector(".progress-bar--notched");
        const progressBarNotches = document.querySelector(".progress-bar__notches");
      
        let duration = 0;
      
        for (var i = 0; i < steps.length; i++) {
          duration = duration + steps[i];
        }
      
        let portion = [];
      
        steps.forEach(step => {
          portion.push(duration/step);
        });
      
        let distance = [];
        let add = 0;
      
        let currentStep = 3;
        let progressBarFlowWidth = 0;
      
        portion.forEach(port => {
          distance.push(document.body.clientWidth/2/port);
        });
      
        progressBar.forEach(bar => {
          let progressBarFlow = bar.querySelector(".progress-bar__flow");
          if (bar.classList.value.indexOf("progress-bar--notched") != -1) {
            progressBarFlowWidth = 0;
            distance.forEach(dist => {
              let li = document.createElement("li");
              li.classList.add("progress-bar__notches__el");
              li.style.marginLeft = dist - 15 + "px";
              progressBarNotches.appendChild(li);
            });
            const notches = document.querySelectorAll(".progress-bar__notches__el");
      
            for (var i = 0; i < currentStep; i++) {
              notches[i].classList.add("progress-bar__notches__el--active");
              progressBarFlowWidth = progressBarFlowWidth + distance[i];
            }
            progressBarFlow.style.width = progressBarFlowWidth + "px";
          } else {
            progressBarFlowWidth = 0;
            for (var i = 0; i < currentStep; i++) {
              progressBarFlowWidth = progressBarFlowWidth + distance[i];
            }
            progressBarFlow.style.width = progressBarFlowWidth + "px";
          }
        });
                
              

      To make sure your progress bar will stay in place on window resize, add this:

                
        window.onresize = function () {
          distance = [];
          portion.forEach(port => {
            distance.push(document.body.clientWidth/2/port);
          });
      
          progressBar.forEach(bar => {
            let progressBarFlow = bar.querySelector(".progress-bar__flow");
            if (bar.classList.value.indexOf("progress-bar--notched") != -1) {
              progressBarFlowWidth = 0;
              const notches = document.querySelectorAll(".progress-bar__notches__el");
              for (var i = 0; i < notches.length; i++) {
                notches[i].style.marginLeft = distance[i] - 15 + "px";
              }
              for (var j = 0; j < currentStep; j++) {
                progressBarFlowWidth = progressBarFlowWidth + distance[j];
              }
              progressBarFlow.style.width = progressBarFlowWidth + "px";
            } else {
              progressBarFlowWidth = 0;
              for (var i = 0; i < currentStep; i++) {
                progressBarFlowWidth = progressBarFlowWidth + distance[i];
              }
              progressBarFlow.style.width = progressBarFlowWidth + "px";
            }
          });
        }
                
              

      Tag

      Tags are #096AD8 and have a 0.625em padding.

      Text inside tags have no uppercase.

      Basic tag

      new

      Tag with icon

      new

      Tag

      Import Library

                
      @import 'components/tag';
                
              

      Classes

      Tag

      .tag

      Tag icon

      .tag__icon