Skip to main content
This guide will help you set up achievements-manager in your project. Choose the section that matches your framework:

React

React 19+ with hooks and factory

Vanilla JS

Framework-agnostic core engine

React

For React applications, use the factory pattern to create typed hooks and a Provider in one call.
1

Install the package

npm install achievements-react
2

Define your achievements

Create a module-level file that exports your typed hooks and engine:
src/achievements.ts
import { createAchievements, defineAchievements, localStorageAdapter } from 'achievements-react'

const definitions = defineAchievements([
  { id: 'first-visit',  label: 'First Visit',  description: 'Open the app.'     },
  { id: 'click-frenzy', label: 'Click Frenzy', description: 'Click 50 times.',
    maxProgress: 50 },
])

export type AchievementId = typeof definitions[number]['id']

export const { engine, Provider, useAchievements, useIsUnlocked, useProgress } =
  createAchievements<AchievementId>({
    definitions,
    storage: localStorageAdapter('my-app'),
  })
The defineAchievements helper provides full type inference for your achievement IDs. No manual type annotations needed!
3

Wrap your app with the Provider

Add the Provider to your root component:
src/main.tsx
import { Provider } from './achievements'
import { createRoot } from 'react-dom/client'
import App from './App'

createRoot(document.getElementById('root')!).render(
  <Provider>
    <App />
  </Provider>,
)
4

Use the hooks in your components

Import and use the pre-typed hooks anywhere inside the Provider:
src/components/Example.tsx
import { useAchievements, useIsUnlocked, useProgress } from '../achievements'

function Example() {
  const { unlock, incrementProgress } = useAchievements()
  const visited = useIsUnlocked('first-visit')
  const { progress, max } = useProgress('click-frenzy')

  return (
    <>
      <button onClick={() => unlock('first-visit')}>Visit</button>
      <button onClick={() => incrementProgress('click-frenzy')}>
        Click ({progress}/{max})
      </button>
      {visited && <p>Welcome back!</p>}
    </>
  )
}
All hooks are fully typed with your custom AchievementId type. No generic annotations needed at the call site!

Available React Hooks

The factory returns these pre-typed hooks:
HookReturnsDescription
useAchievementsAchievementEngine<TId>Full engine with all methods
useIsUnlocked(id)booleanRe-renders only when this achievement’s lock state changes
useProgress(id){ progress: number, max?: number }Re-renders only when this achievement’s progress changes
useUnlockedCountnumberRe-renders only when the total count changes
useAchievementToast{ queue, dismiss }Toast queue for displaying notifications
useTamperDetectedstring | nullStorage key that failed integrity check

Vanilla / Framework-Agnostic

For vanilla JavaScript, Vue, Svelte, or server-side use, work directly with the engine:
1

Install the package

npm install achievements
2

Create the engine

import { createAchievements, localStorageAdapter } from 'achievements'

const engine = createAchievements({
  definitions: [
    { id: 'first-visit', label: 'First Visit', description: 'Open the app.' },
  ],
  storage: localStorageAdapter('my-app'),
  onUnlock: (id) => showNotification(id),
})

engine.unlock('first-visit')
engine.subscribe((state) => render(state))
3

Track achievements

Call engine methods from anywhere in your app:
// Unlock achievements
engine.unlock('first-visit')

// Track progress
engine.setProgress('click-frenzy', 25)
engine.incrementProgress('click-frenzy')

// Collect items (idempotent)
engine.collectItem('explorer', 'module-core')
engine.collectItem('explorer', 'module-react')

// Read state
const unlocked = engine.isUnlocked('first-visit')
const progress = engine.getProgress('click-frenzy')
const count = engine.getUnlockedCount()
4

Subscribe to changes

Listen for state updates and render your UI:
engine.subscribe((state) => {
  console.log('Unlocked:', [...state.unlockedIds])
  console.log('Progress:', state.progress)
  console.log('Toast queue:', state.toastQueue)
  
  // Update your UI
  renderAchievements(state)
})
The subscribe callback receives a full state snapshot after every mutation. Use it to keep your UI in sync.

Engine API Methods

Write Methods

  • unlock(id) - Unlocks an achievement
  • setProgress(id, value) - Sets absolute progress value
  • incrementProgress(id) - Increments progress by 1
  • collectItem(id, item) - Adds unique item to set
  • setMaxProgress(id, max) - Updates max progress at runtime
  • dismissToast(id) - Removes ID from toast queue
  • reset() - Wipes all state and storage

Read Methods

  • isUnlocked(id) - Returns boolean
  • getProgress(id) - Returns current progress number
  • getItems(id) - Returns Set of collected items
  • getUnlocked() - Returns Set of all unlocked IDs
  • getUnlockedCount() - Returns number of unlocked
  • getDefinition(id) - Returns achievement definition
  • getState() - Returns full state snapshot

Achievement Definition Fields

When defining achievements, use these fields:
FieldTypeRequiredDescription
idstringYesUnique identifier (inferred as literal type)
labelstringYesHuman-readable name for display
descriptionstringYesShort description of unlock condition
maxProgressnumberNoEnables progress tracking, auto-unlocks at threshold
hiddenbooleanNoHides achievement entirely until unlocked
hintbooleanNoHides only description until unlocked

Storage Adapters

The library includes built-in storage adapters:
import { localStorageAdapter } from 'achievements'

// With prefix (recommended)
const storage = localStorageAdapter('my-app')
// Keys: "my-app:unlocked", "my-app:progress", etc.

// Without prefix
const storage = localStorageAdapter()

Anti-Cheat & Tamper Detection

Every persisted entry is stored with an integrity hash (FNV-1a by default). On load, the hash is recomputed:
const engine = createAchievements({
  definitions,
  storage: localStorageAdapter('my-app'),
  onTamperDetected: (key) => {
    console.warn('Tamper detected on key:', key)
    // The corrupted entry is automatically wiped
    // You can show a warning or reset progress
  },
})
Hashes are stored in localStorage as plain strings, so a determined user can still forge them. This is a friction layer, not cryptographic security.

Complete Example

Here’s a complete working example with progress tracking and toasts:
Complete Example
import { createAchievements, defineAchievements, localStorageAdapter } from 'achievements-react'
import { useEffect } from 'react'

// Define achievements
const definitions = defineAchievements([
  { id: 'first-visit', label: 'First Visit', description: 'Open the app.' },
  { id: 'click-frenzy', label: 'Click Frenzy', description: 'Click 50 times.', maxProgress: 50 },
  { id: 'night-owl', label: 'Night Owl', description: 'Use the app after midnight.', hidden: true },
])

export type AchievementId = typeof definitions[number]['id']

export const {
  engine,
  Provider,
  useAchievements,
  useIsUnlocked,
  useProgress,
  useAchievementToast,
  useUnlockedCount,
} = createAchievements<AchievementId>({
  definitions,
  storage: localStorageAdapter('demo-app'),
  onUnlock: (id) => console.log('🎉 Unlocked:', id),
})

// Toast component
function AchievementToast() {
  const { queue, dismiss } = useAchievementToast()
  const id = queue[0]
  const def = id ? engine.getDefinition(id) : undefined

  useEffect(() => {
    if (!id) return
    const timer = setTimeout(() => dismiss(id), 3000)
    return () => clearTimeout(timer)
  }, [id, dismiss])

  if (!id || !def) return null

  return (
    <div className="toast">
      <strong>{def.label}</strong>
      <p>{def.description}</p>
      <button onClick={() => dismiss(id)}>×</button>
    </div>
  )
}

// Main component
function App() {
  const { unlock, incrementProgress } = useAchievements()
  const { progress, max } = useProgress('click-frenzy')
  const count = useUnlockedCount()

  return (
    <Provider>
      <AchievementToast />
      <h1>Achievements Demo</h1>
      <p>Unlocked: {count} / {definitions.length}</p>
      
      <button onClick={() => unlock('first-visit')}>
        Unlock First Visit
      </button>
      
      <button onClick={() => incrementProgress('click-frenzy')}>
        Click Me ({progress}/{max})
      </button>
    </Provider>
  )
}

Next Steps

Core API Reference

Complete engine API documentation

React Hooks

Detailed guide to all React hooks

Storage Adapters

Learn about storage backends and custom adapters

Anti-cheat

Tamper detection and hash adapters

Build docs developers (and LLMs) love