Headless UI v1.6、Tailwind UI 团队管理、Tailwind Play 改进等

Adam Wathan

我已经很久没有写过我们的工作内容了,所以有很多东西想分享!说实话,太多了 - 我发布这次更新的主要原因是下周我们会有更多内容,我觉得在分享所有已经发布的内容之前,我不能分享这些内容。

¥It's been a while since I've written about what we've been working on so I have a lot to share! Too much honestly — my main motivator for even getting this update out is that we've got even more stuff coming next week, and I feel like I'm not allowed to share that stuff until I share all of the stuff we've already shipped.

所以穿上泳衣,坐在你的躺椅上,准备吸收一些 CSS 的精华。

¥So put your swim suit on, sit back in your lounge chair, and prepare to soak up some vitamin CSS.


Headless UI v1.6 已发布(Headless UI v1.6 is out)

¥Headless UI v1.6 is out

几周前,我们发布了 Headless UI 的新版本。Headless UI 是我们构建的无样式 UI 库,旨在为 Tailwind UI 添加 React 和 Vue 支持。

¥A few weeks ago we released a new minor version of Headless UI, the unstyled UI library we built to make it possible to add React and Vue support to Tailwind UI.

查看 the release notes 了解所有细节,以下是一些亮点。

¥Check out the release notes for all of the details, but here are some of the highlights.

多选支持(Multiselect support)

¥Multiselect support

我们为 ComboboxListbox 组件添加了新的 multiple 属性,以便用户可以选择多个选项。

¥We've added a new multiple prop to both the Combobox and Listbox components so people can select more than one option.

只需添加 multiple 属性,并将数组绑定为 value,即可开始使用:

¥Just add the multiple prop and bind an array as your value and you are ready to go:

function MyCombobox({ items }) {
const [selectedItems, setSelectedItems] = useState([]);
return (
<Combobox value={selectedItems} onChange={setSelectedItems} multiple>
{selectedItems.length > 0 && (
<ul>
{selectedItems.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
)}
<Combobox.Input />
<Combobox.Options>
{items.map((item) => (
<Combobox.Option key={item} value={item}>
{item}
</Combobox.Option>
))}
</Combobox.Options>
</Combobox>
);
}

查看 the combobox documentationlistbox documentation 了解更多信息。

¥Check out the combobox documentation and listbox documentation for more.

可空组合框(Nullable comboboxes)

¥Nullable comboboxes

在 v1.6 之前,如果你删除了组合框的内容并按 Tab 键退出,它将恢复之前选择的选项。这在很多时候都很有意义,但有时你确实需要清除组合框的值。

¥Prior to v1.6, if you deleted the contents of a combobox and tabbed away, it would restore the previously selected option. This makes sense a lot of the time, but sometimes you really do want to clear the value of a combobox.

我们添加了新的 nullable 属性来实现这一点 - 只需添加该属性,现在你可以删除该值而不会恢复之前的值:

¥We've added a new nullable prop that makes this possible — just add the prop and now you can delete the value without the previous value being restored:

function MyCombobox({ items }) {
const [selectedItem, setSelectedItem] = useState([]);
return (
<Combobox value={selectedItem} onChange={setSelectedItem} nullable>
<Combobox.Input />
<Combobox.Options>
{items.map((item) => (
<Combobox.Option key={item} value={item}>
{item}
</Combobox.Option>
))}
</Combobox.Options>
</Combobox>
);
}

便捷的 HTML 表单支持(Easy HTML form support)

¥Easy HTML form support

现在,如果你将 name 属性添加到 ListboxComboboxSwitchRadioGroup 等组件中,我们将自动创建一个与组件值同步的隐藏输入框。

¥Now if you add a name prop to form components like Listbox, Combobox, Switch, and RadioGroup, we'll automatically create a hidden input that syncs with the component's value.

这样可以非常轻松地通过常规表单提交或类似 Remix 中的 <Form> 组件将数据发送到服务器。

¥This makes it super easy to send that data to the server with a regular form submission, or with something like the <Form> component in Remix.

<form action="/projects/1/assignee" method="post">
<Listbox
value={selectedPerson}
onChange={setSelectedPerson}
name="assignee"
>
{/* ... */}
</Listbox>
<button>Submit</button>
</form>

这适用于数字和字符串等简单值,也适用于对象 - 我们使用 1996 年的方括号表示法自动将它们序列化为多个字段:

¥This works with simple values like numbers and string, but also with objects — we automatically serialize them into multiple fields using that square bracket notation from 1996:

<input type="hidden" name="assignee[id]" value="1" />
<input type="hidden" name="assignee[name]" value="Durward Reynolds" />

如果你想在不同的域名上重新阅读我刚才写的内容,请查看 the documentation

¥Check out the documentation if you want to read exactly what I just wrote all over again but on a different domain.

可滚动对话框改进(Scrollable dialog improvements)

¥Scrollable dialog improvements

对话框确实是地球上最难构建的东西。我们一直在与棘手的 scrolling issues 斗争,并认为我们终于在 v1.6 中解决了所有问题。

¥Dialogs are literally the hardest thing to build on the planet. We've been wrestling with gnarly scrolling issues for a while now, and think we've finally got it all sorted out in v1.6.

关键在于我们改变了 "点击外部关闭" 的工作方式。我们过去使用放置在实际对话框后面的 Dialog.Overlay 组件,并在其上添加了一个点击处理程序,用于在点击时关闭对话框。实际上,我非常喜欢这种简单性 - 检测特定元素的点击时间比检测特定元素以外的任何内容的点击时间要简单得多,尤其是在对话框中渲染的内容本身也在门户网站等中渲染其他内容的情况下。

¥The crux of it is that we've changed how "click outside to close" works. We used to use this Dialog.Overlay component that you put behind your actual dialog, and we had a click handler on that that would close the dialog on click. I actually really love the simplicity of this in principle — it's a lot less quirky to detect when a specific element is clicked than it is to detect when anything other than a specific element is clicked, especially when you have things rendered inside your dialog that themselves are rendering other things in portals and stuff.

这种方法的问题在于,如果你有一个需要滚动的长对话框,你的覆盖层将位于滚动条的顶部,而尝试点击滚动条会关闭对话框。不是你想要的!

¥The problem with this approach is that if you had a long dialog that required scrolling, your overlay would sit on top of your scrollbar, and trying to click the scrollbar would close the dialog. Not what you want!

为了以不破坏代码的方式解决这个问题,我们添加了一个新的 Dialog.Panel 组件供你使用,现在,当你点击该组件外部时,对话框就会关闭,而不是在点击覆盖层时特意关闭它:

¥So to fix this in a non-breaking way, we've added a new Dialog.Panel component you can use instead, and now we close the dialog any time you click outside of that component, rather than closing it specifically when the overlay is clicked:

<Dialog
open={isOpen}
onClose={closeModal}
className="fixed inset-0 flex items-center justify-center ..."
>
<Dialog.Overlay className="fixed inset-0 bg-black/25" />
<div className="fixed inset-0 bg-black/25" />
<div className="bg-white shadow-xl rounded-2xl ...">
<Dialog.Panel className="bg-white shadow-xl rounded-2xl ...">
<Dialog.Title>Payment successful</Dialog.Title>
{/* ... */}
</div>
</Dialog.Panel>
</Dialog>

查看 the updated dialog documentation 了解使用新面板组件(而非覆盖层)的更完整示例。

¥Check out the updated dialog documentation for more complete examples using the new panel component instead of the overlay.

更好的焦点捕获(Better focus trapping)

¥Better focus trapping

对话框之所以成为世界上最难构建的东西,原因之一就是焦点捕获。我们首次尝试劫持 Tab 键并手动聚焦下一个/上一个元素,以便在你到达末尾时,我们可以回到焦点陷阱中的第一个项目。

¥One of the many reasons dialogs are the hardest thing to build on the planet is because of focus trapping. Our first attempt at this involved hijacking the tab key and manually focusing the next/previous element, so that we could circle back to the first item in the focus trap when you get to the end.

在人们开始在焦点陷阱中使用门户网站之前,这种方法可以正常工作。现在这基本上无法管理,因为你可以通过 Tab 键切换到日期选择器或其他概念上位于对话框内部的东西,但实际上它并不在对话框中,因为出于样式原因,它是在门户中渲染的。

¥This works okay until people start using portals inside the focus trap. Now it's basically impossible to manage because you could tab to a datepicker or something that is conceptually inside the dialog, but isn't actually because it's rendered in a portal for styling reasons.

Robin 为此设计了一个超级简单的 非常棒的解决方案 库 - 无需手动控制 Tab 键的工作方式,只需在焦点陷阱的开头和结尾分别添加一个不可见的可聚焦元素即可。现在,每当这些标记元素之一获得焦点时,你只需将焦点移动到它实际应该在的位置,具体取决于你是位于第一个元素还是最后一个元素,以及用户是向前还是向后 Tab 键。

¥Robin came up with a really cool solution for this that is super simple — instead of trying to manually control how tabbing works, just throw an invisible focusable element at the beginning of the focus trap and another one at the end. Now whenever one of these sentinel elements receives focus you just move focus to where it actually should be, based on whether you're at the first element or the last element and whether the user was tabbing forwards or backwards.

使用这种方法,你完全无需劫持 Tab 键 - 你只需让浏览器完成所有工作,并且只在你的某个哨兵元素获得焦点时才手动移动焦点。

¥With this approach, you don't have to hijack the tab key at all — you just let the browser do all of the work and only move focus manually when one of your sentinel elements receives focus.

弄清楚之后,我们发现其他几个库已经在做同样的事情,所以这算不上突破性或新颖的,但我认为这非常巧妙,值得与那些从未想过这种技术的人分享。

¥After figuring this out we noticed a couple of other libraries already doing the same thing so it's nothing groundbreaking or new, but I thought it was pretty damn clever and worth sharing for anyone who hadn't thought of this technique.


Tailwind UI 的团队管理功能(Team management features for Tailwind UI)

¥Team management features for Tailwind UI

我们首次发布 Tailwind UI 时,"team" 只有我和 Steve 两个人,所以如果我们想靠我们两个人的努力把这个产品真正推出去,我们必须把很多事情做得简单。

¥When we first released Tailwind UI, the "team" was just me and Steve, so we had to keep a lot of things simple if we wanted any chance of actually getting the thing out the door with just the two of us working on it.

其中之一就是团队授权。我们没有提供任何花哨的团队成员邀请流程,我们只是要求人们与他们的团队分享他们的 Tailwind UI 凭据。这足以让我们顺利完成工作,因为 Tailwind UI 实际上并不以用户特定的方式执行任何操作,而且团队中的每个成员都会获得相同的体验。

¥One of those things was team licensing. We didn't ship with any fancy team member invitation flow or anything, we just asked people to share their Tailwind UI credentials with their team. This was good enough for us to get things out the door, because Tailwind UI doesn't really do anything in a user-specific way, and every member of your team gets the same experience anyways.

此外,对于我们来说,获取团队中每个人的电子邮件地址,将其输入到某个表单中,向每个人发送邀请邮件,并让他们接受邀请,这感觉就像一场行政地狱,尤其是在每个人登录后都获得相同体验的情况下。

¥Plus to us, having to get the email addresses of everyone on your team, enter them into some form, send each person an invitation email, and have them accept the invitation felt like administrative hell, especially when every single person gets the same experience after they sign in.

但与此同时,共享任何内容的凭证都相当低端,而且我们对此并不感到自豪。我的 Tailwind UI 密码 (slayerfan1234) 和我的银行账户密码一样 - 我不想和任何人分享这个密码!

¥At the same time though, sharing credentials for anything is pretty low-end, and it's not a design decision we took a lot of pride in. I use the same password (slayerfan1234) for Tailwind UI as I do for my bank account — I don't want to share that with anyone!

几周前,我们决定解决这个问题并构建一些东西。

¥So a couple of weeks ago we decided to figure it out and build something.

Interface with a copyable invite URL and list of team members

我们最终采用了一个纯粹基于链接的邀请系统,你只需复制邀请链接,在 Slack/Discord 或其他平台上与你的团队分享,并在需要时重置链接即可。你还可以授予用户 "会员" 或 "所有者" 权限,以控制他们是否可以管理团队成员或查看账单历史记录。

¥What we landed on was a purely link-based invitation system, where you could just copy your invite link, share it with your team in Slack/Discord/whatever, and reset your link if needed. You can also give people either "Member" or "Owner" permissions, which control whether they can manage team members or view billing history.

这样可以非常轻松地邀请你的团队,而无需进行大量繁琐的数据输入;如果有人离开,也可以在 UI 中直接撤销访问权限,而无需更改共享密码。

¥This makes it super easy to invite your team without a bunch of tedious data entry, and revoke access if someone leaves right in the UI instead of by changing your shared password.

现在,任何拥有 Tailwind UI 团队账户的人都可以使用此功能 - 只需打开下拉菜单,点击“"我的团队"”为你的团队命名,然后开始邀请你的同事。

¥This is available now for anyone with a Tailwind UI team account — just open the dropdown menu and click "My Team" to name your team and start inviting your co-workers.

你可以在 Tailwind UI 网站上使用 purchase a license for your team,如果你拥有个人许可证并希望与你的团队开始使用 Tailwind UI,则可以使用 upgrade to a team license

¥You can purchase a license for your team on the Tailwind UI website, or upgrade to a team license if you have a personal license and want to start using Tailwind UI with your team.


将 Tailwind UI 中的 Vue 示例更新至 <script setup>(Updating the Vue examples in Tailwind UI to <script setup>)

¥Updating the Vue examples in Tailwind UI to <script setup>

自从 Vue 发布对 Tailwind UI 的支持以来,Vue 3 中新的 <script setup> 语法已成为编写单文件组件的推荐方式。

¥Since releasing Vue support for Tailwind UI, the new <script setup> syntax in Vue 3 has become the recommended way to write your single-file components.

我们已更新 Tailwind UI 中的所有 Vue 示例,以使用这种新格式,从而减少了大量样板代码:

¥We've updated all of the Vue examples in Tailwind UI to use this new format, which cuts out a ton of boilerplate:

<template>
<Listbox as="div" v-model="selected">
<!-- ... -->
</Listbox>
</template>
<script setup>
import { ref } from "vue";
import { Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions } from "@headlessui/vue";
import { CheckIcon, SelectorIcon } from "@heroicons/vue/solid";
const people = [
{ id: 1, name: "Wade Cooper" },
// ...
];
const selected = ref(people[3]);
</script>

对我来说,最棒的部分是你不再需要明确在 components 下注册任何东西 - 任何在范围内的组件都会自动提供给模板使用。

¥To me, the absolute best part is that you don't have to explicitly register anything under components anymore — any components that are in scope are automatically available to the template.

使用 <script setup> 也允许你像使用 Listbox.Button 一样使用 namespaced components,就像我们在 React 风格的 Headless UI 中所做的那样。我们尚未更新 Headless UI 以通过这种方式展示组件,但我们可能很快就会更新,这将帮助你减少大量的导入工作。

¥Using <script setup> also lets you use namespaced components like Listbox.Button like we do in the React flavor of Headless UI. We haven't updated Headless UI to expose the components this way yet, but we're probably going to do it soon, which will let you shave off a ton of imports.


VS Code 的新 Tailwind CSS 语言模式(New Tailwind CSS language mode for VS Code)

¥New Tailwind CSS language mode for VS Code

Tailwind 使用了许多非标准的 @ 规则,例如 @tailwind@apply,因此如果你使用常规 CSS 语言模式,VS Code 中会收到 lint 警告。

¥Tailwind uses a bunch of non-standard at-rules like @tailwind and @apply, so you get lint warnings in VS Code if you use the regular CSS language mode.

为了解决这个问题,我们一直建议用户使用 PostCSS Language Support 插件,它可以消除这些警告,但也会移除所有其他 CSS IntelliSense 支持。

¥To get around this, we've always recommended people use the PostCSS Language Support plugin, which gets rid of those warnings but also removes all of the other CSS IntelliSense support.

几周前,我们发布了第一方 Tailwind CSS 语言模式,作为 Tailwind CSS IntelliSense 扩展的一部分。该模式基于内置 CSS 语言模式,添加了 Tailwind 特有的语法高亮,并修复了你通常会看到的 lint 警告,同时又保留了你想要保留的任何 CSS IntelliSense 功能。

¥So a few weeks ago, we released a first-party Tailwind CSS language mode as part of our Tailwind CSS IntelliSense extension, which builds on the built-in CSS language mode to add Tailwind-specific syntax highlighting and fix the lint warnings you'd usually see, without losing any of CSS IntelliSense features you do want to keep.

Sample CSS code shown with lint warnings when using a built-in CSS language mode, and no lint warnings when using the Tailwind CSS language mode.

下载最新版本的 Tailwind CSS IntelliSense 并选择 "Tailwind CSS" 作为 CSS 文件的语言模式,即可试用。

¥Try it out by downloading the latest version of Tailwind CSS IntelliSense and choosing "Tailwind CSS" as the language mode for your CSS files.


Tailwind Play 中的“生成 CSS”面板(“Generated CSS” panel in Tailwind Play)

¥“Generated CSS” panel in Tailwind Play

在过去的几个月里,我们对 Tailwind Play 做了许多小改进,其中我最喜欢的是新的 "生成的 CSS" 面板。

¥We've made a bunch of little improvements to Tailwind Play over the last couple of months, with my favorite being the new "Generated CSS" panel.

Tailwind Play interface with a panel showing the CSS generated for that playground.

它会显示从 HTML 生成的所有 CSS,并允许你按层进行筛选,这对于故障排除非常有用。在内部,我们一直在使用它来调试与未检测到的类相关的奇怪问题,以便我们可以执行任何必要的 horrific regex surgery 来使其正常工作。

¥It shows you all of the CSS that was generated from your HTML and lets you filter by layer, which is incredibly useful for troubleshooting. Internally, we are using this all the time to debug weird issues around classes not being detected so we can perform whatever horrific regex surgery is necessary to make it work.

我们还在每个窗格中添加了一个 "整洁" 按钮(Cmd + S),它可以自动格式化你的代码(并对你的类进行排序!),以及一个 "复制" 按钮(Cmd + A Cmd + C,不过你已经知道了)。

¥We also added a "Tidy" button (Cmd + S) to each pane that will automatically format your code (and sort your classes!) and a "Copy" button (Cmd + A Cmd + C, but you already know that) too.


重新设计 Refactoring UI 网站(Redesigning the Refactoring UI website)

¥Redesigning the Refactoring UI website

2018 年 12 月发布 Refactoring UI 时,我和 Steve 几乎是在发布前一天晚上凌晨 1 点左右设计并构建了最终的落地页。

¥When we released Refactoring UI back in December 2018, Steve and I literally designed and built the final landing page the night before launch at like 1am.

当时的情况是,我们设计好了整个诱人的落地页,然后我正准备写一份公告邮件,准备发给邮件列表里的所有人,我们俩都想到了 "伙计,这封邮件的内容很棒,比我们这个落地页设计的内容更引人注目。"。

¥What happened is we had this whole sexy landing page designed, then I was writing up the announcement email to send to everyone on our mailing list and we both thought "man the content in this email is great and a lot more compelling than what we have in this landing page design".

但这些内容与我们之前的设计并不契合,所以在最后一刻,我们放弃了所有设计,并根据新内容拼凑了一个更简单的页面。它看起来不错,但并没有达到我们真正想要的那种超级美妙的体验。

¥But that content didn't really fit into what we had designed, so at the eleventh hour we scrapped everything we had designed and whipped together a much simpler page based on the new content. It looked fine but it wasn't the super beautiful experience we really wanted it to be.

几周前,我们最终决定设计 something new

¥So a few weeks ago, we decided to finally design something new.

Header section of redesigned Refactoring UI website.

我仍然对这本书感到非常自豪 - 或许比我们以往创作的任何作品都更加自豪。它在 Goodreads 上的评分为 4.68,有超过 1100 个评分和近 200 条评论,对于一本自发布的电子书来说,这感觉相当不可思议。

¥I'm still extremely proud of this book — probably more so than anything we've ever made. It's got a 4.68 rating on Goodreads with over 1100 ratings and almost 200 reviews, which feels pretty incredible to me for a self-published ebook.

期待未来有一天能用我们学到的所有知识来制作第二版!

¥Looking forward to doing a second edition one day with everything we've learned since!


Tailwind CSS 模板即将推出(Tailwind CSS templates are coming soon)

¥Tailwind CSS templates are coming soon

我们在 on Twitter 版中对此进行了一些预告,但在过去的几个月里,我们一直在努力开发一系列功能齐全的 Tailwind CSS 网站模板。

¥We've teased this a bit on Twitter, but for the last couple of months we've been working really hard on a bunch of full-fledged Tailwind CSS website templates.

以下是其中一个模板的预览 - 一个使用 Next.js 和 Stripe 的新 Markdoc 库构建的文档站点模板:

¥Here's a sneak peek at one of them — a documentation site template built with Next.js and Stripe's new Markdoc library:

Artboards for a documentation site design that includes mobile and desktop layouts, and light and dark color schemes.

我非常期待这些代码的发布。我为 Tailwind UI 这款产品感到无比自豪,但这种可复制粘贴的代码片段格式的局限性之一在于,我们没有机会真正向你展示如何组件化、最大限度地减少重复,以及如何将内容构建成一个完整的、可立即投入生产的网站。

¥I'm unreasonably excited about getting these out. I'm really proud of Tailwind UI as a product, but one of the limitations of the copy-and-pasteable-code-snippet format is that we don't get an opportunity to really show you how to componentize things, minimize duplication, and architect things as a complete, production-ready website.

我们现在正在开发的模板将会非常出色地填补这一空白。除了获得精美的模板作为你自己项目的起点之外,你还可以深入研究代码,并确切地了解我们如何使用 Tailwind CSS 构建网站。

¥The templates we're working on now are going to be amazing at filling that gap. On top of just getting beautiful templates to use as a starting point for your own projects, you'll be able to dig through the code and study exactly how we build websites with Tailwind CSS ourselves.

我们尚未确定这些功能的确切发布日期,但希望下个月能有所进展。随着我们取得更多进展,我们将分享更多信息!

¥We haven't set an exact release date on these yet but we're hoping to have something out next month. Will share more as we make more progress!

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