# Filterable Glossary

> A jargon glossary whose letter filter is derived from the terms themselves, narrowing a two-column hairline definition list with an empty state and a suggest-a-term action.

A shadcn/ui block from the [@beste-ui](https://ui.beste.co) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/beste-ui/faq89).

- Page, with a live preview: https://shadcn.exchange/r/beste-ui/faq89
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/beste-ui/faq89-light-5be709263053878c.png
- Registry item: https://ui.beste.co/r/faq89.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://ui.beste.co/r/faq89.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `clsx`, `lucide-react`, `tailwind-merge`
- Registry items: `button`

## Files

### `registry/faq89/faq89.tsx`

```tsx
"use client";

import { Badge23 } from "@/components/beste/component/badge23";
import { Button21 } from "@/components/beste/component/button21";
import { cn } from "@/lib/utils";
import Link from "next/link";
import { useState } from "react";

interface Badge {
  label: string;
}

interface ActionLink {
  label: string;
  href: string;
}

interface Term {
  term: string;
  definition: string;
}

interface Faq89Props {
  badge?: Badge;
  heading?: string;
  description?: string;
  allLabel?: string;
  terms?: Term[];
  emptyLabel?: string;
  closing?: string;
  button?: ActionLink;
  className?: string;
}

export const faq89Demo: Faq89Props = {
  badge: { label: "Plain English" },
  heading: "The words we use, and what we actually mean by them",
  description:
    "Clinical software collects vocabulary. Here is ours, defined by someone who has to explain it on calls.",
  allLabel: "All",
  terms: [
    {
      term: "Backfill",
      definition:
        "When a cancelled slot is offered to the waiting list automatically and taken by whoever confirms first. Nobody rings round.",
    },
    {
      term: "Capacity",
      definition:
        "Rooms multiplied by opening hours, minus what is already booked. It is measured against rooms rather than clinicians, because rooms are the thing you cannot add on a Tuesday.",
    },
    {
      term: "Care plan",
      definition:
        "A repeating series of appointments attached to one member, billed as a block rather than one visit at a time.",
    },
    {
      term: "Dry run",
      definition:
        "Your real data loaded into a private workspace nobody outside your practice can reach, so your team can break it without consequence.",
    },
    {
      term: "Field mapping",
      definition:
        "Agreeing what each column in your old export becomes in the new record. One hour, once, with your practice lead.",
    },
    {
      term: "Member",
      definition:
        "The person receiving care. We avoid 'patient' because half the practices on the platform do not use it.",
    },
    {
      term: "Role",
      definition:
        "A named job with an access level attached, like Reception or Locum. There are six and none of them are a checkbox grid.",
    },
    {
      term: "Seat",
      definition:
        "One person with a login. Seats move up and down mid-month and we prorate both directions.",
    },
    {
      term: "Site",
      definition:
        "One physical clinic. Sites share a waiting list and capacity but keep their own rooms, hours, and rules.",
    },
    {
      term: "Utilisation",
      definition:
        "The share of available room hours that were actually booked. The number most practices are trying to move.",
    },
  ],
  emptyLabel: "Nothing filed under that letter.",
  closing: "Missing a word you keep having to explain to us? Tell us and it goes on the list.",
  button: { label: "Suggest a term", href: "https://beste.co" },
};

export function Faq89({
  badge,
  heading,
  description,
  allLabel = "All",
  terms = [],
  emptyLabel,
  closing,
  button,
  className,
}: Faq89Props) {
  const [letter, setLetter] = useState<string | null>(null);

  const letters = Array.from(new Set(terms.map((term) => term.term.charAt(0).toUpperCase()))).sort();
  const visible = letter
    ? terms.filter((term) => term.term.charAt(0).toUpperCase() === letter)
    : terms;

  return (
    <section className={cn("bg-background py-16 md:py-24 w-full", className)}>
      <div className="mx-auto max-w-7xl px-4 md:px-6">
        {badge && <Badge23 label={badge.label} />}

        <div className="mt-6 border-t border-border pt-8 md:pt-10">
          <div className="grid gap-6 md:grid-cols-2 md:gap-12">
            {heading && (
              <h2 className="text-3xl font-light leading-[1.1] tracking-tight text-foreground md:text-5xl">
                {heading}
              </h2>
            )}
            {description && (
              <div className="flex md:justify-end">
                <p className="max-w-md text-base leading-relaxed text-muted-foreground">
                  {description}
                </p>
              </div>
            )}
          </div>
        </div>

        <div className="mt-10 flex flex-wrap gap-2">
          <button
            type="button"
            onClick={() => setLetter(null)}
            aria-pressed={letter === null}
            className={cn(
              "cursor-pointer rounded-md border px-3 py-1.5 text-sm transition-colors",
              letter === null
                ? "border-foreground bg-foreground text-background"
                : "border-border bg-transparent text-muted-foreground hover:text-foreground"
            )}
          >
            {allLabel}
          </button>
          {letters.map((entry, index) => {
            const isActive = letter === entry;
            return (
              <button
                key={index}
                type="button"
                onClick={() => setLetter(entry)}
                aria-pressed={isActive}
                className={cn(
                  "size-9 cursor-pointer rounded-md border text-sm transition-colors",
                  isActive
                    ? "border-foreground bg-foreground text-background"
                    : "border-border bg-transparent text-muted-foreground hover:text-foreground"
                )}
              >
                {entry}
              </button>
            );
          })}
        </div>

        <dl className="mt-10">
          {visible.map((term, index) => (
            <div
              key={index}
              className="grid gap-2 border-t border-border py-5 md:grid-cols-[minmax(0,1fr)_minmax(0,2.4fr)] md:gap-12"
            >
              <dt className="text-lg font-medium text-foreground">{term.term}</dt>
              <dd className="text-base leading-relaxed text-muted-foreground">
                {term.definition}
              </dd>
            </div>
          ))}
        </dl>

        {visible.length === 0 && emptyLabel && (
          <p className="border-t border-border py-6 text-base text-muted-foreground">
            {emptyLabel}
          </p>
        )}

        <div className="mt-8 flex flex-col items-start gap-4 border-t border-border pt-8 sm:flex-row sm:items-center sm:justify-between">
          {closing && <p className="text-base text-muted-foreground">{closing}</p>}
          {button && (
            <Button21 asChild label={button.label} tone="outline">
              <Link href={button.href} />
            </Button21>
          )}
        </div>
      </div>
    </section>
  );
}
```

### `registry-components/badge23/badge23.tsx`

```tsx
"use client";

import { cn } from "@/lib/utils";

type Tone = "muted" | "foreground" | "primary";

interface Badge23Props {
  /** Eyebrow label */
  label: string;
  /** Text/border tone */
  tone?: Tone;
  /** Additional classes merged onto the root */
  className?: string;
}

const toneStyles: Record<Tone, string> = {
  muted: "border-border text-muted-foreground",
  foreground: "border-foreground/30 text-foreground",
  primary: "border-primary/40 text-primary",
};

export const badge23Demo: Badge23Props = {
  label: "Product",
};

export function Badge23({ label, tone = "muted", className }: Badge23Props) {
  return (
    <span
      className={cn(
        "inline-flex items-center rounded-md border bg-transparent px-2.5 py-1 text-sm font-medium leading-none",
        toneStyles[tone],
        className
      )}
    >
      {label}
    </span>
  );
}
```

### `registry-components/button21/button21.tsx`

```tsx
"use client";

import type { LucideIcon } from "lucide-react";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";

type Tone = "primary" | "neutral" | "outline";

interface Button21Props {
  /** Button label */
  label: string;
  /**
   * Compose the rendered element shadcn-style (radix asChild): your element
   * (e.g. a router Link) becomes the root and the button content is injected
   * as its children. Pass the element without children of its own.
   */
  asChild?: boolean;
  /** The element to render when `asChild` is set (e.g. your framework's Link) */
  children?: React.ReactElement;
  /**
   * Fallback link: renders a plain `<a>`. Prefer `asChild` with your
   * framework's Link component for client-side navigation.
   */
  href?: string;
  /** Optional trailing icon */
  icon?: LucideIcon;
  /** Surface tone: solid accent (default), soft neutral pill, or hairline outline */
  tone?: Tone;
  /** Click handler (used when there is no href and no asChild) */
  onClick?: React.MouseEventHandler<HTMLButtonElement>;
  /** Additional classes merged onto the button */
  className?: string;
}

const toneStyles: Record<Tone, string> = {
  primary: "bg-primary text-primary-foreground hover:bg-primary/90",
  neutral: "bg-muted text-foreground hover:bg-muted/70",
  outline: "border border-border bg-transparent text-foreground hover:bg-muted",
};

export const button21Demo: Button21Props = {
  label: "Book a demo",
};

export function Button21({
  label,
  asChild = false,
  children,
  href,
  icon: Icon,
  tone = "primary",
  onClick,
  className,
}: Button21Props) {
  const classes = cn(
    "group/button21 h-auto w-fit gap-2 rounded-md px-5 py-2.5 text-sm font-medium tracking-tight transition-colors",
    toneStyles[tone],
    className
  );

  const inner = (
    <>
      {label}
      {Icon && (
        <Icon className="size-4 transition-transform motion-safe:group-hover/button21:translate-x-0.5" />
      )}
    </>
  );

  if (asChild && React.isValidElement(children)) {
    return (
      <Button asChild className={classes}>
        {React.cloneElement(children, undefined, inner)}
      </Button>
    );
  }

  if (href) {
    return (
      <Button asChild className={classes}>
        <a href={href}>{inner}</a>
      </Button>
    );
  }

  return (
    <Button type="button" className={classes} onClick={onClick}>
      {inner}
    </Button>
  );
}
```
