新しいSaaSマーケティングサイトテンプレート「Radiant」の作業が完了し、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> )}/* ... */
ロゴタイムラインアニメーションは少し異なります。ホバーを停止したときにロゴを元の位置に戻すのではなく、現在の位置で一時停止させたかったためです。これは、開始状態と終了状態を指定する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のクールな点の1つは、コンテンツが構造化された形式で返されることです。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です!内部を見て、試してみて、感想をお聞かせください。
他のすべてのテンプレートと同様に、1回限りの購入Tailwind UIオールアクセスライセンスに含まれています。これは、Tailwind CSSの作業をサポートし、私たちが今後何年も素晴らしいものを構築し続けることを可能にする最良の方法です。