@mythxengine/glyphkit
v0.1.0
Published
GlyphKit — 16-bit pixel-art React design system with genre-specific themes, runtime CSS variable generation, and optional Framer Motion integration.
Maintainers
Readme
GlyphKit
@mythxengine/glyphkit— a 16-bit pixel-art React design system
GlyphKit is a dark-mode-first, genre-aware design system built for retro and TTRPG-style applications. Themes are generated as CSS custom properties at runtime, so swapping between Fantasy, Sci-Fi, Paranoia, Horror, and Dungeon aesthetics is a single hook call — no static CSS bundle, no FOUC.
An optional @mythxengine/glyphkit/motion subpath ships pixel-perfect Framer Motion wrappers with step-based easing that matches steps(N, end) timing.
Installation
pnpm add @mythxengine/glyphkit
# or
npm install @mythxengine/glyphkitPeer dependencies:
| Package | Version | Required? |
| --------------- | ---------------------- | ---------------------------------------------------------------- |
| react | ^18.0.0 \|\| ^19.0.0 | yes |
| react-dom | ^18.0.0 \|\| ^19.0.0 | yes |
| framer-motion | ^11.0.0 \|\| ^12.0.0 | optional, only if you import from @mythxengine/glyphkit/motion |
Quick Start
import { ThemeProvider, themeRegistry, useTheme } from "@mythxengine/glyphkit";
function App() {
return (
<ThemeProvider themes={themeRegistry} defaultTheme="kingdom-quest">
<YourApp />
</ThemeProvider>
);
}
function YourApp() {
const { theme, setTheme } = useTheme();
return (
<div>
<h1 style={{ color: "var(--color-primary-500)" }}>{theme.displayName}</h1>
<button onClick={() => setTheme("neon-terminal")}>Switch to Sci-Fi</button>
</div>
);
}ThemeProvider injects the active theme's CSS custom properties on <html> and listens for prefers-color-scheme changes.
For the pixel-utility classes (.border-pixel*, .shadow-pixel-*, .font-pixel, .effect-scanlines, etc.) plus reduced-motion / focus-visible defaults, import the baseline stylesheet once at app entry:
import "@mythxengine/glyphkit/styles";The stylesheet is theme-neutral — ThemeProvider will overwrite the first-paint defaults at runtime.
Subpath exports
| Subpath | Purpose |
| ------------------------------ | ------------------------------------------------------------------------ |
| @mythxengine/glyphkit | Theme provider, registry, generator, components, transitions |
| @mythxengine/glyphkit/motion | Framer Motion wrappers (PixelButton, PixelCard, hooks, …) |
| @mythxengine/glyphkit/icons | Theme-aware icon registry built on pixelarticons |
| @mythxengine/glyphkit/styles | Baseline stylesheet: pixel utility classes, effects, a11y defaults (CSS) |
Theme Presets
| ID | Name | Genre | Primary | Accent |
| --------------- | ------------- | -------- | -------------------- | ---------------------- |
| kingdom-quest | Kingdom Quest | Fantasy | Purple (#9686ab) | Gold (#d4a72c) |
| neon-terminal | Neon Terminal | Sci-Fi | Cyan (#00d4ff) | Magenta (#ff00ff) |
| alpha-complex | Alpha Complex | Paranoia | Red (#cc0000) | Yellow (#ffcc00) |
| shadow-realm | Shadow Realm | Horror | Crimson (#8b2942) | Sickly Green (#4a8b2a) |
| deep-dungeon | Deep Dungeon | Dungeon | Stone Gray (#4a4a58) | Torchlight (#e07020) |
API
<ThemeProvider />
<ThemeProvider
themes={themeRegistry} // required
defaultTheme="kingdom-quest" // optional initial preset
defaultColorScheme="dark" // "dark" | "light"
storageKey="glyphkit-theme" // localStorage key (default)
>
{children}
</ThemeProvider>useTheme()
const {
theme, // PixelThemeConfig
themeId, // ThemePresetId
colorScheme, // "dark" | "light"
setTheme, // (id: ThemePresetId) => void
setColorScheme,
transition, // TransitionVariant
setTransition,
isLoaded, // boolean — true after hydration
} = useTheme();Pre-built UI
import { ThemeSelector, ThemeToggle } from "@mythxengine/glyphkit";
<ThemeSelector /> // button grid
<ThemeSelector compact /> // dropdown
<ThemeToggle /> // cycle through themesCSS Variables
Every preset emits a consistent token surface:
/* Colors */
--color-primary-500;
--color-accent-500;
--background-primary;
--foreground-primary;
--surface-primary;
/* Pixel styling */
--grid-unit: 8px;
--border-thin: 1px;
--border-medium: 2px;
--border-thick: 4px;
--shadow-inset;
--shadow-raised;
--shadow-glow;
/* Typography */
--font-pixel: "Press Start 2P", monospace;
--font-terminal: "VT323", monospace;
/* Animation */
--timing-function: steps(4, end);
--duration-fast: 100ms;
--duration-normal: 200ms;
--duration-slow: 400ms;Programmatic CSS
import { generateFullStylesheet, generateCSSVariables } from "@mythxengine/glyphkit";
const css = generateFullStylesheet(theme, "dark"); // full :root + [data-theme] block
const vars = generateCSSVariables(theme, "dark"); // object for inline styles or SSRMotion Module
pnpm add framer-motionimport { PixelMotion, PixelButton, pixelEasing } from "@mythxengine/glyphkit/motion";
<PixelButton variant="pixel" onClick={onClick}>Press Start</PixelButton>
<PixelMotion
as="div"
steps={4}
duration="fast"
pixelSnap
whileHover={{ y: -2 }}
whileTap={{ y: 2 }}
>
Hover me
</PixelMotion>| Preset | Steps | Duration | Use case |
| --------- | ----- | -------- | -------------------- |
| instant | 1 | 0ms | Snap |
| retro2 | 2 | 100ms | NES-style |
| retro4 | 4 | 200ms | SNES-style (default) |
| retro8 | 8 | 400ms | Smoother retro |
Components: PixelMotion, PixelButton, PixelCard, PixelStatusBar, PixelDialog, PixelIcon.
Hooks: usePixelAnimation, usePixelSpring, useReducedMotion.
All motion respects prefers-reduced-motion.
Escape hatch — when GlyphKit motion isn't enough
The motion module is opinionated about step-based easing (steps(N, end)) because that's what makes the aesthetic work. For animations that need genuinely smooth easing (bezier curves, spring physics that mid-flight, page-turn transitions, etc.), import framer-motion directly:
import { motion, AnimatePresence } from "framer-motion";
// Custom bezier, smooth — explicitly outside the step-easing model.
<motion.div animate={{ x: 100 }} transition={{ duration: 0.4, ease: [0.2, 0.8, 0.2, 1] }} />;The genre/variants helpers (buttonVariants, cardVariantPresets, etc.) compose with raw framer-motion — pass them into your own motion.X and you keep the look without going through PixelMotion.
Motion variant state vocabulary
Each variant family has its own lifecycle state names. Stick to these when composing custom motion.X elements with a preset:
| Family | States |
| ------------------------- | --------------------------------------------------------------------------- |
| buttonVariantPresets | initial, hover, tap, disabled, pulse (neon only) |
| cardVariantPresets | initial, hover, tap, selected, hidden, visible, enter, exit |
| dialogVariantPresets | hidden, visible, exit (note: no initial) |
| statusBarVariantPresets | complete, countdown, counting, critical, damage, … |
| cursorVariantPresets | still, visible, blink, bob, pulse, spin, selected |
| genre presets | each variant exposes its own (chestOpen, hologram, bloodDrip, …) |
Customising the Registry
import { createRegistry, themeRegistry, ThemeProvider } from "@mythxengine/glyphkit";
const custom = createRegistry({
"kingdom-quest": {
...themeRegistry["kingdom-quest"],
colors: {
...themeRegistry["kingdom-quest"].colors,
dark: {
...themeRegistry["kingdom-quest"].colors.dark,
primary: { 500: "#ff0000" },
},
},
},
});
<ThemeProvider themes={custom}>...</ThemeProvider>;Next.js (App Router)
Wrap the provider in a client component:
"use client";
import { ThemeProvider, themeRegistry } from "@mythxengine/glyphkit";
export function Providers({ children }: { children: React.ReactNode }) {
return <ThemeProvider themes={themeRegistry}>{children}</ThemeProvider>;
}The motion subpath requires the same "use client" boundary because Framer Motion is client-only.
Versioning & Releases
GlyphKit follows Semantic Versioning. Releases are managed with Changesets; see the monorepo .changeset/ directory for in-flight changes.
License
MIT © Josh Mabry / protoLabs. See LICENSE.
