Feature card
Icon, title, body copy and an optional badge on a themed card that rises into place.
FeatureCard, rise
Installation
npx shadcn add https://www.reelcn.dev/r/feature-card.jsonFiles land in src/reelcn/ with the items it builds on.
Usage
<Center>
<FeatureCard title="Zero config" body="Install with shadcn, no path aliases." badge="New" />
</Center>Props
PropTypeDescription
delaynumberFrames to wait before entering.
durationnumberEnter duration in frames. Defaults to 0.6s. Some items reuse
duration for an unrelated local timing. See that item's own prop docs.exitnumber | booleanExit at the end of the parent `<Sequence>`.
false keeps it on screen, a number sets the exit length in frames.motionMotionPersonalityOverride the theme's motion personality.
posterbooleanRenders fully entered, with no exit: for a still frame or thumbnail. Defaults to
false.holdFramesnumberDelays the start of the exit window by this many frames, for a minimum on-screen hold. Defaults to
0.iconReactNodeSmall mark shown in a rounded well above the title, e.g. an inline `<svg>`.
title*string
bodystring
badgestringSmall pill in the top-right corner, e.g. "New" or "Popular".
widthnumberCard width in design units. Default
420.backgroundstring
borderColorstring
titleColorstring
bodyColorstring
iconColorstringIcon well color, and the badge's default color. Defaults to the theme accent.
iconBackgroundstringIcon well fill. Defaults to a soft tint of `iconColor`.
badgeColorstring
badgeBackgroundstring
radiusnumber
styleCSSProperties
classNamestring
Built from
Use
- One feature at a time in a launch video, or several staggered inside `stagger` or `bento-grid`
- A pricing tier or plan card with a "New" or "Popular" badge
Avoid
- Several feature tiles on one canvas at once. Use
bento-grid
Guides
Components take their colors from a theme, their timing from motion props and their layout from the canvas.
Source
registry/items/feature-card.tsx
/**
* @title Feature Card
* @category product
* @description Icon, title, body copy and an optional badge on a themed card that rises into place.
* @duration 24
* @use One feature at a time in a launch video, or several staggered inside `stagger` or `bento-grid`
* @use A pricing tier or plan card with a "New" or "Popular" badge
* @avoid Several feature tiles on one canvas at once. Use `bento-grid`
* @tags feature, card, icon, badge, pricing, plan
* @example
* <Center>
* <FeatureCard title="Zero config" body="Install with shadcn, no path aliases." badge="New" />
* </Center>
*/
import type React from "react";
import { effectStyle } from "./animate";
import { alpha, type MotionProps, useMotion, useTheme, useViewport } from "./core";
export type FeatureCardProps = MotionProps & {
/** Small mark shown in a rounded well above the title, e.g. an inline `<svg>`. */
icon?: React.ReactNode;
title: string;
body?: string;
/** Small pill in the top-right corner, e.g. "New" or "Popular". */
badge?: string;
/** Card width in design units. */
width?: number;
background?: string;
borderColor?: string;
titleColor?: string;
bodyColor?: string;
/** Icon well color, and the badge's default color. Defaults to the theme accent. */
iconColor?: string;
/** Icon well fill. Defaults to a soft tint of `iconColor`. */
iconBackground?: string;
badgeColor?: string;
badgeBackground?: string;
radius?: number;
style?: React.CSSProperties;
className?: string;
};
export function FeatureCard({
icon,
title,
body,
badge,
width = 420,
background,
borderColor,
titleColor,
bodyColor,
iconColor,
iconBackground,
badgeColor,
badgeBackground,
radius,
style,
className,
...motion
}: FeatureCardProps) {
const theme = useTheme();
const { u } = useViewport();
const m = useMotion(motion);
const accent = iconColor ?? theme.colors.accent;
const { opacity: riseOpacity = 1, ...rise } = effectStyle("up", m.enter, u(46));
return (
<div
className={className}
style={{
position: "relative",
width: u(width),
padding: u(32),
borderRadius: u(radius ?? theme.radius),
background: background ?? theme.colors.surface,
border: `1px solid ${borderColor ?? theme.colors.border}`,
fontFamily: theme.fonts.body,
opacity: Number(riseOpacity) * (1 - m.exit),
...rise,
...style,
}}
>
{badge && (
<div
style={{
position: "absolute",
top: u(20),
right: u(20),
padding: `${u(4)}px ${u(12)}px`,
borderRadius: 9999,
background: badgeBackground ?? alpha(accent, 0.16),
color: badgeColor ?? accent,
fontSize: u(16),
fontWeight: 700,
letterSpacing: "0.04em",
textTransform: "uppercase",
}}
>
{badge}
</div>
)}
{icon && (
<div
style={{
width: u(56),
height: u(56),
borderRadius: u(theme.radius * 0.7),
display: "grid",
placeItems: "center",
background: iconBackground ?? alpha(accent, 0.14),
color: accent,
fontSize: u(28),
}}
>
{icon}
</div>
)}
<div
style={{
marginTop: icon ? u(22) : 0,
fontFamily: theme.fonts.heading,
fontWeight: theme.headingWeight,
fontSize: u(34),
lineHeight: 1.15,
letterSpacing: "-0.02em",
color: titleColor ?? theme.colors.foreground,
}}
>
{title}
</div>
{body && (
<div style={{ marginTop: u(10), fontSize: u(22), lineHeight: 1.4, color: bodyColor ?? theme.colors.muted }}>
{body}
</div>
)}
</div>
);
}