Tailwind UI structure (PRIVATE)
10 Oct 2024
Table of Contents
Note to self: DO NOT PUBLISH. Would violate my Tailwind UI terms of service. Keep in non-search-indexed draft status just for showing to a colleague.
The 3 Tailwind Labs products
Tailwind Labs (Adam Wathan and the people he’s hired) makes 3 products:
- Tailwind CSS (free): a combination of a CSS preprocessor and a bunch of opinionated CSS “utility”-style class name definitions.
- Headless UI (free): an NPM-importable package of unstyled React components that can accept a plaintext string as a parameter and throw it into
className
. - Tailwind UI (paid): a snippet library of HTML fragments that Tailwind Labs think look beautiful, based on the utility classes they strung together in the
class
property of the fragments’ elements. See below for details.
Tailwind UI
Here’s what you get when you buy Tailwind UI:
A website login for copy-paste HTML fragment examples
You get a login that makes it so code actually shows up when you surf the URLs under tailwindui.com/components.
So, instead of “soft buttons” having a “get the code” link, there’s a toggle that lets you actually see what it looks like as an HTML fragment, a React fragment, or a Vue fragment.
None of the fragments actually have any presentational JavaScript in them.
They’re just HTML (or framework-specific variants on HTML, like JSX) that’s marked up with a bunch of CSS class names.
Note: This means that the fragments you’ve just bought the right to see do not inherently meet accessibility needs that can only be solved with JavaScript.
For example, some design system designers’ want you to .preventDefault()
for any <button>
that contains an aria-disabled=true
attribute.
Here are the 3 fragment variants for the Tailwind UI’s largest “soft button” as of 10/10/2024:
HTML button
<button
type="button"
class="
rounded-md
bg-indigo-50
px-3.5 py-2.5
text-sm
font-semibold
text-indigo-600
shadow-sm
hover:bg-indigo-100
">Button text</button>
React button
export default function Example() {
const joinedClassNames = [
'rounded-md',
'bg-indigo-50',
'px-3.5',
'py-2.5',
'text-sm',
'font-semibold',
'text-indigo-600',
'shadow-sm',
'hover:bg-indigo-100',
].join(' ')
return (
<>
<button
type="button"
className={joinedClassNames}
>Button text</button>
</>
)
}
Vue button
<template>
<button
type="button"
class="
rounded-md
bg-indigo-50
px-3.5 py-2.5
text-sm
font-semibold
text-indigo-600
shadow-sm
hover:bg-indigo-100
">Button text</button>
<template>
ZIP files with even more styling-only-no-behavior fragments
You also get the right to download a handful of ZIP files full of JSX fragment files giving you even more looks and feel examples.
Note: They have all of the same no-accessibility-oriented-client-side-JavaScript-built-in issues as the copy-paste ones from part 1 mentioned above.
button.jsx
Here’s the relevant part of the button.jsx
file from the “Salient” theme ZIP file:
import Link from 'next/link';
export function Button( { className, ...props }) {
// some opinionated className beautifying here
return typeof props.href === 'undefined' ? (
<button
className={className}
{...props}
/>
) : (
<Link
className={className}
{...props}
/>
)
}
1 ZIP file of fragments with Headless UI instead of HTML under the hood
Finally, you get the right to download a ZIP file (the “Catalyst” theme) full of JSX fragment files that’s a little different from the others.
It doesn’t invoke React’s abstraction of HTML’s <button/>
data type like they do.
It invokes Headless UI’s <Headless.Button/>
data type.
- (Which is, in turn, a wrapper around React’s abstraction of the HTML’s
<button>
.)
Note: In theory, because <Headless.Button/>
is supposed to deliver accessibility-oriented client-side JS to the browser, the example code files from the Catalyst theme’s ZIP file might conform a little better to the spirit of your organization’s designers’ desires.
Whether they actually work is another matter – I’ve seen a few concerning bug reports in Headless UI:
-
“If a form was submitted, the checkbox data was not included. Clicking or interacting with the checkbox would not do anything.” -5/28/2024 pull request
-
“Constantly waiting on bug fixes.” -10/5/2024 redditor
Here’re more or less the relevant parts of 2 files from the “Catalyst” theme ZIP file that produce a button:
link.jsx
import * as Headless from '@headlessui/react';
import React, { forwardRef } from 'react';
export const Link = forwardRef(
function Link(props, ref) {
return (
<Headless.DataInteractive>
<a
{...props}
ref={ref}
/>
</Headless.DataInteractive>
)
}
)
button.jsx
import * as Headless from '@headlessui/react';
import React, { forwardRef } from 'react'
import { Link } from './link';
export const Button = forwardRef(
function Button ( { className, children, ...props}, ref) {
// some opinionated className beautifying here
return 'href' in props ? (
<Link
{...props}
className={className}
ref={ref}
>
<TouchTarget>
{children}
</TouchTarget>
</Link>
) : (
<Headless.Button
{...props}
className={className}
ref={ref}
>
<TouchTarget>
{children}
</TouchTarget>
</Headless.Button>
)
}
)
/**
* Expand the hit area to at least 44x44px on touch devices
*/
export function TouchTarget({ children )} {
const joinedClassNames = [
'absolute',
'left-1/2',
'top-1/2',
'size-[max(100%,2.75rem)]',
'-translate-x-1/2',
'-translate-y-1/2',
'[@media(pointer:fine)]:hidden]',
].join(' ')
return (
<>
<span
className={joinedClassNames}
aria-hidden="true"
/>
{children}
</>
)
}