Ninja Keys

Keyboard shortcuts interface for your website. Working with static HTML, Va...

README

Ninja Keys


Keyboard shortcut interface for your website that works with Vanilla JS, Vue, and React.
FOSSA Status
npm
npm

Demo


Demo

Motivation

A lot of applications support a common pattern where the user hits +k (or ctrl+k) and a search UI dialog appears.
I've recently seen this in Notion, Slack, Linear, Vercel and Algolia, but I'm sure there are plenty more.
Apple Spotlight, Alfred and the Raycast app also have a similar pattern, but with different shortcuts.
There are already some libraries built for this, but they are too framework specific, like Laravel only or React only
Nevertheless, mine is not a silver bullet and if you need more framework integration, check them out too.

I needed a keyboard interface for navigation with static websites without any frameworks.
At the same time, I have a few Vue projects where something like this could be useful,
so I decided to give it a try for Web Components and Lit Element.

Integrations


Features

- Keyboard navigation
- Light and dark theme built in
- Built-in icon support from Material font and custom svg icons
- Nested menu - a tree or flat data structure can be used
- Auto register your shortcuts
- Root search - for example, if you search "Dark," it will find it within the "Theme" submenu
- CSS variable to customize the view
- Customizable hotkeys to open/close etc. Choose what best fits your website.

Why the "Ninja" name?

Because it appears from nowhere and executes any actions quickly...
Or because it allows your users to become keyboard ninjas 🙃

Install from NPM

  1. ``` sh
  2. npm i ninja-keys
  3. ```
Import if you are using webpack, rollup, vite or other build system.
  1. ``` js
  2. import 'ninja-keys';
  3. ```

Install from CDN

Mostly for usage in HTML/JS without a build system.
  1. ``` html
  2. <script type="module" src="https://unpkg.com/ninja-keys?module"></script>
  3. ```
or inside your module scripts
  1. ``` html
  2. <script type="module">
  3.   import {NinjaKeys} from 'https://unpkg.com/ninja-keys?module';
  4. </script>
  5. ```

Usage


Add the tag to your HTML.

  1. ``` html
  2. <ninja-keys> </ninja-keys>
  3. ```

  1. ``` html
  2. <script>
  3.   const ninja = document.querySelector('ninja-keys');
  4.   ninja.data = [
  5.     {
  6.       id: 'Projects',
  7.       title: 'Open Projects',
  8.       hotkey: 'ctrl+N',
  9.       icon: 'apps',
  10.       section: 'Projects',
  11.       handler: () => {
  12.         // it's auto register above hotkey with this handler
  13.         alert('Your logic to handle');
  14.       },
  15.     },
  16.     {
  17.       id: 'Theme',
  18.       title: 'Change theme...',
  19.       icon: 'desktop_windows',
  20.       children: ['Light Theme', 'Dark Theme', 'System Theme'],
  21.       hotkey: 'ctrl+T',
  22.       handler: () => {
  23.         // open menu if closed. Because you can open directly that menu from it's hotkey
  24.         ninja.open({ parent: 'Theme' });
  25.         // if menu opened that prevent it from closing on select that action, no need if you don't have child actions
  26.         return {keepOpen: true};
  27.       },
  28.     },
  29.     {
  30.       id: 'Light Theme',
  31.       title: 'Change theme to Light',
  32.       icon: 'light_mode',
  33.       parent: 'Theme',
  34.       handler: () => {
  35.         // simple handler
  36.         document.documentElement.classList.remove('dark');
  37.       },
  38.     },
  39.     {
  40.       id: 'Dark Theme',
  41.       title: 'Change theme to Dark',
  42.       icon: 'dark_mode',
  43.       parent: 'Theme',
  44.       handler: () => {
  45.         // simple handler
  46.         document.documentElement.classList.add('dark');
  47.       },
  48.     },
  49.   ];
  50. </script>
  51. ```
Library using flat data structure inside, as in the example above. But you can also use a tree structure as below:
  1. ``` js
  2. {
  3.   id: 'Theme',
  4.   children: [
  5.     { id: 'light' title: 'light_mode', },
  6.     { id: 'System Theme',
  7.       children: [
  8.         { title: 'Sub item 1' },
  9.         { title: 'Sub item 2' }
  10.       ]
  11.     }
  12.   ]
  13. }
  14. ```

Attributes

FieldDefaultDescription
|----------------------|-----------------------------|-------------------------------------------------------------|
placeholderTypePlaceholder
disableHotkeysfalseIf
hideBreadcrumbsfalseHide
openHotkeycmd+k,ctrl+kOpen
navigationUpHotkeyup,shift+tabNavigation
navigationDownHotkeydown,tabNavigation
closeHotkeyescClose
goBackHotkeybackspaceGo
selectHotkeyenterSelect
hotKeysJoinedViewfalseIf
noAutoLoadMdIconsfalseIf

Example

  1. ``` html
  2. <ninja-keys placeholder="Must app is awesome" openHotkey="cmd+l" hideBreadcrumbs></ninja-keys>
  3. ```

Data

Array of INinjaAction - interface properties below
NameTypeDescription
|----------|-------------------------|----------------------------------------------------------------------------------------|
idstringUnique
titlestringTitle
hotkeystring(optional)Shortcut
handlerFunction(optional)Function
mdIconstring(optional)Material
iconstring(optional)Html
parentstring(optional)If
keywordsstring(optional)Keywords
childrenArray(optional)If
sectionstring(optional)Section

Methods

NameArgDescription
|-----------|---------------------|-----------------------------------------------------|
`open`{Open
`close`|
`setParent`parent?:Navigate

Example

  1. ``` js
  2. const ninja = document.querySelector('ninja-keys');
  3. ninja.open()
  4. // or
  5. ninja.open({ parent: 'Theme' })
  6. ```

Events

Component wide events

NameDescriptionPayload
|------------------------------------|-------------------------------------|
`change`Emitted`{
`selected`Emitted`{

Both handler of action and component event selected emitted when user submit form or select item.

But event selected can be used to handle edge cases, so it's not recommended to write each action logic here. It’s better to use the action handler property.

For example, if a user enters a search query and there is an empty list, listening to this event you can handle that.

  1. ``` js
  2. ninja.addEventListener('change', (event) => {
  3.   console.log('ninja on change', event.detail);
  4.   // detail = {search: 'your search query', actions: Array}
  5. })
  6. ninja.addEventListener('selected', (event) => {
  7.   console.log('ninja on selected', event.detail);
  8.   // detail = {search: 'your search query', action: NinjaAction | undefined }
  9.   if (event.detail.action){
  10.   // perform API search for example
  11.   }
  12.   
  13. })
  14. ```

Themes

Component supports a dark theme out-of-box. You just need to add a class.
  1. ``` html
  2. <ninja-keys class="dark"></ninja-keys>
  3. ```

If you need more style control, use any of the CSS variables below.

CSS variables

NameDefault
|------------------------------------|------------------------------------|
--ninja-width640px;
--ninja-backdrop-filternone;
--ninja-overflow-backgroundrgba(255,
--ninja-text-colorrgb(60,
--ninja-font-size16px;
--ninja-top20%;
--ninja-key-border-radius0.25em
--ninja-accent-colorrgb(110,
--ninja-secondary-background-colorrgb(239,
--ninja-secondary-text-colorrgb(107,
--ninja-selected-backgroundrgb(248,
--ninja-icon-colorvar(--ninja-secondary-text-color);
--ninja-icon-size1.2em;
--ninja-separate-border1px
--ninja-modal-background#fff;
--ninja-modal-shadowrgb(0
--ninja-actions-height300px;
--ninja-group-text-colorrgb(144,
--ninja-footer-backgroundrgba(242,
--ninja-placeholder-color#8e8e8e
--ninja-z-index1

Example

  1. ```css
  2. ninja-keys {
  3.   --ninja-width: 400px;
  4. }
  5. ```

CSS Shadow Parts

Allowing you to style specific elements from your style.
Because styles are encapsulated by Shadow DOM, it will be annoying to create css variables for all properties.
That's why you can use ::part to make a custom look for the component.
It's supported by all modern browsers

NameDescription
|------------------------------------|-------------------------------------|
actions-listElement
ninja-actionSingle
ninja-selectedSelected
ninja-inputInput
ninja-input-wrapperWrapper

Example style using parts

  1. ```css
  2. ninja-keys:
  3.   padding: 8px;
  4. }
  5. ninja-keys:
  6.   border-radius: 8px;
  7.   border-left: none;
  8. }

  9. ninja-keys:
  10.   background: rgba(51, 51, 51, 0.1);
  11. }

  12. ninja-keys:
  13.   color: #14b8a6;
  14. }

  15. ninja-keys:
  16.   color: #f43f5e;
  17. }

  18. ninja-keys:
  19.   background: rgba(244, 63, 93, 0.3);
  20. }
  21. ```

Icons

By default, components use icons from https://fonts.google.com/icons

For example, you can just set mdIcon to light_mode to render a sun icon.

To add Material icons for your website, you need to add them to your HTML, for example
  1. ``` html
  2. <link href="https://fonts.googleapis.com/css?family=Material+Icons&display=block" rel="stylesheet">
  3. ```

If you want custom icons, you can use svg or img to insert it with an icon property for action with ninja-icon class.
Example:
  1. ``` js
  2. {
  3.   title: 'Search projects...',
  4.   icon: `<svg xmlns="http://www.w3.org/2000/svg" class="ninja-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  5.     <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
  6.   </svg>`,
  7.   section: 'Projects',
  8. },
  9. ```
You can also change the width and font using CSS variables
  1. ```css
  2. ninja-keys {
  3.   --ninja-icon-size: 1em;
  4. }
  5. ```

Change or hide footer

  1. ``` html
  2. <ninja-keys> 
  3.   <div slot="footer">You can use a custom footer or empty div to hide it</div>
  4. </ninja-keys>
  5. ```

Dev Server


  1. ``` sh
  2. npm run start
  3. ```

Linting


To lint the project run:

  1. ``` sh
  2. npm run lint
  3. ```

Formatting


Prettier is used for code formatting. It has been pre-configured according to the Lit's style.

License


Copyright (c) [Sergei Sleptsov](https://sergei.ws)

Licensed under the MIT license.
FOSSA Status