Radiant:一个精美的全新营销网站模板

Dan Hollick

我们刚刚完成了一个名为 Radiant 的精美全新 SaaS 营销网站模板的开发工作,它现在作为 Tailwind UI 的一部分提供。

¥We just wrapped up work on a beautiful new SaaS marketing site template called Radiant, and it's available now as part of Tailwind UI.

它基于 Next.js、Framer Motion 和 Tailwind CSS 构建,并由 Sanity 提供支持博客。

¥It’s built with Next.js, Framer Motion, and Tailwind CSS, with a blog powered by Sanity.

我们已经有一段时间没有构建像这样的 SaaS 营销模板了,在这段时间里,我们学到了很多关于如何让这样的模板变得实用且易于使用的知识。我们尝试将所有这些经验融入到 Radiant 中。

¥It's been a while since we built a SaaS marketing template like this, and in that time we've learned a lot about what makes a template like this useful and easy to work with. We've tried to incorporate all of those learnings into Radiant.

一如既往地查看 现场预览 以获得完整体验 - 它包含大量精彩细节,你必须在浏览器中查看才能真正体会到。

¥Check out the live preview as always for the full experience — there are tons of cool details in this one that you have to see in the browser to really appreciate.


高雅的交互体验(Tastefully interactive)

¥Tastefully interactive

在这样的网站上,动画很容易过度使用。我们都见过一些网站,你甚至无法滚动几个像素,否则一堆不同的元素就会动画到位。更糟糕的是,当你必须等待内容出现才能阅读时,速度会变得多么缓慢。

¥It's super easy to overdo animation on a site like this. We've all seen sites where you can't even scroll a few pixels without a bunch of different elements animating in to place. Even worse is how slow things feel when you have to wait for the content to appear before you can read it.

Radiant 加载了令人愉悦的动画,但它们都层叠在现有内容上,并由用户交互触发,因此网站仍然感觉很快。在大多数情况下,我们选择循环动画,以便在你与元素交互时使其感觉像 "alive"。

¥Radiant is loaded with delightful animations, but they are all layered on to existing content and triggered by user interaction so the site still feels fast. In most cases, we went for animations that loop to make elements feel "alive" while you're interacting with them.

我们几乎在所有动画中都使用了 Framer Motion。它是声明式的,我们可以轻松地创建自己的 API 来实现复杂的动画,而其他人也可以轻松地进行自定义。

¥We used Framer Motion for almost all of the animations. It's declarative, making it easy to create our own APIs for complex animations that other people can customize without much effort.

不过,它也有一些缺点需要解决。例如,当你有多个独立动画的元素时,将悬停状态传递给每个子元素会很烦人。我们最终利用 Framer 的变体传播功能来实现这一点 - 悬停事件会触发父级的变体变化,并向下传播到子级,因为它们共享相同的变体键。

¥It does have some drawbacks to work around though. For example, when you have multiple elements animating independently it's annoying to pass a hover state down to each child. We ended up leveraging Framer's variant propagation to make this work — a hover event triggers a variant change in the parent that propagates down to the children because they share the same variant keys.

bento-card.tsx
export function BentoCard() {
return (
<motion.div
initial="idle"
whileHover="active"
variants={{ idle: {}, active: {}}}
data-dark={dark ? "true" : undefined}
>
/* ... */
</motion.div>
);
}

父级中的变体之间没有区别,因此实际上不会发生变化,但子级在悬停时仍然会收到更改变体的信号,即使它们是深度嵌套的。

¥There is no difference between the variants in the parent so it doesn't actually change but the children still get a signal to change variants on hover, even if they are deeply nested.

map.tsx
function Marker({
src,
top,
offset,
delay,
}: {
src: string
top: number
offset: number
delay: number
}) {
return (
<motion.div
variants={{
idle: { scale: 0, opacity: 0, rotateX: 0, rotate: 0, y: 0 },
active: { y: [-20, 0, 4, 0], scale: [0.75, 1], opacity: [0, 1] },
}}
transition={{ duration: 0.25, delay, ease: 'easeOut' }}
style={{ '--offset': `${offset}px`, top } as React.CSSProperties}
className="absolute left-[calc(50%+var(--offset))] size-[38px] drop-shadow-[0_3px_1px_rgba(0,0,0,.15)]"
>
/* ... */
</motion.div>
)
}
/* ... */

徽标时间轴动画略有不同,因为我们希望徽标在你停止悬停时暂停在当前位置,而不是返回到其原始位置。这与 Framer 指定起始和结束状态的方法不太兼容,所以用 CSS 实现起来其实更容易。

¥The logo timeline animation is a bit different, because we wanted the logos to pause in their current position when you stop hovering, rather than return to their original position. This doesn't play very well with Framer's approach of specifying start and end states, so it was actually easier to build this in CSS.

它利用了你可以设置负 animation-delay 值来偏移元素起始位置的特性。这样,所有徽标都共享相同的动画关键帧,但它们可以从不同的位置开始,并具有不同的持续时间。

¥It exploits the fact that you can set a negative animation-delay value to offset the start position of the element. That way all the logos share the same animation keyframes but they can start at different positions and have different durations.

logo-timeline.tsx
function Logo({
label,
src,
className,
}: {
label: string
src: string
className: string
}) {
return (
<div
className={clsx(
className,
'absolute top-2 grid grid-cols-[1rem,1fr] items-center gap-2 whitespace-nowrap px-3 py-1',
'rounded-full bg-gradient-to-t from-gray-800 from-50% to-gray-700 ring-1 ring-inset ring-white/10',
'[--move-x-from:-100%] [--move-x-to:calc(100%+100cqw)] [animation-iteration-count:infinite] [animation-name:move-x] [animation-play-state:paused] [animation-timing-function:linear] group-hover:[animation-play-state:running]',
)}
>
<img alt="" src={src} className="size-4" />
<span className="text-sm/6 font-medium text-white">{label}</span>
</div>
)
}
export function LogoTimeline() {
return (
/* ... */
<Row>
<Logo
label="Loom"
src="./logo-timeline/loom.svg"
className="[animation-delay:-26s] [animation-duration:30s]"
/>
<Logo
label="Gmail"
src="./logo-timeline/gmail.svg"
className="[animation-delay:-8s] [animation-duration:30s]"
/>
</Row>
/* ... */

这种方法意味着我们不需要在 JavaScript 中跟踪播放状态,我们只需使用 group-hover:[animation-play-state:running] 类在父级悬停时启动动画即可。

¥This approach means we don't need to track the play state in JavaScript, we can just use a group-hover:[animation-play-state:running] class to start the animation when the parent is hovered.

你可能已经注意到,我们在此组件中为各个 animation 属性使用了一堆任意属性,因为这些实用程序目前在 Tailwind 中尚不存在。构建这些模板的优点在于它能帮助我们找到 Tailwind CSS 中的盲点。谁知道呢,也许我们会在 v4.0 中看到这些实用程序的加入!

¥As you've maybe noticed, we're using a bunch of arbitrary properties for individual animation properties in this component, since these utilities don't exist in Tailwind today. This is what's great about building these templates — it helps us find blind spots in Tailwind CSS. Who knows, maybe we'll see these utilities added for v4.0!


可重复使用(Deliberately reusable)

¥Deliberately reusable

设计这样的 SaaS 模板最棘手的部分是想出一些交互式元素,让人们可以轻松地将它们应用到自己的产品中。没有什么比购买模板后发现它过于局限于示例内容,以至于无法在自己的项目中使用更糟糕的了。

¥The trickiest part of designing a SaaS template like this, is coming up with interactive elements that people can apply to their own product without too much effort. There's nothing worse than buying a template and realizing that it's so specific to the example content that you can't actually use it for your own project.

我们提出了一些大多数 SaaS 产品可能具备的核心图形元素。带有图钉的地图、徽标集群、键盘 - 这些东西可以应用于一系列不同的功能。因为我们希望它们能够轻松地应用于你自己的产品,所以我们用代码构建了许多这样的特效,并为它们设计了优秀的 API。

¥We came up with some core graphical elements that most SaaS products might have. A map with pins, a logo cluster, a keyboard — things that could be applied to a bunch of different features. Because we wanted them to be easy to repurpose for your own product, we built a lot of them in code and designed nice APIs for them.

例如,徽标集群有一个简单的 API,可让你传入自定义徽标,调整其位置并设置悬停动画以匹配。

¥The logo cluster, for example, has a simple API that lets you pass in your own logos, tweak their position and hover animation to match.

<Logo src="./logo-cluster/dribbble.svg" left={285} top={20} hover={{ x: 4, y: -5, rotate: 6, delay: 0.3 }} />

键盘快捷键部分是另一个很好的例子。添加你自己的快捷键非常简单,只需将一个按键名称数组传递给键盘组件即可。由于每个按键本身就是一个组件,因此你可以轻松添加自定义按键或更改布局。

¥The keyboard shortcuts section is another good example. Adding your own shortcuts is as simple as passing an array of key names to the Keyboard component and because each key is a component, you can easily add custom keys or change the layout.

<Keyboard highlighted={["F", "M", "L"]} />

事实证明,用代码构建键盘实际上需要大量工作,但至少现在你不用自己去发现这个问题了。

¥It turns out it's actually quite a lot of work to build a keyboard in code, but at least now you'll never have to find that out for yourself.

当然,我们也留出了放置你自己产品截图的位置。以下是此部分的外观,我们根据 SavvyCal 的朋友们的需求进行了定制,并使用了相同的交互组件。

¥Of course, we also left spots for you to drop in screenshots of your own product. Here's what this section looks like customized to suit our friends at SavvyCal, using the same interactive components.

Radiant as SavvyCal

由……提供支持 CMS(Powered by a CMS)

¥Powered by a CMS

通常,我们在将博客添加到模板时只使用 MDX,但这次我们觉得尝试一下无头 CMS 会很有趣。在 正在对我们的用户进行调查 之后,我们听到了很多好评,因此决定让 Sanity 也尝试一下。

¥Usually we just use MDX when adding a blog to a template, but this time we thought it would be fun to play with a headless CMS for a chance instead. We decided to give Sanity a go for this one after polling our audience and hearing a lot of good things.

CMS 无需手动创建文件、提交代码以及管理图片和其他内容,它允许你从 UI 中处理所有操作,因此即使是非开发者也可以轻松地做出贡献。

¥Instead of creating files, making commits, and managing images and stuff by hand, a CMS lets you handle everything from their UI, so even non-developers can easily contribute.

Sanity Studio

像 Sanity 这样的无头内容管理系统 (CMS) 的一个很酷的特点是,你可以以结构化的格式获取内容,这与 MDX 非常相似,你可以将元素映射到你自己的自定义组件中,以处理所有排版样式。

¥One cool thing about headless CMSes like Sanity is you get your content back in a structured format, so similar to MDX you can map elements to your own custom components to handle all of your typography styles.

<PortableText
value={post.body}
components={{
block: {
normal: ({ children }) => <p className="my-10 text-base/8 first:mt-0 last:mb-0">{children}</p>,
h2: ({ children }) => (
<h2 className="mt-12 mb-10 text-2xl/8 font-medium tracking-tight text-gray-950 first:mt-0 last:mb-0">
{children}
</h2>
),
h3: ({ children }) => (
<h3 className="mt-12 mb-10 text-xl/8 font-medium tracking-tight text-gray-950 first:mt-0 last:mb-0">
{children}
</h3>
),
blockquote: ({ children }) => (
<blockquote className="my-10 border-l-2 border-l-gray-300 pl-6 text-base/8 text-gray-950 first:mt-0 last:mb-0">
{children}
</blockquote>
),
},
types: {
image: ({ value }) => (
<img className="w-full rounded-2xl" src={image(value).width(2000).url()} alt={value.alt || ""} />
),
},
/* ... */
}}
/>

使用 CMS 还意味着你的所有资源(例如图片)都由你托管,并且你可以动态控制图片的大小、质量和格式。

¥Working with a CMS also means all of your assets like images are hosted for you, and you can control the size, quality, and format of the image on the fly.

<div className="text-sm/5 max-sm:text-gray-700 sm:font-medium">
{dayjs(post.publishedAt).format('dddd, MMMM D, YYYY')}
</div>
{post.author && (
<div className="mt-2.5 flex items-center gap-3">
{post.author.image && (
<img
className="aspect-square size-6 rounded-full object-cover"
src={image(post.author.image).width(64).height(64).url()}
alt=""
/>
)}
<div className="text-sm/5 text-gray-700">
{post.author.name}
</div>
</div>
)}

就像你在 Markdown 中使用前置内容一样,你也可以使用自定义字段来丰富内容。例如,我们在博客文章架构中添加了一个 featured 布尔字段,以便你可以在博客的特殊部分中高亮某些文章。

¥Like you might do with front matter in Markdown, you can also enrich content with custom fields. For example, we added a featured boolean field to the blog post schema so you can highlight some posts in a special section on the blog.

Radiant Blog

Sanity 是一款付费产品,但他们也提供了相当慷慨的免费套餐,足够你玩一玩了。如果你想尝试不同的无头 CMS,我认为我们在此处整合的 Sanity 集成仍然是一个很好的示例,可以指导你如何使用其他工具进行连接。

¥Sanity in particular is a paid product, but they have a pretty generous free tier which is more than enough to play around. And if you wanted to try out a different headless CMS, I think the Sanity integration we've put together here will still serve as a great informative example of how you might approach wiring things up with another tool.


这就是 Radiant!深入了解一下,试用一下,然后告诉我们你的想法。

¥And that's Radiant! Have a look under the hood, kick the tires, and let us know what you think.

与我们所有的模板一样,它包含一次性购买的 Tailwind UI 全面访问 许可证,这是支持我们在 Tailwind CSS 上工作的最佳方式,也使我们能够在未来几年继续为你打造精彩的作品。

¥Like all of our templates, it's included with a one-time purchase Tailwind UI all-access license, which is the best way to support our work on Tailwind CSS and make it possible for us to keep building awesome stuff for you for years to come.

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