@inova.dev/neu-ui
v0.1.0
Published
Neumorphism as primitives — soft-UI shapes (NeuShape), embossed type (NeuText), and the one-knob token/shadow algorithm behind them. React + vanilla, framework-agnostic CSS.
Downloads
79
Maintainers
Readme
@inova.dev/neu-ui
Neumorphism as primitives. Not a box with a hard-coded shadow — a tiny system that makes any silhouette or glyph read as soft-UI, driven by one knob.
NeuShape— neumorphism for an arbitrary SVG silhouette (not just a rectangle). A shape filled with the surface colour vanishes into the page; the soft-UI read is carried by two offset shadows that follow the path's alpha —drop-shadow()×2 for raised, an SVG inner-shadow filter for pressed.NeuText— neumorphism for type. The glyphs are the surface colour and are defined only by a dualtext-shadow, so the emboss is font-agnostic by construction.- The algorithm — one CSS custom property,
--neu-base, derives the light/dark edge tones per channel (relative-color syntax), so every surface recolours from a single value. Ships as--neu-out/--neu-inshadow recipes and a.neu-card.
Both primitives come in two flavours from one package: a React entry and a framework-agnostic vanilla entry.
Install
npm i @inova.dev/neu-uireact / react-dom are optional peers — install them only if you use the React
entry. The /vanilla entry and the CSS work with no React on the page.
React
import { NeuShape, NeuText } from '@inova.dev/neu-ui'
import '@inova.dev/neu-ui/css' // the token engine + card recipe
export function Example() {
return (
<div data-theme="dark" style={{ background: 'var(--neu-base)', padding: 40 }}>
<NeuShape
viewBox="0 0 24 24"
d="M12 3c3.5 3 6 5.5 6 9a6 6 0 0 1-12 0c0-3.5 2.5-6 6-9z"
variant="raised"
size={72}
/>
<NeuText as="h2" variant="pressed" depth={3}>Soft UI</NeuText>
<div className="neu-card" style={{ padding: 24 }}>A raised card.</div>
</div>
)
}Vanilla (no React)
<link rel="stylesheet" href="node_modules/@inova.dev/neu-ui/css/index.css" />
<div data-theme="dark" style="background:var(--neu-base);padding:40px" id="host"></div>
<script type="module">
import { neuShape, neuText } from '@inova.dev/neu-ui/vanilla'
document.getElementById('host').append(
neuShape({ d: 'M12 3c3.5 3 6 5.5 6 9a6 6 0 0 1-12 0c0-3.5 2.5-6 6-9z', variant: 'raised', size: 72 }),
neuText({ text: 'Soft UI', as: 'h2', variant: 'pressed', depth: 3 }),
)
</script>neuShapeHTML(opts) / neuTextHTML(opts) return a string for templating / innerHTML.
The CSS algorithm
Importing @inova.dev/neu-ui/css gives you:
| Token | What it is |
| --- | --- |
| --neu-base | the surface material everything melts into (set per theme) |
| --neu-intensity | how deep the light/dark split is |
| --neu-d / --neu-l | dark/light edge tones, derived from --neu-base |
| --neu-out / --neu-out-md / --neu-out-sm | raised shadow recipes |
| --neu-in / --neu-in-sm | pressed (inset) shadow recipes |
| --neu-depth / --neu-blur | @property-registered → animatable (GSAP, Tailwind, keyframes) |
| .neu-card, .neu-card--pressed, .neu-card--sm | ready card recipe |
| .neu-breathe | a ready "breathing" extrusion keyframe |
Theme it by setting --neu-base (and optionally --neu-intensity) on :root,
[data-theme="…"], or any element — the derived tones and every surface follow.
Granular CSS imports are available too: @inova.dev/neu-ui/css/core,
/css/card, /css/shape.
Animating the extrusion
--neu-depth and --neu-blur are registered with @property so they interpolate.
The components read them as var() fallbacks, so the prop is the default and any
override animates:
<NeuShape … className="neu-breathe" /> // ready CSS keyframe
gsap.to(ref.current, { '--neu-depth': 12, duration: 0.4 }) // GSAP
className="transition-[--neu-depth] hover:[--neu-depth:6px]" // TailwindRoadmap
Planned follow-on entries (not in this version):
@inova.dev/neu-ui/shape-path (polygon → rounded SVG path engine) and
@inova.dev/neu-ui/themes (a multi-palette theme registry).
License
MIT
