Skip to content

Instantly share code, notes, and snippets.

@adamwathan
Last active August 21, 2020 01:52
Show Gist options
  • Star 39 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save adamwathan/06faa4e9ee1571d3eb47cc0349b965cd to your computer and use it in GitHub Desktop.
Save adamwathan/06faa4e9ee1571d3eb47cc0349b965cd to your computer and use it in GitHub Desktop.

Quick update: Some folks have expressed concern that it would suck to not be able to actually test out the interactive components like clicking a button to see a dropdown transition from open to closed.

The live previews in Tailwind UI would absolutely still be living, interactive demos. It's just the code snippets that would use HTML comments, we'd still be using Alpine internally under the hood to give you a real demo and just maintaining two code snippets for each interactive component (we already do this in some sense anyways).


Integrating Tailwind UI with JavaScript

To make Tailwind UI as universal as possible, we've built all the components using HTML only.

The vast majority of components don't need JavaScript at all and are completely ready to go out of the box, but things that are interactive like dropdowns, modals, etc. require you to write some JS to make them work the way you'd expect.

In these situations we've provided some simple comments in the HTML to explain things like what classes you need use for different states (like a toggle switch being on or off), or what classes we recommend for transitioning elements on to or off of the screen (like a modal opening).

This guide explains how to take these comments and translate them into working code using some popular JS libraries.

Accessibility

We've done our best to ensure that all of the markup in Tailwind UI is as accessible as possible, but when you're building interactive components, many accessibility best practices can only be implemented with JavaScript.

For example:

  • Making sure components are properly keyboard accessible (dropdowns should be navigated with up/down arrow keys, modals should close when you press escape, tabs should be selected using the left/right arrow keys, etc.)
  • Correctly handling focus (you shouldn't be able to tab to an element behind a modal, the first item in a dropdown should be auto-focused when the dropdown opens, etc.)
  • Synchronizing ARIA attributes with component state (adding aria-expanded="true" when a dropdown is open, setting aria-checked to true when a toggle is on, updating aria-activedescendant when navigating the options in an autocomplete, etc.)
  • ...and many other concerns.

Because the components in Tailwind UI are HTML-only, it is up to you to follow accessibility best practices adding interactive behavior with JavaScript.

To learn more about building accessible UI components, we recommend studying the WAI-ARIA Authoring Practices published by the W3C.

React

The first thing you'll need to do to incorporate any Tailwind UI component into a React project is convert the HTML to JSX. Usually this just means converting class to className, but for components that include inline SVG elements you'll also need to camelize any dash-case SVG attributes like fill-rule, stroke-width, and others.

- <button type="button" class="inline-flex items-center px-4 py-2 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:shadow-outline-indigo active:bg-indigo-700 transition ease-in-out duration-150">
+ <button type="button" className="inline-flex items-center px-4 py-2 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:shadow-outline-indigo active:bg-indigo-700 transition ease-in-out duration-150">
-   <svg class="-ml-1 mr-2 h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
+   <svg className="-ml-1 mr-2 h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
-     <path fill-rule="evenodd" clip-rule="evenodd" d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884zM18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"/>
+     <path fillRule="evenodd" clipRule="evenodd" d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884zM18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"/>
    </svg>
    Button text
  </button>

Dynamic classes

When an element needs different classes applied based on some state (like a toggle being on or off), we list the classes for each state in a comment directly above the element:

<!-- Simple toggle switch -->
<!-- Off: bg-gray-200 -->
<!-- On: bg-indigo-600 -->
<span aria-checked="false" class="relative inline-block flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:shadow-outline" role="checkbox" tabindex="0">
  <!-- Off: translate-x-0 -->
  <!-- On: translate-x-5 -->
  <span aria-hidden="true" class="inline-block h-5 w-5 rounded-full bg-white shadow transform transition ease-in-out duration-200"></span>
</span>

To make this work in React, conditionally include the necessary classes on each element by checking a prop or piece of state:

import { useState } from 'react'

function SimpleToggle() {
  const [isOn, setIsOn] = useState(false)
  return (
    <span
      onClick={() => setIsOn(!isOn)}
      aria-checked={isOn}
      class={`${isOn ? 'bg-indigo-600' : 'bg-gray-200'} relative inline-block flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:shadow-outline`} role="checkbox" tabindex="0"
    >
      <span
        aria-hidden="true"
        class={`${isOn ? 'translate-x-5' : 'translate-x-0'} inline-block h-5 w-5 rounded-full bg-white shadow transform transition ease-in-out duration-200`}
      ></span>
    </span>
  )
}

Enter/Leave transitions

For elements that should be dynamically shown or hidden (like the panel on a dropdown), we include the recommended transition styles in a comment directly above the dynamic element:

<div class="relative ...">
  <button type="button" class="...">
    Options
  </button>

  <!-- Show/hide this element based on the dropdown state -->
  <!-- Entering: transition ease-out duration-100 transform opacity-[0-100] scale-[95-100] -->
  <!-- Closing: transition ease-in duration-75 transform opacity-[100-0] scale-[100-95] -->
  <div class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg">
    <div class="rounded-md bg-white shadow-xs">
      <!-- Snipped  -->
    </div>
  </div>
</div>

You'll notice the class lists often include entries like opacity-[0-100] — all this means is that the element should transition from opacity-0 to opacity-100 during that phase of the transition.

React doesn't include any first-party transition components out of the box, but we've put together a very simple wrapper around react-transition-group that makes it easy to implement our transitions in your project.

To get started, grab the code for our transition component on GitHub and save it alongside your other React components (we've called it Transition.js for our examples).

We'll likely publish the component to npm eventually, but for now you should just include the code in your project manually.

To implement the transition, wrap the dynamic element in the Transition component, passing through the following props:

  • show, whether or not the dynamic element should be showing
  • enter, a list of classes that should be present during the entire enter phase
  • enterFrom, a list of classes that represent the element's state at the beginning of the enter phase
  • enterTo, a list of classes that represent the element's state at the end of the enter phase
  • leave, a list of classes that should be present during the entire leave phase
  • leaveFrom, a list of classes that represent the element's state at the beginning of the leave phase
  • leaveTo, a list of classes that represent the element's state at the end of the leave phase

Here's what that looks like when applied to the dropdown example from above:

import { useState } from 'react'
import Transition from './Transition.js'

function Dropdown() {
  const [isOpen, setIsOpen] = useState(false)
  return (
    <div className="relative ...">
      <button type="button" onClick={() => setIsOpen(!isOpen)} className="...">
        Options
      </button>

      <Transition
        show={isOpen}
        enter="transition ease-out duration-100 transform"
        enterFrom="opacity-0 scale-95"
        enterTo="opacity-100 scale-100"
        leave="transition ease-in duration-75 transform"
        leaveFrom="opacity-100 scale-100"
        leaveTo="opacity-0 scale-95"
      >
        <div className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg">
          <div className="rounded-md bg-white shadow-xs">
            {/* Snipped */}
          </div>
        </div>
      </Transition>
    </div>
  )
}

Vue.js

Since Vue templates are no different than regular HTML, the first step to pulling a Tailwind UI component into a Vue project is to just paste the code inside a <template> tag of a single file Vue component.

Dynamic classes

When an element needs different classes applied based on some state (like a toggle being on or off), we list the classes for each state in a comment directly above the element:

<!-- Simple toggle switch -->
<!-- Off: bg-gray-200 -->
<!-- On: bg-indigo-600 -->
<span aria-checked="false" class="relative inline-block flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:shadow-outline" role="checkbox" tabindex="0">
  <!-- Off: translate-x-0 -->
  <!-- On: translate-x-5 -->
  <span aria-hidden="true" class="inline-block h-5 w-5 rounded-full bg-white shadow transform transition ease-in-out duration-200"></span>
</span>

To make this work in Vue, conditionally include the necessary classes on each element by checking a prop or piece of state:

<template>
  <span
    @click="isOn = !isOn"
    :aria-checked="isOn"
    :class="isOn ? 'bg-indigo-600' : 'bg-gray-200'"
    class="relative inline-block flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:shadow-outline"
    role="checkbox"
    tabindex="0"
  >
    <span
      aria-hidden="true"
      :class="isOn ? 'translate-x-5' : 'translate-x-0'"
      class="inline-block h-5 w-5 rounded-full bg-white shadow transform transition ease-in-out duration-200"
    ></span>
  </span>
</template>

<script>
  export default {
    data: () => ({
      isOn: false,
    })
  }
</script>

Enter/Leave transitions

For elements that should be dynamically shown or hidden (like the panel on a dropdown), we include the recommended transition styles in a comment directly above the dynamic element:

<div class="relative ...">
  <button type="button" class="...">
    Options
  </button>

  <!-- Show/hide this element based on the dropdown state -->
  <!-- Entering: transition ease-out duration-100 transform opacity-[0-100] scale-[95-100] -->
  <!-- Closing: transition ease-in duration-75 transform opacity-[100-0] scale-[100-95] -->
  <div class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg">
    <div class="rounded-md bg-white shadow-xs">
      <!-- Snipped  -->
    </div>
  </div>
</div>

You'll notice the class lists often include entries like opacity-[0-100] — all this means is that the element should transition from opacity-0 to opacity-100 during that phase of the transition.

To implement transitions like this in Vue, use Vue's built-in <transition> component and pass the necessary transition styles through the custom transition class props:

<template>
  <div class="relative ...">
    <button type="button" @click="isOpen = !isOpen" class="...">
      Options
    </button>

    <transition
      enter-active-class="transition ease-out duration-100 transform"
      enter-class="opacity-0 scale-95"
      enter-to-class="opacity-100 scale-100"
      leave-active-class="transition ease-in duration-75 transform"
      leave-class="opacity-100 scale-100"
      leave-to-class="opacity-0 scale-95"
    >
      <div v-show="isOpen" class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg">
        <div class="rounded-md bg-white shadow-xs">
          <!-- Snipped  -->
        </div>
      </div>
    </transition>
  </div>
</template>

<script>
  export default {
    data: () => ({
      isOpen: false,
    })
  }
</script>

Alpine.js

Alpine.js is a fairly new, heavily Vue-inspired library designed to make it easy to add interactive behavior to traditional server-rendered websites, using a light-weight declarative syntax directly in your HTML. It's what we use for the Tailwind UI site itself, and is without a doubt what I'd recommend if you'd otherwise be writing jQuery or vanilla JS.

Dynamic classes

When an element needs different classes applied based on some state (like a toggle being on or off), we list the classes for each state in a comment directly above the element:

<!-- Simple toggle switch -->
<!-- Off: bg-gray-200 -->
<!-- On: bg-indigo-600 -->
<span aria-checked="false" class="relative inline-block flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:shadow-outline" role="checkbox" tabindex="0">
  <!-- Off: translate-x-0 -->
  <!-- On: translate-x-5 -->
  <span aria-hidden="true" class="inline-block h-5 w-5 rounded-full bg-white shadow transform transition ease-in-out duration-200"></span>
</span>

To make this work in Alpine, conditionally include the necessary classes on each element based on some state you've declared in x-data:

<span
  x-data="{ isOn: false }"
  @click="isOn = !isOn"
  :aria-checked="isOn"
  :class="{'bg-indigo-600': isOn, 'bg-gray-200': !isOn }"
  class="relative inline-block flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:shadow-outline"
  role="checkbox"
  tabindex="0"
>
  <span
    aria-hidden="true"
    :class="{'translate-x-5': isOn, 'translate-x-0': !isOn }"
    class="inline-block h-5 w-5 rounded-full bg-white shadow transform transition ease-in-out duration-200"
  ></span>
</span>

Enter/Leave transitions

For elements that should be dynamically shown or hidden (like the panel on a dropdown), we include the recommended transition styles in a comment directly above the dynamic element:

<div class="relative ...">
  <button type="button" class="...">
    Options
  </button>

  <!-- Show/hide this element based on the dropdown state -->
  <!-- Entering: transition ease-out duration-100 transform opacity-[0-100] scale-[95-100] -->
  <!-- Closing: transition ease-in duration-75 transform opacity-[100-0] scale-[100-95] -->
  <div class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg">
    <div class="rounded-md bg-white shadow-xs">
      <!-- Snipped  -->
    </div>
  </div>
</div>

You'll notice the class lists often include entries like opacity-[0-100] — all this means is that the element should transition from opacity-0 to opacity-100 during that phase of the transition.

To implement transitions like this in Alpine, use the x-transition directives:

<div x-data="{ isOpen: false }" class="relative ...">
  <button type="button" @click="isOpen = !isOpen" class="...">
    Options
  </button>

  <div
    x-show="isOpen"
    x-transition:enter="transition ease-out duration-100 transform"
    x-transition:enter-start="opacity-0 scale-95"
    x-transition:enter-end="opacity-100 scale-100"
    x-transition:leave="transition ease-in duration-75 transform"
    x-transition:leave-start="opacity-100 scale-100"
    x-transition:leave-end="opacity-0 scale-95"
    class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg"
  >
    <div class="rounded-md bg-white shadow-xs">
      <!-- Snipped  -->
    </div>
  </div>
</div>
@astrocket
Copy link

Any plan to add stimulus examples?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment