Niveus: a tactile, adaptive interface language

Bevelled controls with light falling across them, fields as wells, solid
windows, and glass kept for the chrome that floats over everything else.
Warm graphite in dark, soft paper in light, one brand color driving both.
Ships its own fonts (Instrument Sans, JetBrains Mono; OFL).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-10-03 17:47:03 +02:00
commit 23b8dd376d
51 changed files with 15664 additions and 0 deletions

View file

@ -0,0 +1,358 @@
/* ==========================================================================
Niveus — Navigation
--------------------------------------------------------------------------
.nv-nav horizontal bar, frosted, sticky by default
.nv-nav--vertical sidebar
.nv-nav--plain solid instead of frosted
.nv-nav__brand / __list / __item / __section / __spacer
.nv-nav-tabs in-page tab strip with a sliding indicator
Navigation is chrome: it sits above content rather than in it. That is why
the bar is frosted and bevelled — the blur is not decoration, it is the
signal that content is passing underneath. A sidebar that scrolls with the page is not
chrome, so .nv-nav--vertical drops the translucency by default.
========================================================================== */
.nv-nav {
position: sticky;
inset-block-start: 0;
z-index: 10;
display: flex;
align-items: center;
gap: var(--nv-space-4);
padding: var(--nv-space-2) var(--nv-space-4);
min-block-size: var(--nv-control-height-lg);
background-color: var(--nv-material-frosted-bg);
border-block-end: var(--nv-border-width) solid var(--nv-color-border-subtle);
/* The travelling selection marker sits behind its item at z-index -1; the
bar has to be its own stacking context or the marker would fall behind
the bar's background and disappear. */
isolation: isolate;
box-shadow: var(--nv-bevel);
backdrop-filter: var(--nv-backdrop-frosted);
-webkit-backdrop-filter: var(--nv-backdrop-frosted);
transition: var(--nv-transition-surface);
}
/* The bar only earns a shadow once something is behind it. Set data-scrolled
from the application on scroll. */
.nv-nav[data-scrolled] {
box-shadow: var(--nv-bevel), var(--nv-elevation-2);
transition: box-shadow var(--nv-duration-normal) var(--nv-ease-standard);
}
.nv-nav--plain {
background-color: var(--nv-color-surface);
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
.nv-nav--static {
position: static;
}
/* A navigation group used inside content rather than as page chrome: no bar,
no border, no blur — just the items and their travelling selection. */
.nv-nav--inline {
position: static;
display: inline-flex;
min-block-size: 0;
padding: var(--nv-space-1);
background-color: transparent;
border-block-end: 0;
box-shadow: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
.nv-nav__brand {
display: flex;
align-items: center;
gap: var(--nv-space-2);
font: var(--nv-font-title);
letter-spacing: var(--nv-letter-spacing-tight);
color: var(--nv-color-text-primary);
text-decoration: none;
padding-inline: var(--nv-space-1);
}
.nv-nav__list {
display: flex;
align-items: center;
gap: var(--nv-space-1);
margin: 0;
padding: 0;
list-style: none;
min-inline-size: 0;
}
.nv-nav__spacer {
flex: 1;
}
/* --- Item --------------------------------------------------------------------
Items are flat until they are touched. The active item is the only one with
material, which keeps the bar quiet while still answering "where am I". */
.nv-nav__item {
/* Rides above a travelling marker once niveus.js has given it a name. */
view-transition-class: nv-carried;
position: relative;
display: flex;
align-items: center;
gap: var(--nv-space-2);
padding: var(--nv-space-2) var(--nv-space-3);
min-block-size: var(--nv-control-height-md);
border: var(--nv-border-width) solid transparent;
border-radius: var(--nv-radius-md);
background-color: transparent;
color: var(--nv-color-text-secondary);
font: var(--nv-font-label);
text-decoration: none;
white-space: nowrap;
cursor: pointer;
transition: var(--nv-transition-control);
}
@media (hover: hover) {
.nv-nav__item:hover {
background-color: var(--nv-color-surface-hover);
color: var(--nv-color-text-primary);
}
}
.nv-nav__item:active {
background-color: var(--nv-color-surface-active);
box-shadow: var(--nv-elevation-inset-sm);
transition-duration: var(--nv-duration-instant);
}
.nv-nav__item:focus-visible {
outline: none;
box-shadow: var(--nv-focus-ring);
}
/* Selection is a material that *moves*. The emphasis lives in a pseudo-element
rather than on the item itself, and carries a view-transition-name: when the
selection changes inside a transition, the browser interpolates the marker's
old box into its new one, so the highlight slides from the previous item to
the next instead of blinking out and in somewhere else.
Only one item in a group may be selected at a time — that is what keeps the
name unique. A page with several selection groups must give each its own
--nv-selection-name, or the duplicate name aborts the transition. */
/* Selection changes color and gains material — but not weight. Bolding the
selected item re-measures its text, which reflows every item beside it on
each change, and makes the old and new labels cross-fade against each other
at different widths. The pill is the cue; the weight was costing a flicker
for nothing. */
.nv-nav__item[aria-current],
.nv-nav__item--active,
.nv-nav__item[aria-selected="true"] {
color: var(--nv-color-text-primary);
}
.nv-nav__item[aria-current]::before,
.nv-nav__item--active::before,
.nv-nav__item[aria-selected="true"]::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
background-color: var(--nv-color-surface-elevated);
background-image: var(--nv-sheen-raised);
border: var(--nv-border-width) solid var(--nv-color-border-subtle);
border-radius: inherit;
box-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-1);
view-transition-class: nv-travelling;
}
/* The marker is named only while its own group is changing (niveus.js sets
data-nv-travelling for the length of the transition). A permanent name
would lift every marker on the page out of the page snapshot on every
transition, theme changes included. */
[data-nv-travelling] .nv-nav__item[aria-current]::before,
[data-nv-travelling] .nv-nav__item--active::before,
[data-nv-travelling] .nv-nav__item[aria-selected="true"]::before {
view-transition-name: var(--nv-selection-name);
}
/* Hovering an already-selected item should not restate the emphasis. */
@media (hover: hover) {
.nv-nav__item[aria-current]:hover,
.nv-nav__item--active:hover,
.nv-nav__item[aria-selected="true"]:hover {
background-color: transparent;
}
}
.nv-nav__item[aria-disabled="true"] {
color: var(--nv-color-text-disabled);
pointer-events: none;
}
.nv-nav__icon {
inline-size: 1.15em;
block-size: 1.15em;
flex: none;
opacity: 0.85;
}
.nv-nav__badge {
margin-inline-start: auto;
padding-inline: var(--nv-space-1-5);
min-inline-size: var(--nv-space-5);
block-size: var(--nv-space-5);
display: inline-flex;
align-items: center;
justify-content: center;
background-color: var(--nv-color-accent-subtle);
color: var(--nv-color-accent-text);
border-radius: var(--nv-radius-full);
font: var(--nv-font-caption);
font-size: var(--nv-font-size-2xs);
font-weight: var(--nv-font-weight-semibold);
}
/* --- Vertical ------------------------------------------------------------------ */
.nv-nav--vertical {
position: sticky;
inset-block-start: 0;
flex-direction: column;
align-items: stretch;
gap: var(--nv-space-1);
inline-size: 15rem;
block-size: 100svh;
padding: var(--nv-space-4) var(--nv-space-3);
background-color: var(--nv-color-background-subtle);
backdrop-filter: none;
-webkit-backdrop-filter: none;
border-block-end: 0;
border-inline-end: var(--nv-border-width) solid var(--nv-color-border-subtle);
box-shadow: none;
overflow-y: auto;
}
.nv-nav--vertical .nv-nav__list {
flex-direction: column;
align-items: stretch;
gap: var(--nv-space-0-5);
}
.nv-nav--vertical .nv-nav__item {
justify-content: flex-start;
}
.nv-nav--vertical .nv-nav__brand {
padding: var(--nv-space-2) var(--nv-space-3);
margin-block-end: var(--nv-space-3);
}
/* Engraved, like .nv-overline: a section marks a region, it is not read. */
.nv-nav__section {
padding: var(--nv-space-4) var(--nv-space-3) var(--nv-space-1-5);
font: var(--nv-font-engraved);
letter-spacing: var(--nv-letter-spacing-engraved);
text-transform: uppercase;
color: var(--nv-color-text-tertiary);
}
.nv-nav__section:first-child {
padding-block-start: 0;
}
/* --- Tabs ------------------------------------------------------------------------
For switching views inside a page, not for moving between pages. The strip
itself is inset, so the selected tab reads as lifted out of the track. */
.nv-nav-tabs {
display: inline-flex;
align-items: center;
gap: var(--nv-space-0-5);
padding: var(--nv-space-1);
background-color: var(--nv-color-surface-inset);
border: var(--nv-border-width) solid var(--nv-color-border-subtle);
border-radius: var(--nv-radius-md);
box-shadow: var(--nv-elevation-inset-md), var(--nv-ledge);
overflow-x: auto;
scrollbar-width: none;
isolation: isolate;
}
.nv-nav-tabs::-webkit-scrollbar {
display: none;
}
.nv-nav-tabs .nv-nav__item {
border-radius: var(--nv-radius-sm);
min-block-size: var(--nv-control-height-sm);
padding-block: var(--nv-space-1);
}
/* Tabs are a selection group of their own, so they get their own marker name;
sharing the navigation bar's would mean two visible elements with the same
view-transition-name. */
.nv-nav-tabs {
--nv-selection-name: nv-tabs-selection;
}
.nv-nav-tabs .nv-nav__item[aria-selected="true"]::before {
border-radius: var(--nv-radius-sm);
}
/* --- Panels ----------------------------------------------------------------------
Switching tabs should feel like moving through one surface rather than
swapping the interface. nv.selectTab() gives the outgoing and incoming
panels one shared name for the length of the change, so they hand over
inside the same box — and slide with the direction of travel. The class is
what picks up the content animation in continuity.css. */
.nv-tab-panel {
view-transition-class: nv-content;
}
/* --- Breadcrumbs -------------------------------------------------------------------- */
.nv-breadcrumbs {
display: flex;
align-items: center;
gap: var(--nv-space-2);
margin: 0;
padding: 0;
list-style: none;
font: var(--nv-font-caption);
color: var(--nv-color-text-tertiary);
}
.nv-breadcrumbs li + li::before {
content: "/";
margin-inline-end: var(--nv-space-2);
color: var(--nv-color-border-strong);
}
.nv-breadcrumbs a {
color: var(--nv-color-text-secondary);
text-decoration: none;
}
.nv-breadcrumbs a:hover {
color: var(--nv-color-text-primary);
}
.nv-breadcrumbs [aria-current] {
color: var(--nv-color-text-primary);
font-weight: var(--nv-font-weight-medium);
}