SpaceUI: A Component Library Evolved from TerraForge
Product

SpaceUI: A Component Library Evolved from TerraForge

A dark, lit-glass React design system extracted from TerraForge, and the first one I've published outside my professional work at Under Armour and Zebra. Zero dependencies, 22 kB gzipped, theming that is nothing but CSS variables.

Every component in SpaceUI existed before the library did. They were running in TerraForge, holding up a dense, dark interface where people adjust values and watch a planet respond in real time. At some point I realized I had been designing a system without calling it one, and that the only thing standing between a folder of components and an actual design system was the decision to treat it like one.

So I extracted it, documented it, and published it. I've spent years building design systems inside organizations, at Under Armour and Zebra, but this is the first one I've put out under my own name, and it came out of a product rather than a spec.

The design language is the component

Most systems hand you unstyled primitives and a theme to paint them with. SpaceUI does not separate those two things. A Button is a lit-glass capsule whose rim catches light the way a planet's atmosphere does. A Slider is a glass tube with a limb-lit moon you drag along it. There is no plain mode, because a plain mode would be a different library.

That is an opinionated position and I want to be upfront about it. You install this because you want the look, not because you want a blank canvas.

Setup is two imports and nothing else:

1import "@inkorange/space-ui/styles.css";
2import { Card, Heading, Text, Badge, Button } from "@inkorange/space-ui";
3
4export function Planet() {
5 return (
6 <Card>
7 <Heading size="5">Kepler-442b</Heading>
8 <Badge color="success">Temperate</Badge>
9 <Text size="2" color="muted">
10 A super-earth in the habitable zone, 1,206 light years out.
11 </Text>
12 <Button>Build planet</Button>
13 </Card>
14 );
15}

Every component arrives already wearing the design.

Motion you can turn off without losing the look

Ambient motion suits a hero button and gets annoying fast in a dense form. So the movement is separable from the appearance. One prop stops the loops and leaves the glass exactly as it was:

1<Button>Build planet</Button>
2<Button animated={false}>Build planet</Button>

Anyone who has asked their system for reduced motion gets the stilled version automatically, regardless of what the prop says. That is not a feature so much as the correct default.

Built for dense, dark interfaces

This is where the TerraForge origin shows. The components had to survive a screen with a lot of controls on it at once, so the small annoyances got solved out of necessity rather than diligence.

Select sizes itself to its widest option, so the trigger does not jump around as you change the value. Dialogs ride the browser's own top layer, which means focus trapping and Escape are the platform's job instead of my approximation of it. Tabs actually connect to the panel underneath them.

None of that is exciting to describe. All of it is the difference between a component you demo and one you ship.

Theming is nothing but CSS variables

No component holds a value of its own. Every color, size, and duration resolves through a custom property in the --sp- namespace, and the defaults ship inside styles.css. Retheming is a block of variables you write. It is never a fork, a config file, or a build step.

There are three tiers. Palette roles come first, named for the job the color does rather than the hue it happens to be:

1:root {
2 --sp-primary-solid: #7c5cff; /* solid accent fill */
3 --sp-primary-text: #b9a6ff; /* accent text and links */
4 --sp-gray-panel: #14161c; /* card and panel fills */
5 --sp-gray-border: #d6ebfd30; /* hairlines and borders */
6 --sp-danger-text: #ff9592; /* destructive, not "red" */
7}

Then surface channels. The lit glass is layered translucency, so these carry color channels and let each layer choose its own alpha:

1:root {
2 --sp-rim-rgb: 190 160 255; /* the lit atmosphere limb */
3 --sp-glow-rgb: 150 130 255; /* hover and focus bloom */
4 --sp-glass-rgb: 18 14 34; /* the glass fill itself */
5}

Those read internally as rgb(var(--sp-rim-rgb) / 0.4), so they need space-separated channels. Hand them a hex value and the alpha has nothing to work with.

Finally, component-level dials, named --sp-[component]-[modifier]-[type]:

1--sp-progress-height: 12px;
2--sp-select-trigger-max-width: 24rem;
3--sp-loader-moon-size: 10px;

Because these are ordinary CSS variables, they cascade. Put them on :root to theme the whole app, or on any element to retheme one subtree. A warmer palette inside a single panel is four lines:

1.launch-panel {
2 --sp-glass-rgb: 30 16 14;
3 --sp-rim-rgb: 255 180 130;
4 --sp-control-height: 32px;
5}

You override only what you name. Everything else keeps its shipped value, and supplying nothing at all still renders exactly what you see in the gallery.

What's in it

Typography covers Text, Link, Heading, Badge, and Separator. Layout has Card, Flex, Grid, and Box. Forms are the deepest part of the set: Autocomplete, TextField, TextArea, Select, Slider, and RadioGroup. Buttons are Button and IconToggle. Feedback covers Loader, Progress, and Message. Overlays include Dialog, AlertDialog, DropdownMenu, Tabs, and Tooltip. There are 20 icons, plus foundation pages for color, spacing, type scale, motion, and token usage.

Every component has a live page in the gallery showing the thing running, the source behind it, its full prop table with defaults, and the custom properties it exposes.

What you get

Zero runtime dependencies and roughly 22 kB over the wire gzipped, which breaks down as a 16 kB ESM bundle plus a 7 kB stylesheet. React 19, precompiled CSS, and no build tooling required downstream. Both size numbers get measured from the build on every CI run, so the badges cannot quietly drift away from reality.

It requires React 19 and a dark surface. There is deliberately no light theme. These components assume the dark they were drawn for, and bolting on a light mode would mean redrawing all of them.

Why it exists

Most of my career has been component libraries and design systems, at Under Armour and Zebra, always for an organization and almost always starting from a specification. Somebody decides what the system should be, and then you build it. This one went the other direction. The components earned their way into a library by working in a real product first, which meant they arrived with their edge cases already found instead of imagined.

That turns out to be a much better way to build a system, and a much harder one to plan for. You cannot extract a design language from a product that does not have one yet.

The gallery lives at space-components.vercel.app, and the package is on npm. MIT licensed. If you are building something dark and dense, start with the introduction page, which covers the token system and how to retheme it.