-
Notifications
You must be signed in to change notification settings - Fork 222
feat(section): add feature grid hover reveal component #455
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f49d4f9
6e1f115
277576a
ef6d39e
30a3b5e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| import type { Meta, StoryObj } from "@storybook/react"; | ||
| import { Brain, Database, Lock, Rocket } from "lucide-react"; | ||
| import FeatureGridHoverReveal, { | ||
| type FeatureGridProps, | ||
| } from "@/animata/section/feature-grid-hover-reveal"; | ||
|
|
||
| const meta = { | ||
| title: "Section/Feature Grid Hover Reveal", | ||
| component: FeatureGridHoverReveal, | ||
| parameters: { | ||
| layout: "fullscreen", | ||
| }, | ||
| tags: ["autodocs"], | ||
| argTypes: { | ||
| columns: { | ||
| control: "select", | ||
| options: [2, 3], | ||
| description: "Number of columns in the grid", | ||
| }, | ||
| title: { | ||
| control: "text", | ||
| description: "Section heading", | ||
| }, | ||
| subtitle: { | ||
| control: "text", | ||
| description: "Section subheading", | ||
| }, | ||
| }, | ||
| } satisfies Meta<typeof FeatureGridHoverReveal>; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<typeof meta>; | ||
|
|
||
| export const Primary: Story = { | ||
| args: {}, | ||
| render: (args: FeatureGridProps) => ( | ||
| <div className="full-content w-full bg-background"> | ||
| <FeatureGridHoverReveal {...args} /> | ||
| </div> | ||
| ), | ||
| }; | ||
|
|
||
| export const TwoColumn: Story = { | ||
| args: { | ||
| columns: 2, | ||
| title: "Built for scale", | ||
| subtitle: "Four core capabilities that grow with your team.", | ||
| features: [ | ||
| { | ||
| icon: <Rocket className="h-6 w-6" />, | ||
| title: "Instant Deploy", | ||
| description: | ||
| "Push to production in seconds with zero-downtime deployments and automatic rollbacks.", | ||
| }, | ||
| { | ||
| icon: <Lock className="h-6 w-6" />, | ||
| title: "Zero-Trust Security", | ||
| description: "Every request is authenticated and authorised. No implicit trust, ever.", | ||
| }, | ||
| { | ||
| icon: <Database className="h-6 w-6" />, | ||
| title: "Managed Database", | ||
| description: | ||
| "Fully managed Postgres with automatic backups, point-in-time recovery, and read replicas.", | ||
| }, | ||
| { | ||
| icon: <Brain className="h-6 w-6" />, | ||
| title: "AI-Powered Insights", | ||
| description: | ||
| "Surface anomalies and opportunities automatically using built-in machine learning.", | ||
| }, | ||
| ], | ||
| }, | ||
| render: (args: FeatureGridProps) => ( | ||
| <div className="full-content w-full bg-background"> | ||
| <FeatureGridHoverReveal {...args} /> | ||
| </div> | ||
| ), | ||
| }; | ||
|
|
||
| export const CustomContent: Story = { | ||
| args: { | ||
| columns: 3, | ||
| title: "Why teams choose us", | ||
| subtitle: "Purpose-built features that eliminate complexity and ship value faster.", | ||
| features: [ | ||
| { | ||
| icon: <Rocket className="h-6 w-6" />, | ||
| title: "One-Click Setup", | ||
| description: | ||
| "Go from sign-up to production in under five minutes with our guided onboarding.", | ||
| }, | ||
| { | ||
| icon: <Brain className="h-6 w-6" />, | ||
| title: "Smart Automation", | ||
| description: | ||
| "Let AI handle the repetitive work so your team can focus on what actually matters.", | ||
| }, | ||
| { | ||
| icon: <Database className="h-6 w-6" />, | ||
| title: "Reliable Storage", | ||
| description: | ||
| "99.999% uptime SLA with geo-redundant storage across three availability zones.", | ||
| }, | ||
| ], | ||
| }, | ||
| render: (args: FeatureGridProps) => ( | ||
| <div className="full-content w-full bg-background"> | ||
| <FeatureGridHoverReveal {...args} /> | ||
| </div> | ||
| ), | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| "use client"; | ||
|
|
||
| import { BarChart, Code, Globe, Layers, Shield, Zap } from "lucide-react"; | ||
| import { useEffect, useState } from "react"; | ||
| import { cn } from "@/lib/utils"; | ||
|
|
||
| export interface FeatureItem { | ||
| icon: React.ReactNode; | ||
| title: string; | ||
| description: string; | ||
| } | ||
|
|
||
| export interface FeatureGridProps { | ||
| features?: FeatureItem[]; | ||
| columns?: 2 | 3; | ||
| title?: string; | ||
| subtitle?: string; | ||
| } | ||
|
|
||
| const defaultFeatures: FeatureItem[] = [ | ||
| { | ||
| icon: <Zap className="h-6 w-6" />, | ||
| title: "Lightning Fast", | ||
| description: | ||
| "Optimised for speed at every layer. Sub-second load times keep your users engaged and your conversion rate high.", | ||
| }, | ||
| { | ||
| icon: <Shield className="h-6 w-6" />, | ||
| title: "Enterprise Security", | ||
| description: | ||
| "SOC 2 Type II certified with end-to-end encryption, SSO, and role-based access controls built in from day one.", | ||
| }, | ||
| { | ||
| icon: <BarChart className="h-6 w-6" />, | ||
| title: "Advanced Analytics", | ||
| description: | ||
| "Real-time dashboards and custom reports give your team the clarity to make confident, data-driven decisions.", | ||
| }, | ||
| { | ||
| icon: <Globe className="h-6 w-6" />, | ||
| title: "Global CDN", | ||
| description: | ||
| "Content delivered from 200+ edge locations worldwide so every user gets a fast, reliable experience.", | ||
| }, | ||
| { | ||
| icon: <Code className="h-6 w-6" />, | ||
| title: "Developer First", | ||
| description: | ||
| "Powerful REST and GraphQL APIs, webhooks, and an SDK for every major language. Automate anything.", | ||
| }, | ||
| { | ||
| icon: <Layers className="h-6 w-6" />, | ||
| title: "Seamless Integrations", | ||
| description: | ||
| "Connect to 200+ tools including Slack, Salesforce, and Stripe in minutes — no custom code required.", | ||
| }, | ||
| ]; | ||
|
|
||
| const columnClass: Record<2 | 3, string> = { | ||
| 2: "md:grid-cols-2", | ||
| 3: "md:grid-cols-2 lg:grid-cols-3", | ||
| }; | ||
|
|
||
| export default function FeatureGridHoverReveal({ | ||
| features = defaultFeatures, | ||
| columns = 3, | ||
| title = "Everything you need to ship faster", | ||
| subtitle = "A complete platform built for modern teams — from zero to production without the growing pains.", | ||
| }: FeatureGridProps) { | ||
| const [prefersReducedMotion, setPrefersReducedMotion] = useState(false); | ||
|
|
||
| useEffect(() => { | ||
| const mq = window.matchMedia("(prefers-reduced-motion: reduce)"); | ||
| setPrefersReducedMotion(mq.matches); | ||
| const handler = (e: MediaQueryListEvent) => setPrefersReducedMotion(e.matches); | ||
| mq.addEventListener("change", handler); | ||
| return () => mq.removeEventListener("change", handler); | ||
| }, []); | ||
|
|
||
| const safeFeatures = Array.isArray(features) ? features : defaultFeatures; | ||
|
|
||
| return ( | ||
| <section className="w-full px-4 py-16 sm:py-20 lg:py-24"> | ||
| <div className="mx-auto max-w-6xl"> | ||
| {(title || subtitle) && ( | ||
| <div className="mb-12 text-center"> | ||
| {title && ( | ||
| <h2 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl"> | ||
| {title} | ||
| </h2> | ||
| )} | ||
| {subtitle && ( | ||
| <p className="mt-2 text-base text-muted-foreground sm:text-lg">{subtitle}</p> | ||
| )} | ||
| </div> | ||
| )} | ||
|
|
||
| <div className={cn("grid grid-cols-1 gap-6", columnClass[columns])}> | ||
| {safeFeatures.map((feature, index) => ( | ||
| <FeatureCard | ||
| key={index} | ||
| feature={feature} | ||
| prefersReducedMotion={prefersReducedMotion} | ||
| /> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| </section> | ||
| ); | ||
| } | ||
|
|
||
| interface FeatureCardProps { | ||
| feature: FeatureItem; | ||
| prefersReducedMotion: boolean; | ||
| } | ||
|
|
||
| function FeatureCard({ feature, prefersReducedMotion }: FeatureCardProps) { | ||
| const [hovered, setHovered] = useState(false); | ||
|
|
||
| return ( | ||
| <article | ||
| onMouseEnter={() => setHovered(true)} | ||
| onMouseLeave={() => setHovered(false)} | ||
| onFocus={() => setHovered(true)} | ||
| onBlur={() => setHovered(false)} | ||
| className={cn( | ||
|
Comment on lines
+121
to
+126
|
||
| "group relative flex flex-col gap-4 rounded-2xl border p-6", | ||
| "bg-card text-card-foreground", | ||
| "shadow-sm transition-shadow duration-300", | ||
| !prefersReducedMotion && "transition-transform duration-300", | ||
| !prefersReducedMotion && hovered && "-translate-y-1.5 shadow-md", | ||
| hovered ? "border-primary/40" : "border-border", | ||
| )} | ||
| > | ||
| <div | ||
| className={cn( | ||
| "flex h-11 w-11 items-center justify-center rounded-xl", | ||
| "bg-primary/10 text-primary", | ||
| "transition-transform duration-200", | ||
| !prefersReducedMotion && hovered && "scale-115", | ||
| )} | ||
|
Comment on lines
+136
to
+141
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Check whether Tailwind extends scale with 1.15 or already uses this utility elsewhere.
fd -a 'tailwind.config.*' .
rg -n --hidden --glob 'tailwind.config.*' 'scale.*115|extend.*scale|safelist' .
rg -n --hidden 'scale-115|scale-\[1\.15\]' animata content .Repository: codse/animata Length of output: 39 🏁 Script executed: # Search for Tailwind config files with different naming patterns
find . -maxdepth 2 -type f \( -name "tailwind.config.*" -o -name "tailwind.*" \) 2>/dev/null | head -20Repository: codse/animata Length of output: 39 🏁 Script executed: # Check if there's a package.json and what Tailwind version is used
cat package.json | grep -A 5 -B 5 tailwindRepository: codse/animata Length of output: 1486 🏁 Script executed: # Search for scale-115 usage throughout the entire codebase
rg -i 'scale-115|scale-\[1\.15\]' --type tsx --type ts --type js --type jsonRepository: codse/animata Length of output: 85 🏁 Script executed: # Look for any Tailwind theme extensions or safelist configurations
rg -i 'extend.*scale|scale.*extend|theme.*scale' --type tsx --type ts --type js --type jsonRepository: codse/animata Length of output: 85 🏁 Script executed: # Check the specific file mentioned in the review
cat -n animata/section/feature-grid-hover-reveal.tsx | sed -n '130,150p'Repository: codse/animata Length of output: 932 🏁 Script executed: # Search for tailwind config with various extensions
find . -maxdepth 3 -name "tailwind.config*" -o -name "tailwind.*"Repository: codse/animata Length of output: 39 🏁 Script executed: # Also check for postcss config which might reference tailwind
find . -maxdepth 2 \( -name "postcss.config*" -o -name ".tailwindrc*" \)Repository: codse/animata Length of output: 78 🏁 Script executed: # Search for scale extensions in the codebase using rg with correct syntax
rg 'scale.*1\.15|extend.*scale' --max-count 20Repository: codse/animata Length of output: 186 🏁 Script executed: # Check if scale-115 appears anywhere in the codebase (configuration or usage)
rg 'scale-115' --max-count 20Repository: codse/animata Length of output: 261 🏁 Script executed: # Look at the root directory structure to understand config location
ls -la | head -20Repository: codse/animata Length of output: 1249 🏁 Script executed: cat postcss.config.mjsRepository: codse/animata Length of output: 203 🏁 Script executed: # Search for any Tailwind configuration inline or in other files
rg 'theme|extend|scale' postcss.config.mjs tailwind.config.* app.css globals.css 2>/dev/null | head -40Repository: codse/animata Length of output: 39 🏁 Script executed: # Check if there's a theme configuration in CSS or config files
find . -maxdepth 3 -type f \( -name "*.css" -o -name "globals*" \) | head -10Repository: codse/animata Length of output: 151 🏁 Script executed: # Look for any theme or extend configurations
rg 'theme.*{' --max-count 10Repository: codse/animata Length of output: 137 🏁 Script executed: cat styles/globals.cssRepository: codse/animata Length of output: 13019 🏁 Script executed: # Search for `@theme`, `@layer`, or theme extensions in CSS files
rg '@theme|@layer|theme\s*\{' --type cssRepository: codse/animata Length of output: 222 🏁 Script executed: # Look for any Tailwind configuration in app or lib directories
find . -maxdepth 3 \( -name "app.tsx" -o -name "app.ts" -o -name "layout.tsx" -o -name "config.ts" \) | head -10Repository: codse/animata Length of output: 98 🏁 Script executed: # Search for 115 or 1.15 scale definitions anywhere
rg '115|1\.15' --type css --type ts --type tsx --max-count 20Repository: codse/animata Length of output: 85 Use
🤖 Prompt for AI Agents
Comment on lines
+127
to
+141
|
||
| > | ||
| {feature.icon} | ||
| </div> | ||
|
|
||
| <div> | ||
| <h3 className="text-lg font-semibold text-foreground">{feature.title}</h3> | ||
| <p className="mt-1 text-sm leading-relaxed text-muted-foreground">{feature.description}</p> | ||
| </div> | ||
|
|
||
| {!prefersReducedMotion && ( | ||
| <span | ||
| className={cn( | ||
| "pointer-events-none absolute inset-0 rounded-2xl ring-1 ring-inset ring-primary/20 transition-opacity duration-300", | ||
| hovered ? "opacity-100" : "opacity-0", | ||
| )} | ||
| aria-hidden="true" | ||
| /> | ||
| )} | ||
| </article> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| --- | ||
| title: Feature Grid Hover Reveal | ||
| description: A responsive SaaS feature section with icon cards, hover lift, border highlight, and icon scale micro-interactions. | ||
| author: KeenIsHere | ||
| published: true | ||
| --- | ||
|
|
||
| <ComponentPreview name="section-feature-grid-hover-reveal--primary" /> | ||
|
|
||
| ## Overview | ||
|
|
||
| Feature Grid Hover Reveal is a polished section component for SaaS landing pages. It displays a grid of feature cards with icon, title, and description. On hover, cards lift, the border highlights, and the icon scales up using only transform and opacity. Animations are skipped when prefers-reduced-motion is enabled. | ||
|
|
||
| ## Features | ||
|
|
||
|
Comment on lines
+8
to
+15
|
||
| - Responsive grid: 1 column mobile, 2 tablet, 3 desktop | ||
| - Hover lift: -translate-y-1.5 with 300ms transition | ||
| - Icon scale: scale-115 with 200ms transition | ||
| - Border highlight: border-primary/40 on hover | ||
| - Ring overlay: inset ring fades in via opacity | ||
| - prefers-reduced-motion: all animations off when set | ||
| - Dark mode: full Tailwind token support | ||
| - Configurable: 2 or 3 column layout via prop | ||
| - Zero-props render: 6 default SaaS feature items built in | ||
|
|
||
| ## Usage | ||
|
|
||
| ```tsx | ||
| import FeatureGridHoverReveal from "@/animata/section/feature-grid-hover-reveal"; | ||
|
|
||
| export default function Page() { | ||
| return <FeatureGridHoverReveal />; | ||
| } | ||
| ``` | ||
|
|
||
| ## Props | ||
|
|
||
| | Prop | Type | Default | Description | | ||
| |------|------|---------|-------------| | ||
| | features | FeatureItem[] | 6 built-in items | Array of feature cards | | ||
| | columns | 2 or 3 | 3 | Grid column count | | ||
| | title | string | Built-in heading | Section title | | ||
| | subtitle | string | Built-in subtitle | Section subtitle | | ||
|
|
||
| ## Types | ||
|
|
||
| ```typescript | ||
| interface FeatureItem { | ||
| icon: React.ReactNode; | ||
| title: string; | ||
| description: string; | ||
| } | ||
|
|
||
| interface FeatureGridProps { | ||
| features?: FeatureItem[]; | ||
| columns?: 2 | 3; | ||
| title?: string; | ||
| subtitle?: string; | ||
| } | ||
| ``` | ||
|
|
||
| ## Accessibility | ||
|
|
||
| - Semantic section and article elements | ||
| - Hover state triggered on focus and blur for keyboard access | ||
|
|
||
| - Decorative ring span has aria-hidden set to true | ||
| - Icon container meets 44x44px minimum touch target | ||
| - All motion respects prefers-reduced-motion | ||
|
|
||
| ## Credits | ||
|
|
||
| Built by [Keen Sha](https://github.com/KeenIsHere). | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make the cards actually keyboard-focusable.
onFocus/onBlurnever fire on a plain<article>, so keyboard users can't trigger the reveal. AddtabIndex={0}or switch to a genuinely interactive element if keyboard reveal is required.♻️ Suggested fix
<article + tabIndex={0} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} onFocus={() => setHovered(true)} onBlur={() => setHovered(false)}📝 Committable suggestion
🤖 Prompt for AI Agents