JSFiddle - React, Tailwind, and code Playground

by Maksim Kachurin

HTML

<div class="container">

    <div class="pagination" data-visible="5">
        <ul>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
        </ul>
    </div>

    <div class="buttons">
        <button data-direction="start">Start</button>
        <button data-direction="prev">Prev</button>
        <button data-direction="next">Next</button>      
        <button data-direction="end">End</button>
    </div>
</div>

SCSS

.container {
    display: flex;
    justify-content: center;
    flex-flow: row wrap;
}

.pagination {
    width: 100%;
}

ul {
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 20px 0;
}

li {
    display: block;
    text-align: center;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: #1b1c1c;
    margin: 0 4px;
    cursor: default;
    pointer-events: all;
    transition: all .2s;
    font-size: 0;
    opacity: 0;
}

li[data-state=normal] {
    cursor: pointer;
    pointer-events: all;
    font-size: 9px;
    opacity: .6;
}

li[data-state=active] {
    opacity: 1;
}

li[data-state=more] {
    width: 8px;
    height: 8px;
    opacity: .3;
}

li[data-state=next] {
    width: 12px;
    height: 12px;
    opacity: .3;
    pointer-events: all;
}

li[data-state=hidden] {
    width: 0;
    height: 0;
    margin: 0;
    opacity: 0;
}

/* Compensate empty space*/
li[data-state=normal]:first-child,
li[data-state=active]:first-child {
    margin-left: 40px;
}

li[data-state=next]:first-child {
    margin-left: 20px;
}

li[data-state=normal]:last-child,
li[data-state=active]:last-child {
    margin-right: 40px;
}

li[data-state=next]:last-child {
    margin-right: 20px;
}

JavaScript

class BulletsPagination {
    constructor(options) {
    	this.options = Object.assign({
        	pagination: '.pagination',
            targets: 'li',
            visible: 3,
            active: 0
        }, options);
	
		this.active     = this.options.active;
		this.pagination = this.options.pagination;
        
        // Pagination El not valid Element
        if (typeof this.pagination !== 'object' || !this.pagination.querySelectorAll) {
        	return false;
        }
        
        if (Array.isArray(this.options.targets) || (this.options.targets instanceof NodeList) || (this.options.targets instanceof HTMLCollection)) {
        	this.targets = [...this.options.targets];
        }
        else {
        	this.targets = [...this.pagination.querySelectorAll(this.options.targets)];
        }
        
        this.setPagination(this.active, true);
    }
    
    static isNormal(state) {
   		return ['normal', 'active'].includes(state);
    }

    static getMove({ from, to, map, move = 0 }) {
        // let move = (to - from);
        const normal = this.isNormal(map[to - move].state);

        if (normal) {
            return move;
        }

        // move forward
        if (from < to) {
            return this.getMove({ from, to, map, move: (move + 1) });
        }
        // move backward
        else if (to < from) {
            return this.getMove({ from, to, map, move: (move - 1) });
        }
    }
    
    setPagination(index, force) {
    	let { targets, options, constructor, newMap, oldMap, active } = this;
        
        // Normalize new active index
        index = Math.max(Math.min(index, targets.length - 1), 0);

        let first;
        let last;

        // Fill previuos state
        oldMap = (oldMap && oldMap.length) ? oldMap : targets.map((target) => {
            const state = target.getAttribute('data-state') || 'hidden';

            return {
                target,
                state: state
            };
        });

       ...