Skip to main content
Compendium / Pillars & Roles / Comparison / Pillar Switch
#023

Pillar Switch

Segmented control to select one of the four pillars — DO / CO / DE / GO — styled as a ballot selector.

STABLE
#form-control #navigation #real-content #dark-mode-tested
Live
Ballot · Select One
Form S-23
Selected: CO · Architect
Spec

Intent

A four-way segmented control that lets the user pick a pillar. Each segment is a stampable ballot tile; the active selection is filled and badged.

Acceptance Criteria

  • [ ] Four interactive segments labelled DO/CO/DE/GO
  • [ ] Active state is visually unambiguous (filled + role caption)
  • [ ] Clicking a segment updates the active state
  • [ ] Keyboard focus is clearly visible
  • [ ] Does not pollute the global page when multiple instances exist

Constraints

  • JS scoped per component instance
  • Theme-aware via tokens
  • No hex literals in brand-colour slots
  • ≤ 150 lines body
// pillar-switch.astro
---
import type { CompendiumMeta } from '../../../data/types';

export const meta: CompendiumMeta = {
  id: 23,
  slug: 'pillar-switch',
  name: 'Pillar Switch',
  category: 'pillars-roles',
  subcategory: 'comparison',
  description: 'Segmented control to select one of the four pillars — DO / CO / DE / GO — styled as a ballot selector.',
  spec: `## Intent
A four-way segmented control that lets the user pick a pillar. Each segment is a stampable ballot tile; the active selection is filled and badged.

## Acceptance Criteria
- [ ] Four interactive segments labelled DO/CO/DE/GO
- [ ] Active state is visually unambiguous (filled + role caption)
- [ ] Clicking a segment updates the active state
- [ ] Keyboard focus is clearly visible
- [ ] Does not pollute the global page when multiple instances exist

## Constraints
- JS scoped per component instance
- Theme-aware via tokens
- No hex literals in brand-colour slots
- ≤ 150 lines body`,
  tags: ['form-control', 'navigation', 'real-content', 'dark-mode-tested'],
  status: 'stable',
  health: { ics: 100, a11y: 'aa', themed: true, animated: false },
};

const pillars = [
  { k: 'do', short: 'DO', role: 'Author',     activeCls: 'data-[active=true]:bg-do' },
  { k: 'co', short: 'CO', role: 'Architect',  activeCls: 'data-[active=true]:bg-co' },
  { k: 'de', short: 'DE', role: 'Gatekeeper', activeCls: 'data-[active=true]:bg-de' },
  { k: 'go', short: 'GO', role: 'Governor',   activeCls: 'data-[active=true]:bg-go' },
];
---

<div data-this-component class="w-full bg-paper text-black border-4 border-black shadow-nb-sm p-3 overflow-hidden">
  <div class="flex items-center justify-between mb-2 gap-2">
    <div class="font-mono text-[9px] uppercase tracking-[0.25em] opacity-60 truncate">Ballot · Select One</div>
    <div class="font-mono text-[9px] uppercase tracking-[0.25em] opacity-60 shrink-0">Form S-23</div>
  </div>

  <div class="grid grid-cols-4 border-2 border-black" role="radiogroup" aria-label="Select pillar">
    {pillars.map((p, i) => (
      <button
        type="button"
        role="radio"
        aria-checked={i === 1 ? 'true' : 'false'}
        data-key={p.k}
        data-active={i === 1 ? 'true' : 'false'}
        class:list={[
          'group relative px-1 py-3 text-center transition-none focus:outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-4px] focus-visible:outline-black cursor-pointer',
          i < 3 ? 'border-e-2 border-black' : '',
          p.activeCls,
        ]}
      >
        <div class="font-black text-base leading-none">{p.short}</div>
        <div class="font-mono text-[8px] uppercase tracking-widest mt-0.5 opacity-70">{p.role}</div>
        <div class="mt-1 mx-auto w-3 h-3 border-2 border-black bg-paper flex items-center justify-center text-[9px] font-black group-data-[active=true]:bg-ink group-data-[active=true]:text-paper">✓</div>
      </button>
    ))}
  </div>

  <div class="mt-2 flex items-center justify-between font-mono text-[9px] uppercase tracking-widest gap-2">
    <span class="opacity-60 shrink-0">Selected:</span>
    <span data-current class="font-black truncate">CO · Architect</span>
  </div>
</div>

<script>
  const LABELS: Record<string, string> = {
    do: 'DO · Author',
    co: 'CO · Architect',
    de: 'DE · Gatekeeper',
    go: 'GO · Governor',
  };
  // Scope to every instance on the page, not just the first.
  document.querySelectorAll('[data-this-component]').forEach((root) => {
    const buttons = root.querySelectorAll<HTMLButtonElement>('button[data-key]');
    if (!buttons.length) return;
    const current = root.querySelector('[data-current]');
    buttons.forEach((btn) => {
      btn.addEventListener('click', () => {
        buttons.forEach((b) => {
          b.setAttribute('data-active', 'false');
          b.setAttribute('aria-checked', 'false');
        });
        btn.setAttribute('data-active', 'true');
        btn.setAttribute('aria-checked', 'true');
        const k = btn.getAttribute('data-key') ?? 'co';
        if (current) current.textContent = LABELS[k];
      });
    });
  });
</script>