Theming

Every color, corner, easing curve and shadow in Noph UI comes from a CSS custom property. Components never hard code a value, so a theme is nothing more than a set of variables you own. Change one and every component that reads it follows, at build time or while the page is running.

How the theme works

A theme declares 54 color roles on :root. Each one holds both schemes at once, in a light-dark() pair, and color-scheme: light dark tells the browser it may pick either.

:root {
	color-scheme: light dark;
	--np-color-primary: light-dark(#00668c, #75ceff);
	--np-color-on-primary: light-dark(#f4f9ff, #00435d);
	--np-color-primary-container: light-dark(#75ceff, #4eaad9);
	--np-color-on-primary-container: light-dark(#00435d, #002637);
}

The same file also carries the shape, motion and elevation tokens, which are listed further down. Because it is plain CSS, you can ship it as a stylesheet, inline it, or overwrite single values from a component.

Using the default theme

Import the default theme once, in your root layout.

import 'noph-ui/defaultTheme'

Then hand the page background and text color to the theme.

body {
	background-color: var(--np-color-background);
	color: var(--np-color-on-background);
}

The default theme is the source color #5fb9e9 run through the Content variant on the 2025 spec with no contrast adjustment. That is why the generator below starts out matching the shipped file exactly, token for token.

Generate your own theme

Pick a source color and Noph UI derives all 54 roles for both schemes. Every change here applies to this page right away, so you can judge a palette on real components instead of swatches. Copy CSS puts a complete replacement for the default theme on your clipboard. Reset brings back the shipped theme.

Spec version
Contrast 0.0

Each swatch shows the value for the color scheme you are viewing. The two hex values below a token are its light and its dark value.

--np-color-background #f7fafe · #0b0f12
--np-color-error #bb1b1b · #ff7164
--np-color-error-container #fe4e44 · #ac0c12
--np-color-inverse-on-surface #9a9da1 · #515559
--np-color-inverse-primary #75ceff · #00668c
--np-color-inverse-surface #0b0f12 · #f7fafe
--np-color-neutral-palette-key-color #73777b · #73777b
--np-color-neutral-variant-palette-key-color #6f787f · #6f787f
--np-color-on-background #2f3336 · #e3e6ea
--np-color-on-error #fff7f6 · #4a0002
--np-color-on-error-container #570003 · #ffb8b0
--np-color-on-primary #f4f9ff · #00435d
--np-color-on-primary-container #00435d · #002637
--np-color-on-primary-fixed #002e41 · #002e41
--np-color-on-primary-fixed-variant #004d6a · #004d6a
--np-color-on-secondary #f4f9ff · #254455
--np-color-on-secondary-container #375567 · #a4c4d9
--np-color-on-secondary-fixed #244354 · #244354
--np-color-on-secondary-fixed-variant #415f71 · #415f71
--np-color-on-surface #2f3336 · #e3e6ea
--np-color-on-surface-variant #5b5f63 · #a8abaf
--np-color-on-tertiary #fff7fc · #623378
--np-color-on-tertiary-container #582a6e · #582a6e
--np-color-on-tertiary-fixed #421358 · #421358
--np-color-on-tertiary-fixed-variant #623378 · #623378
--np-color-outline #777b7f · #727679
--np-color-outline-variant #afb2b6 · #44484c
--np-color-primary #00668c · #75ceff
--np-color-primary-container #75ceff · #4eaad9
--np-color-primary-fixed #75ceff · #75ceff
--np-color-primary-fixed-dim #66c0f0 · #66c0f0
--np-color-primary-palette-key-color #0980ad · #0980ad
--np-color-scrim #000000 · #000000
--np-color-secondary #456375 · #abcbe0
--np-color-secondary-container #c7e7fd · #203f50
--np-color-secondary-fixed #c7e7fd · #c7e7fd
--np-color-secondary-fixed-dim #b9d9ee · #b9d9ee
--np-color-secondary-palette-key-color #5d7b8e · #5d7b8e
--np-color-shadow #000000 · #000000
--np-color-surface #f7fafe · #0b0f12
--np-color-surface-bright #f7fafe · #282d30
--np-color-surface-container #ebeef2 · #161a1d
--np-color-surface-container-high #e5e8ec · #1c2023
--np-color-surface-container-highest #e0e3e7 · #222629
--np-color-surface-container-low #f1f4f8 · #101417
--np-color-surface-container-lowest #ffffff · #000000
--np-color-surface-dim #d7dade · #0b0f12
--np-color-surface-tint #00668c · #75ceff
--np-color-surface-variant #e0e3e7 · #222629
--np-color-tertiary #7c4b92 · #efc2ff
--np-color-tertiary-container #e8affe · #e8affe
--np-color-tertiary-fixed #e8affe · #e8affe
--np-color-tertiary-fixed-dim #d9a2ef · #d9a2ef
--np-color-tertiary-palette-key-color #d29be8 · #d29be8

Spec versions

SPEC_2021 is the original Material 3 color system. SPEC_2025 is the Material 3 Expressive revision, with brighter containers and different surface tones, and it is what the default theme uses.

Light and dark color schemes

Because every token is a light-dark() pair, the browser follows the operating system setting on its own. No JavaScript, no class toggling.

To force one scheme, set data-theme on the html element. Valid values are light and dark.

<html lang="en" data-theme="dark">

If visitors get to choose, store the choice and set the attribute in an inline <script> in the <head> of your app.html, before the page paints. That avoids a flash of the wrong scheme.

const stored = localStorage.getItem('theme')
if (stored === 'light' || stored === 'dark') {
	document.documentElement.setAttribute('data-theme', stored)
}

Applying a theme at runtime

Setting the custom properties on the root element beats swapping stylesheets, since only the changed values repaint. This is exactly what the generator above does.

const setTheme = (tokens) => {
	for (const [token, value] of Object.entries(tokens)) {
		document.documentElement.style.setProperty(token, value)
	}
}

setTheme({ '--np-color-primary': 'light-dark(#00668c, #75ceff)' })

Call removeProperty with the same token names to fall back to the stylesheet again.

Shape tokens

Corner radii are shared by every component, so rounding stays consistent across the app.

TokenValue
--np-shape-corner-none0
--np-shape-corner-extra-small0.25rem
--np-shape-corner-small0.5rem
--np-shape-corner-medium0.75rem
--np-shape-corner-large1rem
--np-shape-corner-extra-large1.75rem
--np-shape-corner-full9999px

Motion tokens

Motion tokens pair a duration with an easing curve. Spatial tokens move things, effects tokens change color and opacity. The expressive curves overshoot slightly and settle, the standard curves do not.

TokenDuration
--np-motion-expressive-fast-spatial350ms
--np-motion-expressive-default-spatial500ms
--np-motion-expressive-slow-spatial650ms
--np-motion-expressive-fast-effects150ms
--np-motion-expressive-default-effects200ms
--np-motion-expressive-slow-effects300ms
--np-motion-standard-fast-spatial350ms
--np-motion-standard-default-spatial500ms
--np-motion-standard-slow-spatial750ms
--np-motion-standard-fast-effects150ms
--np-motion-standard-default-effects200ms
--np-motion-standard-slow-effects300ms
transition: background-color var(--np-motion-expressive-default-effects);

Elevation tokens

Three layered shadows, from the lightest lift to the most pronounced. Cards, menus and dialogs use them, and so can you.

box-shadow: var(--np-elevation-1);
box-shadow: var(--np-elevation-2);
box-shadow: var(--np-elevation-3);

Overriding a single component

Components expose their own properties, named --np-<component>-<part>-<role>. Set one as a style prop to restyle a single instance, or on any ancestor to restyle a whole region.

<script lang="ts">
	import { Button } from 'noph-ui'
</script>

<Button variant="filled">Default</Button>
<Button
	variant="filled"
	--np-filled-button-container-color="var(--np-color-tertiary)"
	--np-filled-button-label-text-color="var(--np-color-on-tertiary)"
>
	Tertiary
</Button>
<Button variant="filled" shape="square" --np-button-shape="var(--np-shape-corner-extra-small)">
	Square
</Button>