Skip to content

Instantly share code, notes, and snippets.

@henrybabbage
Created November 1, 2024 12:46
Show Gist options
  • Select an option

  • Save henrybabbage/485ff33aba6badb4b6f227b3cfce15d6 to your computer and use it in GitHub Desktop.

Select an option

Save henrybabbage/485ff33aba6badb4b6f227b3cfce15d6 to your computer and use it in GitHub Desktop.
CSS :has() nav sliding hover tips
<nav class="nav" data-orientation="horizontal">
<ul>
<li>
<a href="#"
><svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z"
/>
</svg>
</a>
</li>
<li>
<a href="#">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 0 0 .75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 0 0-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0 1 12 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 0 1-.673-.38m0 0A2.18 2.18 0 0 1 3 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 0 1 3.413-.387m7.5 0V5.25A2.25 2.25 0 0 0 13.5 3h-3a2.25 2.25 0 0 0-2.25 2.25v.894m7.5 0a48.667 48.667 0 0 0-7.5 0M12 12.75h.008v.008H12v-.008Z"
/>
</svg>
</a>
</li>
<li>
<a href="#"
><svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 7.5h1.5m-1.5 3h1.5m-7.5 3h7.5m-7.5 3h7.5m3-9h3.375c.621 0 1.125.504 1.125 1.125V18a2.25 2.25 0 0 1-2.25 2.25M16.5 7.5V18a2.25 2.25 0 0 0 2.25 2.25M16.5 7.5V4.875c0-.621-.504-1.125-1.125-1.125H4.125C3.504 3.75 3 4.254 3 4.875V18a2.25 2.25 0 0 0 2.25 2.25h13.5M6 7.5h3v3H6v-3Z"
/>
</svg>
</a>
</li>
<li>
<a href="#"
><svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 12.76c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.076-4.076a1.526 1.526 0 0 1 1.037-.443 48.282 48.282 0 0 0 5.68-.494c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z"
/>
</svg>
</a>
</li>
<li>
<a href="#">
<!-- <button class="theme-toggler"> -->
<span class="sr-only">Toggle Theme</span>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="theme-icon"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25"
/>
<!-- <path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z"
/> -->
</svg>
</a>
<!-- </button> -->
</li>
<li>
<!-- <button popovertarget="menu" popovertargetaction="toggle">-->
<a
href="https://twitter.com/intent/follow?screen_name=jh3yy"
target="_blank"
rel="noreferrer noopener"
>
<svg
role="img"
class="x"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>X</title>
<path
fill="currentColor"
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/>
</svg>
<span class="sr-only">Follow</span>
</a>
<!-- </button> -->
</li>
</ul>
<button aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>cursor-move</title>
<path
fill="currentColor"
d="M13,6V11H18V7.75L22.25,12L18,16.25V13H13V18H16.25L12,22.25L7.75,18H11V13H6V16.25L1.75,12L6,7.75V11H11V6H7.75L12,1.75L16.25,6H13Z"
/>
</svg>
</button>
</nav>
<div class="tip" aria-hidden="true">
<div class="tip__track">
<div>About</div>
<div>Work</div>
<div>Posts</div>
<div>Contact</div>
<div>Theme</div>
<div>Follow</div>
</div>
</div>
<a
class="bear-link"
href="https://twitter.com/intent/follow?screen_name=jh3yy"
target="_blank"
rel="noreferrer noopener"
>
<svg
class="w-9"
viewBox="0 0 969 955"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="161.191"
cy="320.191"
r="133.191"
stroke="currentColor"
stroke-width="20"
></circle>
<circle
cx="806.809"
cy="320.191"
r="133.191"
stroke="currentColor"
stroke-width="20"
></circle>
<circle
cx="695.019"
cy="587.733"
r="31.4016"
fill="currentColor"
></circle>
<circle
cx="272.981"
cy="587.733"
r="31.4016"
fill="currentColor"
></circle>
<path
d="M564.388 712.083C564.388 743.994 526.035 779.911 483.372 779.911C440.709 779.911 402.356 743.994 402.356 712.083C402.356 680.173 440.709 664.353 483.372 664.353C526.035 664.353 564.388 680.173 564.388 712.083Z"
fill="currentColor"
></path>
<rect
x="310.42"
y="448.31"
width="343.468"
height="51.4986"
fill="#FF1E1E"
></rect>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M745.643 288.24C815.368 344.185 854.539 432.623 854.539 511.741H614.938V454.652C614.938 433.113 597.477 415.652 575.938 415.652H388.37C366.831 415.652 349.37 433.113 349.37 454.652V511.741L110.949 511.741C110.949 432.623 150.12 344.185 219.845 288.24C289.57 232.295 384.138 200.865 482.744 200.865C581.35 200.865 675.918 232.295 745.643 288.24Z"
fill="currentColor"
></path>
</svg>
</a>
import gsap from 'https://cdn.skypack.dev/gsap@3.12.0'
import Draggable from 'https://cdn.skypack.dev/gsap@3.12.0/Draggable'
import { Pane } from 'https://cdn.skypack.dev/tweakpane'
gsap.registerPlugin(Draggable)
const config = {
theme: 'light',
locked: false,
speed: 0.26,
blur: 4,
debug: false,
flow: 'auto',
}
const ctrl = new Pane({
title: 'Config',
expanded: true,
})
const update = () => {
document.documentElement.dataset.theme = config.theme
document.documentElement.dataset.debug = config.debug
document.documentElement.dataset.locked = config.locked
document.documentElement.dataset.flow = config.flow
document.documentElement.style.setProperty('--speed', config.speed)
document.documentElement.style.setProperty('--blur', config.blur)
}
const sync = (event) => {
if (
!document.startViewTransition ||
event.target.controller.view.labelElement.innerText !== 'Theme'
)
return update()
document.startViewTransition(() => update())
}
ctrl.addBinding(config, 'locked', {
label: 'Locked',
})
ctrl.addBinding(config, 'flow', {
label: 'Flow',
options: {
Auto: 'auto',
Horizontal: 'horizontal',
Vertical: 'vertical',
},
})
ctrl.addBinding(config, 'speed', {
label: 'Speed (s)',
min: 0,
max: 2,
step: 0.01,
})
ctrl.addBinding(config, 'blur', {
label: 'Blur (px)',
min: 0,
max: 20,
step: 1,
})
ctrl.addBinding(config, 'debug', {
label: 'Debug',
})
ctrl.addBinding(config, 'theme', {
label: 'Theme',
options: {
System: 'system',
Light: 'light',
Dark: 'dark',
},
})
ctrl.on('change', sync)
update()
const nav = document.querySelector('nav')
const navSize = nav.getBoundingClientRect().width
nav.style.opacity = '1'
nav.style.setProperty('--width', navSize)
Draggable.create(nav, {
type: 'left,top',
handle: 'nav button',
onDrag: function () {
document.documentElement.dataset.dragging = true
document.documentElement.dataset.orientation =
this.x < navSize + 10 || this.x > window.innerWidth - (navSize + 10)
? 'vertical'
: 'horizontal'
// Need to say which quadrant it's in because that's missing for unlocked
// We just need to account for inverting on top and right...
// const navBounds = nav.getBoundingClientRect()
// nav.dataset.flipX =
// this.x > window.innerWidth - (navBounds.width + 16 + 100)
},
onDragEnd: function () {
document.documentElement.dataset.dragging = false
},
})
document.documentElement.dataset.orientation = 'horizontal'
// This is the part required for the pointer tracking...
let bounds
const track = ({ x, y }) => {
console.info({ x, y })
document.documentElement.style.setProperty('--tip-x', x - bounds.left)
document.documentElement.style.setProperty('--tip-y', y - bounds.top)
}
const teardown = () => {
nav.removeEventListener('pointermove', track)
nav.removeEventListener('pointerleave', teardown)
}
const initPointerTrack = () => {
bounds = nav.getBoundingClientRect()
nav.addEventListener('pointermove', track)
nav.addEventListener('pointerleave', teardown)
}
nav.addEventListener('pointerenter', initPointerTrack)
@import url('https://unpkg.com/normalize.css') layer(normalize);
@layer normalize, base, demo;
@layer demo {
:root {
--bg: light-dark(white, black);
--bd-top: light-dark(hsl(0 0% 90%), hsl(0 0% 50%));
--bd-bottom: light-dark(hsl(0 0% 40% / 0.5), black);
--color: light-dark(hsl(0 0% 10%), hsl(0 0% 90%));
--tip: light-dark(
color-mix(in hsl, var(--bg), canvasText 5%),
color-mix(in hsl, var(--bg), canvasText 25%)
);
--tip-height: 2rem;
--ease-in: linear(
0 0%,
0.0039 6.25%,
0.0156 12.5%,
0.0352 18.75%,
0.0625 25%,
0.0977 31.25%,
0.1407 37.5%,
0.1914 43.74%,
0.2499 49.99%,
0.3164 56.25%,
0.3906 62.5%,
0.5625 75%,
0.7656 87.5%,
1 100%
);
--ease: ease;
}
.nav li {
width: 40px;
height: 40px;
border-radius: 50%;
display: grid;
place-items: center;
position: relative;
&::after {
content: '';
position: absolute;
inset: -1px 0;
}
}
.nav {
opacity: 0;
height: 42px;
color: var(--color);
box-shadow: 0 4px 8px 0 hsl(0 0% 0% / 0.2);
position: fixed;
bottom: 3rem;
left: 50%;
translate: 0% 0;
margin-left: calc(var(--width) * -0.5px);
border-radius: 100px;
background: linear-gradient(var(--bg), var(--bg)) padding-box,
linear-gradient(var(--bd-top), var(--bd-bottom)) border-box;
border: 1px solid #0000;
transition: opacity 0s var(--transition);
}
nav button {
color: canvasText;
position: absolute;
left: 0;
top: 0;
translate: -100% -50%;
opacity: 0.25;
transition: opacity calc(var(--speed) * 1s);
&:is(:hover, :active) {
opacity: 1;
}
}
.nav:has(ul:is(:hover, :focus-within)) button {
opacity: 0;
}
.nav :where(a, button) {
width: 32px;
aspect-ratio: 1;
border-radius: 50%;
display: grid;
place-items: center;
text-decoration: none;
border: 0;
cursor: pointer;
font-weight: 500;
color: inherit;
background: transparent;
&:is(:focus-visible) {
outline-color: transparent;
outline-width: 0px;
background: var(--tip);
}
}
.nav ul {
display: flex;
padding: 0;
margin: 0;
list-style-type: none;
}
li svg {
width: 24px;
}
/* Positioning stuff */
.nav {
anchor-name: --nav;
}
.tip {
position: fixed;
position-anchor: --nav;
background: var(--tip);
color: var(--color);
font-size: 0.875rem;
border-radius: 100px;
width: 100px;
pointer-events: none;
overflow: hidden;
border: 1px solid
light-dark(
color-mix(in hsl, var(--bd-top), canvasText 15%),
color-mix(in hsl, var(--bd-top), canvasText 15%)
);
height: var(--tip-height);
z-index: 999999999;
}
.x {
width: 18px;
}
[data-debug='true'] .tip {
overflow: visible;
.tip__track {
outline: 1px dashed orange;
outline-offset: 1px;
}
.tip__track div {
opacity: 1;
outline: 1px dashed red;
}
}
.tip__track {
display: grid;
height: var(--tip-height);
}
[data-flow='horizontal'] .tip,
[data-flow='auto'][data-orientation='horizontal'] .tip {
.tip__track {
grid-auto-flow: column;
grid-auto-columns: 100%;
}
}
.tip__track div {
height: var(--tip-height);
padding-inline: 0.5rem;
display: grid;
place-items: center;
}
.tip {
position-try-fallbacks: flip-block, flip-inline;
}
[data-locked='true'][data-orientation='horizontal'] {
.tip {
left: anchor(left);
bottom: calc(anchor(top) + 1rem);
translate: 21px 0;
}
}
[data-locked='true'][data-orientation='vertical'] {
.tip {
left: calc(anchor(right) + 1rem);
bottom: unset;
top: anchor(top);
translate: 0 21px;
}
}
[data-locked='false'][data-orientation='horizontal'] .tip {
left: anchor(left);
bottom: calc(anchor(top) + 1rem);
translate: calc(-50% + (var(--tip-x) * 1px)) calc(var(--tip-y) * 1px + -50%);
}
[data-locked='false'][data-orientation='vertical'] .tip {
left: calc(anchor(right) + 1rem);
top: anchor(top);
bottom: unset;
translate: calc(-1rem + (var(--tip-x) * 1px))
calc(var(--tip-y) * 1px + -50%);
}
[data-orientation='vertical'] {
nav {
width: 42px;
height: fit-content;
margin-left: 0;
ul {
flex-direction: column;
}
}
}
/* :has() hovering stuff */
:root:has(nav li:nth-of-type(1):is(:hover, :focus-within)) {
--active: 1;
.tip__track div:nth-of-type(1) {
--shown: 1;
}
}
:root:has(nav li:nth-of-type(2):is(:hover, :focus-within)) {
--active: 2;
.tip__track div:nth-of-type(2) {
--shown: 1;
}
}
:root:has(nav li:nth-of-type(3):is(:hover, :focus-within)) {
--active: 3;
.tip__track div:nth-of-type(3) {
--shown: 1;
}
}
:root:has(nav li:nth-of-type(4):is(:hover, :focus-within)) {
--active: 4;
.tip__track div:nth-of-type(4) {
--shown: 1;
}
}
:root:has(nav li:nth-of-type(5):is(:hover, :focus-within)) {
--active: 5;
.tip__track div:nth-of-type(5) {
--shown: 1;
}
}
:root:has(nav li:nth-of-type(6):is(:hover, :focus-within)) {
--active: 6;
.tip__track div:nth-of-type(6) {
--shown: 1;
}
}
.tip {
scale: var(--show, 0);
filter: blur(calc(var(--blur, 0) * 1px)));
transition: scale calc(var(--speed) * 1s) var(--ease),
filter calc(var(--speed) * 1s) ease;
}
[data-flow='horizontal'] .tip__track,
[data-flow='auto'][data-orientation='horizontal'] .tip__track {
translate: calc((var(--active) - 1) * (-100%)) 0;
}
[data-flow='vertical'] .tip__track,
[data-flow='auto'][data-orientation='vertical'] .tip__track {
translate: 0 calc((var(--active) - 1) * (var(--tip-height) * -1));
}
.tip__track {
transition: translate calc(var(--speed) * 1s) calc(var(--speed) * 1s)
var(--ease);
}
.tip__track div {
filter: blur(calc((var(--blur, 0) * 1px) * (1 - var(--shown, 0))));
opacity: var(--shown, 0);
transition: filter calc(var(--speed) * 1s) var(--ease-in),
opacity calc(var(--speed) * 1s) var(--ease);
}
:root:has(.nav ul:is(:hover, :focus-within)) {
.tip {
--show: 1;
filter: blur(0);
}
.tip__track {
transition-delay: 0s;
}
}
}
@layer base {
:root {
--font-size-min: 16;
--font-size-max: 20;
--font-ratio-min: 1.2;
--font-ratio-max: 1.33;
--font-width-min: 375;
--font-width-max: 1500;
}
html {
color-scheme: light dark;
}
[data-theme='light'] {
color-scheme: light only;
}
[data-theme='dark'] {
color-scheme: dark only;
}
:where(.fluid) {
--fluid-min: calc(
var(--font-size-min) * pow(var(--font-ratio-min), var(--font-level, 0))
);
--fluid-max: calc(
var(--font-size-max) * pow(var(--font-ratio-max), var(--font-level, 0))
);
--fluid-preferred: calc(
(var(--fluid-max) - var(--fluid-min)) /
(var(--font-width-max) - var(--font-width-min))
);
--fluid-type: clamp(
(var(--fluid-min) / 16) * 1rem,
((var(--fluid-min) / 16) * 1rem) -
(((var(--fluid-preferred) * var(--font-width-min)) / 16) * 1rem) +
(var(--fluid-preferred) * var(--variable-unit, 100vi)),
(var(--fluid-max) / 16) * 1rem
);
font-size: var(--fluid-type);
}
*,
*:after,
*:before {
box-sizing: border-box;
}
body {
display: grid;
place-items: center;
min-height: 100vh;
font-family: 'SF Pro Text', 'SF Pro Icons', 'AOS Icons', 'Helvetica Neue',
Helvetica, Arial, sans-serif, system-ui;
}
body::before {
--size: 45px;
--line: color-mix(in lch, canvasText, transparent 70%);
content: '';
height: 100vh;
width: 100vw;
position: fixed;
background: linear-gradient(
90deg,
var(--line) 1px,
transparent 1px var(--size)
)
50% 50% / var(--size) var(--size),
linear-gradient(var(--line) 1px, transparent 1px var(--size)) 50% 50% /
var(--size) var(--size);
mask: linear-gradient(-20deg, transparent 50%, white);
top: 0;
transform-style: flat;
pointer-events: none;
z-index: -1;
}
.bear-link {
color: canvasText;
position: fixed;
top: 1rem;
left: 1rem;
width: 48px;
aspect-ratio: 1;
display: grid;
place-items: center;
opacity: 0.8;
}
:where(.x-link, .bear-link):is(:hover, :focus-visible) {
opacity: 1;
}
.bear-link svg {
width: 75%;
}
/* Utilities */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment