Design language
Principles
| Rule | What it means in practice |
|---|---|
| Glow is light, not paint. | A lit thing has a hot core, colored glass and a halo that spills onto what's around it. A flat colored shadow is paint. |
| Calm at rest, lit when it matters. | Nothing glows by default. Light marks state: on, selected, focused, live, or the one thing to press. A screen where everything glows has nothing lit. |
| One variable. | Color comes from --neon and nowhere else, so a single class recolors a component and everything inside it. |
| Hardware, built precisely. | Knobs have detents, meters have ballistics, split-flaps flip one card at a time. If a real object works a certain way, the component does too. |
| Yours once installed. | Every component is one file with no runtime package. If a rule here doesn't suit your product, change the file. |
Color
--neon holds three space-separated RGB channels, so any alpha works with rgb(var(--neon) / 0.4). Components that aren't given a color read --neon-accent from the nearest parent, and fall back to cyan. Details and recipes are on Theming.
- cyan#22d3ee
- blue#4f8bff
- violet#a855f7
- magenta#f046eb
- pink#ff5fa2
- red#ff4d5e
- orange#ff8a3d
- amber#fbbf24
- lime#a3e635
- green#34d399
- white#e6edf7
Surfaces
The page is near-black, #05060a, so light has something to fall on. Panels lift by a few points (#0a0b11, #10131b) and are separated by 1px white borders at 6 to 10% rather than by shadow. Text is #e8edf5 for content and #8a93a6 for anything secondary.
Light
A lit element is drawn in up to three passes. Most components use two; signage uses all three.
| Pass | Recipe |
|---|---|
| Core | White or a white-mixed tint, thin: about a third of the tube. Only on things that are fully on. |
| Glass | The color at full strength. This is the part that reads as the element's color. |
| Halo | The color again, blurred 14 to 40px at 30 to 55%. It's what makes a dark surface look lit. |
/* A lit control, as the switch does it */
box-shadow:
0 0 14px rgb(var(--neon) / calc(0.45 * var(--neon-glow, 1))),
inset 0 0 8px rgb(var(--neon) / calc(0.3 * var(--neon-glow, 1)));Every halo alpha is multiplied by --neon-glow, 1 by default. Set it to 0 for a flat dark interface, or up to 1.5 for something closer to signage. The Theme Studio exports it with the rest of your theme.
:root { --neon-glow: 0.6; } /* quieter everywhere */
.hero { --neon-glow: 1.4; } /* louder in one place */When things glow
| State | Treatment |
|---|---|
| At rest | No glow. A 1px border at white 8 to 10%, the glass dark. |
| Hover | The border tints toward the color at 35 to 45%. Still no halo: hovering isn't a state worth lighting. |
| Focus (keyboard) | A 2px ring at 60% of the color, offset 2px from the element. It reaches 4.4:1 against the page in every palette color. |
| On, checked, selected | Lit: glass plus halo, and a core on anything small enough to read as a tube. |
| Live | Lit, with a slow pulse only where something is actually happening right now: a recording, a deploy, a stream. |
| Error | The red gel, lit on the one field that's wrong. The rest of the form stays calm. |
| Disabled | Never lit, at 40 to 50% opacity. |
Type
Components inherit your app's font, so they fit whatever you already use. The NeonKit site and templates use this set; it's a good starting point if you have nothing yet.
| Role | Setting |
|---|---|
| Display | Mona Sans, width 112, weight 620, tracking −0.038em, line height 0.98. Short headlines, two lines at most. |
| Body | Mona Sans at width 100 and weight 400, 15.5 to 17px, line height 1.55 to 1.65, at most about 62 characters a line. |
| Labels | 13 to 14px, sentence case, weight 500. No small uppercase tracked labels above headings. |
| Values and code | Fragment Mono, only for code, commands, file paths and values that change. Use tabular-nums on anything that ticks. |
| Signs | Tilt Neon, only on actual neon lettering: Neon Sign, Neon Text and the like. |
Shape and space
| Token | Value |
|---|---|
| Controls | 8px radius at the default size (6px small, 12px large). Heights 32, 40 and 48px. |
| Panels | 10 to 14px radius, 1px border, no drop shadow on dark surfaces. |
| Nesting | Never more than two rounded boxes deep. Use space and a rule instead of a third box. |
| Hit targets | At least 40px on touch, even when the visible control is smaller. |
Motion
| Kind | Timing |
|---|---|
| Interface | 150 to 300ms on cubic-bezier(0.23, 1, 0.32, 1). Transform and opacity only, and always interruptible. |
| Ignition | 520ms: strike at 55%, full, a dip to 60%, full, a dip to 85%, hold. Only when something turns on. |
| Instruments | Their own physics: meters strike instantly and decay over 200ms, needles overshoot, peaks hold 900ms before falling. |
| Reduced motion | Every component gives a still, complete composition. Nothing that carries meaning depends on movement. |
Icons
NeonKit draws its own icon set as bent glass tube. Each glyph sits on a 24px grid with a 1.75 stroke, round caps and joins, and a minimum bend radius of 1. Where a tube passes behind another there's a blockout gap of 2 to 4 units, the way real neon is masked with paint. Names and props match lucide-react, so moving over is a change of import path.
import { PlugZap } from "@/components/ui/icons"
<PlugZap size={20} strokeWidth={1.5} />Accessibility
Components use native elements where they exist (a switch is a button with role="switch"), follow the WAI-ARIA patterns for the rest, and are fully usable from the keyboard. Glow never carries meaning on its own: a lit state always comes with a change in text, icon, position or ARIA state. Text meets AA contrast on the default surfaces.