
Research
/Security News
Popular Rust Crates Compromised in Build-Time Supply Chain Attack
Three compromised Rust crates pulled in a malicious dependency that downloaded and executed cross-platform malware during Cargo builds.
@mythxengine/glyphkit
Advanced tools
GlyphKit — 16-bit pixel-art React design system with genre-specific themes, runtime CSS variable generation, and optional Framer Motion integration.
@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 the 8 built-in presets (Fantasy, Sci-Fi, Paranoia, Horror, and Dungeon aesthetics) is a single hook call — no static CSS bundle, no FOUC.
The top-level entry is motion-free: components render static DOM unless you opt in to animation. The optional @mythxengine/glyphkit/motion subpath ships GlyphMotionProvider (which switches the core components to pixel-perfect Framer Motion animation) plus standalone motion wrappers with step-based easing that matches steps(N, end) timing.
pnpm add @mythxengine/glyphkit
# or
npm install @mythxengine/glyphkit
Peer 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 (incl. GlyphMotionProvider) |
The top-level import never loads framer-motion — theming, components, and icons work without it installed.
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.), the stepped animation utilities (.animate-pixel-pulse/bounce/blink/spin/fade-in/pop), the screen transitions (.transition-pixel-scanline-in, -flash-*, -wipe-*, -pixelate-*, -glitch-*), 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 | Purpose |
|---|---|
@mythxengine/glyphkit | Theme provider, registry, generator, components, transitions, icons — motion-free |
@mythxengine/glyphkit/motion | GlyphMotionProvider + 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) |
| ID | Name | Genre | Primary | Accent |
|---|---|---|---|---|
deadband-grim | Deadband — Doomed Seas | Horror | Rust (#c8804a) | Lantern Gold (#e0a020) |
cassette-retro | Deadband Terminal | Sci-Fi | Oxide Amber (#d35600) | CRT Amber (#f2a900) |
cassette-phosphor | Deadband Phosphor | Sci-Fi | Phosphor Green (#33ff66) | Lime (#aaff33) |
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) |
<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();
import { ThemeSelector, ThemeToggle } from "@mythxengine/glyphkit";
<ThemeSelector /> // button grid
<ThemeSelector compact /> // dropdown
<ThemeToggle /> // cycle through themes
All exported from the top level (motion-free):
Frame, Card, Dialog, AlertDialog / ConfirmDialog, Drawer, Popover, Tooltip, Separator, ScrollArea, EmptyStateCollapsible / CollapsibleContent (stepped grid-rows height animation, framer-free), Accordion (single/multiple, composed from Collapsible)Button, Toggle, ToggleGroup, DropdownMenu, ContextMenu (right-click at pointer, viewport-clamped), Menubar (APG menubar keyboard pattern) — all three menus share the DropdownMenuItem[] model (items, separators, disabled, danger). ⚠️ ContextMenu positions with position: fixed + viewport coordinates, so it must not sit inside a transformed/filtered ancestor (CSS transform, filter, will-change, … make that ancestor the containing block — this includes framer-motion elements mid-animation); see the component JSDoc.Input, Textarea, Label, Select, Combobox (APG editable combobox: typeahead-filtered listbox popup, groups, loading + empty states), PixelCheckbox, RadioGroup, Switch, PixelSliderCommand (inline ⌘K-style palette: filter input over grouped results, match highlighting, Kbd shortcuts) and CommandDialog (the same palette composed inside Dialog) — see Combobox & CommandBadge, Chip, Alert, Progress (determinate / indeterminate / pixel-segment), Spinner, Skeleton, Stat, Avatar / AvatarGroup, KbdPixelToast (standalone), ToastProvider + useToast + Toaster (FIFO queue — see Toasts)PixelTabs, Breadcrumb (nav landmark, collapsed-middle overflow), Pagination (ellipsis ranges, compact variant)Icon, ThemeSelector, ThemeToggleTypewriterText, SegmentClock, OutcomeBadge, StakesChips, GrainVignette — see Domain primitives (TTRPG)Interactive primitives follow the WAI-ARIA authoring patterns (roving
tabindex + arrow keys on RadioGroup/ToggleGroup/PixelTabs and across
Accordion headers / Menubar items / menu items, role="switch" on
Switch, aria-pressed on Toggle, keyboard-operable dismiss on
Chip/Alert, focus trap + focus return on AlertDialog (and opt-in on
Dialog via trapFocus — one shared trap stack, so stacked modals
compose; CommandDialog uses it),
aria-current="page" on Breadcrumb/Pagination, and the editable
combobox pattern on Combobox/Command — aria-activedescendant
visual cursor, DOM focus stays in the input).
import { ToastProvider, Toaster, useToast } from "@mythxengine/glyphkit";
// once, near the app root
<ToastProvider maxVisible={3} duration={5000}>
<App />
<Toaster placement="bottom-right" />
</ToastProvider>;
// anywhere below the provider
const { toast, update, dismiss } = useToast();
const id = toast({ title: "SAVING…", variant: "loading" }); // persists
update(id, { title: "SAVED", variant: "success", duration: 3000 });
Variants: info / success / warning / error (assertive role="alert") /
loading (persistent, blinking indicator). The queue is FIFO with at most
maxVisible on screen; hovering or focusing the viewport pauses every
visible countdown. Toasts render the PixelToast visuals — which also
remain available standalone (variant and inline props are additive).
Both ride one internal listbox core (APG editable-combobox pattern:
role="combobox" input, aria-activedescendant visual cursor with
wrap-around arrows + disabled-skip, DOM focus never leaves the input;
case-insensitive substring filtering by default, filter prop to
customize; "NO SIGNAL" pixel empty state).
import { Combobox, Command, CommandDialog } from "@mythxengine/glyphkit";
<Combobox
aria-label="Weapon"
options={[
{ value: "sword", label: "Rust Sword", group: "Melee" },
{ value: "bow", label: "Short Bow", group: "Ranged", disabled: true },
]}
onValueChange={setWeapon} // controlled via `value`, uncontrolled via `defaultValue`
loading={isFetching} // popup shows a Spinner
/>;
<Command
aria-label="Actions"
items={[{ label: "Roll dice", onSelect: roll, group: "Play", shortcut: "R" }]}
/>; // inline panel; matched substrings highlight, shortcuts render as Kbd
<CommandDialog open={paletteOpen} onClose={closePalette} items={items} />;
Escape unwinds one layer per press (topmost consumes exclusively):
Combobox closes the popup, then clears; CommandDialog clears an active
filter first, then closes the dialog. The inline surfaces never trap
Tab — each is a single tab stop, and Tab (or any focus departure) closes
the Combobox popup and reverts an abandoned draft to the selected
option's label (value untouched, no onValueChange; Escape instead keeps
the draft when closing, per the APG close-then-clear ladder).
CommandDialog is modal and does trap Tab: it sets Dialog's opt-in
trapFocus prop (default false; shares the AlertDialog trap stack so
stacked modals compose) and returns focus to the opener on close.
Positioning is the Popover model (no portal — string placement, popup
rendered next to the input), so the same containing-block caveats apply.
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;
/* Outcome tiers (five-tier resolution vocabulary) */
--outcome-critical-success;
--outcome-success;
--outcome-partial;
--outcome-failure;
--outcome-critical-failure;
/* 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;
Outcome-tier tokens are emitted for every preset: explicit per-preset values via colors.<scheme>.outcome (a partial block — pinned tiers win, unpinned tiers derive), otherwise derived from the preset's status colors (crit-gold ← warning, success ← success, partial ← warning, failure/crit-failure ← error). Built-in presets pin the tiers whose derived color fell below 3:1 contrast on the 20%-mix badge background (guarded by unit tests). resolveOutcomeColors(colors) exposes the resolved palette programmatically.
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 SSR
Higher-level primitives for tabletop play surfaces, ported from the MythX frame loop:
import {
TypewriterText, useTypewriter, // char-reveal narration; click-to-skip, reduced-motion aware
SegmentClock, // situation clocks: segment / compact / pie, paused + redacted (▓▓▓▓▓)
OutcomeBadge, OUTCOME_TIERS, // five-tier outcome badge + tier metadata (labels/icons/tokens)
StakesChips, // position/effect (+ "WIT · hard" test line) chips
GrainVignette, // film-grain + vignette overlay wash (mount once)
useReducedMotion, // framer-free matchMedia hook (also on /motion)
} from "@mythxengine/glyphkit";
<TypewriterText text={narration} onDone={showActions} />
<SegmentClock label="The Ritual Completes" filled={4} total={6} mode="countdown" />
<OutcomeBadge outcome="partial" />
<StakesChips stakes={{ position: "risky", effect: "standard", ability: "WIT", difficulty: "hard" }} />
<GrainVignette grain={0.06} vignette={0.55} />
OutcomeBadge colors read the --outcome-* tokens; OUTCOME_TIERS exposes each tier's label, short label, banner label, icon id, css var, and mount animation so consumers can compose their own tier surfaces.
The top-level @mythxengine/glyphkit chunk never imports framer-motion — its whole module graph is motion-free (a build-time test enforces this). Components that support motion (Button, Card, Dialog, Drawer, Icon, PixelToast) render static DOM by default. The motion prop and animation passthrough props (whileHover, transition, …) are still accepted for API compatibility; without a motion runtime they're simply stripped before reaching the DOM.
This means styles-only / theming-only consumers don't need framer-motion installed at all.
Install the optional peer and mount GlyphMotionProvider once near your app root:
pnpm add framer-motion
import { ThemeProvider, themeRegistry, Button } from "@mythxengine/glyphkit";
import { GlyphMotionProvider } from "@mythxengine/glyphkit/motion";
<ThemeProvider themes={themeRegistry}>
<GlyphMotionProvider>
{/* every GlyphKit component below now animates with its retro presets */}
<Button variant="pixel">START</Button>
</GlyphMotionProvider>
</ThemeProvider>;
No prop changes needed — the provider injects a framer-motion-backed runtime through context and the same components switch to their animated rendering path (prefers-reduced-motion still wins). Advanced consumers can supply a custom runtime via MotionRuntimeProvider from the top-level entry.
The /motion subpath also ships the standalone animation-only wrappers:
import { 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.
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.
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, …) |
import { Icon } from "@mythxengine/glyphkit";
import { getIconIdsByCategory, registerIcons } from "@mythxengine/glyphkit/icons";
<Icon name="ui-close" />
<Icon name="condition-poisoned" size="lg" animation="pulse" />
The built-in catalogs (UI / condition / genre) register lazily on first registry read — there is no side-effect import to keep alive, so the icon modules are safe under aggressive tree-shaking (the package's "sideEffects": ["**/*.css"] only shields stylesheet imports; every JS module remains tree-shakeable). Custom icons registered via registerIcon / registerIcons always win over the defaults, regardless of ordering. If you want eager registration (SSR warmup, tests), call registerDefaultIcons().
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>;
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.
New components live in src/components/<Name>.tsx and follow a settled shape — copy an existing sibling (e.g. Frame.tsx for static, Button.tsx for motion-capable) rather than inventing structure:
cva from class-variance-authority; every color/size/typography value reads a theme CSS var with a fallback (text-[length:var(--typo-body,14px)], bg-[var(--primary)], border-[var(--foreground-muted)]). No hardcoded palette values, no border-radius (this is a pixel system — rounded-none). Reuse the utility classes from @mythxengine/glyphkit/styles (border-pixel*, shadow-pixel-*, font-pixel).src/components/. Read the injected runtime via useMotionRuntime() from ../motion-runtime/index.js and branch: static DOM when null (strip passthrough props with stripMotionProps), runtime.motion.* elements when present. Preset data comes from src/motion/variants/* (type-only framer imports there are fine). If you expose a motion prop, declare the preset union as a literal list (see BUTTON_MOTION_PRESETS) and add it to src/__tests__/motion-presets-sync.test.ts.src/generator/css-generator.ts if the component needs a new global utility class or keyframe (like the skeleton pulse). Component-local styling belongs in the component's cva classes.Props type) from the component file, re-export through src/components/index.ts. Never add a default export; never make the module side-effectful ("sideEffects" lists only **/*.css — JS modules must stay pure so they remain tree-shakeable).stories/3-Primitives/<Name>.stories.tsx in the monorepo root with title: "3 Primitives/<Name>". Cover each visual variant, and give at least the primary stories a play function (storybook/test: within(canvasElement) + userEvent + expect) asserting render and core interaction. pnpm test-storybook runs them headless.react-dom/server) goes in src/__tests__/*.test.ts(x); pnpm --filter @mythxengine/glyphkit test must stay green, including the motion-free dist-graph test.pnpm changeset).GlyphKit follows Semantic Versioning. Releases are managed with Changesets; see the monorepo .changeset/ directory for in-flight changes.
MIT © Josh Mabry / protoLabs. See LICENSE.
FAQs
GlyphKit — 16-bit pixel-art React design system with genre-specific themes, runtime CSS variable generation, and optional Framer Motion integration.
The npm package @mythxengine/glyphkit receives a total of 8 weekly downloads. As such, @mythxengine/glyphkit popularity was classified as not popular.
We found that @mythxengine/glyphkit demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
Three compromised Rust crates pulled in a malicious dependency that downloaded and executed cross-platform malware during Cargo builds.

Research
/Security News
Socket uncovered 77 linked Firefox extensions, including 40 that steal wallet secrets or credentials and 37 deceptive sports-score shells.

Security News
NIST disclosed an unreleased AI tool called V-etalon and opened a broad inquiry into NVD modernization after years of automation plans produced no public enrichment system.