Tailwind Plus 的原生 JavaScript 支持

Philipp Spiess
Adam Wathan

Tailwind Plus 中有很多 UI 块需要 JavaScript 才能真正发挥作用,例如对话框、下拉菜单、命令面板等等。除非你是 React 或 Vue 用户,否则使用这些 UI 块意味着你必须自己编写所有复杂的 JavaScript 代码。

¥There are a lot of UI blocks in Tailwind Plus that need JavaScript to really be useful, like dialogs, dropdowns, command palettes, and more. And unless you're a React or Vue user, using those UI blocks has always meant writing all of that tricky JavaScript yourself.

今天,情况终于发生了变化 - Tailwind Plus 中的每个 UI 块现在都功能齐全、易于访问且具有交互性,包括纯 HTML 示例。

¥Well today that finally changes — every UI block in Tailwind Plus is now fully functional, accessible, and interactive, including the plain HTML examples.

现在,你可以在任何项目中使用任何 dropdown命令面板dialogdrawer 等元素 - 无需 JavaScript 框架。

¥Now you can use any dropdown, command palette, dialog, drawer, and more in any project you're working on — no JavaScript framework required.


无需框架(No framework required)

¥No framework required

为了实现这一点,我们构建了 @tailwindplus/elements - 一个专为 Tailwind 附加功能 客户发布的库。

¥To pull this off, we built @tailwindplus/elements — a library we're releasing exclusively for Tailwind Plus customers.

Elements 是无头 自定义元素 的集合,它封装了仅使用 HTML 构建自定义交互式 UI 所需的所有复杂行为,并且可以使用实用程序类或自定义 CSS 以任何你喜欢的方式进行样式设置。

¥Elements is a collection of headless custom elements that wrap up all of the complex behavior needed to build custom interactive UIs using just HTML, and can be styled any way you like using utility classes or custom CSS.

这些自定义元素无需与特定的 JavaScript 框架绑定,而是可以在任何可以使用 <script> 标签的地方使用:

¥Instead of being coupled to a specific JavaScript framework, these custom elements work anywhere you can use a <script> tag:

index.html
<script src="https://cdn.jsdelivr.net/npm/@tailwindplus/elements@1" type="module"></script>

使用 Elements 构建 自定义下拉菜单 菜单的示例如下:

¥Here's what it look like to build a custom dropdown menu with Elements:

<el-dropdown class="relative inline-block text-left">  <button class="inline-flex w-full justify-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-gray-300 ring-inset hover:bg-gray-50">    Options    <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="-mr-1 size-5 text-gray-400">      <path d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" fill-rule="evenodd" />    </svg>  </button>  <el-menu anchor="bottom end" popover class="w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black/5 transition transition-discrete [--anchor-gap:--spacing(2)] focus:outline-hidden data-closed:scale-95 data-closed:transform data-closed:opacity-0 data-enter:duration-100 data-enter:ease-out data-leave:duration-75 data-leave:ease-in">    <div class="py-1">      <a href="#" class="block px-4 py-2 text-sm text-gray-700 focus:bg-gray-100 focus:text-gray-900 focus:outline-hidden">Account settings</a>      <a href="#" class="block px-4 py-2 text-sm text-gray-700 focus:bg-gray-100 focus:text-gray-900 focus:outline-hidden">Support</a>      <a href="#" class="block px-4 py-2 text-sm text-gray-700 focus:bg-gray-100 focus:text-gray-900 focus:outline-hidden">License</a>      <form action="#" method="POST">        <button type="submit" class="block w-full px-4 py-2 text-left text-sm text-gray-700 focus:bg-gray-100 focus:text-gray-900 focus:outline-hidden">Sign out</button>      </form>    </div>  </el-menu></el-dropdown>

构建 自定义选择 的步骤如下:

¥And here's what it looks like to build a custom select:

<label for="select" class="block text-sm/6 font-medium text-gray-900">Assigned to</label><el-select id="select" name="selected" value="4" class="mt-2 block">  <button type="button" class="grid w-full cursor-default grid-cols-1 ...">    <el-selectedcontent></el-selectedcontent>    <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="col-start-1 row-start-1 ...">      <!-- ... -->    </svg>  </button>  <el-options anchor="bottom start" popover class="max-h-60 w-(--button-width) [--anchor-gap:--spacing(1)] ...">    <el-option value="1" class="group/option relative block focus:bg-indigo-600 ...">      <div class="flex items-center">        <span aria-hidden="true" class="inline-block size-2 shrink-0 ..."></span>        <span class="ml-3 block group-aria-selected/option:font-semibold ...">          Wade Cooper          <span class="sr-only"> is online</span>        </span>      </div>      <span class="group-not-aria-selected/option:hidden group-focus/option:text-white in-[el-selectedcontent]:hidden ...">        <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-5">            <!-- ... -->        </svg>      </span>    </el-option>    <!-- ... -->  </el-options></el-select>

看到像 <el-select><el-options> 这样的自定义 HTML 元素了吗?这些是让一切正常运行的秘诀,包括自动 ARIA 属性管理、焦点处理、键盘支持等等。

¥See those custom HTML elements like <el-select> and <el-options>? Those are the secret sauce that make everything work, including automatic ARIA attribute management, focus handling, keyboard support, and more.

如果你愿意,甚至可以在 命令面板 中使用 Elements,而不必使用 Headless UI 或 React Aria 等仅支持 React 的库:

¥You can even build something as sophisticated as a custom command palette with Elements, without having to write any of your own JavaScript:

<button command="show-modal" commandfor="dialog" class="rounded-md bg-white/80 px-2.5 py-1.5 text-sm font-semibold text-gray-900 hover:bg-white/90">  Open command palette</button><el-dialog>  <dialog id="dialog" class="backdrop:bg-transparent">    <el-dialog-backdrop class="fixed inset-0 bg-gray-500/25 transition-opacity data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in"></div>    <div tabindex="0" class="fixed inset-0 w-screen overflow-y-auto p-4 focus:outline-none sm:p-6 md:p-20">      <el-dialog-panel class="mx-auto block max-w-3xl transform overflow-hidden rounded-xl bg-white shadow-2xl ring-1 ring-black/5 transition-all data-closed:scale-95 data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in">        <el-command-palette class="divide-y divide-gray-100">          <div class="grid grid-cols-1">            <input type="text" autofocus placeholder="Search..." class="col-start-1 row-start-1 h-12 w-full pr-4 pl-11 text-base text-gray-900 outline-hidden placeholder:text-gray-400 sm:text-sm" />            <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="pointer-events-none col-start-1 row-start-1 ml-4 size-5 self-center text-gray-400">              <path d="M9 3.5a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11ZM2 9a7 7 0 1 1 12.452 4.391l3.328 3.329a.75.75 0 1 1-1.06 1.06l-3.329-3.328A7 7 0 0 1 2 9Z" clip-rule="evenodd" fill-rule="evenodd" />            </svg>          </div>          <div class="flex transform-gpu divide-x divide-gray-100">            <div class="max-h-96 min-w-0 flex-auto scroll-py-4 overflow-y-auto px-6 py-4">              <el-command-list class="-mx-2 block text-sm text-gray-700">                <el-defaults>                  <h2 class="mx-2 mt-2 mb-4 text-xs font-semibold text-gray-500">Recent searches</h2>                  <div class="text-sm text-gray-700">                    <a id="person-suggestion-6" href="#" class="group flex cursor-default items-center rounded-md p-2 select-none focus:outline-hidden aria-selected:bg-gray-100 aria-selected:text-gray-900">                      <img src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="" class="size-6 flex-none rounded-full" />                      <span class="ml-3 flex-auto truncate">Tom Cook</span>                      <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="ml-3 hidden size-5 flex-none text-gray-400 group-aria-selected:block">                        <path d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" fill-rule="evenodd" />                      </svg>                    </a>                   <!-- ... -->                  </div>                </el-defaults>                <el-command-group hidden class="sm:h-96">                  <a id="person-1" href="#" hidden class="group flex cursor-default items-center rounded-md p-2 select-none focus:outline-hidden aria-selected:bg-gray-100 aria-selected:text-gray-900">                    <img src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="" class="size-6 flex-none rounded-full" />                    <span class="ml-3 flex-auto truncate">Leslie Alexander</span>                    <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="ml-3 hidden size-5 flex-none text-gray-400 group-aria-selected:block">                      <path d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" fill-rule="evenodd" />                    </svg>                  </a>                  <!-- ... -->                </el-command-group>              </el-command-list>              <el-no-results hidden class="block px-6 py-14 text-center text-sm sm:px-14">                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true" class="mx-auto size-6 text-gray-400">                  <path d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" stroke-linecap="round" stroke-linejoin="round" />                </svg>                <p class="mt-4 font-semibold text-gray-900">No people found</p>                <p class="mt-2 text-gray-500">We couldn't find anything with that term. Please try again.</p>              </el-no-results>            </div>            <el-command-preview for="person-1" hidden class="h-96 w-1/2 flex-none flex-col divide-y divide-gray-100 overflow-y-auto sm:flex">              <div class="flex-none p-6 text-center">                <img src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="" class="mx-auto size-16 rounded-full" />                <h2 class="mt-3 font-semibold text-gray-900">Leslie Alexander</h2>                <p class="text-sm/6 text-gray-500">Co-Founder / CEO</p>              </div>              <div class="flex flex-auto flex-col justify-between p-6">                <dl class="grid grid-cols-1 gap-x-6 gap-y-3 text-sm text-gray-700">                  <dt class="col-end-1 font-semibold text-gray-900">Phone</dt>                  <dd>1-493-747-9031</dd>                  <dt class="col-end-1 font-semibold text-gray-900">URL</dt>                  <dd class="truncate"><a href="https://example.com" class="text-indigo-600 underline">https://example.com</a></dd>                  <dt class="col-end-1 font-semibold text-gray-900">Email</dt>                  <dd class="truncate"><a href="mailto:lesliealexander@example.com" class="text-indigo-600 underline">lesliealexander@example.com</a></dd>                </dl>                <button type="button" class="mt-6 w-full rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Send message</button>              </div>            </el-command-preview>            <!-- ... -->          </div>        </el-command-palette>      </el-dialog-panel>    </div>  </dialog></el-dialog>

为了支持 Tailwind Plus 中的所有 UI 块,我们在 Elements 的第一个版本中提供了以下原语:

¥To support all of the UI blocks in Tailwind Plus, we're shipping the following primitives in this first release of Elements:

如果你是 Tailwind Plus 用户,请前往全新的 元素文档 了解更多工作原理并查看一些示例。

¥If you're a Tailwind Plus customer, head over to the brand new Elements documentation to learn more about how everything works and check out some examples.


充分利用现代 Web 技术(Leveraging the modern web)

¥Leveraging the modern web

我们借鉴了许多现代平台的功能,以尽可能保持 Elements 的轻量级和原生性。

¥We leaned on a lot of modern platform features to keep Elements as light and native as a possible.

我们还打包了这些功能所需的所有 polyfill,以确保 Elements 能够在所有 支持的浏览器由 Tailwind CSS v4.0 提供 中正常工作。这意味着随着这些现代平台功能的普及,Elements 只会变得更小。

¥We also bundle any necessary polyfills for these features to make sure that Elements works in all of the same browsers supported by Tailwind CSS v4.0. This means Elements will only get smaller as these modern platform features become more widely available.


随处可用的组件(Components that work everywhere)

¥Components that work everywhere

由于 HTML 是所有 Web 框架之间的最小公分母,Elements 使得 Tailwind Plus 中所有纯 HTML 的 UI 块几乎可以在任何地方工作。

¥Since HTML is the lowest common denominator between all web frameworks, Elements makes it possible for all of HTML-only UI blocks in Tailwind Plus to work literally anywhere.

这是我们在 Svelte 中使用双向绑定的 组合框 示例之一:

¥Here's one of our Combobox examples wired up with two-way binding in Svelte:

Combobox.svelte
<script>  let input = $state("");  function handleSubmit() {    alert(`Selected: ${input}`);  }</script><form onsubmit={handleSubmit}>  <label for="autocomplete" class="block text-sm/6 font-medium text-gray-900">Assigned to</label>  <el-autocomplete class="relative mt-2 block">    <input bind:value={input} id="autocomplete" type="text" class="block w-full rounded-md ..." />    <button type="button" class="absolute inset-y-0 right-0 flex ...">      <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-5 text-gray-400">        <path d="M5.22 8.22a.75.75 ..." clip-rule="evenodd" fill-rule="evenodd" />      </svg>    </button>    <el-options anchor="bottom end" popover class="max-h-60 w-(--input-width) [--anchor-gap:--spacing(1)] ...">      <el-option value="Leslie Alexander" class="block truncate aria-selected:bg-indigo-600 aria-selected:text-white ...">Leslie Alexander</el-option>    </el-options>  </el-autocomplete>  <button type="submit">Submit</button></form>

或者,这里是 Rails 中的自定义选择,它包含在表单提交中,就像原生表单控件一样:

¥Or here's a custom select in Rails that's included in form submissions, just like a native form control:

app/controllers/orders_controller.rb
class OrdersController < ApplicationController  def new    @cars = Car.all    @selected_car = @cars.first  end  def create    car = Car.find(params[:car_id])    flash[:notice] = "Selected car: #{car.name}"    redirect_to root_path  endend
app/views/orders/new.html.erb
<%= form_with do |form| %>  <%= form.label :car_id, "Select model:" %>  <el-select name="car_id" id="car_id" value="<%= @selected_car.id %>">    <button type="button" class="grid w-full cursor-default grid-cols-1 ...">      <el-selectedcontent class="col-start-1 row-start-1 truncate pr-6">        <%= @selected_car.name %>      </el-selectedcontent>      <svg viewBox="0 0 16 16" aria-hidden="true" class="col-start-1 row-start-1 size-5 ...">        <path d="M5.22 10.22a.75.75 ..." clip-rule="evenodd" fill-rule="evenodd" />      </svg>    </button>    <el-options anchor="bottom end" popover="">      <% @cars.each do |car| %>        <el-option value="<%= car.id %>">          <span class="block truncate font-normal group-aria-selected/option:font-semibold">            <%= car.name %>          </span>          <span class="flex group-not-aria-selected/option:hidden group-focus/option:text-white in-[el-selectedcontent]:hidden ...">            <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-5">              <path d="M16.704 4.153a.75.75 0 ..." clip-rule="evenodd" fill-rule="evenodd" />            </svg>          </span>        </el-option>      <% end %>    </el-options>  </el-select>  <%= form.submit "Place order" %><% end %>

如果你愿意,甚至可以使用 React 中的 Elements,而不必使用 Headless UI 或 React Aria 等纯 React 库:

¥You can even use Elements in React instead of using a React-only library like Headless UI or React Aria if you want:

import Link from "next/link";export function Menu() {  return (    <el-dropdown className="relative inline-block text-left">      <button className="inline-flex w-full justify-center ...">        Menu        <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" className="-mr-1 size-5 text-gray-400">          <path d="M5.22 8.22a.75.75 ..." clip-rule="evenodd" fill-rule="evenodd" />        </svg>      </button>      <el-menu anchor="bottom end" popover className="transition transition-discrete [--anchor-gap:--spacing(2)] focus:outline-hidden data-closed:scale-95 ...">        <div className="py-1">          <Link href="/" className="block px-4 py-2 focus:bg-gray-100 focus:text-gray-900 focus:outline-hidden ...">Home</Link>          <Link href="/about" className="block px-4 py-2 focus:bg-gray-100 focus:text-gray-900 focus:outline-hidden ...">About</Link>          <Link href="/faq" className="block px-4 py-2 focus:bg-gray-100 focus:text-gray-900 focus:outline-hidden ...">FAQ</Link>        </div>      </el-menu>    </el-dropdown>  );}

立即试用(Try it out today)

¥Try it out today

所有更新的 UI 块和新的 Elements 库现在都可供所有 Tailwind 附加功能 客户使用。

¥All of the updated UI blocks and the new Elements library are available now to all Tailwind Plus customers.

查看 下拉菜单命令面板 等 UI 块类别,亲自体验这些更新的交互式示例,并探索全新的 元素文档,了解其工作原理以及如何根据项目进行自定义。

¥Check out UI block categories like Dropdowns and Command Palettes to play with these updated interactive examples yourself, and explore the new Elements documentation to learn how it all works and how to customize things for your projects.

我们迫不及待地想看看你将使用这些东西构建什么!

¥We can't wait to see what you'll build with this stuff!

TailwindCSS v4.1 中文网 - 粤ICP备13048890号