1 звезда на GitHub = 1 новый компонент.

Поставить звезду
Возможности & Бенто

Feature Icon Grid

Адаптивная сетка возможностей в 2–4 колонки с SVG-иконками, описаниями и доступными ссылками.

featuresgridiconscards
Живое превью

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 JS
  • Accessible by default

    Semantic markup, focus rings, ARIA where it matters and 4.5:1 contrast everywhere.

    Learn more about Accessible by default
  • Design 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.

Использование

Скопируйте и вставьте этот код в ваш React-компонент:

tsx
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.' },
  ]} />

Ручная установка

Если вы не используете пакет npm, создайте файл и вставьте следующий код: src/components/sections/FeatureIconGrid.tsx + FeatureIconGrid.css:

FeatureIconGrid.tsx
'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.css
/* 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; } }