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

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

Feature Tabs Showcase

Интерактивные вкладки по паттерну WAI-ARIA с навигацией стрелками и панелями показателей.

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

One platform, every workflow

Switch between capabilities to see how teams use the kit.

Launch pages in an afternoon

Assemble hero, features, pricing and FAQ sections into conversion-ready landing pages.

  • 24 new section blocks
  • SEO-friendly markup
  • Instant static builds
3.4×faster page launches

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

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

tsx
import { FeatureTabsShowcase } from 'react-component-kit-ui/sections';

<FeatureTabsShowcase variant="underline" tabs={[
    { label: 'Marketing', title: 'Launch faster', text: '…', points: ['24 sections'], stat: '3.4×', statLabel: 'faster' },
    { label: 'Product', title: 'In-app analytics', text: '…', points: ['45 charts'], stat: '0 kB', statLabel: 'runtime' },
  ]} />

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

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

FeatureTabsShowcase.tsx
'use client';

import { useEffect, useRef } from 'react';
import './FeatureTabsShowcase.css';

interface Tab { label: string; title: string; text: string; points: string[]; stat: string; statLabel: string }

/**
 * FeatureTabsShowcase — Interactive feature section using the WAI-ARIA tabs
 * pattern (arrow keys, Home/End) to switch between product capabilities.
 *
 * @param {string} title - Section heading.
 * @param {string} subtitle - Section description.
 * @param {Array<{label: string, title: string, text: string, points: string[], stat: string, statLabel: string}>} tabs - Tab panels.
 * @param {string} variant - Tab style (pills|underline|cards).
 * @param {string} size - Vertical rhythm (sm|md|lg).
 * @param {string} className - Extra CSS class.
 */
export interface FeatureTabsShowcaseProps {
  title?: string;
  subtitle?: string;
  tabs?: Tab[];
  variant?: 'pills' | 'underline' | 'cards';
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

export function FeatureTabsShowcase({
  title = 'One platform, every workflow',
  subtitle = 'Switch between capabilities to see how teams use the kit.',
  tabs = [
    { label: 'Marketing', title: 'Launch pages in an afternoon', text: 'Assemble hero, features, pricing and FAQ sections into conversion-ready landing pages.', points: ['24 new section blocks', 'SEO-friendly markup', 'Instant static builds'], stat: '3.4×', statLabel: 'faster page launches' },
    { label: 'Product', title: 'In-app analytics without a chart library', text: 'Render KPI tiles, funnels and cohort tables straight from your data at build time.', points: ['45 SVG charts', 'Tooltips & table view', 'Zero client JS'], stat: '0 kB', statLabel: 'chart runtime shipped' },
    { label: 'Engineering', title: 'Accessible components you can trust', text: 'Every component is keyboard navigable and screen-reader tested, with visible focus states.', points: ['WCAG 2.2 AA', 'Typed props', 'Reduced-motion aware'], stat: '100', statLabel: 'Lighthouse accessibility' },
  ],
  variant = 'pills',
  size = 'md',
  className = '',
}: FeatureTabsShowcaseProps) {
  const uid = Math.random().toString(36).slice(2, 8);
  const id = `fts-${uid}`;

  // Ported from the Astro <script>: runs once after mount (DOM logic kept as in the Astro kit).
  const mounted = useRef(false);
  useEffect(() => {
    if (mounted.current) return;
    mounted.current = true;
    document.querySelectorAll<HTMLElement>('[data-fts]').forEach((root) => {
        const tabs = Array.from(root.querySelectorAll<HTMLButtonElement>('[role="tab"]'));
        const select = (next: HTMLButtonElement, focus = true) => {
          tabs.forEach((t) => {
            const on = t === next;
            t.setAttribute('aria-selected', String(on));
            t.tabIndex = on ? 0 : -1;
            const panel = document.getElementById(t.getAttribute('aria-controls') || '');
            if (panel) panel.hidden = !on;
          });
          if (focus) next.focus();
        };
        tabs.forEach((tab, i) => {
          tab.addEventListener('click', () => select(tab, false));
          tab.addEventListener('keydown', (e) => {
            const map: Record<string, number> = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: tabs.length - 1 };
            if (!(e.key in map)) return;
            e.preventDefault();
            select(tabs[(map[e.key] + tabs.length) % tabs.length]);
          });
        });
      });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <div className="cq-feature-tabs-showcase-cq" data-rk-6565e6="">
      <section className={`fts fts--${variant} fts--${size} ${className}`} aria-labelledby={id} data-rk-6565e6="">
        <div className="fts__inner" data-rk-6565e6="">
          <header className="fts__head" data-rk-6565e6="">
            <h2 id={id} className="fts__title" data-rk-6565e6="">{title}</h2>
            <p className="fts__subtitle" data-rk-6565e6="">{subtitle}</p>
          </header>
      
          <div className="fts__tabs" data-fts data-rk-6565e6="">
            <div className="fts__list" role="tablist" aria-label={title} data-rk-6565e6="">
              {tabs.map((t, i) => (
                <button key={i} type="button" role="tab" className="fts__tab" id={`${id}-tab-${i}`} aria-controls={`${id}-panel-${i}`} aria-selected={i === 0 ? 'true' : 'false'} tabIndex={i === 0 ? 0 : -1} data-rk-6565e6="">{t.label}</button>
              ))}
            </div>
      
            {tabs.map((t, i) => (
              <div key={i} className="fts__panel" role="tabpanel" id={`${id}-panel-${i}`} aria-labelledby={`${id}-tab-${i}`} tabIndex={0} hidden={i !== 0} data-rk-6565e6="">
                <div className="fts__copy" data-rk-6565e6="">
                  <h3 className="fts__panel-title" data-rk-6565e6="">{t.title}</h3>
                  <p className="fts__text" data-rk-6565e6="">{t.text}</p>
                  <ul className="fts__points" data-rk-6565e6="">
                    {t.points.map((p, __i: number) => (
                      <li key={__i} data-rk-6565e6=""><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" data-rk-6565e6=""><path d="M20 6 9 17l-5-5" data-rk-6565e6="" /></svg>{p}</li>
                    ))}
                  </ul>
                </div>
                <div className="fts__stat" data-rk-6565e6="">
                  <span className="fts__stat-value" data-rk-6565e6="">{t.stat}</span>
                  <span className="fts__stat-label" data-rk-6565e6="">{t.statLabel}</span>
                </div>
              </div>
            ))}
          </div>
        </div>
      </section>
      </div>
    </>
  );
}

export default FeatureTabsShowcase;
FeatureTabsShowcase.css
/* FeatureTabsShowcase — ported from astro-component-kit (sections/FeatureTabsShowcase.astro) */

.cq-feature-tabs-showcase-cq:where([data-rk-6565e6]) { container-type: inline-size; }
.fts:where([data-rk-6565e6]) {
  --fts-py: 5.5rem;
  padding: var(--fts-py) var(--sp-6, 1.5rem);
  background: var(--c-bg, #050505);
}
.fts--sm:where([data-rk-6565e6]) { --fts-py: 3.5rem; }
.fts--lg:where([data-rk-6565e6]) { --fts-py: 7.5rem; }
.fts__inner:where([data-rk-6565e6]) { max-width: 1080px; margin: 0 auto; }
.fts__head:where([data-rk-6565e6]) { text-align: center; max-width: 640px; margin: 0 auto 2.5rem; }
.fts__title:where([data-rk-6565e6]) { margin: 0 0 0.75rem; font-size: clamp(1.9rem, 4vw, 2.75rem); font-weight: 800; letter-spacing: -0.03em; color: var(--c-text-1, #fff); }
.fts__subtitle:where([data-rk-6565e6]) { margin: 0; font-size: 1.0625rem; color: var(--c-text-2, #a3a3a3); }
.fts__list:where([data-rk-6565e6]) { display: flex; justify-content: center; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 2rem; }
.fts__tab:where([data-rk-6565e6]) {
  min-height: 44px;
  padding: 0 1.25rem;
  border: 1px solid transparent;
  border-radius: var(--r-full, 999px);
  background: transparent;
  color: var(--c-text-2, #a3a3a3);
  font: inherit;
  font-size: 0.9375rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
.fts__tab:hover:where([data-rk-6565e6]) { color: var(--c-text-1, #fff); }
.fts__tab:focus-visible:where([data-rk-6565e6]) { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }
.fts--pills:where([data-rk-6565e6]) .fts__list:where([data-rk-6565e6]) { width: fit-content; margin-inline: auto; padding: 0.35rem; border-radius: var(--r-full, 999px); background: rgb(var(--ink-rgb, 255 255 255) / 0.04); border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.07); }
.fts--pills:where([data-rk-6565e6]) .fts__tab[aria-selected='true']:where([data-rk-6565e6]) { background: var(--c-text-1, #fff); color: var(--c-on-primary, #0a0a0a); }
.fts--underline:where([data-rk-6565e6]) .fts__list:where([data-rk-6565e6]) { border-bottom: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.08); gap: 1.5rem; }
.fts--underline:where([data-rk-6565e6]) .fts__tab:where([data-rk-6565e6]) { border-radius: 0; padding: 0 0.25rem; border-bottom: 2px solid transparent; margin-bottom: -1px; }
.fts--underline:where([data-rk-6565e6]) .fts__tab[aria-selected='true']:where([data-rk-6565e6]) { color: var(--c-text-1, #fff); border-bottom-color: var(--c-primary-light, #d4a017); }
.fts--cards:where([data-rk-6565e6]) .fts__tab:where([data-rk-6565e6]) { border-radius: var(--r-md, 12px); border-color: rgb(var(--ink-rgb, 255 255 255) / 0.08); }
.fts--cards:where([data-rk-6565e6]) .fts__tab[aria-selected='true']:where([data-rk-6565e6]) { border-color: var(--c-primary-light, #d4a017); color: var(--c-text-1, #fff); background: var(--c-primary-glow, color-mix(in srgb, var(--c-primary, #b8860b) 12%, transparent)); }

.fts__panel:where([data-rk-6565e6]) {
  display: grid;
  grid-template-columns: 1.4fr 1fr;
  gap: 2rem;
  align-items: center;
  padding: clamp(1.5rem, 4vw, 3rem);
  border-radius: var(--r-xl, 24px);
  background: linear-gradient(160deg, rgb(var(--ink-rgb, 255 255 255) / 0.045), rgb(var(--ink-rgb, 255 255 255) / 0.01));
  border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.07));
  animation: feature-tabs-showcase-fts-in 0.35s ease;
}
.fts__panel[hidden]:where([data-rk-6565e6]) { display: none; }
.fts__panel:focus-visible:where([data-rk-6565e6]) { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 3px; }
@keyframes feature-tabs-showcase-fts-in { from { opacity: 0; transform: translateY(6px); } }
.fts__panel-title:where([data-rk-6565e6]) { margin: 0 0 0.75rem; font-size: clamp(1.4rem, 3vw, 1.875rem); font-weight: 750; letter-spacing: -0.02em; color: var(--c-text-1, #fff); }
.fts__text:where([data-rk-6565e6]) { margin: 0 0 1.25rem; line-height: 1.65; color: var(--c-text-2, #a3a3a3); }
.fts__points:where([data-rk-6565e6]) { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.6rem; }
.fts__points:where([data-rk-6565e6]) li:where([data-rk-6565e6]) { display: flex; align-items: center; gap: 0.55rem; color: var(--c-text-1, #fff); font-size: 0.9375rem; }
.fts__points:where([data-rk-6565e6]) svg:where([data-rk-6565e6]) { color: var(--chart-pos, #34c38f); }
.fts__stat:where([data-rk-6565e6]) {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-height: 200px;
  border-radius: var(--r-lg, 20px);
  background: radial-gradient(circle at 50% 30%, var(--c-primary-glow, color-mix(in srgb, var(--c-primary, #b8860b) 16%, transparent)), transparent 70%), rgb(var(--ink-rgb, 255 255 255) / 0.02);
  border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.06);
  text-align: center;
  padding: 1.5rem;
}
.fts__stat-value:where([data-rk-6565e6]) { font-size: clamp(2.75rem, 6vw, 4rem); font-weight: 800; letter-spacing: -0.04em; color: var(--c-text-1, #fff); font-variant-numeric: tabular-nums; line-height: 1; }
.fts__stat-label:where([data-rk-6565e6]) { font-size: 0.875rem; color: var(--c-text-2, #a3a3a3); }
@container (max-width: 760px) { .fts__panel:where([data-rk-6565e6]) { grid-template-columns: 1fr; } }
@media (prefers-reduced-motion: reduce) { .fts__panel:where([data-rk-6565e6]) { animation: none; } }