<body>
<div id="app">
<!-- Exploding button: contact -->
<section>
<explode-button
open-text='Contact'
close-text='Dismiss'
:children="[
{
text: 'DEV',
href: 'https://dev.to/adam_cyclones'
},
{
text: 'Email',
},
{
text: 'Phone',
},
{
text: 'Twitter',
},
{
text: 'Fax',
}
]"/>
</section>
<!-- /End Exploding button: contact -->
</div>
</body>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: floralwhite;
}
.exp {
position: relative;
display: flex;
justify-content: center;
align-items: center;
&-Button,
&-Children_Button {
user-select: none;
position: absolute;
padding: .3rem .8rem;
border-radius: 9e9em;
background: #fff;
color: #3c4437;
border: 1px solid #3c4437;
font-size: 1.2rem;
text-transform: capitalize;
cursor: pointer;
box-sizing: border-box;
&:focus {
outline: 0;
box-shadow: 0 0 0 3px rgba(0, 123, 255, .5);
}
}
&-Button {
z-index: 2;
background-color: #5d5761;
color: #f9f9f9;
border: 0;
}
.exp-Children {
z-index: 1;
}
&-Children_Button {
will-change: transform;
}
&-Children {
position: absolute;
padding: 0;
margin: 0;
top: 0;
left: 0;
height: inherit;
width: inherit;
display: flex;
align-items: center;
justify-content: center;
}
&-Menu_List {
position: absolute;
padding: 0;
margin: 0;
top: 0;
left: 0;
}
}
View Compiled
const explodeButton = Vue.component('explode-button', {
template: `
<div class='exp' :style='{
width: CSS.px(radius * 2).toString(), height: CSS.px(radius * 2),
}'>
<button
class='exp-Button'
@mousedown='explodeImplode'
@mouseover="isMoused = true"
@mouseleave="isMoused = false"
type='button'>{{text}}</button>
<div class='exp-Children'>
<button
v-if='child.text && !child.href'
@click='setCurrent'
@mouseover="isMoused = true"
@mouseleave="isMoused = false"
class='exp-Children_Button'
v-for='(child, index) in children'
v-show='!getInitialIteraction'
:style='"transform:" + setPosition(index) + "; will-change: transform;"'>
{{child.text}}
</button>
<a
target='_blank'
:href='child.href'
v-if='child.text && child.href'
@click='setCurrent'
@mouseover="isMoused = true"
@mouseleave="isMoused = false"
class='exp-Children_Button'
v-for='(child, index) in children'
v-show='!getInitialIteraction'
:style='"transform:" + setPosition(index) + "; will-change: transform;"'>
{{child.text}}
<i v-if='child.icon' :class="'fab ' + child.icon"></i>
</a>
</div>
</div>
`,
props: {
children: {
type: Array,
required: true
},
openText: {
type: String,
required: true
},
closeText: {
type: String,
required: true
},
shape: {
type: String,
default: 'circle'
}
},
data () {
return {
isMoused: false,
isTouched: false,
animation: {
explode: {
timing: ({delay}) => ({
duration: 200,
direction: 'reverse',
fill: 'forwards',
easing: 'ease-in',
delay
})
},
implode: {
timing: ({interactions}) => ({
duration: 300,
direction: 'reverse',
fill: 'forwards',
easing: 'ease-out'
})
}
},
spread: 360,
stagger: true,
open: false,
hideChildren: true,
intereactions: 0,
radius: 100
}
},
computed: {
getSliceAngle () {
return this.spread / this.$props.children.length;
},
getLen () {
return this.$props.children.length;
},
getInitialIteraction () {
return this.intereactions === 0;
},
text () {
return this.open ? this.closeText : this.openText;
},
},
methods: {
constrainAngle(angle) {
angle = Math.abs(angle);
let ret = angle <= this.spread ? angle : 0;
while (angle > this.spread) {
angle -= this.spread;
ret = angle;
}
return ret;
},
doAnimation (animationName) {
const wasExplode = animationName === 'explode';
const children = this.$el.querySelectorAll('.exp-Children_Button');
const explode = (child) => [
{ // to
visibility: 'visible',
transform: `rotate(0) ${child.style.transform}`
},
{ // from
visibility: 'hidden',
transform: 'rotate(0) translate3d(0, 0, 0)'
},
];
const implode = (child) => [
{
transform: 'rotate(0) translate3d(0, 0, 0)',
visibility: 'hidden',
},
{
transform: `rotate(0) ${child.style.transform}`,
visibility: 'visible',
},
];
return new Promise(resolve => {
for (let multiplier = 0; multiplier < this.getLen; multiplier ++) {
const child = children[multiplier];
if ( this.intereactions === 1 ) {
child.style.visibility = 'hidden';
} else {
child.style.visibility = 'visible';
}
const animation = child.animate(
wasExplode ? explode(child) : implode(child),
this.animation[animationName].timing({
delay: this.stagger ? multiplier * 100 : 0
})
);
}
this.open = wasExplode;
});
},
explodeImplode () {
if (!this.isTouched) {
this.incrIntereactions();
if (this.open) {
this.doAnimation('implode');
} else {
this.doAnimation('explode');
}
}
},
calculateCoordinates (
angle
) {
const x = this.radius * Math.sin(Math.PI * 2 * angle / this.spread);
const y = this.radius * Math.cos(Math.PI * 2 * angle / this.spread);
const coordinates = {x, y};
return coordinates;
},
setPosition (multiplier) {
// where multiplier is index
const angle = this.constrainAngle(this.getSliceAngle * multiplier);
const shape = {
circle: 1,
semi: 2,
quater: 4
}
const north = 0;
const { x, y } = this.calculateCoordinates( this.constrainAngle( (angle / shape[this.shape]) - north ));
const cssUnitX = CSS.px(x).toString();
const cssUnitY = CSS.px(y).toString();
const translate = `translate3d(${cssUnitX}, ${cssUnitY}, 0)`;
return translate;
},
incrIntereactions () {
this.intereactions += 1;
},
setCurrent ({ target }) {
if (!this.isMoused) {
target.setAttribute('aria-current', true);
}
},
unsetCurrent ({ target }) {
if (!this.isMoused) {
target.setAttribute('aria-current', false);
}
}
}
});
new Vue({
el: '#app',
components: {
explodeButton
}
});
This Pen doesn't use any external CSS resources.