Cards
FreeFeature Card
The icon tile turns and lifts, a hairline sweeps under the title, and a 'learn more' arrow slides in.
Install
No key needed.
npx @galactrex/ui add feature-cardUsage
import { FeatureCard } from "@/components/galactrex/feature-card";
<FeatureCard icon={<LayersIcon />} title="Layers">Masks and blend modes.</FeatureCard>Source
Installs to components/galactrex/feature-card.tsx, with the shared motion primitives beside it.
"use client";
import type { ReactNode } from "react";
import { motion } from "motion/react";
import { cx } from "@/components/galactrex/motion/cx";
import { curves } from "@/components/galactrex/motion/springs";
import { useReducedMotion } from "@/components/galactrex/motion/use-reduced-motion";
export type FeatureCardProps = {
/** an icon, 24px works best */
icon: ReactNode;
title: string;
children: ReactNode;
/** a link for "learn more" */
href?: string;
/** a number or step label, like "01" */
index?: string;
className?: string;
};
/** The feature card, done with care: the icon sits in a tile that turns and lifts on hover, a hairline sweeps
under the title, and a "learn more" arrow slides in. For feature grids, process steps and service lists. One
focusable link when it has `href`. */
export function FeatureCard({ icon, title, children, href, index, className }: FeatureCardProps) {
const reduced = useReducedMotion();
const Tag = href ? motion.a : motion.div;
return (
<Tag
href={href}
initial="rest"
animate="rest"
whileHover={reduced ? undefined : "on"}
whileFocus={reduced ? undefined : "on"}
className={cx("group relative block rounded-2xl border border-gxui-line bg-gxui-surface p-6 text-gxui-ink outline-none transition-colors hover:border-gxui-muted focus-visible:ring-2 focus-visible:ring-gxui-ink", className)}
>
<div className="flex items-start justify-between">
<motion.span aria-hidden className="grid size-11 place-items-center rounded-xl border border-gxui-line bg-gxui-raised" variants={{ rest: { rotate: 0, y: 0 }, on: { rotate: -8, y: -3 } }} transition={{ type: "spring", stiffness: 380, damping: 18 }}>
{icon}
</motion.span>
{index && <span className="font-mono text-xs text-gxui-muted">{index}</span>}
</div>
<p className="relative mt-5 inline-block font-semibold">
{title}
<motion.span aria-hidden className="absolute -bottom-1 left-0 h-px w-full origin-left bg-gxui-ink" variants={{ rest: { scaleX: 0 }, on: { scaleX: 1 } }} transition={{ duration: 0.35, ease: curves.glide }} />
</p>
<div className="mt-2 text-sm text-gxui-muted">{children}</div>
{href && (
<motion.span aria-hidden className="mt-4 inline-flex items-center gap-1 font-mono text-xs" variants={{ rest: { opacity: 0.5, x: 0 }, on: { opacity: 1, x: 4 } }}>
Learn more →
</motion.span>
)}
</Tag>
);
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
icon | ReactNode | — | 24px icon. |
title | string | — | Title. |
children | ReactNode | — | Text. |
href | string | — | Makes the whole card one link. |
index | string | — | Step number. |
className | string | — | Extra classes for the outer element. |
Variants
- Steps. With an index like 01.
Accessibility
- One focusable link with href.
Motion and reduced motion
- Spring variants on hover and focus.
- Reduced motion: still.