Feature Icon Grid
Responsive 2–4 column feature grid with SVG icons, descriptions and accessible learn-more links.
Features
Everything you need, nothing you don’t
A focused toolkit that keeps your pages fast, accessible and on-brand.
Zero runtime JS
Components render to static HTML and CSS. Interactivity is progressive and tiny.
Learn more about Zero runtime JSAccessible by default
Semantic markup, focus rings, ARIA where it matters and 4.5:1 contrast everywhere.
Learn more about Accessible by defaultDesign tokens
Colors, spacing and radii come from CSS custom properties you can re-theme in one file.
SVG charts
45 dependency-free charts with tooltips, legends and data-table fallbacks.
i18n ready
Every string is a prop. Ship English, Spanish and Russian docs out of the box.
Copy & paste
No package lock-in. Grab the source from the docs and own every line.
Usage
Copy and paste this code into your React component:
import { FeatureIconGrid } from 'react-component-kit-ui/sections';
<FeatureIconGrid title="Everything you need" columns={3} features={[
{ icon: 'bolt', title: 'Fast', text: 'Static HTML by default.' },
{ icon: 'shield', title: 'Accessible', text: 'WCAG AA out of the box.' },
{ icon: 'code', title: 'Yours', text: 'Copy the source, own it.' },
]} />Manual Installation
If you are not using the npm package, create a new file and paste the following code: src/components/sections/FeatureIconGrid.tsx + FeatureIconGrid.css:
'use client';
import type { CSSProperties } from 'react';
import './FeatureIconGrid.css';
/** "a: b; --c: d" → style object */
function st(css: string | undefined): CSSProperties {
const o: Record<string, string> = {};
for (const decl of String(css ?? '').split(';')) {
const i = decl.indexOf(':');
if (i < 0) continue;
const k = decl.slice(0, i).trim();
if (k) o[k.startsWith('--') ? k : k.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = decl.slice(i + 1).trim();
}
return o as CSSProperties;
}
interface Feature { icon: string; title: string; text: string; href?: string }
const icons: Record<string, string> = {
bolt: '<path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/>',
shield: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="m9 12 2 2 4-4"/>',
layers: '<path d="m12 2 10 5-10 5L2 7l10-5z"/><path d="m2 17 10 5 10-5"/><path d="m2 12 10 5 10-5"/>',
chart: '<path d="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/>',
globe: '<circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15 15 0 0 1 0 20 15 15 0 0 1 0-20z"/>',
code: '<path d="m16 18 6-6-6-6M8 6l-6 6 6 6"/>',
sparkles: '<path d="M12 3l1.9 5.8L20 10l-6.1 1.2L12 17l-1.9-5.8L4 10l6.1-1.2z"/><path d="M19 17v4M17 19h4"/>',
lock: '<rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/>',
};
/**
* FeatureIconGrid — Classic 3-column feature grid with SVG icons, titles,
* descriptions and optional "learn more" links. Cards light up on hover/focus.
*
* @param {string} eyebrow - Small label above the heading.
* @param {string} title - Section heading.
* @param {string} subtitle - Section description.
* @param {Array<{icon: string, title: string, text: string, href?: string}>} features - Feature cards.
* `icon` is one of: bolt, shield, layers, chart, globe, code, sparkles, lock, users.
* @param {number} columns - Max columns on desktop (2|3|4).
* @param {string} variant - Card style (card|plain|bordered).
* @param {string} size - Vertical rhythm (sm|md|lg).
* @param {string} className - Extra CSS class.
*/
export interface FeatureIconGridProps {
eyebrow?: string;
title?: string;
subtitle?: string;
features?: Feature[];
columns?: 2 | 3 | 4;
variant?: 'card' | 'plain' | 'bordered';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
export function FeatureIconGrid({
eyebrow = 'Features',
title = 'Everything you need, nothing you don’t',
subtitle = 'A focused toolkit that keeps your pages fast, accessible and on-brand.',
features = [
{ icon: 'bolt', title: 'Zero runtime JS', text: 'Components render to static HTML and CSS. Interactivity is progressive and tiny.', href: '#' },
{ icon: 'shield', title: 'Accessible by default', text: 'Semantic markup, focus rings, ARIA where it matters and 4.5:1 contrast everywhere.', href: '#' },
{ icon: 'layers', title: 'Design tokens', text: 'Colors, spacing and radii come from CSS custom properties you can re-theme in one file.' },
{ icon: 'chart', title: 'SVG charts', text: '45 dependency-free charts with tooltips, legends and data-table fallbacks.' },
{ icon: 'globe', title: 'i18n ready', text: 'Every string is a prop. Ship English, Spanish and Russian docs out of the box.' },
{ icon: 'code', title: 'Copy & paste', text: 'No package lock-in. Grab the source from the docs and own every line.' },
],
columns = 3,
variant = 'card',
size = 'md',
className = '',
}: FeatureIconGridProps) {
const id = `fig-${Math.random().toString(36).slice(2, 8)}`;
return (
<>
<section className={`fig fig--${variant} fig--${size} ${className}`} aria-labelledby={id} style={st(`--fig-cols:${columns}`)} data-rk-d09f2e="">
<div className="fig__inner" data-rk-d09f2e="">
<header className="fig__head" data-rk-d09f2e="">
<p className="fig__eyebrow" data-rk-d09f2e="">{eyebrow}</p>
<h2 id={id} className="fig__title" data-rk-d09f2e="">{title}</h2>
<p className="fig__subtitle" data-rk-d09f2e="">{subtitle}</p>
</header>
<ul className="fig__grid" data-rk-d09f2e="">
{features.map((f, __i: number) => (
<li key={__i} className="fig__card" data-rk-d09f2e="">
<span className="fig__icon" aria-hidden="true" data-rk-d09f2e="">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" dangerouslySetInnerHTML={{ __html: icons[f.icon] ?? icons.sparkles }} data-rk-d09f2e=""></svg>
</span>
<h3 className="fig__card-title" data-rk-d09f2e="">{f.title}</h3>
<p className="fig__text" data-rk-d09f2e="">{f.text}</p>
{f.href && (
<a className="fig__link" href={f.href} data-rk-d09f2e="">
Learn more<span className="fig__sr" data-rk-d09f2e=""> about {f.title}</span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true" data-rk-d09f2e=""><path d="M5 12h14M13 6l6 6-6 6" data-rk-d09f2e="" /></svg>
</a>
)}
</li>
))}
</ul>
</div>
</section>
</>
);
}
export default FeatureIconGrid;/* FeatureIconGrid — ported from astro-component-kit (sections/FeatureIconGrid.astro) */
.fig:where([data-rk-d09f2e]) {
--fig-py: 5.5rem;
padding: var(--fig-py) var(--sp-6, 1.5rem);
background: var(--c-bg, #050505);
}
.fig--sm:where([data-rk-d09f2e]) { --fig-py: 3.5rem; }
.fig--lg:where([data-rk-d09f2e]) { --fig-py: 7.5rem; }
.fig__inner:where([data-rk-d09f2e]) { max-width: 1180px; margin: 0 auto; }
.fig__head:where([data-rk-d09f2e]) { max-width: 640px; margin: 0 auto 3.5rem; text-align: center; }
.fig__eyebrow:where([data-rk-d09f2e]) { margin: 0 0 0.75rem; font-size: 0.8125rem; font-weight: 650; letter-spacing: 0.08em; text-transform: uppercase; color: var(--c-primary-light, #d4a017); }
.fig__title:where([data-rk-d09f2e]) { margin: 0 0 1rem; font-size: clamp(1.9rem, 4vw, 2.75rem); font-weight: 800; letter-spacing: -0.03em; line-height: 1.12; color: var(--c-text-1, #fff); text-wrap: balance; }
.fig__subtitle:where([data-rk-d09f2e]) { margin: 0; font-size: 1.0625rem; line-height: 1.6; color: var(--c-text-2, #a3a3a3); }
.fig__grid:where([data-rk-d09f2e]) {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100%, calc((100% - (var(--fig-cols) - 1) * 1.25rem) / var(--fig-cols))), 1fr));
gap: 1.25rem;
}
.fig__card:where([data-rk-d09f2e]) {
position: relative;
padding: 1.75rem;
border-radius: var(--r-lg, 20px);
transition: border-color 0.25s ease, background 0.25s ease, transform 0.25s ease;
}
.fig--card:where([data-rk-d09f2e]) .fig__card:where([data-rk-d09f2e]) {
background: linear-gradient(160deg, rgb(var(--ink-rgb, 255 255 255) / 0.04), rgb(var(--ink-rgb, 255 255 255) / 0.01));
border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.07));
}
.fig--bordered:where([data-rk-d09f2e]) .fig__card:where([data-rk-d09f2e]) { border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.1); }
.fig--plain:where([data-rk-d09f2e]) .fig__card:where([data-rk-d09f2e]) { padding: 0.5rem 0; }
.fig--card:where([data-rk-d09f2e]) .fig__card:hover:where([data-rk-d09f2e]), .fig--card:where([data-rk-d09f2e]) .fig__card:focus-within:where([data-rk-d09f2e]),
.fig--bordered:where([data-rk-d09f2e]) .fig__card:hover:where([data-rk-d09f2e]), .fig--bordered:where([data-rk-d09f2e]) .fig__card:focus-within:where([data-rk-d09f2e]) {
border-color: color-mix(in srgb, var(--c-primary-light, #d4a017) 35%, transparent);
transform: translateY(-3px);
}
.fig__icon:where([data-rk-d09f2e]) {
display: grid;
place-items: center;
width: 46px;
height: 46px;
margin-bottom: 1.25rem;
border-radius: var(--r-md, 12px);
background: var(--c-primary-glow, color-mix(in srgb, var(--c-primary, #b8860b) 12%, transparent));
border: 1px solid color-mix(in srgb, var(--c-primary-light, #d4a017) 25%, transparent);
color: var(--c-primary-light, #d4a017);
}
.fig__card-title:where([data-rk-d09f2e]) { margin: 0 0 0.5rem; font-size: 1.125rem; font-weight: 650; color: var(--c-text-1, #fff); letter-spacing: -0.01em; }
.fig__text:where([data-rk-d09f2e]) { margin: 0; font-size: 0.9375rem; line-height: 1.6; color: var(--c-text-2, #a3a3a3); }
.fig__link:where([data-rk-d09f2e]) {
display: inline-flex;
align-items: center;
gap: 0.35rem;
min-height: 44px;
margin-top: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
color: var(--c-primary-light, #d4a017);
text-decoration: none;
}
.fig__link:where([data-rk-d09f2e]) svg:where([data-rk-d09f2e]) { transition: transform 0.2s ease; }
.fig__link:hover:where([data-rk-d09f2e]) svg:where([data-rk-d09f2e]) { transform: translateX(3px); }
.fig__link:focus-visible:where([data-rk-d09f2e]) { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; border-radius: 4px; }
.fig__sr:where([data-rk-d09f2e]) { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
@media (prefers-reduced-motion: reduce) { .fig__card:where([data-rk-d09f2e]), .fig__link:where([data-rk-d09f2e]) svg:where([data-rk-d09f2e]) { transition: none; } }