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
- Sl
Slack
AvailablePost alerts and weekly digests to channels.
Messaging - Gi
GitHub
AvailableAnnotate charts with deploys and releases.
Developer - St
Stripe
AvailableSync MRR, churn and invoices automatically.
Payments - Li
Linear
BetaTurn insights into issues in one click.
Developer - No
Notion
AvailableEmbed live charts in your team wiki.
Docs - Hu
HubSpot
BetaAttribute revenue to campaigns and deals.
CRM - Po
Postgres
AvailableQuery your warehouse with zero ETL.
Data - Di
Discord
Coming soonCommunity 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; } }