
我们刚刚完成了一个名为 Radiant 的美丽新SaaS营销网站模板的开发,现在作为 Tailwind UI的一部分提供。
它是使用Next.js、Framer Motion和Tailwind CSS构建的,并且有一个由Sanity支持的博客。
自从我们像这样的SaaS营销模板以来已经有一段时间了,在此期间我们学到了很多关于使模板有用且易于使用的经验。我们尽量将这些经验融入到Radiant中。
和往常一样,请查看 在线预览,体验完整内容——这个模板中有很多酷炫的细节,您必须在浏览器中查看才能真正欣赏。
适度互动
在像这样的网站上过度使用动画是非常容易的。我们都曾见过一些网站,您即使滚动几个像素,也会看到多个不同元素一起动画显示。更糟糕的是,当您必须等内容出现才能阅读时,整个感觉会变得非常慢。
Radiant装载了令人愉悦的动画,但所有这些动画都是建立在现有内容之上,并由用户交互触发,因此网站仍然感觉很快。在大多数情况下,我们选择了循环的动画,使元素在与它们交互时感觉“生动”。
我们几乎使用 Framer Motion 进行所有动画。它是声明式的,使我们能够轻松创建自己的API,以便于其他人进行复杂动画的自定义,而无需付出太大努力。
不过,它确实有一些需要绕过的缺点。例如,当您有多个独立动画的元素时,将悬停状态传递到每个子容器往往很麻烦。我们最终利用Framer的变体传播来解决这个问题——悬停事件触发父元素的变体更改,这个改变再传播到子元素,因为它们共享相同的变体键。
export function BentoCard() { return ( <motion.div initial="idle" whileHover="active" variants={{ idle: {}, active: {} }} data-dark={dark ? "true" : undefined} > /* ... */ </motion.div> );}
父元素中的变体没有区别,因此实际上不会更改,但子元素在悬停时仍然获得变更变体的信号,即使它们嵌套得很深。
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> )}/* ... */
Logo时间轴动画有点不同,因为我们希望徽标在您停止悬停时能够暂停在当前位置,而不是返回到其原始位置。这与Framer的起始和结束状态指定方法不太兼容,因此实际上用CSS来构建这一点更容易。
它利用了您可以设置一个负的 animation-delay
值来抵消元素的起始位置的事实。这样,所有徽标共享相同的动画关键帧,但可以从不同的位置开始,并具有不同的持续时间。
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]
类,在父级悬停时启动动画。
如您所注意到的,我们在这个组件中使用了很多用于个别 animation
属性的任意属性,因为这些工具在今天的Tailwind中并不存在。这就是构建这些模板的好处——它帮助我们发现Tailwind CSS中的盲点。谁知道呢,也许我们会在v4.0中看到这些工具的添加!
刻意可重用
设计这样的SaaS模板最棘手的部分是想出用户可以轻松应用于自己产品的交互元素。没有什么比买一个模板却发现它太具体于示例内容以至于无法真正用于自己的项目更糟了。
我们想出了些大多数SaaS产品可能拥有的核心图形元素。带图钉的地图、徽标集群、键盘——这些东西可以应用于各种不同的功能。因为我们希望它们易于为您的产品重新利用,所以我们在代码中构建了很多,并为它们设计了良好的API。
例如,徽标集群具有一个简单的API,能够让您传入自己的徽标,调整它们的位置和悬停动画以匹配。
<Logo src="./logo-cluster/dribbble.svg" left={285} top={20} hover={{ x: 4, y: -5, rotate: 6, delay: 0.3 }} />
键盘快捷键部分是另一个很好的例子。只需将一个键名数组传递给Keyboard组件,即可添加您自己的快捷键,并且因为每个键都是一个组件,您可以轻松添加自定义键或更改布局。
<Keyboard highlighted={["F", "M", "L"]} />
事实证明,实际上在代码中构建一个键盘是相当麻烦的,但至少现在您再也不必自己查找这一点了。
当然,我们还留出了空间,让您可以插入自己产品的截图。这一部分看起来就像是定制以适应我们在 SavvyCal 的朋友们,使用相同的交互组件。

由CMS驱动
通常我们在向模板添加博客时仅使用MDX,但这次我们想尝试一下无头CMS。我们决定使用 Sanity 来尝试一下,因为在 对我们的受众进行调查 后,我们听到了很多好评。
与其创建文件、进行提交,并手动管理图像等内容,不如通过CMS从其UI中处理一切,这样甚至非开发人员也可以轻松贡献。

像Sanity这样的无头CMS的一个很酷的特点是,您获得的内容是结构化格式的,因此与MDX类似,您可以将元素映射到自己的自定义组件,以处理所有的排版样式。
<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还意味着您的所有资产,如图片,都是由您托管,并且您可以动态控制图像的大小、质量和格式。
<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
布尔字段,以便您可以在博客的特殊部分突出显示某些帖子。

Sanity尤其是一个付费产品,但它有一个相当慷慨的免费套餐,这足够您进行尝试。如果您想尝试不同的无头CMS,我认为我们在这里设置的Sanity集成仍然是一个很好的示例,展示了如何与其他工具连接。
这就是 Radiant! 了解它的内部运作,尝试一下,告诉我们您的看法。
像我们所有的模板一样,它包含在一次性购买的Tailwind UI全访问许可证中,这是支持我们在Tailwind CSS上工作的最佳方式,让我们能够继续为您构建出色的产品,未来几年内不断创新。