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>
@GDanielRG
Copy link

GDanielRG commented Apr 22, 2020

After using Tailwind UI for the past weeks by using it in real project with clients of myself, and writing an internal component library for these components (I've really given it a thought), this is what I have to say:

What really got me into Tailwind UI in the first place, was the idea of being able to build beautiful products easily, but what set this apart from another UI components package was that I felt that I could (and it also kinda of felt that I 'should', or something that was 'encouraged' by the product itself) modify and change the components as needed. Maybe this thought was enhanced by the fact that the code felt really familiar to me since I've been using Tailwind for the past few years and also Vue (first example components sent via email where with Vue). So whenever I looked at an example sent via email, I felt so happy that even then with a preview, not the actual content yet, I was really able to copy/paste and actually be using on my products right there and then, changing what I had to to make it work with my project and voila! After the early access was released I definitely noticed the difference from when I was able to just copy/paste, to now having to translate from Alpine to Vue (not really a big deal). What worries me is that since some other frameworks could be really different from alpine and Vue, having the alpine code in the html code you provide could be a problem since maybe other frameworks need a lot of other things or different html structure to make whatever the component was doing to work. (A very simple example I have faced is the transition animations, in alpine is as easy as adding come classes and in Vue you need a transition tag, as far as I am concerned). So the comments only version could really be better for those scenarios, while the alpine version will definitely be better for Alpine, Vue and some-other-syntax-similiar-framework users.

I'd love to come up with a way to give people copy & paste snippets that are closer to the syntax of their framework of choice, but I don't know how I can possibly do that without also misleading people to believe they are bullet-proof production-ready JS implementations, and actually providing that is just way out of scope for what we're working on right now.

After thinking about that and reading your comment above, it really hit me that moving everything to comments only version will certainly remove the problem that I presented above, the experience now won't vary that much from framework to framework, everyone will need to write some code in their own framework to 'make a dropdown work', but all of this to me raises a bigger question.

...but I don't know how I can possibly do that without also misleading people to believe they are bullet-proof production-ready JS implementations,

When I read that it really shocked me that the components you are giving are obviously not bulletproof, but unconsciously I was treating them as such! The part of not being able to provide them as bullet-proof production-ready JS is a business decision? or why is this not happening? as you can see from my first paragraph above, what really generated value and set this apart from everything was the feeling of being able to just copy paste and just modify what was needed, I really didn't felt that there was something missing to the component itself. So now I am thinking that to me the answer that really brings this product to its better version (to my eyes) is whichever decision helps you get to a bulletproof production-ready example. It doesn't matter if its with alpine with Vue or with comments only, but I would love to know that after 'translating' everything thats going on in the code you provided it will be production ready, or if the comments-only version is the one presented, would love to have all the comments, instructions, annotations etc. that need to be considered so the component gets to the production-ready state. As long as that statement is fulfilled with any version you provide its excellent. At the end I believe the product is targeted at developers, so whether they are able to translate them or not to their own framework shouldn't be your concern I think), but were you can really make a difference is by providing the guidelines to make it production-ready (comments only version) or the complete final component itself in any framework you choose to (I think alpine is ok). Both solutions of course would be the best. Having the complete comments-only version to make it production ready, and maybe a code example with alpine since you are already coding that to showcase it ;)

Thanks for all of your hard work on every product you have released!

@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