<link href='https://fonts.googleapis.com/css?family=Roboto:100' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:600' rel='stylesheet' type='text/css'>
<!-- Place this tag in your head or just before your close body tag. -->

<h1 class="header">
  Hold left mouse button to show wheel chat
</h1>


<script type='text/javascript'>
  window.onload = function () {
    chat.init();

    new WheelMenu('html', {
        size: 170,
        classes: '-dota-',
        pointerOffset: 8,
        borderWidth: 11,
        pointerSize: 80,
        items: [
            'Push',
            'Well played',
            'Missing',
            'Care',
            'Get Back',
            'Need Wards',
            'Stun',
            'Help'
        ],
        onChange: function (value) {
            chat.write('Tusk', value)
        }
    });
};

</script>
/* 
  See full code at https://github.com/t1m0n/wheel-menu 
  Best viewed on full screen.

  Tested in Chrome and Firefox
*/

/* -------------------------------------------------
    Page general styles
   ------------------------------------------------- */

.header {
  color: #fff;
  text-align: center;
  font-family: 'Roboto', sans-serif;
  font-weight: 100;
  font-size: 32px;
  padding-top: 42px;
}
body,
html { height: 100%; width: 100%; }

html {
  background: url(https://cdn.rawgit.com/t1m0n/wheel-menu/fdf1b6f7/img/dota-bg.jpg) center/cover  no-repeat;
  overflow: hidden;
}


$duration: 350ms;

/* -------------------------------------------------
    Wheel menu general
   ------------------------------------------------- */

.-wheel-menu-visible- {
  cursor: none;
  user-select: none;
}

.wheel-menu-container {
  left: -9999px;
  top: 0;
  position: absolute;

  transition: left 0s $duration;

  &.active {
    left: 0;

    transition-delay: 0s
  }
}

/* -------------------------------------------------
    Wheel menu ring
   ------------------------------------------------- */
$radius: 100px;

.wheel-menu--ring {
  box-sizing: border-box;
  opacity: 0;
  width: $radius;
  height: $radius;
  border-radius: 50%;
  border: 1px gold solid;
  position: absolute;

  transform: scale(.1);
  transition: top 0s $duration, left 0s $duration, opacity $duration .15s, transform $duration .15s;

  &.active {
    opacity: .6;
    transform: scale(1);
    transition: top 0s 0s, left 0s 0s, opacity $duration 0s, transform $duration 0s;

    .-item-activated- & {
      opacity: 1;

    }
  }
}

.-wheel-menu-moving- {
  .wheel-menu--ring  {
    transition: opacity .3s;
  }
}

/* -------------------------------------------------
    Wheel menu cursor
   ------------------------------------------------- */

$pointerDuration: .3s;

.wheel-menu--pointer{
  background: #f4777f;
  width: 8px;
  height: 2px;
  position: absolute;
  left: -1000px;
  top: -1000px;
  
  opacity: 0;
  
  transition: opacity $pointerDuration, transform $pointerDuration;

  &.active {
    opacity: 1;
  }
}

.-wheel-menu-moving- {
  .wheel-menu--pointer {
    transition: opacity $pointerDuration;
  }
}

/* -------------------------------------------------
    Menu cursor
   ------------------------------------------------- */

.wheel-menu--cursor {
  border: 1px solid #ff9ca6;
  border-radius: 50%;
  position: absolute;
  width: 16px;
  height: 16px;
  box-sizing: border-box;

  opacity: 0;
  transform: scale(.4);
  transition: transform $duration 0s, opacity $duration 0s;

  &.active {
    opacity: .6;
    transform: scale(1);
    transition: transform $duration .1s, opacity $duration .1s;
  }
}

.active.-item-activated- .wheel-menu--cursor {
  opacity: 1;
  transition: transform $duration 0s, opacity $duration 0s;
}

/* -------------------------------------------------
    Wheel menu item
   ------------------------------------------------- */

.wheel-menu--items {
  position: absolute;

  transform: scale(.3);
  transition: transform $duration .0s;

  &.active {
    transform: scale(1);
    transition: transform $duration .1s;
  }

  z-index: 100;
}

.wheel-menu--item {
  position: absolute;
  color: #333;
  font-family: Tahoma, sans-serif;
  font-size: 14px;
  text-align: center;
  opacity: 0;
  border-radius: 2px;
  padding: 4px 8px;
  background: rgba(0, 0, 0, 0.10);
  white-space: nowrap;

  transform: scale(.1);
  transition: transform $duration, opacity $duration;

  .active & {
    opacity: .7;

    transition: transform $duration, opacity $duration;
    transform: scale(1);

    @for $i from 1 through 8 {
      &:nth-child(#{$i}n) {
        transition-delay: 40ms * $i;
      }
    }

    .-wheel-menu-moving- & {
      transition-delay: 0s;
    }
  }

  //  Active state
  // -------------------------------------------------

  &.active {
    background: #ff9ca6;
  }
}


/* -------------------------------------------------
    Dota styles
   ------------------------------------------------- */

.-dota- {

//  Ring
// -------------------------------------------------

.wheel-menu--ring {
  border: none;

  &:after {
    content: '';
    background: url(https://cdn.rawgit.com/t1m0n/wheel-menu/fdf1b6f7/img/wheel.png) left top no-repeat;
    width: 208px;
    height: 208px;
    position: absolute;
    left: -33px;
    top: -18px;
  }
}

//  Pointer
// -------------------------------------------------

.wheel-menu--pointer {
  border: none;
  background: url(https://cdn.rawgit.com/t1m0n/wheel-menu/fdf1b6f7/img/pointer.png) left top no-repeat;
  width: 33px;
  height: 66px;
  position: absolute;
}

//  Cursor
// -------------------------------------------------
   
.wheel-menu--cursor {
  border: none;
  background: url(https://cdn.rawgit.com/t1m0n/wheel-menu/fdf1b6f7/img/cursor.png) left top no-repeat;
  width: 60px;
  height: 63px;
}


//  Item
// -------------------------------------------------

.wheel-menu--item {
  background: none;
  font-family: 'Source Sans Pro', sans-serif;
  font-size: 24px;
  color: #fff;
  text-shadow: 0 1px 4px #000;
  line-height: 1;
  padding: 0;

  &.active {
    background: none;
    font-size: 36px;
  }
}

.-wheel-menu-moving- & {
  .wheel-menu--item {
    &.active {
     opacity: 1;
     transition: none;
    }
  }
}
}


/* -------------------------------------------------
    Chat
   ------------------------------------------------- */

.chat {
  position: fixed;
  bottom: 80px;
  text-align: center;
  left: 0;
  right: 0;
}

.chat--messages {
  display: inline-block;
  text-align: left;
  width: 370px;
}

//  Message
// -------------------------------------------------

.chat-msg {
  font-family: 'Source Sans Pro', sans-serif;
  color: #fff;
  font-weight: bold;
  text-shadow: 0 1px 1px #000;
  font-size: 16px;
  opacity: 0;

  transition: all .5s;
  
  * {
    display: inline-block;
  }

  &.active {
    opacity: 1;
  }
}

.chat-msg--image {
  margin-right: 15px;
  width: 52px;
  vertical-align: middle;

}

.chat-msg--name {
  color: #eb67a1;
}

.chat-msg--to-whom {
  margin-right: 6px;
}

.chat-msg--text {
  padding-left: 28px;
  position: relative;

  &:before {
    content: '';
    background: url(https://cdn.rawgit.com/t1m0n/wheel-menu/fdf1b6f7/img/chat-pointer.png) center no-repeat;
    position: absolute;
    left: 7px;
    top: 2px;
    width: 18px;
    height: 21px;
    opacity: .8;
  }
}

.git-button {
  position: absolute;
  right:  32px;
  top:  50px;
}
View Compiled
;(function (window) {
    var doc = document,
        $body = doc.querySelector('body'),
        $html = doc.querySelector('html'),
        inited = false,
        DOMGenerated = false,
        idCounter = 1,
        idPrefix = 'wheel-menu-',
        transformProp = '',
        $el,
        $ring,
        $cursor,
        $pointer,

    // Default params
        defaults = {
            size: 100,
            classes: '',
            borderWidth: 20, // Need for correct cursor positioning inside the ring
            inActiveRadius: 20,
            pointerOffset: 10,
            pointerFixed: true,
            pointerSize: 50,
            rotateRing: true, // If ring must be rotated according to active item or not
            transitionDuration: 400,

            // On change callback. Called when mouseup event is triggered,
            // and if active item exists. It receives item array element as parameter.
            onChange: '',

            // Item element can be either string or object.
            // Object must contain 'content' field, it will be used as item's html.
            items: [
                'Hello',
                'Set your',
                'Menu items',
                'Here'
            ]
        };

    function getTransform () {
        var styles = getComputedStyle(document.documentElement),
            names = ['transform', 'msTransform', 'mozTransform', 'webkitTransform'];

        names.forEach(function (name) {
            if (transformProp) return;
            if (name in styles) transformProp = name;
        })
    }

    getTransform();

    window.WheelMenu = function (el, params) {
        this.inited = false;
        this.el = doc.querySelector(el);

        this.opts = extend({}, defaults, params);

        if (!DOMGenerated) {
            this.createDOM();
        }

        this.init();
    };

    WheelMenu.prototype = {
        init: function () {
            this.inited = true;

            this.cache = [];
            this.cacheInited = false;
            this.currentActive = '';
            this.removeClassTimeout = '';

            this.createItemsDOM();
            this._saveCursorDimensions();

            this.el.addEventListener('mousedown', this.onMouseDown.bind(this));
            $html.addEventListener('mouseup', this.onMouseUp.bind(this));
            $html.addEventListener('mousemove', this.onMouseMove.bind(this));
        },

        /**
         * Show menu
         */
        show: function () {
            this.visible = true;

            if (this.removeClassTimeout) {
                clearTimeout(this.removeClassTimeout);
            }

            if (this.opts.classes) {
                $el.classList.add(this.opts.classes);
                this._saveCursorDimensions();
            }

            this.disable(); // Disable previous active item if exist

            this.$itemsConteiner.style.left = this.currentX - this.opts.size/2 + 'px';
            this.$itemsConteiner.style.top = this.currentY - this.opts.size/2 + 'px';

            this.setMenuItemsPosition();
            $el.classList.add('active');
            $ring.classList.add('active');
            $cursor.classList.add('active');
            this.$itemsConteiner.classList.add('active');
            $html.classList.add('-wheel-menu-visible-');


            this.setCursorPosition();
            this.setRingRotation(this.getVector(this.cache[0].x, this.cache[0].y, true));

            this.setPiePosition();
        },

        /**
         * Hide menu
         */
        hide: function () {
            var _this = this;

            this.visible = false;

            $el.classList.remove('active');
            $ring.classList.remove('active');
            $cursor.classList.remove('active');
            $pointer.classList.remove('active');
            this.$itemsConteiner.classList.remove('active');
            $html.classList.remove('-wheel-menu-visible-','-wheel-menu-moving-');

            if (this.opts.classes) {
                this.removeClassTimeout = setTimeout(function () {
                    $el.classList.remove(_this.opts.classes);
                }, this.opts.transitionDuration)
            }

            $ring.style.transform = '';

            $ring.style.top = 0 + 'px';
            $ring.style.left = 0 + 'px';

            // Reset item cache
            this.cache = [];
            this.cacheInited = false;
        },

        /**
         * Activates received item.
         * @param {Object} item - Cached menu item from this.cache
         */
        activate: function (item) {
            if (this.currentActive && this.currentActive == item) return;

            if (this.currentActive) {
                this.disable(this.currentActive);
            }

            var vector = this.getVector(item.x, item.y, true);

            this.setPointerPosition(vector);
            item.item.classList.add('active');

            $pointer.classList.add('active');
            $el.classList.add('-item-activated-');
            this.setPointerPosition(vector);
            this.setRingRotation(vector);
            this.currentActive = item;

            // Refresh items position, because of style changes may happen
            this.setMenuItemsPosition();
        },


        /**
         * Disables received item.
         *  @param {Object} [item] - Cached menu item from this.cache
         */
        disable: function (item) {
            item = item ? item : this.currentActive;

            if (!item) return;

            $pointer.classList.remove('active');

            item.item.classList.remove('active');
            $el.classList.remove('-item-activated-');
            this.setMenuItemsPosition();
            this.currentActive = ''
        },

        /**
         * Sets circle position
         */
        setPiePosition: function () {
            var x = this.centerX - this.opts.size / 2,
                y = this.centerY - this.opts.size / 2;

            $ring.style.top = y + 'px';
            $ring.style.left = x + 'px';
        },

        /**
         * Loops through each menu item and sets its position.
         * Refreshes items cache array.
         */
        setMenuItemsPosition: function (pos) {
            var step = Math.PI*2 / this.opts.items.length,
                angle = Math.PI/2,
                opts = this.opts,
                range,
                _this = this,
                position;



            Array.prototype.forEach.call(this.$items, function ($item, i) {
                position = _this.getItemPosition($item, angle);

                if (!pos) {
                    $item.style.left = position.x + 'px';
                    $item.style.top = position.y + 'px';
                } else {
                    $item.style.left = position.fromX + 'px';
                    $item.style.top = position.fromY + 'px';
                }

                if (!_this.cacheInited) {
                    _this.cache.push({
                        item: $item,
                        correctedX: position.x,
                        correctedY: position.y,
                        x: position.originalX,
                        y: position.originalY,
                        fromX: position.fromX,
                        fromY: position.fromY,
                        range: position.range
                    });
                }

                angle -= step;
            });

            this.cacheInited = true;
        },

        getX: function (angle, size) {
            return Math.cos(angle) * (size || this.opts.size + this.opts.pointerSize)/2 + this.opts.size/2
        },

        getY: function (angle, size) {
            return -Math.sin(angle) * (size || this.opts.size + this.opts.pointerSize)/2 + this.opts.size/2
        },

        /**
         * Computes correct 'x' and 'y' item position
         * @param {Object} item - DOM item object
         * @param {Number} angle - Angle at which item should be positioned
         * @returns {{x: *, y: *}}
         */
        getItemPosition: function (item, angle) {
            var width = item.offsetWidth,
                height = item.offsetHeight,
                opts = this.opts,
                degrees = angle * 180/Math.PI,
                range = this.getAngleRange(angle),
                x, y,
                fromX, fromY,
                originalX, originalY;

            x = originalX = this.getX(angle);
            y = originalY = this.getY(angle);
            fromX = this.getX(angle, this.opts.size / 4);
            fromY = this.getY(angle, this.opts.size / 4);

            // Correct x position
            switch (true) {
                case degrees == 90 || degrees == -90:
                    x = x - width/2;
                    fromX = fromX - width /2;
                    break;
                case  degrees <= -90 && degrees >= -270:
                    x = x - width;
                    fromX = fromX - width;
                    break;
                default:
                    break;
            }

            // Correct y position
            switch (true) {
                case degrees == 90:
                    y = y - height;
                    fromY = fromY - height;
                    break;
                case degrees == -90:
                    break;
                case degrees == 0 || degrees == -180:
                    y = y - height/2;
                    fromY = fromY - height/2;
                    break;
                default:
                    y = y - height/2;
                    fromY = fromY - height/2;
                    break;
            }

            return {
                x: x,
                y: y,
                originalX: originalX,
                originalY: originalY,
                fromX: fromX,
                fromY: fromY,
                range: range
            }
        },

        /**
         * Defines to what angle range item is belong to. Need for activating proper item
         * @param {Number} angle - Angle in radians to compute range from.
         * @returns {Array} - Range array [from, to] in degrees. 'from' can be larger then 'to'
         */
        getAngleRange: function (angle) {
            var range = [],
                opts = this.opts,
                halfStep = (Math.PI*2 / this.opts.items.length) / 2,
                from = angle - halfStep,
                to = angle + halfStep,
                fromX, fromY,
                toX, toY;

            fromX = this.getX(from);
            fromY = this.getY(from);

            toX = this.getX(to);
            toY = this.getY(to);

            range[0] = -Math.atan2(-(fromY - this.opts.size/2), -(fromX - this.opts.size/2)) * 180/Math.PI + 180;

            range[1] = -Math.atan2(-(toY - this.opts.size/2), -(toX - this.opts.size/2)) * 180/Math.PI + 180;

            return range;
        },

        /**
         * Defines and saves menu's center position
         * @param {Event} e - Mousedown event
         */
        defineCoordsCenter: function (e) {
            this.centerX = e.pageX;
            this.centerY = e.pageY;
        },

        /**
         * Creates base elements and appends them to the body.
         */
        createDOM: function () {
            DOMGenerated = true;

            var html = '' +
                '<div class="wheel-menu--ring"></div>' +
                '<div class="wheel-menu--pointer"></div>' +
                '<div class="wheel-menu--cursor"></div>';

            $el = doc.createElement('div');
            $el.classList.add('wheel-menu-container');

            $el.innerHTML = html;

            $ring = $el.querySelector('.wheel-menu--ring');
            $pointer = $el.querySelector('.wheel-menu--pointer');
            $cursor = $el.querySelector('.wheel-menu--cursor');

            $ring.style.width = this.opts.size + 'px';
            $ring.style.height = this.opts.size + 'px';

            $body.appendChild($el);
        },

        /**
         * Creates menu items html and appends it to the menu container
         */
        createItemsDOM: function () {
            var $itemsContainer = doc.createElement('div'),
                itemHtml,
                items = '';

            $itemsContainer.classList.add('wheel-menu--items');
            $itemsContainer.setAttribute('id', idPrefix + idCounter++);
            $itemsContainer.style.width = this.opts.size + 'px';
            $itemsContainer.style.height = this.opts.size + 'px';

            this.opts.items.forEach(function (item) {
                if (typeof item == 'string') {
                    itemHtml = item;
                } else {
                    itemHtml = item.content ? item.content : 'undefined'
                }
                items += '<span class="wheel-menu--item">' + itemHtml + '</span>';
            });

            $itemsContainer.innerHTML = items;

            $el.appendChild($itemsContainer);

            this.$itemsConteiner = $itemsContainer;
            this.$items = $itemsContainer.querySelectorAll('.wheel-menu--item');
        },

        saveCurrentMousePosition: function (event) {
            this.currentX = event.pageX;
            this.currentY = event.pageY;
        },

        defineVector: function () {
            this.vector = this.getVector(this.currentX, this.currentY);
        },

        getVector: function (x, y, isItem) {
            var _x = x - this.centerX,
                _y = y - this.centerY;

            if (isItem) {
                _x = x - this.opts.size/2;
                _y = y - this.opts.size/2;
            }


            var length = Math.sqrt(_x * _x + _y * _y);

            return {
                x: _x,
                y: _y,
                length: length
            };
        },

        /**
         * Calculates angle between vector point and circle center in degrees
         * Begins from 0 to 360
         * @param {Object} [vector] - Vector to calculate from. 'this.vector' by default.
         * @returns {number} - Angle in degrees
         */
        getCursorAngle: function (vector) {
            vector = vector ? vector : this.vector;

            return -Math.atan2(-vector.y, -vector.x) * 180/Math.PI + 180;
        },

        setPointerPosition: function (vector) {
            vector = vector ? vector : this.vector;

            var width = $pointer.offsetWidth,
                height = $pointer.offsetHeight;

            var x = vector.x / vector.length * (this.opts.size/2 + this.opts.pointerOffset) + this.centerX - width/2,
                y = vector.y / vector.length * (this.opts.size/2 + this.opts.pointerOffset) + this.centerY - height/2,
                angle = this.getCursorAngle(vector);

            $pointer.style.left = x + 'px';
            $pointer.style.top = y + 'px';
            $pointer.style[transformProp] = 'rotate(' + -angle.toFixed(1) + 'deg)'
        },

        setCursorPosition: function () {
            var x = this.currentX - this.cursorDims.width/2,
                y = this.currentY - this.cursorDims.height/ 2,
                dims = this.cursorDims,
                vector = this.vector;

            if (this.vector.length > this.opts.size/2 - this.opts.borderWidth - dims.width/2) {
                x = vector.x / vector.length * (this.opts.size/2 - this.opts.borderWidth - dims.width/2) + this.centerX - dims.width/2;
                y = vector.y / vector.length * (this.opts.size/2 - this.opts.borderWidth - dims.height/2) + this.centerY - dims.height/2;
            }

            this.cursorDims.x = x;
            this.cursorDims.y = y;

            $cursor.style.left = x + 'px';
            $cursor.style.top = y + 'px';
        },

        setRingRotation: function (vector) {
            var active = this.currentActive,
                angle;

            vector = vector || this.getVector(active.x, active.y);
            angle = this.getCursorAngle(vector);

            $ring.style[transformProp] = 'rotate(' + -angle.toFixed(1) + 'deg)';
        },

        /**
         * Detects intersection between mouse cursor (vector from center to mouse position)
         * and menu item. If detects, activates this item.
         */
        intersection: function () {
            var tan = this.vector.y / this.vector.x,
                _this = this,
                from, to,
                cursorDegree = -Math.atan2(-this.vector.y, -this.vector.x) * 180/Math.PI + 180;


            for (var i= 0, max = this.cache.length; i < max; i++) {
                var item = _this.cache[i];

                from = item.range[0];
                to = item.range[1];

                // If one of item's sides area is on the edge state. For example
                // when we have item which 'from' begins from 157 and ends to -157, when all
                // 'cursorDegree' values are appear hear. To not let this happen, we compare
                // 'from' and 'to' and reverse comparing operations.
                if (from > to) {
                    if (cursorDegree <= from && cursorDegree <= to || cursorDegree >= from && cursorDegree >= to) {
                        _this.activate(item);
                    }
                } else {
                    if (cursorDegree >= from && cursorDegree <= to) {
                        _this.activate(item);
                    }
                }
            }
            this.cache.forEach(function (item) {

            })
        },

        /**
         * Defines if mouse cursor is in inActive radius
         * @returns {boolean} - True if so
         * @private
         */
        _isInactive: function () {
            return this.vector.length < this.opts.inActiveRadius
        },

        _saveCursorDimensions: function () {
            this.cursorDims = {
                width: $cursor.offsetWidth,
                height: $cursor.offsetHeight
            }
        },

        //  Events
        // -------------------------------------------------

        onMouseDown: function (e) {
            e.preventDefault();

            if (e.which !== 1) return;

            this.defineCoordsCenter(e);
            this.saveCurrentMousePosition(e);
            this.defineVector();
            this.show();
        },

        onMouseUp: function (e) {
            if (!this.visible) return;

            if (this.currentActive && this.opts.onChange) {
                var index = this.cache.indexOf(this.currentActive);
                this.opts.onChange(this.opts.items[index]);
            }

            this.hide();
        },

        onMouseMove: function (e) {
            if (this.visible) {
                e.preventDefault();

                this.saveCurrentMousePosition(e);
                this.defineVector();
                this.setCursorPosition();

                if (!this.opts.pointerFixed) {
                    this.setPointerPosition();
                }
                if (this._isInactive()) {
                    this.disable();
                } else {
                    $html.classList.add('-wheel-menu-moving-');
                    this.intersection();
                }
            }
        }

    };

})(window);

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdn.rawgit.com/t1m0n/wheel-menu/fdf1b6f7/js/chat.js
  2. https://cdn.rawgit.com/t1m0n/wheel-menu/fdf1b6f7/js/utils.js