mirror of
https://github.com/Nexoniarz/Niveus.git
synced 2026-11-03 12:44:26 +00:00
A button with aria-pressed="true" inside .nv-button-group is now drawn pressed into the surface in the brand colour, so the appearance toggle and any segmented choice show which option is in use. Previously no state was visible. Checked the four example apps in the reworked light theme. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
324 lines
10 KiB
CSS
324 lines
10 KiB
CSS
/* ==========================================================================
|
|
Niveus — Button
|
|
--------------------------------------------------------------------------
|
|
.nv-button secondary by default — a raised surface
|
|
.nv-button--primary the single affirmative action in a view
|
|
.nv-button--ghost low-emphasis, no material until hovered
|
|
.nv-button--danger destructive, and only when it is
|
|
.nv-button--frosted for use over imagery or frosted chrome
|
|
.nv-button--sm | --lg 36px by default
|
|
.nv-button--icon square, for a lone icon (needs a label)
|
|
.nv-button--block fills its container
|
|
|
|
A button is a key: a bevelled face with light falling across it, a contact
|
|
line where it meets the surface, and a press that sinks it into an inset.
|
|
|
|
Every variant is defined purely by reassigning the private --_* properties
|
|
below. Interaction — the lift on hover, the sink on press, the focus ring —
|
|
lives once in the base rule, so a new variant can never drift from the way
|
|
the rest of the system responds to a pointer.
|
|
========================================================================== */
|
|
|
|
.nv-button {
|
|
/* Variant surface, overridden per modifier. */
|
|
--_bg: var(--nv-color-surface-elevated);
|
|
--_bg-hover: var(--nv-color-surface-hover);
|
|
--_bg-active: var(--nv-color-surface-active);
|
|
--_fg: var(--nv-color-text-primary);
|
|
--_border: var(--nv-color-border);
|
|
--_sheen: var(--nv-sheen-raised);
|
|
--_shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-2);
|
|
--_shadow-hover: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-3);
|
|
--_shadow-active: var(--nv-elevation-inset-sm), var(--nv-outline);
|
|
|
|
/* Size, overridden per size modifier. */
|
|
--_height: var(--nv-control-height-md);
|
|
--_padding: var(--nv-control-padding-md);
|
|
--_gap: var(--nv-control-gap-md);
|
|
--_font-size: var(--nv-font-size-sm);
|
|
--_radius: var(--nv-radius-md);
|
|
|
|
position: relative;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: var(--_gap);
|
|
|
|
block-size: var(--_height);
|
|
padding-inline: var(--_padding);
|
|
border: var(--nv-border-width) solid var(--_border);
|
|
border-radius: var(--_radius);
|
|
|
|
background-color: var(--_bg);
|
|
background-image: var(--_sheen);
|
|
color: var(--_fg);
|
|
box-shadow: var(--_shadow);
|
|
|
|
font-family: var(--nv-font-family-sans);
|
|
font-size: var(--_font-size);
|
|
font-weight: var(--nv-font-weight-medium);
|
|
line-height: var(--nv-line-height-snug);
|
|
letter-spacing: var(--nv-letter-spacing-normal);
|
|
white-space: nowrap;
|
|
text-decoration: none;
|
|
|
|
cursor: pointer;
|
|
user-select: none;
|
|
-webkit-tap-highlight-color: transparent;
|
|
transition: var(--nv-transition-control);
|
|
}
|
|
|
|
/* A control smaller than the minimum target still gets a full-size hit area:
|
|
the pseudo-element grows outward without changing the visual size. */
|
|
.nv-button::after {
|
|
content: "";
|
|
position: absolute;
|
|
inset-block-start: 50%;
|
|
inset-inline-start: 50%;
|
|
translate: -50% -50%;
|
|
inline-size: max(100%, var(--nv-target-min));
|
|
block-size: max(100%, var(--nv-target-min));
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.nv-button:hover:not(:disabled, [aria-disabled="true"]) {
|
|
background-color: var(--_bg-hover);
|
|
box-shadow: var(--_shadow-hover);
|
|
translate: 0 var(--nv-lift-hover);
|
|
}
|
|
}
|
|
|
|
/* Pressed, the face is pushed below the light: the sheen goes and the inset
|
|
takes over. */
|
|
.nv-button:active:not(:disabled, [aria-disabled="true"]) {
|
|
background-color: var(--_bg-active);
|
|
background-image: none;
|
|
box-shadow: var(--_shadow-active);
|
|
translate: 0 var(--nv-press-depth);
|
|
transition-duration: var(--nv-duration-instant);
|
|
}
|
|
|
|
.nv-button:focus-visible {
|
|
outline: none;
|
|
box-shadow: var(--_shadow), var(--nv-focus-ring);
|
|
}
|
|
|
|
.nv-button:disabled,
|
|
.nv-button[aria-disabled="true"] {
|
|
background-color: var(--nv-color-surface-inset);
|
|
background-image: none;
|
|
color: var(--nv-color-text-disabled);
|
|
border-color: var(--nv-color-border-subtle);
|
|
box-shadow: none;
|
|
translate: none;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* --- Variants --------------------------------------------------------------- */
|
|
|
|
.nv-button--primary {
|
|
--_bg: var(--nv-color-accent);
|
|
--_bg-hover: var(--nv-color-accent-hover);
|
|
--_bg-active: var(--nv-color-accent-active);
|
|
--_fg: var(--nv-color-on-accent);
|
|
--_border: transparent;
|
|
--_sheen: var(--nv-sheen-filled);
|
|
/* A filled key gives off a little of its own color: the glow is what makes
|
|
it read as lit rather than printed. */
|
|
--_shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-2),
|
|
0 6px 16px -8px color-mix(in oklab, var(--_bg) 80%, transparent);
|
|
--_shadow-hover: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-3),
|
|
0 8px 22px -8px color-mix(in oklab, var(--_bg) 85%, transparent);
|
|
font-weight: var(--nv-font-weight-semibold);
|
|
}
|
|
|
|
.nv-button--danger {
|
|
--_bg: var(--nv-color-danger);
|
|
--_bg-hover: var(--nv-color-danger-hover);
|
|
--_bg-active: var(--nv-color-danger-active);
|
|
--_fg: var(--nv-color-on-danger);
|
|
--_border: transparent;
|
|
--_sheen: var(--nv-sheen-filled);
|
|
--_shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-2),
|
|
0 6px 16px -8px color-mix(in oklab, var(--_bg) 80%, transparent);
|
|
--_shadow-hover: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-3),
|
|
0 8px 22px -8px color-mix(in oklab, var(--_bg) 85%, transparent);
|
|
--nv-color-focus-ring: var(--nv-color-danger-border);
|
|
font-weight: var(--nv-font-weight-semibold);
|
|
}
|
|
|
|
.nv-button--ghost {
|
|
--_bg: transparent;
|
|
--_bg-hover: var(--nv-color-surface-hover);
|
|
--_bg-active: var(--nv-color-surface-active);
|
|
--_fg: var(--nv-color-text-secondary);
|
|
--_border: transparent;
|
|
--_sheen: none;
|
|
--_shadow: none;
|
|
--_shadow-hover: none;
|
|
--_shadow-active: var(--nv-elevation-inset-sm);
|
|
}
|
|
|
|
/* A ghost has no material to lose, so disabling it only dims the label —
|
|
it does not suddenly grow a well its enabled siblings never had. */
|
|
.nv-button--ghost:disabled,
|
|
.nv-button--ghost[aria-disabled="true"] {
|
|
background-color: transparent;
|
|
border-color: transparent;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.nv-button--ghost:hover:not(:disabled, [aria-disabled="true"]) {
|
|
color: var(--nv-color-text-primary);
|
|
translate: none; /* nothing to lift — a ghost has no material to raise */
|
|
}
|
|
}
|
|
|
|
.nv-button--subtle {
|
|
--_bg: var(--nv-color-accent-subtle);
|
|
--_bg-hover: var(--nv-color-accent-subtle-hover);
|
|
--_bg-active: var(--nv-color-accent-subtle-hover);
|
|
--_fg: var(--nv-color-accent-text);
|
|
--_border: var(--nv-color-accent-border);
|
|
--_sheen: none;
|
|
--_shadow: none;
|
|
--_shadow-hover: var(--nv-elevation-1);
|
|
}
|
|
|
|
.nv-button--frosted {
|
|
--_bg: var(--nv-material-glass-bg);
|
|
--_bg-hover: var(--nv-material-frosted-bg);
|
|
--_bg-active: var(--nv-material-frosted-bg);
|
|
--_border: var(--nv-material-glass-border);
|
|
--_sheen: none;
|
|
backdrop-filter: var(--nv-backdrop-glass);
|
|
-webkit-backdrop-filter: var(--nv-backdrop-glass);
|
|
}
|
|
|
|
/* --- Sizes ------------------------------------------------------------------- */
|
|
|
|
.nv-button--sm {
|
|
--_height: var(--nv-control-height-sm);
|
|
--_padding: var(--nv-control-padding-sm);
|
|
--_gap: var(--nv-control-gap-sm);
|
|
--_font-size: var(--nv-font-size-xs);
|
|
--_radius: var(--nv-radius-sm);
|
|
}
|
|
|
|
.nv-button--lg {
|
|
--_height: var(--nv-control-height-lg);
|
|
--_padding: var(--nv-control-padding-lg);
|
|
--_gap: var(--nv-control-gap-lg);
|
|
--_font-size: var(--nv-font-size-md);
|
|
}
|
|
|
|
.nv-button--icon {
|
|
--_padding: 0;
|
|
inline-size: var(--_height);
|
|
}
|
|
|
|
.nv-button--pill {
|
|
--_radius: var(--nv-radius-full);
|
|
}
|
|
|
|
.nv-button--block {
|
|
display: flex;
|
|
inline-size: 100%;
|
|
}
|
|
|
|
/* --- Floating ------------------------------------------------------------------
|
|
A control that sits *over* content rather than in the flow: a carousel arrow,
|
|
a floating action. Two things change.
|
|
|
|
It is already off the surface, so there is nothing to lift it from — it grows
|
|
instead. And it is almost always positioned with `translate`, which the hover
|
|
lift would overwrite, throwing the button half its own height down the page
|
|
the moment a pointer touched it. --nv-button-offset carries that positioning
|
|
through every state. */
|
|
|
|
.nv-button--float {
|
|
--_shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-3);
|
|
--_shadow-hover: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-4);
|
|
--_shadow-active: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-2);
|
|
|
|
translate: var(--nv-button-offset, 0 0);
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.nv-button--float:hover:not(:disabled, [aria-disabled="true"]) {
|
|
translate: var(--nv-button-offset, 0 0);
|
|
scale: 1.06;
|
|
}
|
|
}
|
|
|
|
.nv-button--float:active:not(:disabled, [aria-disabled="true"]) {
|
|
translate: var(--nv-button-offset, 0 0);
|
|
scale: 0.97;
|
|
}
|
|
|
|
/* --- Parts --------------------------------------------------------------------- */
|
|
|
|
.nv-button__icon {
|
|
inline-size: 1em;
|
|
block-size: 1em;
|
|
font-size: 1.15em;
|
|
opacity: 0.9;
|
|
}
|
|
|
|
/* Busy: the label stays in place to keep the button from resizing mid-action,
|
|
and the spinner replaces it. */
|
|
.nv-button[aria-busy="true"] {
|
|
color: transparent;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.nv-button[aria-busy="true"]::before {
|
|
content: "";
|
|
position: absolute;
|
|
inline-size: 1em;
|
|
block-size: 1em;
|
|
border: 2px solid var(--_fg);
|
|
border-block-start-color: transparent;
|
|
border-radius: var(--nv-radius-full);
|
|
animation: nv-spin var(--nv-duration-slower) var(--nv-ease-linear) infinite;
|
|
}
|
|
|
|
@keyframes nv-spin {
|
|
to { rotate: 1turn; }
|
|
}
|
|
|
|
/* --- Group ---------------------------------------------------------------------
|
|
Segmented controls: buttons share an edge, so only the outer corners round
|
|
and the shared border is not drawn twice. */
|
|
|
|
.nv-button-group {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.nv-button-group > .nv-button:not(:first-child) {
|
|
border-start-start-radius: 0;
|
|
border-end-start-radius: 0;
|
|
margin-inline-start: calc(var(--nv-border-width) * -1);
|
|
}
|
|
|
|
.nv-button-group > .nv-button:not(:last-child) {
|
|
border-start-end-radius: 0;
|
|
border-end-end-radius: 0;
|
|
}
|
|
|
|
.nv-button-group > .nv-button:hover,
|
|
.nv-button-group > .nv-button:focus-visible {
|
|
z-index: 1;
|
|
translate: none;
|
|
}
|
|
|
|
/* The chosen segment is a key held down: pressed into the surface, in the
|
|
brand colour, rather than a flat wash. This is what makes the appearance
|
|
toggle (and any segmented choice) show which option is in use. */
|
|
.nv-button-group > .nv-button[aria-pressed="true"] {
|
|
background-color: var(--nv-color-accent-subtle);
|
|
background-image: none;
|
|
color: var(--nv-color-accent-text);
|
|
box-shadow: var(--nv-elevation-inset-sm), var(--nv-ledge);
|
|
}
|