---
title: "Feature Gate"
description: "Kullanıcının planında olmayan bir özelliği gizlemeden kilitler. Plan rozeti, açıklama ve yükseltme aksiyonu taşır."
---

<Callout type="info" title="Ne zaman kullanılır">
Bir özellik üst planda ise ama kullanıcının özelliğin varlığını görmesi gerekiyorsa. Kilitli bir özellik boş bir ekran değildir: `EmptyState` ile anlatılmaz, `FeatureGate` ile kilitlenir. İçerik görünür kalır ama tıklanmaz.
</Callout>

## Kurulum

```bash
npx shadcn@latest add @ikas/feature-gate
```

Registry tanımı yoksa önce [Kurulum](https://builders.ikas.com/tr/docs/app-development/ui-kit/installation) adımlarını tamamlayın.

## Örnekler

### Overlay

Varsayılan varyant. Kullanıcının zaten sahip olduğu içeriği bulanıklaştırır ve üstüne kilit kartını koyar. Liste ve kartlar için.

**Örnek: Overlay** (`feature-gate/overlay`) · [Canlı önizleme](https://builders.ikas.com/ui-preview/demo/feature-gate/overlay)

```tsx
import { toast } from "sonner"

import { Card, CardHeader, CardTitle } from "@/components/ui/card"
import { FeatureGate } from "@/components/ikas/feature-gate"
import { auditActionBadge, auditLogs, formatDate } from "@/demos/_data"

export default function FeatureGateOverlay() {
  return (
    <Card className="w-full max-w-xl gap-0 overflow-hidden py-0">
      <CardHeader className="border-b py-3.5">
        <CardTitle>Denetim kayıtları</CardTitle>
      </CardHeader>
      <FeatureGate
        locked
        title="Denetim kayıtları"
        description="Mağazanızdaki her değişikliği kimin yaptığını görmek için Pro plana geçin."
        onUpgrade={() => toast("Planlar açılıyor")}
        onCompare={() => toast("Plan karşılaştırması açılıyor")}
      >
        <ul className="divide-y text-sm">
          {auditLogs.slice(0, 6).map((log) => (
            <li key={log.id} className="flex items-center gap-4 px-4 py-2.5">
              <span className="w-28 shrink-0 text-muted-foreground tabular-nums">{formatDate(log.date)}</span>
              <span className="min-w-0 flex-1 truncate">
                <span className="font-medium">{log.actor}</span> {auditActionBadge[log.action].label.toLocaleLowerCase("tr")} · {log.record}
              </span>
            </li>
          ))}
        </ul>
      </FeatureGate>
    </Card>
  )
}
```

### Önizleme

Henüz gerçek veri yoksa `variant="preview"` ile örnek içerik gösterin. `preview` verilmezse `children` kullanılır.

**Örnek: Önizleme** (`feature-gate/preview`) · [Canlı önizleme](https://builders.ikas.com/ui-preview/demo/feature-gate/preview)

```tsx
import { EyeIcon, WebhookIcon } from "lucide-react"
import { toast } from "sonner"

import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardAction, CardHeader, CardTitle } from "@/components/ui/card"
import { EmptyState } from "@/components/ikas/empty-state"
import { FeatureGate } from "@/components/ikas/feature-gate"

const sample = [
  {
    url: "https://api.magaza.example/siparis",
    event: "order.created",
    live: true,
  },
  {
    url: "https://erp.magaza.example/stok",
    event: "product.stock.updated",
    live: true,
  },
  {
    url: "https://hooks.magaza.example/iade",
    event: "order.refunded",
    live: false,
  },
  {
    url: "https://crm.magaza.example/musteri",
    event: "customer.created",
    live: true,
  },
]

export default function FeatureGatePreview() {
  return (
    <Card className="w-full max-w-xl gap-0 overflow-hidden py-0">
      <CardHeader className="border-b py-3.5">
        <CardTitle>{"Webhook'lar"}</CardTitle>
        <CardAction>
          <Badge>
            <EyeIcon />
            Örnek önizleme
          </Badge>
        </CardAction>
      </CardHeader>
      <FeatureGate
        locked
        variant="preview"
        title="Webhook'lar Pro planda"
        description="Olayları dış sistemlerinize anlık gönderin."
        onUpgrade={() => toast("Planlar açılıyor")}
        preview={
          <ul className="divide-y">
            {sample.map((hook) => (
              <li key={hook.url} className="flex items-center gap-3 px-4 py-3">
                <span className="grid size-7 place-items-center rounded-md bg-muted text-icon">
                  <WebhookIcon className="size-3.5" />
                </span>
                <span className="flex min-w-0 flex-1 flex-col">
                  <span className="truncate font-mono text-xs">{hook.url}</span>
                  <span className="text-xs text-muted-foreground">{hook.event}</span>
                </span>
                <Badge status={hook.live ? "success" : "warning"} dot>
                  {hook.live ? "Etkin" : "Yeniden deniyor"}
                </Badge>
              </li>
            ))}
          </ul>
        }
      >
        <EmptyState
          media={<WebhookIcon />}
          title="Henüz webhook yok"
          description="İlk webhook'u eklediğinizde olaylar burada listelenir."
          actions={<Button variant="outline">Webhook ekle</Button>}
        />
      </FeatureGate>
    </Card>
  )
}
```

### Banner

Bölüm okunabilir ama salt okunur kalır; üstte tek satırlık bir uyarı çıkar. Ayar formları için.

**Örnek: Banner** (`feature-gate/banner`) · [Canlı önizleme](https://builders.ikas.com/ui-preview/demo/feature-gate/banner)

```tsx
import { toast } from "sonner"

import { Card } from "@/components/ui/card"
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { FeatureGate } from "@/components/ikas/feature-gate"

export default function FeatureGateBanner() {
  return (
    <Card className="w-full max-w-xl overflow-hidden py-0">
      <FeatureGate
        locked
        variant="banner"
        title="Gelişmiş SEO ayarları Pro planda"
        description="Şimdilik yalnızca görüntüleyebilirsiniz."
        onUpgrade={() => toast("Planlar açılıyor")}
      >
        <FieldGroup className="p-4">
          <Field>
            <FieldLabel htmlFor="seo-canonical">Kanonik URL</FieldLabel>
            <Input id="seo-canonical" defaultValue="https://magaza.example/urunler" />
          </Field>
          <Field>
            <FieldLabel htmlFor="seo-robots">Robots yönergesi</FieldLabel>
            <Input id="seo-robots" defaultValue="index, follow" />
          </Field>
        </FieldGroup>
      </FeatureGate>
    </Card>
  )
}
```

### Satır içi

Bir listedeki tek kontrolün yerine plan rozeti ve yükseltme butonu gelir. `SettingRow` içindeki switch'ler için.

**Örnek: Satır içi** (`feature-gate/inline`) · [Canlı önizleme](https://builders.ikas.com/ui-preview/demo/feature-gate/inline)

```tsx
import { toast } from "sonner"

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Switch } from "@/components/ui/switch"
import { FeatureGate } from "@/components/ikas/feature-gate"
import { SettingRow } from "@/components/ikas/setting-row"

export default function FeatureGateInline() {
  return (
    <Card className="w-full max-w-lg">
      <CardHeader>
        <CardTitle>Satış ayarları</CardTitle>
      </CardHeader>
      <CardContent className="divide-y">
        <SettingRow
          htmlFor="multi-language"
          title="Çoklu dil"
          description="Mağazayı 3 dile kadar çevirin."
          control={<Switch id="multi-language" defaultChecked />}
        />
        <SettingRow
          htmlFor="subscriptions"
          title="Abonelikli ürünler"
          description="Tekrarlayan ödemeleri otomatik alın."
          control={
            <FeatureGate locked variant="inline" title="Abonelikli ürünler" onUpgrade={() => toast("Planlar açılıyor")}>
              <Switch id="subscriptions" />
            </FeatureGate>
          }
        />
        <SettingRow htmlFor="two-factor" title="İki adımlı doğrulama" description="Personel girişlerinde kod isteyin." control={<Switch id="two-factor" />} />
      </CardContent>
    </Card>
  )
}
```

### Aksiyon

Tek bir butonu sarar. Kilitliyken tıklama butonun kendi işini yapmaz, planı anlatan küçük bir katman açar.

**Örnek: Aksiyon** (`feature-gate/action`) · [Canlı önizleme](https://builders.ikas.com/ui-preview/demo/feature-gate/action)

```tsx
import { DownloadIcon, ListFilterIcon } from "lucide-react"
import { toast } from "sonner"

import { Button } from "@/components/ui/button"
import { FeatureGate } from "@/components/ikas/feature-gate"

export default function FeatureGateAction() {
  return (
    <div className="flex items-center gap-2">
      <Button variant="outline">
        <ListFilterIcon data-icon="inline-start" />
        Filtrele
      </Button>
      <FeatureGate
        locked
        variant="action"
        title="Toplu dışa aktarma"
        description="Siparişleri CSV ve Excel olarak indirmek Pro planda."
        onUpgrade={() => toast("Planlar açılıyor")}
        onCompare={() => toast("Plan karşılaştırması açılıyor")}
      >
        <Button variant="outline">
          <DownloadIcon data-icon="inline-start" />
          Dışa aktar
        </Button>
      </FeatureGate>
    </div>
  )
}
```

### Kilidin açılması

`locked` false olunca kısa bir açılma animasyonu oynar ve içerik olduğu gibi görünür.

**Örnek: Kilidin açılması** (`feature-gate/unlock`) · [Canlı önizleme](https://builders.ikas.com/ui-preview/demo/feature-gate/unlock)

```tsx
import * as React from "react"

import { Card, CardAction, CardHeader, CardTitle } from "@/components/ui/card"
import { FeatureGate } from "@/components/ikas/feature-gate"
import { SegmentedControl } from "@/components/ikas/segmented-control"
import { auditActionBadge, auditLogs, formatDate } from "@/demos/_data"

export default function FeatureGateUnlock() {
  const [plan, setPlan] = React.useState<"free" | "pro">("free")

  return (
    <Card className="w-full max-w-xl gap-0 overflow-hidden py-0">
      <CardHeader className="border-b py-3">
        <CardTitle>Denetim kayıtları</CardTitle>
        <CardAction>
          <SegmentedControl
            size="sm"
            mode="radio"
            aria-label="Plan"
            value={plan}
            onValueChange={setPlan}
            options={[
              { value: "free", label: "Ücretsiz" },
              { value: "pro", label: "Pro" },
            ]}
          />
        </CardAction>
      </CardHeader>
      <FeatureGate
        locked={plan === "free"}
        title="Denetim kayıtları"
        description="Mağazanızdaki her değişikliği kimin yaptığını görmek için Pro plana geçin."
        onUpgrade={() => setPlan("pro")}
      >
        <ul className="divide-y text-sm">
          {auditLogs.slice(0, 6).map((log) => (
            <li key={log.id} className="flex items-center gap-4 px-4 py-2.5">
              <span className="w-28 shrink-0 text-muted-foreground tabular-nums">{formatDate(log.date)}</span>
              <span className="min-w-0 flex-1 truncate">
                <span className="font-medium">{log.actor}</span> {auditActionBadge[log.action].label.toLocaleLowerCase("tr")} · {log.record}
              </span>
            </li>
          ))}
        </ul>
      </FeatureGate>
    </Card>
  )
}
```

### Renkler

`color` paletteki her rengi alır: kilit, rozet ve yükseltme butonu aynı rengi kullanır. Varsayılan `violet`.

**Örnek: Renkler** (`feature-gate/colors`) · [Canlı önizleme](https://builders.ikas.com/ui-preview/demo/feature-gate/colors)

```tsx
import { Card } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton"
import { FeatureGate, type FeatureGateColor } from "@/components/ikas/feature-gate"

const palette: FeatureGateColor[] = ["violet", "neutral", "blue", "green", "lime", "amber", "red"]

export default function FeatureGateColors() {
  return (
    <div className="grid w-full max-w-4xl gap-4 sm:grid-cols-2 lg:grid-cols-3">
      {palette.map((color) => (
        <div key={color} className="flex flex-col gap-2">
          <code className="font-mono text-xs text-muted-foreground">{`color="${color}"`}</code>
          <Card className="overflow-hidden py-0">
            <FeatureGate locked color={color} title="Webhook'lar" description="Olayları dış sistemlerinize anlık gönderin.">
              <div className="flex flex-col gap-4 p-4">
                {[0, 1, 2, 3].map((row) => (
                  <div key={row} className="flex items-center gap-3">
                    <Skeleton className="size-7 rounded-md" />
                    <Skeleton className="h-2 flex-1" />
                    <Skeleton className="h-4 w-10 rounded-full" />
                  </div>
                ))}
              </div>
            </FeatureGate>
          </Card>
        </div>
      ))}
    </div>
  )
}
```

### Deneme alanı

Varyantları ve renkleri birlikte deneyin.

**Örnek: Deneme alanı** (`feature-gate/playground`) · [Canlı önizleme](https://builders.ikas.com/ui-preview/demo/feature-gate/playground)

```tsx
import * as React from "react"
import { DownloadIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import { Card, CardHeader, CardTitle } from "@/components/ui/card"
import { Field, FieldLabel } from "@/components/ui/field"
import { Skeleton } from "@/components/ui/skeleton"
import { Switch } from "@/components/ui/switch"
import { ColorPicker } from "@/components/ikas/color-picker"
import { FeatureGate, type FeatureGateVariant } from "@/components/ikas/feature-gate"
import { SegmentedControl } from "@/components/ikas/segmented-control"
import { SettingRow } from "@/components/ikas/setting-row"

/** The palette colors as starting points; any other color works too. */
const starters = ["#7B3DEA", "#2563EB", "#008256", "#F5A420", "#DC2626", "#171717"]

/** WCAG relative luminance of a #RRGGBB color. */
function luminance(hex: string) {
  const [r, g, b] = [1, 3, 5].map((i) => {
    const c = parseInt(hex.slice(i, i + 2), 16) / 255
    return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
  })
  return 0.2126 * r + 0.7152 * g + 0.0722 * b
}

/**
 * Points one palette color at the picked color, the way a theme would: the soft fill and border mix it into the card,
 * the tinted text mixes it into the foreground, so it reads in light and dark.
 */
function paletteFrom(color: string) {
  const solid = color.slice(0, 7)
  // Light picks (lime, yellow) need more foreground in their tinted text to stay readable on the soft fill.
  const light = luminance(solid) > 0.45
  return {
    "--violet": solid,
    "--violet-foreground": light ? `color-mix(in oklab, ${solid} 18%, black)` : "white",
    "--violet-subtle": `color-mix(in oklab, ${solid} 10%, var(--card))`,
    "--violet-subtle-foreground": `color-mix(in oklab, ${solid} ${light ? 45 : 72}%, var(--foreground))`,
    "--violet-border": `color-mix(in oklab, ${solid} 26%, var(--card))`,
  }
}

/** Set on the root, not a wrapper, so the action variant's popover (portaled to body) picks the color up too. */
function usePalette(color: string) {
  React.useEffect(() => {
    const root = document.documentElement
    const vars = Object.entries(paletteFrom(color))
    for (const [name, value] of vars) root.style.setProperty(name, value)
    return () => {
      for (const [name] of vars) root.style.removeProperty(name)
    }
  }, [color])
}

const variants: { value: FeatureGateVariant; label: string }[] = [
  { value: "overlay", label: "Overlay" },
  { value: "banner", label: "Banner" },
  { value: "inline", label: "Inline" },
  { value: "action", label: "Action" },
]

function Rows() {
  return (
    <div className="flex flex-col gap-4 p-4">
      {[0, 1, 2, 3, 4].map((row) => (
        <div key={row} className="flex items-center gap-3">
          <Skeleton className="size-7 rounded-md" />
          <div className="flex flex-1 flex-col gap-1.5">
            <Skeleton className="h-2 w-2/3" />
            <Skeleton className="h-1.5 w-1/3" />
          </div>
          <Skeleton className="h-4 w-12 rounded-full" />
        </div>
      ))}
    </div>
  )
}

export default function FeatureGatePlayground() {
  const [color, setColor] = React.useState(starters[0])
  const [swatches, setSwatches] = React.useState(starters)
  const [variant, setVariant] = React.useState<FeatureGateVariant>("overlay")
  usePalette(color)
  const gate = { locked: true, color: "violet", title: "Webhook'lar", description: "Olayları dış sistemlerinize anlık gönderin.", onUpgrade: () => {} } as const

  return (
    <div className="grid w-full max-w-3xl items-start gap-6 md:grid-cols-[16rem_1fr]">
      <div className="flex flex-col gap-4">
        <Field>
          <FieldLabel htmlFor="gate-color">Gate rengi</FieldLabel>
          <ColorPicker
            id="gate-color"
            label="Gate rengi"
            alpha={false}
            value={color}
            onValueChange={setColor}
            swatches={swatches}
            onSwatchesChange={setSwatches}
          />
        </Field>
        <Field>
          <FieldLabel>Varyasyon</FieldLabel>
          <SegmentedControl size="sm" mode="radio" aria-label="Varyasyon" value={variant} onValueChange={setVariant} options={variants} />
        </Field>
      </div>
      <div>
        {variant === "overlay" && (
          <Card className="overflow-hidden py-0">
            <FeatureGate {...gate} onCompare={() => {}}>
              <Rows />
            </FeatureGate>
          </Card>
        )}
        {variant === "banner" && (
          <Card className="overflow-hidden py-0">
            <FeatureGate {...gate} variant="banner" title="Webhook'lar Pro planda" description="Şimdilik yalnızca görüntüleyebilirsiniz.">
              <Rows />
            </FeatureGate>
          </Card>
        )}
        {variant === "inline" && (
          <Card>
            <CardHeader>
              <CardTitle>Entegrasyonlar</CardTitle>
            </CardHeader>
            <div className="divide-y px-6">
              <SettingRow
                title="Sipariş bildirimleri"
                description="Yeni siparişte e-posta gönderin."
                control={<Switch defaultChecked aria-label="Sipariş bildirimleri" />}
              />
              <SettingRow
                title="Webhook'lar"
                description="Olayları dış sistemlerinize anlık gönderin."
                control={
                  <FeatureGate {...gate} variant="inline">
                    <Switch aria-label="Webhook'lar" />
                  </FeatureGate>
                }
              />
            </div>
          </Card>
        )}
        {variant === "action" && (
          <Card className="items-start p-6">
            <FeatureGate {...gate} variant="action" title="Toplu dışa aktarma" description="Siparişleri CSV olarak indirmek Pro planda." onCompare={() => {}}>
              <Button variant="outline">
                <DownloadIcon data-icon="inline-start" />
                Dışa aktar
              </Button>
            </FeatureGate>
          </Card>
        )}
      </div>
    </div>
  )
}
```

## Hangi varyant

| Durum | Varyant |
| --- | --- |
| Kullanıcının verisi var, özellik kilitli (liste, kart) | `overlay` |
| Özellik kilitli ve henüz gösterilecek veri yok | `preview` |
| Bölüm görünür ama değiştirilemez (ayar formu) | `banner` |
| Listede tek bir kontrol kilitli | `inline` |
| Tek bir buton kilitli | `action` |

## Kullanım ilkeleri

**Yapın:**

- Özelliğin adını title'a, planın ne açtığını tek cümleyle description'a yazın.
- onUpgrade ile planları veya ödeme ekranını açın.
- Uygulamada tek bir renk seçin ve her kilitte onu kullanın.

**Yapmayın:**

- Kilitli özelliği gizlemeyin veya EmptyState ile anlatmayın.
- Kendi kilit kartınızı ya da gri disabled butonu kullanmayın.
- Plan butonunu uygulamanın kendi üst barına koymayın; panel zaten gösterir.

## Erişilebilirlik

- Kilit kartı, başlığa bağlı bir `region` olarak okunur.
- Kilitli içerik `inert` ve `aria-hidden` olur; klavye ve ekran okuyucu ona ulaşmaz.
- `prefers-reduced-motion` açıkken kilidin açılması animasyonsuzdur.

## API

### `FeatureGate`

| Prop | Tip | Varsayılan | Açıklama |
| --- | --- | --- | --- |
| `locked` (zorunlu) | `boolean` | - | Plan özelliği kapsamıyorsa true. false olunca children olduğu gibi görünür. |
| `variant` | `FeatureGateVariant` | `"overlay"` | overlay, preview, banner, inline veya action. Yukarıdaki tabloya bakın. |
| `title` (zorunlu) | `React.ReactNode` | - | Kilidin yanındaki özellik adı. |
| `description` | `React.ReactNode` | - | Planın ne açtığını anlatan tek cümle. |
| `plan` | `React.ReactNode` | `"Pro"` | Özelliği içeren plan; rozet olarak görünür. |
| `color` | `FeatureGateColor` | `"violet"` | Kilit, rozet ve yükseltme butonunun palet rengi. |
| `upgradeLabel` | `React.ReactNode` | - | Yükseltme butonunun metni. Varsayılan "Planı yükselt", kompakt varyantlarda "Yükselt". |
| `onUpgrade` | `() => void` | - | Planları veya ödeme ekranını açar. |
| `compareLabel` | `React.ReactNode` | `"Planları karşılaştır"` | İkincil bağlantının metni. |
| `onCompare` | `() => void` | - | İkincil bağlantıyı buton olarak gösterir. |
| `compareHref` | `string` | - | İkincil bağlantıyı link olarak gösterir. |
| `preview` | `React.ReactNode` | - | variant="preview" için gerçek veri yokken gösterilen örnek içerik. Varsayılan children. |
| `children` | `React.ReactNode` | - | Kilitlenen içerik. variant="action" için tek bir buton. |
| `className` | `string` | - | Köke eklenen sınıflar. |
