1 GitHub star = 1 new component.

Star the repo
Features & Grids

Integrations Grid

Integrations directory with category filter chips, monogram tiles and availability badges.

integrationsfilterdirectoryapps
Live Preview

Plays well with your stack

Connect the tools your team already uses. New integrations every month.

8 integrations

  • Slack

    Available

    Post alerts and weekly digests to channels.

    Messaging
  • GitHub

    Available

    Annotate charts with deploys and releases.

    Developer
  • Stripe

    Available

    Sync MRR, churn and invoices automatically.

    Payments
  • Linear

    Beta

    Turn insights into issues in one click.

    Developer
  • Notion

    Available

    Embed live charts in your team wiki.

    Docs
  • HubSpot

    Beta

    Attribute revenue to campaigns and deals.

    CRM
  • Postgres

    Available

    Query your warehouse with zero ETL.

    Data
  • Discord

    Coming soon

    Community metrics and bot notifications.

    Messaging

Usage

Copy and paste this code into your React component:

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

<IntegrationsGrid items={[
    { name: 'Slack', category: 'Messaging', description: 'Alerts in channels.', status: 'available' },
    { name: 'Linear', category: 'Developer', description: 'Insights to issues.', status: 'beta' },
  ]} />

Manual Installation

If you are not using the npm package, create a new file and paste the following code: src/components/sections/IntegrationsGrid.tsx + IntegrationsGrid.css:

IntegrationsGrid.tsx
'use client';

import { useEffect, useRef } from 'react';
import type { CSSProperties } from 'react';
import './IntegrationsGrid.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 Item { name: string; category: string; description: string; status?: 'available' | 'beta' | 'soon' }

/**
 * IntegrationsGrid โ€” Integrations directory with category filter chips
 * (progressively enhanced), monogram tiles and connection status badges.
 *
 * @param {string} title - Section heading.
 * @param {string} subtitle - Section description.
 * @param {Array<{name: string, category: string, description: string, status?: 'available'|'beta'|'soon'}>} items - Integrations.
 * @param {string} variant - Tile style (card|compact|outline).
 * @param {string} size - Vertical rhythm (sm|md|lg).
 * @param {string} className - Extra CSS class.
 */
export interface IntegrationsGridProps {
  title?: string;
  subtitle?: string;
  items?: Item[];
  variant?: 'card' | 'compact' | 'outline';
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

export function IntegrationsGrid({
  title = 'Plays well with your stack',
  subtitle = 'Connect the tools your team already uses. New integrations every month.',
  items = [
    { name: 'Slack', category: 'Messaging', description: 'Post alerts and weekly digests to channels.', status: 'available' },
    { name: 'GitHub', category: 'Developer', description: 'Annotate charts with deploys and releases.', status: 'available' },
    { name: 'Stripe', category: 'Payments', description: 'Sync MRR, churn and invoices automatically.', status: 'available' },
    { name: 'Linear', category: 'Developer', description: 'Turn insights into issues in one click.', status: 'beta' },
    { name: 'Notion', category: 'Docs', description: 'Embed live charts in your team wiki.', status: 'available' },
    { name: 'HubSpot', category: 'CRM', description: 'Attribute revenue to campaigns and deals.', status: 'beta' },
    { name: 'Postgres', category: 'Data', description: 'Query your warehouse with zero ETL.', status: 'available' },
    { name: 'Discord', category: 'Messaging', description: 'Community metrics and bot notifications.', status: 'soon' },
  ],
  variant = 'card',
  size = 'md',
  className = '',
}: IntegrationsGridProps) {
  const id = `ig-${Math.random().toString(36).slice(2, 8)}`;
  const categories = ['All', ...new Set(items.map((i) => i.category))];
  const hues = ['var(--chart-1, #3987e5)', 'var(--chart-2, #d95926)', 'var(--chart-3, #199e70)', 'var(--chart-4, #c98500)', 'var(--chart-5, #d55181)', 'var(--chart-7, #9085e9)'];
  const statusLabel = { available: 'Available', beta: 'Beta', soon: 'Coming soon' };

  // 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-ig]').forEach((root) => {
        const chips = root.querySelectorAll<HTMLButtonElement>('.ig__chip');
        const tiles = root.querySelectorAll<HTMLElement>('.ig__tile');
        const count = root.querySelector('.ig__count');
        chips.forEach((chip) => {
          chip.addEventListener('click', () => {
            const f = chip.dataset.filter;
            chips.forEach((c) => c.setAttribute('aria-pressed', String(c === chip)));
            let shown = 0;
            tiles.forEach((t) => {
              const on = f === 'All' || t.dataset.category === f;
              t.hidden = !on;
              if (on) shown += 1;
            });
            if (count) count.textContent = `${shown} integration${shown === 1 ? '' : 's'}${f === 'All' ? '' : ` in ${f}`}`;
          });
        });
      });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <section className={`ig ig--${variant} ig--${size} ${className}`} aria-labelledby={id} data-ig data-rk-32bfaf="">
        <div className="ig__inner" data-rk-32bfaf="">
          <header className="ig__head" data-rk-32bfaf="">
            <h2 id={id} className="ig__title" data-rk-32bfaf="">{title}</h2>
            <p className="ig__subtitle" data-rk-32bfaf="">{subtitle}</p>
          </header>
      
          <div className="ig__filters" role="group" aria-label="Filter integrations by category" data-rk-32bfaf="">
            {categories.map((c, i) => (
              <button key={i} type="button" className="ig__chip" data-filter={c} aria-pressed={i === 0 ? 'true' : 'false'} data-rk-32bfaf="">{c}</button>
            ))}
          </div>
          <p className="ig__count" role="status" aria-live="polite" data-rk-32bfaf="">{items.length} integrations</p>
      
          <ul className="ig__grid" data-rk-32bfaf="">
            {items.map((it, i) => (
              <li key={i} className="ig__tile" data-category={it.category} data-rk-32bfaf="">
                <span className="ig__logo" style={st(`--c:${hues[i % hues.length]}`)} aria-hidden="true" data-rk-32bfaf="">{it.name.slice(0, 2)}</span>
                <div className="ig__body" data-rk-32bfaf="">
                  <div className="ig__row" data-rk-32bfaf="">
                    <h3 className="ig__name" data-rk-32bfaf="">{it.name}</h3>
                    {it.status && <span className={`ig__status ig__status--${it.status}`} data-rk-32bfaf="">{statusLabel[it.status]}</span>}
                  </div>
                  <p className="ig__desc" data-rk-32bfaf="">{it.description}</p>
                  <span className="ig__cat" data-rk-32bfaf="">{it.category}</span>
                </div>
              </li>
            ))}
          </ul>
        </div>
      </section>
    </>
  );
}

export default IntegrationsGrid;
IntegrationsGrid.css
/* IntegrationsGrid โ€” ported from astro-component-kit (sections/IntegrationsGrid.astro) */

.ig:where([data-rk-32bfaf]) {
  --ig-py: 5.5rem;
  padding: var(--ig-py) var(--sp-6, 1.5rem);
  background: var(--c-bg, #050505);
}
.ig--sm:where([data-rk-32bfaf]) { --ig-py: 3.5rem; }
.ig--lg:where([data-rk-32bfaf]) { --ig-py: 7.5rem; }
.ig__inner:where([data-rk-32bfaf]) { max-width: 1180px; margin: 0 auto; }
.ig__head:where([data-rk-32bfaf]) { text-align: center; max-width: 620px; margin: 0 auto 2rem; }
.ig__title:where([data-rk-32bfaf]) { 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); }
.ig__subtitle:where([data-rk-32bfaf]) { margin: 0; font-size: 1.0625rem; color: var(--c-text-2, #a3a3a3); }
.ig__filters:where([data-rk-32bfaf]) { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.5rem; }
.ig__chip:where([data-rk-32bfaf]) {
  min-height: 40px;
  padding: 0 1rem;
  border-radius: var(--r-full, 999px);
  border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.1);
  background: transparent;
  color: var(--c-text-2, #a3a3a3);
  font: inherit;
  font-size: 0.875rem;
  font-weight: 550;
  cursor: pointer;
  transition: all 0.2s ease;
}
.ig__chip:hover:where([data-rk-32bfaf]) { color: var(--c-text-1, #fff); border-color: rgb(var(--ink-rgb, 255 255 255) / 0.25); }
.ig__chip[aria-pressed='true']:where([data-rk-32bfaf]) { background: var(--c-text-1, #fff); color: var(--c-on-primary, #0a0a0a); border-color: transparent; }
.ig__chip:focus-visible:where([data-rk-32bfaf]) { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }
.ig__count:where([data-rk-32bfaf]) { margin: 1rem 0 1.5rem; text-align: center; font-size: 0.8125rem; color: var(--c-text-3, #8a8a8a); }
.ig__grid:where([data-rk-32bfaf]) { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 1rem; }
.ig__tile:where([data-rk-32bfaf]) {
  display: flex;
  gap: 1rem;
  padding: 1.25rem;
  border-radius: var(--r-lg, 18px);
  transition: border-color 0.2s ease, transform 0.2s ease;
}
.ig__tile[hidden]:where([data-rk-32bfaf]) { display: none; }
.ig--card:where([data-rk-32bfaf]) .ig__tile:where([data-rk-32bfaf]) { background: rgb(var(--ink-rgb, 255 255 255) / 0.025); border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.07)); }
.ig--outline:where([data-rk-32bfaf]) .ig__tile:where([data-rk-32bfaf]) { border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.12); }
.ig--compact:where([data-rk-32bfaf]) .ig__tile:where([data-rk-32bfaf]) { padding: 0.75rem; }
.ig--compact:where([data-rk-32bfaf]) .ig__desc:where([data-rk-32bfaf]) { display: none; }
.ig__tile:hover:where([data-rk-32bfaf]) { border-color: rgb(var(--ink-rgb, 255 255 255) / 0.2); transform: translateY(-2px); }
.ig__logo:where([data-rk-32bfaf]) {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 12px;
  background: color-mix(in srgb, var(--c) 22%, transparent);
  border: 1px solid color-mix(in srgb, var(--c) 45%, transparent);
  color: var(--c-text-1, #fff);
  font-weight: 750;
  font-size: 0.9rem;
}
.ig__body:where([data-rk-32bfaf]) { min-width: 0; flex: 1; }
.ig__row:where([data-rk-32bfaf]) { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.ig__name:where([data-rk-32bfaf]) { margin: 0; font-size: 1rem; font-weight: 650; color: var(--c-text-1, #fff); }
.ig__status:where([data-rk-32bfaf]) { font-size: 0.6875rem; font-weight: 650; padding: 0.15rem 0.5rem; border-radius: var(--r-full, 999px); white-space: nowrap; }
.ig__status--available:where([data-rk-32bfaf]) { background: rgba(52, 195, 143, 0.14); color: #7ee2b8; }
.ig__status--beta:where([data-rk-32bfaf]) { background: rgba(201, 133, 0, 0.16); color: #f2c66a; }
.ig__status--soon:where([data-rk-32bfaf]) { background: rgb(var(--ink-rgb, 255 255 255) / 0.06); color: var(--c-text-2, #a3a3a3); }
.ig__desc:where([data-rk-32bfaf]) { margin: 0.35rem 0 0.6rem; font-size: 0.875rem; line-height: 1.5; color: var(--c-text-2, #a3a3a3); }
.ig__cat:where([data-rk-32bfaf]) { font-size: 0.75rem; color: var(--c-text-3, #8a8a8a); }
@media (prefers-reduced-motion: reduce) { .ig__tile:where([data-rk-32bfaf]), .ig__chip:where([data-rk-32bfaf]) { transition: none; } }