Frecency
Demonstrates frecency-boosted search ranking. Items you use frequently and recently get a bonus added to their match score for non-empty queries, which changes the ranked result order the machine uses for the default active item and keyboard navigation.
Basic Frecency
Frecency is off unless enabled: true is set (enabled is optional and defaults to false), so every sample here sets it.
'use client';
import { Command } from 'modern-cmdk/react';
export function FrecencyExample() {
return (
<Command frecency={{ enabled: true }} label="Frecency Demo">
<Command.Input placeholder="Search (try selecting items multiple times)..." />
<Command.List>
<Command.Empty>No results.</Command.Empty>
<Command.Group heading="Actions">
<Command.Item value="new-file" onSelect={() => console.log('New File')}>
New File
<Command.Shortcut shortcut="Mod+N" />
</Command.Item>
<Command.Item value="open-file" onSelect={() => console.log('Open File')}>
Open File
<Command.Shortcut shortcut="Mod+O" />
</Command.Item>
<Command.Item value="save" onSelect={() => console.log('Save')}>
Save
<Command.Shortcut shortcut="Mod+S" />
</Command.Item>
<Command.Item value="save-as" onSelect={() => console.log('Save As')}>
Save As
<Command.Shortcut shortcut="Mod+Shift+S" />
</Command.Item>
</Command.Group>
<Command.Group heading="Navigation">
<Command.Item value="settings" onSelect={() => console.log('Settings')}>
Settings
<Command.Shortcut shortcut="Mod+," />
</Command.Item>
<Command.Item value="profile" onSelect={() => console.log('Profile')}>
Profile
</Command.Item>
<Command.Item value="billing" onSelect={() => console.log('Billing')}>
Billing
</Command.Item>
</Command.Group>
</Command.List>
</Command>
);
}Select "Settings" a few times, then type a query that matches it (for example s). Its frecency bonus is added to its match score, so it moves up the machine's ranked results (state.filteredIds): that order picks the default active item and the arrow-key order. The rendered (non-virtualized) list keeps the JSX order of the <Command.Item> elements, so the item does not move on screen. With an empty search frecency is not applied, and state.filteredIds is registration order.
IndexedDB Persistence
Persist frecency data across browser sessions with IdbFrecencyStorage:
'use client';
import { Command } from 'modern-cmdk/react';
import { IdbFrecencyStorage } from 'modern-cmdk';
// Create storage once (singleton)
const storage = new IdbFrecencyStorage('my-app-db', 'frecency');
export function PersistentFrecencyExample() {
return (
<Command
frecency={{
enabled: true,
storage,
namespace: 'command-palette',
}}
label="Persistent Frecency"
>
<Command.Input placeholder="Your history persists across reloads..." />
<Command.List>
<Command.Empty>No results.</Command.Empty>
<Command.Item value="settings" onSelect={() => console.log('Settings')}>
Settings
</Command.Item>
<Command.Item value="profile" onSelect={() => console.log('Profile')}>
Profile
</Command.Item>
<Command.Item value="billing" onSelect={() => console.log('Billing')}>
Billing
</Command.Item>
<Command.Item value="analytics" onSelect={() => console.log('Analytics')}>
Analytics
</Command.Item>
</Command.List>
</Command>
);
}TIP
IdbFrecencyStorage uses idb-keyval@6.3.0 under the hood. It is included as a direct dependency of modern-cmdk, so no separate install is needed.
Custom Decay Curve
Tune the decay weights to match your usage patterns:
import { Command } from 'modern-cmdk/react';
export function TunedFrecencyExample() {
return (
<Command
label="Tuned Frecency"
frecency={{
enabled: true,
decayConfig: {
hourWeight: 6.0, // Very strong boost for items used in the last hour
dayWeight: 3.0, // Strong boost for today
weekWeight: 1.5, // Moderate boost for this week
monthWeight: 0.8, // Slight boost for this month
olderWeight: 0.2, // Minimal boost for older items
},
}}
>
{/* ... */}
</Command>
);
}Core Engine Usage
Frecency works at the core state machine level, independent of React:
import { createCommandMachine, itemId } from 'modern-cmdk';
import type { CommandState } from 'modern-cmdk';
import { IdbFrecencyStorage } from 'modern-cmdk';
async function demo() {
await using storage = new IdbFrecencyStorage();
using machine = createCommandMachine({
items: [
{ id: itemId('settings'), value: 'Settings' },
{ id: itemId('profile'), value: 'Profile' },
],
frecency: {
enabled: true,
storage,
namespace: 'my-app',
},
});
// Select an item -- recorded with Date.now() when the event runs
machine.send({ type: 'ITEM_SELECT', id: itemId('settings') });
// Frecency adds its bonus only for a non-empty query; an empty query keeps
// registration order.
machine.send({ type: 'SEARCH_CHANGE', query: 'set' });
// send() is batched: both events run on the machine's next scheduler tick
// (requestAnimationFrame in browsers). Wait for that tick before the `using`
// scope ends, because disposing the machine drops events that have not run.
const state = await new Promise<CommandState>((resolve) => {
const sub = machine.subscribeState((next) => {
if (next.search !== 'set') return;
sub[Symbol.dispose]();
resolve(next);
});
});
return state.filteredIds; // the ranked order for 'set'
}