SkillAgentSearch skills...

Turnstone

React customisable autocomplete component with typeahead and grouped results from multiple APIs.

Install / Use

/learn @tomsouthall/Turnstone
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<p align="center" width="100%"> <img src="https://github.com/tomsouthall/turnstone/raw/main/images/turnstone.svg" alt="Turnstone - A React Search Component" width="80%" align="center"> <br/><br/> </p>

Turnstone is a highly customisable, easy-to-use autocomplete search component for React.

Turnstone In Action

View Demos | Demo #1 | Demo #2 | Demo #3 (Basic)

Play with Turnstone at CodeSandbox

Turnstone in action

Features

  • Lightweight React search box component
  • Group search results from multiple APIs or other data sources with customisable headings
  • Specify the maximum number of listbox options as well as weighted display ratios for each group
  • Completely customise listbox options with your own React component. Add images, icons, additional sub-options, differing visual treatments by group or index and much more...
  • Display typeahead autosuggest text beneath entered text
  • Easily styled with various CSS methods including CSS Modules and Tailwind CSS
  • Search input can be easily styled to attach to top of screen at mobile screen sizes with customisable cancel/back button to exit
  • Multiple callbacks including: onSelect, onChange, onTab, onEnter and more...
  • Built in WAI-ARIA accessibility
  • Keyboard highlighting and selection using arrow, Tab and Enter keys
  • Automated caching to reduce data fetches
  • Debounce text entry to reduce data fetches
  • Optional Clear button (customisable)
  • Customisable placeholder text
  • Add more functionality with plugins
  • and much more...

Installation & Usage

$ npm install --save turnstone

Usage

Barebones unstyled example

import React from 'react'
import Turnstone from 'turnstone'

const App = () => {
  const listbox = {
    data: ['Peach', 'Pear', 'Pineapple', 'Plum', 'Pomegranate', 'Prune']
  }

  return (
    <Turnstone listbox={listbox} />
  )
}

Styled example with grouped results from two API sources

import React, { useState } from 'react'
import Turnstone from 'turnstone'

const styles = {
  input,
  inputFocus,
  query,
  typeahead,
  cancelButton,
  clearButton,
  listbox,
  groupHeading,
  item,
  highlightedItem
}

const maxItems = 10

const listbox = [
  {
    id: 'cities',
    name: 'Cities',
    ratio: 8,
    displayField: 'name',
    data: (query) =>
      fetch(`/api/cities?q=${encodeURIComponent(query)}&limit=${maxItems}`)
        .then(response => response.json()),
    searchType: 'startswith'
  },
  {
    id: 'airports',
    name: 'Airports',
    ratio: 2,
    displayField: 'name',
    data: (query) =>
      fetch(`/api/airports?q=${encodeURIComponent(query)}&limit=${maxItems}`)
        .then(response => response.json()),
    searchType: 'contains'
  }
]

export default function Example() {
  return (
    <Turnstone
      cancelButton={true}
      debounceWait={250}
      id="search"
      listbox={listbox}
      listboxIsImmutable={true}
      matchText={true}
      maxItems={maxItems}
      name="search"
      noItemsMessage="We found no places that match your search"
      placeholder="Enter a city or airport"
      styles={styles}
      typeahead={true}
    />
  )
}

Example Markup

This is an example of markup produced by the component, in this case with the text New entered into the search box.

<div class="container" role="combobox" aria-expanded="true" aria-owns="search-listbox" aria-haspopup="listbox">
  <input type="text" id="search" name="search" class="input query" style="position:relative;z-index:1;background-color:transparent" placeholder="Enter a city or airport" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" aria-autocomplete="both" aria-controls="search-listbox">
  <input type="text" class="input typeahead" style="position:absolute;z-index:0;top:0;left:0" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" tabindex="-1" readonly="" aria-hidden="true">
  <button class="clearButton" tabindex="-1" aria-label="Clear contents" style="z-index: 2;">×</button>
  <button class="cancelButton" tabindex="-1" aria-label="Cancel" style="z-index: 3;">Cancel</button>
  <div id="search-listbox" class="listbox" role="listbox" style="position: absolute; z-index: 4;">
    <div class="groupHeading">Cities</div>
    <div class="highlightedItem" role="option" aria-selected="true" aria-label="New York City, New York, United States"><strong>New</strong> York City, New York, United States</div>
    <div class="item" role="option" aria-selected="false" aria-label="New South Memphis, Tennessee, United States"><strong>New</strong> South Memphis, Tennessee, United States</div>
    <div class="item" role="option" aria-selected="false" aria-label="New Kingston, Jamaica"><strong>New</strong> Kingston, Jamaica</div>
    <div class="item" role="option" aria-selected="false" aria-label="Newcastle, South Africa"><strong>New</strong>castle, South Africa</div>
    <div class="item" role="option" aria-selected="false" aria-label="New Orleans, Louisiana, United States"><strong>New</strong> Orleans, Louisiana, United States</div>
    <div class="item" role="option" aria-selected="false" aria-label="New Delhi, India"><strong>New</strong> Delhi, India</div>
    <div class="item" role="option" aria-selected="false" aria-label="Newcastle, Australia"><strong>New</strong>castle, Australia</div>
    <div class="item" role="option" aria-selected="false" aria-label="Newport, Wales"><strong>New</strong>port, Wales</div>
    <div class="groupHeading">Airports</div>
    <div class="item" role="option" aria-selected="false" aria-label="John F Kennedy Intl (JFK), New York, United States"item>John F Kennedy Intl (JFK), <strong>New</strong> York, United States</div>
    <div class="item" role="option" aria-selected="false" aria-label="Newark Liberty Intl (EWR), Newark, United States"><strong>New</strong>ark Liberty Intl (EWR), <strong>New</strong>ark, United States</div>
  </div>
</div>

API

Props

The following props can be supplied to the <Turnstone> component:

autoFocus

  • Type: boolean
  • Default: false
  • If true the search input automatically receives focus
  • Note: If defaultListbox prop is supplied, setting autoFocus to true causes the default listbox to be automatically opened.

cancelButton

  • Type: boolean
  • Default: false
  • If true a cancel button is rendered. The cancel button is displayed only when the search box receives focus. It is particularly useful for mobile screen sizes where a "back" button is required in order to exit the focused state of the search box.

cancelButtonAriaLabel

  • Type: string
  • Default: "Cancel"
  • The value of the aria-label attribute on the cancel button element.

clearButton

  • Type: boolean
  • Default: false
  • If true a clear button is rendered whenever the user has entered at least one character into the search box.
  • Clicking the clear button has the same effect as pressing the Esc key while entering text into the search box. The contents of the searchbox are cleared and focus is retained.
  • Suggested styling for the clear button is to position it absolutely overlaying the right of the search box, for example:
    .clearButton {
      display: block;
      width: 2rem;
      right: 0px;
      top: 0px;
      bottom: 0px;
      position: absolute;
      color: #a8a8a8;
      cursor: pointer;
      border: none;
      background: transparent;
      padding:0;
    }
    

clearButtonAriaLabel

  • Type: string
  • Default: "Clear contents"
  • The value of the aria-label attribute on the clear button element.

debounceWait

  • Type: number
  • Default: 250
  • The wait time in milliseconds after the user finishes typing before the search query is sent to the fetch function.
  • This reduces the number of API calls made by the fetch function
  • Set to 0 if you want no wait at all (e.g. if your listbox data is not fetched asynchronously)

defaultListbox

  • Type: array or object or function
  • Default: undefined
  • The default listbox is displayed when the search box has focus and is empty.
  • Supply an array if you wish multiple groups of items to appear in the default listbox. Groups can be drawn from multiple sources. For example:
    [
      {
        name: 'Recent Searches',
        displayField: 'name',
        data: () => Promise.resolve(JSON.parse(localStorage.getItem('recent')) || []),
        id: 'recent',
        ratio: 1
      },
      {
        name: 'Popular Cities',
        displayField: 'name',
        data: [
          { name: 'Paris, France', coords: '48.86425, 2.29416' },
          { name: 'Rome, Italy', coords: '41.89205, 12.49209' },
          { name: 'Orlando, Florida, United States', coords: '28.53781, -81.38592' },
          { name: 'London, England', coords: '51.50420, -0.12426' },
          { name: 'Barcelona, Spain', coords: '41.40629, 2.17555' },
          { name: 'New Orleans, Louisiana, United States', coords: '29.95465,-90.07507' },
          { name: 'Chicago, Illinois, United States', coords: '41.85003,-87.65005' },
          { name: 'Manchester, England', coords: '53.48095,-2.23743' }
        ],
        id: 'popular',
        ratio: 1
      }
    ]
    
  • Supply an object if you wish an ungrouped set of items to appear in the default listbox. For example:
    {
    

Related Skills

View on GitHub
GitHub Stars192
CategoryDevelopment
Updated26d ago
Forks11

Languages

JavaScript

Security Score

100/100

Audited on Mar 5, 2026

No findings