Sign In

@mythxengine/glyphkit

Package Overview
Dependencies
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@mythxengine/glyphkit

GlyphKit — 16-bit pixel-art React design system with genre-specific themes, runtime CSS variable generation, and optional Framer Motion integration.

latest
Source
npmnpm
Version
0.2.0
Version published
Maintainers
1
Created
Source

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 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.

Installation

pnpm add @mythxengine/glyphkit
# or
npm install @mythxengine/glyphkit

Peer dependencies:

PackageVersionRequired?
react^18.0.0 || ^19.0.0yes
react-dom^18.0.0 || ^19.0.0yes
framer-motion^11.0.0 || ^12.0.0optional — 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.

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.), 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 exports

SubpathPurpose
@mythxengine/glyphkitTheme provider, registry, generator, components, transitions, icons — motion-free
@mythxengine/glyphkit/motionGlyphMotionProvider + Framer Motion wrappers (PixelButton, PixelCard, hooks, …)
@mythxengine/glyphkit/iconsTheme-aware icon registry built on pixelarticons
@mythxengine/glyphkit/stylesBaseline stylesheet: pixel utility classes, effects, a11y defaults (CSS)

Theme Presets

IDNameGenrePrimaryAccent
deadband-grimDeadband — Doomed SeasHorrorRust (#c8804a)Lantern Gold (#e0a020)
cassette-retroDeadband TerminalSci-FiOxide Amber (#d35600)CRT Amber (#f2a900)
cassette-phosphorDeadband PhosphorSci-FiPhosphor Green (#33ff66)Lime (#aaff33)
kingdom-questKingdom QuestFantasyPurple (#9686ab)Gold (#d4a72c)
neon-terminalNeon TerminalSci-FiCyan (#00d4ff)Magenta (#ff00ff)
alpha-complexAlpha ComplexParanoiaRed (#cc0000)Yellow (#ffcc00)
shadow-realmShadow RealmHorrorCrimson (#8b2942)Sickly Green (#4a8b2a)
deep-dungeonDeep DungeonDungeonStone 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 themes

Component set

All exported from the top level (motion-free):

  • Surfaces & structureFrame, Card, Dialog, AlertDialog / ConfirmDialog, Drawer, Popover, Tooltip, Separator, ScrollArea, EmptyState
  • DisclosureCollapsible / CollapsibleContent (stepped grid-rows height animation, framer-free), Accordion (single/multiple, composed from Collapsible)
  • Actions & menusButton, 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.
  • FormsInput, Textarea, Label, Select, Combobox (APG editable combobox: typeahead-filtered listbox popup, groups, loading + empty states), PixelCheckbox, RadioGroup, Switch, PixelSlider
  • Command paletteCommand (inline ⌘K-style palette: filter input over grouped results, match highlighting, Kbd shortcuts) and CommandDialog (the same palette composed inside Dialog) — see Combobox & Command
  • Status & dataBadge, Chip, Alert, Progress (determinate / indeterminate / pixel-segment), Spinner, Skeleton, Stat, Avatar / AvatarGroup, Kbd
  • FeedbackPixelToast (standalone), ToastProvider + useToast + Toaster (FIFO queue — see Toasts)
  • NavigationPixelTabs, Breadcrumb (nav landmark, collapsed-middle overflow), Pagination (ellipsis ranges, compact variant)
  • Icons & themingIcon, ThemeSelector, ThemeToggle
  • Domain (TTRPG)TypewriterText, 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/Commandaria-activedescendant visual cursor, DOM focus stays in the input).

Toasts

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).

Combobox & Command

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.

CSS 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;

/* 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.

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 SSR

Domain primitives (TTRPG)

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.

Motion

The motion-free core (default)

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.

Turning animation on

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.

Motion wrappers

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>
PresetStepsDurationUse case
instant10msSnap
retro22100msNES-style
retro44200msSNES-style (default)
retro88400msSmoother 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:

FamilyStates
buttonVariantPresetsinitial, hover, tap, disabled, pulse (neon only)
cardVariantPresetsinitial, hover, tap, selected, hidden, visible, enter, exit
dialogVariantPresetshidden, visible, exit (note: no initial)
statusBarVariantPresetscomplete, countdown, counting, critical, damage, …
cursorVariantPresetsstill, visible, blink, bob, pulse, spin, selected
genre presetseach variant exposes its own (chestOpen, hologram, bloodDrip, …)

Icons

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().

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.

Contributing: adding a component

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:

  • Styling — variants via 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).
  • Motion (only if the component animates) — never import framer-motion in 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.
  • CSS generator — only touch 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.
  • Exports — named export (+ 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 — add 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.
  • Unit tests — pure logic (class output, registry behavior, SSR rendering via 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.
  • Docs + changeset — update the README (component mention, any new preset/util) and add a changeset (pnpm changeset).

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.

Keywords

design-system

FAQs

Package last updated on 12 Jul 2026

Did you know?

Socket

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.

Install

Related posts