Card
Composable content cards with optional selection and a shared highlight across a grid.
Installation
Import from the Reflow workspace package.
Playground
Open demoimport { CardGroup, Card, CardHeader, CardMedia, Icon, CardTitle, CardDescription } from '@rtzll/reflow'
import { type Handle } from 'remix/component'
export function Example(handle: Handle) {
const features = [
{
"icon": "circle",
"title": "Fluid motion",
"description": "Spring-tuned transitions calibrated across three tiers"
},
{
"icon": "shield",
"title": "Accessible by default",
"description": "Focus-visible rings and ARIA roles in every part"
},
{
"icon": "palette",
"title": "Yours to theme",
"description": "Adjust radius, icons, and density to fit your app"
},
{
"icon": "moon",
"title": "Dark mode ready",
"description": "Tokens adapt to light and dark automatically"
}
] as const
const icon = <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6h16M4 12h16M4 18h16" /></svg>
let plan = 'free'
const select = (value: string) => { plan = value; handle.update() }
return () => (
<>
<CardGroup orientation="card" columns={2} border="none" separated={false} fluidHover={true}>
{features.map(({ icon, title, description }) => (
<Card key={title} label={title} onClick={() => select(title)}>
<CardHeader>
<CardMedia treatment="icon"><Icon name={icon} size={18} /></CardMedia>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
</Card>
))}
</CardGroup>
</>
)
}Basic
import { CardGroup, Card, CardMedia, Icon, CardHeader, CardTitle, CardDescription, CardFooter, CardButton } from '@rtzll/reflow'
import { type Handle } from 'remix/component'
export function Example(handle: Handle) {
const features = [
{
"icon": "circle",
"title": "Fluid motion",
"description": "Spring-tuned transitions calibrated across three tiers"
},
{
"icon": "shield",
"title": "Accessible by default",
"description": "Focus-visible rings and ARIA roles in every part"
},
{
"icon": "palette",
"title": "Yours to theme",
"description": "Adjust radius, icons, and density to fit your app"
},
{
"icon": "moon",
"title": "Dark mode ready",
"description": "Tokens adapt to light and dark automatically"
}
] as const
const icon = <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6h16M4 12h16M4 18h16" /></svg>
return () => (
<>
<div style={{ width: '100%', maxWidth: '520px' }}>
<CardGroup orientation="inline">
{features.map(({ icon, title, description }) => (
<Card key={title} label={title} onClick={() => open(title)}>
<CardMedia treatment="icon"><Icon name={icon} size={18} /></CardMedia>
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardFooter><CardButton>Connect</CardButton></CardFooter>
</Card>
))}
</CardGroup>
</div>
</>
)
}Grid: 2-D fluid hover
import { CardGroup, Card, CardImage, CardHeader, CardTitle, CardDescription, CardFooter, CardButton } from '@rtzll/reflow'
import { type Handle } from 'remix/component'
export function Example(handle: Handle) {
const features = [
{
"icon": "circle",
"title": "Fluid motion",
"description": "Spring-tuned transitions calibrated across three tiers"
},
{
"icon": "shield",
"title": "Accessible by default",
"description": "Focus-visible rings and ARIA roles in every part"
},
{
"icon": "palette",
"title": "Yours to theme",
"description": "Adjust radius, icons, and density to fit your app"
},
{
"icon": "moon",
"title": "Dark mode ready",
"description": "Tokens adapt to light and dark automatically"
}
] as const
const banner = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='320' height='180'%3E%3Cdefs%3E%3CradialGradient id='a' cx='12%25' cy='16%25' r='70%25'%3E%3Cstop offset='0%25' stop-color='%236B97FF' stop-opacity='0.9'/%3E%3Cstop offset='100%25' stop-color='%236B97FF' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='b' cx='90%25' cy='12%25' r='65%25'%3E%3Cstop offset='0%25' stop-color='%236B97FF' stop-opacity='0.45'/%3E%3Cstop offset='100%25' stop-color='%236B97FF' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='c' cx='82%25' cy='94%25' r='75%25'%3E%3Cstop offset='0%25' stop-color='%236B97FF' stop-opacity='0.8'/%3E%3Cstop offset='100%25' stop-color='%236B97FF' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='d' cx='24%25' cy='90%25' r='68%25'%3E%3Cstop offset='0%25' stop-color='%236B97FF' stop-opacity='0.55'/%3E%3Cstop offset='100%25' stop-color='%236B97FF' stop-opacity='0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Crect width='320' height='180' fill='%23ffffff'/%3E%3Crect width='320' height='180' fill='%236B97FF' fill-opacity='0.2'/%3E%3Crect width='320' height='180' fill='url(%23a)'/%3E%3Crect width='320' height='180' fill='url(%23b)'/%3E%3Crect width='320' height='180' fill='url(%23c)'/%3E%3Crect width='320' height='180' fill='url(%23d)'/%3E%3C/svg%3E"
return () => (
<>
<div style={{ width: '100%', maxWidth: '560px' }}>
<CardGroup columns={2} border="outlined" separated>
{features.map(({ title, description }) => (
<Card key={title} label={title} onClick={() => open(title)}>
<CardImage src={banner} alt="" />
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardFooter>
<CardButton variant="primary">Get started</CardButton>
<CardButton variant="secondary">Learn more</CardButton>
</CardFooter>
</Card>
))}
</CardGroup>
</div>
</>
)
}Outlined group
import { CardGroup, Card, CardMedia, CardHeader, CardTitle, CardDescription, CardFooter, CardButton } from '@rtzll/reflow'
import { type Handle } from 'remix/component'
export function Example(handle: Handle) {
const features = [
{
"icon": "circle",
"title": "Fluid motion",
"description": "Spring-tuned transitions calibrated across three tiers"
},
{
"icon": "shield",
"title": "Accessible by default",
"description": "Focus-visible rings and ARIA roles in every part"
},
{
"icon": "palette",
"title": "Yours to theme",
"description": "Adjust radius, icons, and density to fit your app"
},
{
"icon": "moon",
"title": "Dark mode ready",
"description": "Tokens adapt to light and dark automatically"
}
] as const
const thumb = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cdefs%3E%3CradialGradient id='a' cx='20%25' cy='18%25' r='80%25'%3E%3Cstop offset='0%25' stop-color='%236B97FF' stop-opacity='0.95'/%3E%3Cstop offset='100%25' stop-color='%236B97FF' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='b' cx='86%25' cy='14%25' r='75%25'%3E%3Cstop offset='0%25' stop-color='%236B97FF' stop-opacity='0.5'/%3E%3Cstop offset='100%25' stop-color='%236B97FF' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='c' cx='72%25' cy='94%25' r='85%25'%3E%3Cstop offset='0%25' stop-color='%236B97FF' stop-opacity='0.85'/%3E%3Cstop offset='100%25' stop-color='%236B97FF' stop-opacity='0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Crect width='40' height='40' fill='%23ffffff'/%3E%3Crect width='40' height='40' fill='%236B97FF' fill-opacity='0.35'/%3E%3Crect width='40' height='40' fill='url(%23a)'/%3E%3Crect width='40' height='40' fill='url(%23b)'/%3E%3Crect width='40' height='40' fill='url(%23c)'/%3E%3C/svg%3E"
return () => (
<>
<div style={{ width: '100%', maxWidth: '520px' }}>
<CardGroup orientation="inline" border="outlined">
{features.map(({ title, description }) => (
<Card key={title} label={title} onClick={() => open(title)}>
<CardMedia logo={thumb} />
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardFooter><CardButton variant="primary">Get started</CardButton></CardFooter>
</Card>
))}
</CardGroup>
</div>
</>
)
}Separated tiles
import { CardGroup, Card, CardImage, CardHeader, CardTitle, CardDescription, CardFooter, CardButton } from '@rtzll/reflow'
import { type Handle } from 'remix/component'
export function Example(handle: Handle) {
const features = [
{
"icon": "circle",
"title": "Fluid motion",
"description": "Spring-tuned transitions calibrated across three tiers"
},
{
"icon": "shield",
"title": "Accessible by default",
"description": "Focus-visible rings and ARIA roles in every part"
},
{
"icon": "palette",
"title": "Yours to theme",
"description": "Adjust radius, icons, and density to fit your app"
},
{
"icon": "moon",
"title": "Dark mode ready",
"description": "Tokens adapt to light and dark automatically"
}
] as const
const banner = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='320' height='180'%3E%3Cdefs%3E%3CradialGradient id='a' cx='12%25' cy='16%25' r='70%25'%3E%3Cstop offset='0%25' stop-color='%236B97FF' stop-opacity='0.9'/%3E%3Cstop offset='100%25' stop-color='%236B97FF' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='b' cx='90%25' cy='12%25' r='65%25'%3E%3Cstop offset='0%25' stop-color='%236B97FF' stop-opacity='0.45'/%3E%3Cstop offset='100%25' stop-color='%236B97FF' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='c' cx='82%25' cy='94%25' r='75%25'%3E%3Cstop offset='0%25' stop-color='%236B97FF' stop-opacity='0.8'/%3E%3Cstop offset='100%25' stop-color='%236B97FF' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='d' cx='24%25' cy='90%25' r='68%25'%3E%3Cstop offset='0%25' stop-color='%236B97FF' stop-opacity='0.55'/%3E%3Cstop offset='100%25' stop-color='%236B97FF' stop-opacity='0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Crect width='320' height='180' fill='%23ffffff'/%3E%3Crect width='320' height='180' fill='%236B97FF' fill-opacity='0.2'/%3E%3Crect width='320' height='180' fill='url(%23a)'/%3E%3Crect width='320' height='180' fill='url(%23b)'/%3E%3Crect width='320' height='180' fill='url(%23c)'/%3E%3Crect width='320' height='180' fill='url(%23d)'/%3E%3C/svg%3E"
return () => (
<>
<div style={{ width: '100%', maxWidth: '560px' }}>
<CardGroup orientation="inline" separated>
{features.map(({ title, description }) => (
<Card key={title} label={title} onClick={() => open(title)}>
<CardImage src={banner} alt="" />
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardFooter>
<CardButton variant="primary">Get started</CardButton>
<CardButton variant="secondary">Learn more</CardButton>
<CardButton>Connect</CardButton>
</CardFooter>
</Card>
))}
</CardGroup>
</div>
</>
)
}Fully yours: compose anything
import { Card, CardImage, CardHeader, CardTitle, CardContent, CardFeature, CardFooter, CardButton } from '@rtzll/reflow'
import { type Handle } from 'remix/component'
export function Example(handle: Handle) {
const banner = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='320' height='180'%3E%3Cdefs%3E%3CradialGradient id='a' cx='12%25' cy='16%25' r='70%25'%3E%3Cstop offset='0%25' stop-color='%236B97FF' stop-opacity='0.9'/%3E%3Cstop offset='100%25' stop-color='%236B97FF' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='b' cx='90%25' cy='12%25' r='65%25'%3E%3Cstop offset='0%25' stop-color='%236B97FF' stop-opacity='0.45'/%3E%3Cstop offset='100%25' stop-color='%236B97FF' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='c' cx='82%25' cy='94%25' r='75%25'%3E%3Cstop offset='0%25' stop-color='%236B97FF' stop-opacity='0.8'/%3E%3Cstop offset='100%25' stop-color='%236B97FF' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='d' cx='24%25' cy='90%25' r='68%25'%3E%3Cstop offset='0%25' stop-color='%236B97FF' stop-opacity='0.55'/%3E%3Cstop offset='100%25' stop-color='%236B97FF' stop-opacity='0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Crect width='320' height='180' fill='%23ffffff'/%3E%3Crect width='320' height='180' fill='%236B97FF' fill-opacity='0.2'/%3E%3Crect width='320' height='180' fill='url(%23a)'/%3E%3Crect width='320' height='180' fill='url(%23b)'/%3E%3Crect width='320' height='180' fill='url(%23c)'/%3E%3Crect width='320' height='180' fill='url(%23d)'/%3E%3C/svg%3E"
const icon = <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6h16M4 12h16M4 18h16" /></svg>
return () => (
<>
<div style={{ width: '100%', maxWidth: '300px' }}>
<Card border="outlined" dismissible onDismiss={() => {}}>
<CardImage src={banner} alt="" />
<CardHeader><CardTitle>Meet the new Card component</CardTitle></CardHeader>
<CardContent>
<CardFeature icon="paintbrush" title="Always pixel-perfect"
description="Renders your design exactly — token-driven and crisp in light and dark, at any radius" />
<CardFeature icon="square-library" title="Stacked, inline, or grid"
description="One compositional API — borderless by default, with separated tiles or a shared frame" />
</CardContent>
<CardFooter>
<CardButton variant="primary">Get started</CardButton>
<CardButton variant="ghost">Learn more</CardButton>
</CardFooter>
</Card>
</div>
</>
)
}Selected
import { CardGroup, Card, CardMedia, Icon, CardHeader, CardTitle, CardDescription } from '@rtzll/reflow'
import { type Handle } from 'remix/component'
export function Example(handle: Handle) {
const features = [
{
"icon": "circle",
"title": "Fluid motion",
"description": "Spring-tuned transitions calibrated across three tiers"
},
{
"icon": "shield",
"title": "Accessible by default",
"description": "Focus-visible rings and ARIA roles in every part"
},
{
"icon": "palette",
"title": "Yours to theme",
"description": "Adjust radius, icons, and density to fit your app"
},
{
"icon": "moon",
"title": "Dark mode ready",
"description": "Tokens adapt to light and dark automatically"
}
] as const
const [selected, setSelected] = useState('Accessible by default')
const icon = <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6h16M4 12h16M4 18h16" /></svg>
let selected: string[] = []
const setSelected = (values: string[]) => { selected = values; handle.update() }
return () => (
<>
<div style={{ width: '100%', maxWidth: '520px' }}>
<CardGroup orientation="inline">
{features.map(({ icon, title, description }) => (
<Card key={title} label={title} selected={selected === title} onClick={() => setSelected(title)}>
<CardMedia treatment="icon"><Icon name={icon} size={18} /></CardMedia>
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
</Card>
))}
</CardGroup>
</div>
</>
)
}API Reference
Public props for the Reflow implementation. Native controls also accept their standard HTML attributes and Remix event mixes.
Card
| Prop | Type | Default | Description |
|---|---|---|---|
children | RemixNode | — | Child nodes to render inside the element. |
href | string | — | Destination URL when rendered as a link. |
label | string | — | Accessible name or visible label for the control. |
onClick | ((event: MouseEvent) => void) | — | Called when the card is activated. |
disabled | boolean | — | Prevents interaction and conveys the disabled state. |
selected | boolean | — | Marks the item as selected. |
dismissible | boolean | — | Shows an action to dismiss the component. |
dismissOnHover | boolean | true | Reveals the dismiss control on hover. |
onDismiss | (() => void) | — | Called after dismissal. |
size | "default" | "compact" | — | Control density or icon-button dimensions. |
border | "none" | "outlined" | — | Border treatment. |
CardGroup
| Prop | Type | Default | Description |
|---|---|---|---|
children | RemixNode | — | Child nodes to render inside the element. |
orientation | "inline" | "card" | card | Layout direction. |
columns | number | 1 | Number of columns in the card grid. |
border | "none" | "outlined" | none | Border treatment. |
separated | boolean | false | Gives each card its own surface. |
fluidHover | boolean | true | Enables the shared moving hover surface. |