Створення радіального меню в CSS


321

Як створити меню, яке виглядає приблизно так ...

Зображення підказки

Посилання на PSD

Я не хочу використовувати зображення PSD. Я вважаю за краще використовувати піктограми з якогось пакета, наприклад FontAwesome, а фони / css генеруються у CSS.

Можна знайти версію меню, яке використовує PSD для створення зображень підказки, а потім їх використання тут .


193
Це прекрасна підказка від дизайнера, який явно ненавидить розробників передньої частини.
punkrockbuddyholly

24
Порада. Це пиріг, а не підказка.
Марк Едвардс

17
Або "радіальне меню". Однозначно, не підказка. Підказки відображаються лише на наведення курсора, і з ним не можна взаємодіяти. (Вони описують інструмент; вони не є інструментом.)
Алан Х.

2
Також є подібний проект на github nikesh.github.io/Pie-Menu від Nikesh Hayaran
Pavel Hlobil

Відповіді:


982

Майже через 3 роки я нарешті знайшов час переглянути це і опублікувати вдосконалену версію. Ви все ще можете переглянути оригінальну відповідь в кінці для ознайомлення.

Хоча SVG може бути кращим вибором, особливо сьогодні, моя мета при цьому полягала в тому, щоб це було лише HTML і CSS, не JS, не SVG, ані зображення (крім фону на кореневому елементі).

Демонстрація 2015 року

Скріншоти

Chrome 43:

Скріншот Chrome

Firefox 38:

Скріншот Firefox

IE 11:

Скріншот IE

Код

HTML досить простий. Я використовую прапорець, щоб відкрити / приховати меню.

<input type='checkbox' id='t'/>
<label for='t'></label>
<ul>
    <li><a href='#'></a></li>
    <li><a href='#'></a></li>
    <li><a href='#'></a></li>
</ul>

Я використовую Sass, щоб дотримуватися цього логічно і полегшити змінити речі, якщо потрібно. Сильно коментує.

$d: 2em; // diameter of central round button
$r: 16em; // radius of menu
$n: 3; // must match number of list items in DOM
$exp: 3em; // menu item height
$tip: .75em; // dimension of tip on middle menu item
$w: .5em; // width of ends
$cover-dim: 2*($r - $exp); // dimension of the link cover
$angle: 15deg; // angle for a menu item
$skew-angle: 90deg - $angle; // how much to skew a menu item to $angle
$scale-factor: cos($skew-angle); // correction factor - see vimeo.com/98137613 from min 15
$off-angle: .125deg; // offset angle so we have a little space between menu items

// don't show the actual checkbox
input {
  transform: translate(-100vw); // move offscreen
  visibility: hidden; // avoid paint
}

// change state of menu to revealed on checking the checkbox
input:checked ~ ul {
    transform: scale(1); 
    opacity: .999;
    // ease out back from easings.net/#easeOutBack
    transition: .5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}

// position everything absolutely such that their left bottom corner 
// is in the middle of the screen
label, ul, li {
    position: absolute;
    left: 50%; bottom: 50%;
}

// visual candy styles
label, a {
    color: #858596;
    font: 700 1em/ #{$d} sans-serif;
    text-align: center;
    text-shadow: 0 1px 1px #6c6f7e;
    cursor: pointer;
}

label {
    z-index: 2; // place it above the menu which has z-index: 1
    margin: -$d/2; // position correction such that it's right in the middle
    width: $d; height: $d;
    border-radius: 50%;
    box-shadow: 0 0 1px 1px white, 
                0 .125em .25em #876366, 
                0 .125em .5em #876366;
    background: radial-gradient(#d4c7c5, #e5e1dd);
}

ul {
    z-index: 1;
    margin: -$r + $exp + 1.5*$d 0; // position correction
    padding: 0;
    list-style: none;
    transform-origin: 50% (-$r + $exp);
    transform: scale(.001); // initial state: scaled down to invisible
    will-change: transform; // better perf on transitioning transform
    opacity: .001; // initial state: transparent
    filter: drop-shadow(0 .125em .25em #847c77) 
            drop-shadow(0 .125em .5em #847c77);
    // ease in back, also from easings.net
    transition: .5s cubic-bezier(0.6, -0.28, 0.735, 0.045);

    // menu ends
    &:before, &:after {
        position: absolute;
        margin: -$exp (-$w/2);
        width: $w; height: $exp;
        transform-origin: 50% 100%;
        background: linear-gradient(#ddd, #c9c4bf);
        content: '';
    }

    &:before {
        border-radius: $w 0 0 $w;
        transform: rotate(-.5*$n*$angle) 
                   translate(-$w/2, -$r + $exp);
        box-shadow: inset 1px 0 1px #eee;
    }
    &:after {
        border-radius: 0 $w $w 0;
        transform: rotate(.5*$n*$angle) 
            translate($w/2, -$r + $exp);
        box-shadow: inset -1px 0 1px #eee;
    }
}

li {
    overflow: hidden;
    width: $r; height: $r;
    transform-origin: 0 100%;

    @for $i from 0 to $n {
        &:nth-child(#{$i + 1}) {
            $curr-angle: $i*$angle + 
                ($i + .5)*$off-angle - 
                .5*$n*($angle + $off-angle);

            // make each list item a rhombus rotated around its bottom left corner
            // see explanation from minute 33:10 youtube.com/watch?v=ehjoh_MmE9A
            transform: rotate($curr-angle)
                       skewY(-$skew-angle) 
                       scaleX($scale-factor);

            // add tip for the item n the middle, just a rotated square
            @if $i == ($n - 1)/2 {
                a:after {
                    position: absolute;
                    top: $exp; left: 50%;
                    margin: -$tip/2;
                    width: $tip; height: $tip;
                    transform: rotate(45deg);
                    box-shadow: 
                        inset -1px -1px 1px #eee;
                    background: linear-gradient(-45deg, 
                        #bbb, #c9c4bf 50%);
                    content: '';
                }
            }
        }
    }

    a, &:before {
        margin: 0 (-$r);
        width: 2*$r; height: 2*$r;
        border-radius: 50%;
    }

    &:before, &:after {
        position: absolute;
        border-radius: 50%;
        // undo distorting transforms from menu item (parent li)
        transform: scaleX(1/$scale-factor) 
                   skewY($skew-angle);
        content: '';
    }

    // actual background of the arched menu items
    &:before {
        box-shadow: 
            inset 0 0 1px 1px #fff, 
            inset 0 0 $exp #ebe7e2, 
            inset 0 0 1px ($exp - .0625em) #c9c4bf, 
            inset 0 0 0 $exp #dcdcdc;
    }

    // cover to prevent click action in between the star and menu items
    &:after {
        top: 100%; left: 0;
        margin: -$cover-dim/2;
        width: $cover-dim; height: $cover-dim;
        border-radius: 50%;
    }
}

a {
    display: block;
    // undo distorting transforms from menu item and rotate into right position
    transform: scaleX(1/$scale-factor) 
               skewY($skew-angle) 
               rotate($angle/2);
    line-height: $exp;
    text-align: center;
    text-decoration: none;
}


Оригінальна відповідь

Моя спроба зробити щось подібне з чистим CSS:

демонстрація

(натисніть зірку)

Працює в Chrome, Firefox (трохи дивно розмиває ефект від наведення курсора), Opera (кінці виглядають менше) та Safari (кінці виглядають меншими).

* { margin: 0; padding: 0; }
body {
	overflow: hidden;
}
/* generic styles for button & circular menu */
.ctrl {
	position: absolute;
	top: 70%; left: 50%;
	font: 1.5em/1.13 Verdana, sans-serif;
	transition: .5s;
}
/* generic link styles */
a.ctrl, .ctrl a {
	display: block;
	opacity: .56;
	background: #c9c9c9;
	color: #7a8092;
	text-align: center;
	text-decoration: none;
	text-shadow: 0 -1px dimgrey;
}
a.ctrl:hover, .ctrl a:hover, a.ctrl:focus, .ctrl a:focus { opacity: 1; }
a.ctrl:focus, .ctrl a:focus { outline: none; }
.button {
	z-index: 2;
	margin: -.625em;
	width: 1.25em; height: 1.25em;
	border-radius: 50%;
	box-shadow: 0 0 3px 1px white;
}
/* circular menu */
.tip {
	z-index: 1;
	/**outline: dotted 1px white;/**/
	margin: -5em;
	width: 10em; height: 10em;
	transform: scale(.001);
	list-style: none;
	opacity: 0;
}
/* the ends of the menu */
.tip:before, .tip:after {
	position: absolute;
	top: 34.3%;
	width: .5em; height: 14%;
	opacity: .56;
	background: #c9c9c9;
	content: '';
}
.tip:before {
	left: 5.4%;
	border-radius: .25em 0 0 .25em;
	box-shadow: -1px 0 1px dimgrey, inset 1px 0 1px white, inset -1px 0 1px grey, 
				inset 0 1px 1px white, inset 0 -1px 1px white;
	transform: rotate(-75deg);
}
.tip:after {
	right: 5.4%;
	border-radius: 0 .25em .25em 0;
	box-shadow: 1px 0 1px dimgrey, inset -1px 0 1px white, inset 1px 0 1px grey,
				inset 0 1px 1px white, inset 0 -1px 1px white;
	transform: rotate(75deg);
}
/* make the menu appear on click */
.button:focus + .tip {
	transform: scale(1);
	opacity: 1;
}
/* slices of the circular menu */
.slice {
	overflow: hidden;
	position: absolute;
	/**outline: dotted 1px yellow;/**/
	width: 50%; height: 50%;
	transform-origin: 100% 100%;
}
/* 
 * rotate each slice at the right angle = (A/2)° + (k - (n+1)/2)*A°
 * where A is the angle of 1 slice (30° in this case)
 * k is the number of the slice (in {1,2,3,4,5} here)
 * and n is the number of slices (5 in this case)
 * formula works for odd number of slices (n odd)
 * for even number of slices (n even) the rotation angle is (k - n/2)*A°
 * 
 * after rotating, skew on Y by 90°-A°; here A° = the angle for 1 slice = 30° 
 */
.slice:first-child { transform: rotate(-45deg) skewY(60deg); }
.slice:nth-child(2) { transform: rotate(-15deg) skewY(60deg); }
.slice:nth-child(3) { transform: rotate(15deg) skewY(60deg); }
.slice:nth-child(4) { transform: rotate(45deg) skewY(60deg); }
.slice:last-child { transform: rotate(75deg) skewY(60deg); }
/* covers for the inner part of the links so there's no hover trigger between
   star button & menu links; give them a red background to see them */
.slice:after {
	position: absolute;
	top: 32%; left: 32%;
	width: 136%; height: 136%;
	border-radius: 50%;
	/* "unskew" = skew by minus the same angle by which parent was skewed */
	transform: skewY(-60deg);
	content: '';
}
/* menu links */
.slice a {
	width: 200%; height: 200%;
	border-radius: 50%;
	box-shadow: 0 0 3px dimgrey, inset 0 0 4px white;
	/* "unskew" & rotate by -A°/2 */
	transform: skewY(-60deg) rotate(-15deg);
	background: /* lateral separators */
			linear-gradient(75deg, 
		transparent 50%, grey 50%, transparent 54%) no-repeat 36.5% 0,
			linear-gradient(-75deg, 
		transparent 50%, grey 50%, transparent 54%) no-repeat 63.5% 0,
		/* make sure inner part is transparent */
		radial-gradient(rgba(127,127,127,0) 49%, 
					rgba(255,255,255,.7) 51%, #c9c9c9 52%);
	background-size: 15% 15%, 15% 15%, cover;
	line-height: 1.4;
}
/* arrow for middle link */
.slice:nth-child(3) a:after {
	position: absolute;
	top: 13%; left: 50%;
	margin: -.25em;
	width: .5em; height: .5em;
	box-shadow: 2px 2px 2px white;
	transform: rotate(45deg);
	background: linear-gradient(-45deg, #c9c9c9 50%, transparent 50%);
	content: '';
}
<a class='button ctrl' href='#' tabindex='1'></a>
<ul class='tip ctrl'>
	<li class='slice'><a href='#'></a></li>
	<li class='slice'><a href='#'></a></li>
	<li class='slice'><a href='#'></a></li>
	<li class='slice'><a href='#'></a></li>
	<li class='slice'><a href='#'></a></li>
</ul>


84
і хто сказав, що CSS не програмує? : D
lucassp

3
Дуже хороший! Я поставив би меню трохи ближче до центральної точки. Я трохи ледачий, коли справа стосується переміщення курсору миші по моєму екрану :)
c.hill

13
FWIW, цей підхід порушується в останньому Chrome, якщо ви використовуєте Tab для перегляду елементів.
Суперпіг

4
@Superpig Правда. Ця версія більше не повинна мати цієї проблеми codepen.io/thebabydino/pen/jfqtv
Ана,

4
@Chii Дивіться tympanus.net/codrops/2012/12/17/css-click-events - те, що я тут використав - це " :focusшлях" . Насправді це досить старий метод, я вперше побачив його, який використав Стю Ніколс у своїх експериментах на cssplay.co.uk досить декілька років тому. У CSS вище, це те, .button:focus + .tipщо робить трюк.
Ана

61

Відповідь Ана - удар по попу! Ось якийсь серйозний CSS-фу.

Моє рішення може бути не зовсім тим, на що ви сподіваєтесь, але це інше можливе рішення. Я зараз працюю над компасним інтерфейсом, який має схожий стиль дугоподібних кнопок. Я вирішив розробити його за допомогою Рафаеля та SVG.

Я створив форму дуги в Illustrator, експортував для неї SVG, схопив визначення шляху для дуги з експортованого SVG-файлу і використав Raphael для створення мого інтерфейсу з ним.

Ось JSFiddle цього .

Ось JavaScript:

var arc = {
    fill: '#333',
    stroke: '#333',
    path: 'M53.286,44.333L69.081,7.904C48.084-1.199,23.615-2.294,0.648,6.78l14.59,36.928C28.008,38.662,41.612,39.27,53.286,44.333z'
};

var paper = Raphael(document.getElementById("notepad"), 500, 500);

var arcDegrees = 45;
var centerX = 210;
var centerY = 210;
var compassRadius = 68;
var currentlyActive = 45;
var directions = [
    {label:'N', degrees:0, rotatedDegrees:270}, 
    {label:'NE', degrees:45, rotatedDegrees:315}, 
    {label:'E', degrees:90, rotatedDegrees:0}, 
    {label:'SE', degrees:135, rotatedDegrees:45}, 
    {label:'S', degrees:180, rotatedDegrees:90}, 
    {label:'SW', degrees:225, rotatedDegrees:135}, 
    {label:'W', degrees:270, rotatedDegrees:180}, 
    {label:'NW', degrees:315, rotatedDegrees:225}
];

function arcClicked()
{
    var label = $(this).data('direction-label');
    $("#activeArc").attr('id', null);
    $(this).attr('id', 'activeArc');
}

for (i = 0; i < 360; i += arcDegrees) {
    var direction = _.find(directions, function(d) { return d.rotatedDegrees == i; });
    var radians = i * (Math.PI / 180);
    var x = centerX + Math.cos(radians) * compassRadius;
    var y = centerY + Math.sin(radians) * compassRadius;

    var newArc = paper.path(arc.path);
    // newArc.translate(x, y);
    // newArc.rotate(i + 89);
    newArc.transform('T' + x + ',' + y + 'r' + (i + 89));

    if (direction.degrees == currentlyActive) {
        $(newArc.node).attr('id', 'activeArc');
    }

    $(newArc.node)
        .attr('class', 'arc')
        .data('direction-label', direction.label)
        .on('click', arcClicked);
}

Ось пов'язаний CSS:

#notepad {
    background: #f7f7f7;
    width: 500px;
    height: 500px;
}

.arc {
    fill: #999;
    stroke: #888;
    cursor: pointer;
}

.arc:hover {
    fill: #777;
    stroke: #666;
}

#activeArc {
    fill: #F18B21 !important;
    stroke: #b86a19 !important;
}

16

Ще одним дуже хорошим способом було б використання JavaScript для позиціонування.

DEMO + TUTORIAL щодо створення анімованого радіального меню

Професіоналом цього методу є те, що ви можете використовувати будь-яку кількість елементів, і він буде постійно розміщувати їх радіально, не змінюючи жодного свого CSS.

Розглянутий JavaScript:

var items = document.querySelectorAll('.circle a');

for(var i = 0, l = items.length; i < l; i++) {
  items[i].style.left = (50 - 35*Math.cos(-0.5 * Math.PI - 2*(1/l)*i*Math.PI)).toFixed(4) + "%";

  items[i].style.top = (50 + 35*Math.sin(-0.5 * Math.PI - 2*(1/l)*i*Math.PI)).toFixed(4) + "%";
}

document.querySelector('.menu-button').onclick = function(e) {
   e.preventDefault(); document.querySelector('.circle').classList.toggle('open');
}

простий і легкий для розуміння. також дуже гнучка, оскільки ви можете додавати новий елемент без коригування коду js. молодець!
видруд

Якщо нам доведеться змінити положення кнопки меню, то як нам це робити? Чи можете ви мені поясніть, що це за 50 і 35?
Тарун

Набагато важливіший приклад. Розширення або налагодження єдиної версії css було б майже неможливо
Дренай
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.