Skip to main content

Eligibility Screener

The screener is a guided questionnaire that collects household information and progressively matches users to benefit programs.

Design Philosophy

The screener follows three principles:
  1. Start with location: Geography determines which programs are available
  2. Show progress immediately: Update match counts after each step
  3. Allow incompleteness: Users can skip questions and still get useful results

Step-by-Step Breakdown

Step 1: Location

Purpose: Filter to geographically available programs
interface LocationStep {
  zipCode: string;
  // Derived from geocoding:
  state: string;
  countyFips: string;
  city?: string;
  coordinates: { lat: number; lng: number };
}
UI Component: ZIP code input with auto-geocoding
<LocationStep
  value={screenerData.zipCode}
  onChange={(location) => {
    updateScreener({
      zipCode: location.zipCode,
      state: location.state,
      countyFips: location.fips,
    });
  }}
/>
What happens:
  1. User enters ZIP code
  2. Geocoding resolves to coordinates
  3. Reverse geocoding determines county FIPS
  4. Programs filtered to federal + state + county scope

Step 2: Household Composition

Purpose: Determine household size for FPL calculations and family-based programs
interface HouseholdStep {
  householdSize: number;
  adults: number;
  children: number;
  childrenAges?: number[];  // For age-specific programs
}
UI Component: Numeric inputs with validation
<HouseholdStep
  onComplete={(data) => {
    updateScreener({
      householdSize: data.adults + data.children,
      adults: data.adults,
      children: data.children,
      childrenAges: data.childrenAges,
    });
  }}
/>
Programs affected:
  • WIC (children under 5, pregnant women)
  • Head Start (children 3-5)
  • CHIP (children under 19)
  • Free/Reduced Lunch (school-age children)

Step 3: Income

Purpose: Apply income-based eligibility rules
interface IncomeStep {
  hasIncome: boolean;
  grossMonthlyIncome?: number;
  netMonthlyIncome?: number;
  incomeTypes?: IncomeType[];
}
UI Component: Income calculator with breakdowns
<IncomeStep
  householdSize={screenerData.householdSize}
  onComplete={(income) => {
    updateScreener({
      hasIncome: income.hasIncome,
      grossMonthlyIncome: income.gross,
      netMonthlyIncome: income.net,
    });
  }}
/>
Income types tracked:
  • Wages and salaries
  • Self-employment
  • Social Security (retirement)
  • SSI/SSDI (disability)
  • Unemployment benefits
  • Child support/alimony
  • Pension/retirement
  • Investment income
  • Rental income
FPL Display:
<FPLIndicator
  monthlyIncome={3000}
  householdSize={4}
/>
// Shows: "115% of Federal Poverty Level"

Step 4: Demographics

Purpose: Apply categorical eligibility rules
interface DemographicsStep {
  citizenshipStatus: CitizenshipStatus;
  hasElderly: boolean;      // Anyone 60+ in household
  hasDisabled: boolean;     // Anyone with disability
  isPregnant: boolean;
  isVeteran: boolean;
  isStudent: boolean;
}
UI Component: Multiple choice questions
<DemographicsStep
  questions={[
    {
      id: "citizenship",
      label: "What is your citizenship status?",
      options: [
        { value: "citizen", label: "U.S. Citizen" },
        { value: "permanent_resident", label: "Permanent Resident (Green Card)" },
        { value: "qualified_alien", label: "Qualified Immigrant" },
        { value: "refugee", label: "Refugee or Asylee" },
        { value: "other", label: "Other / Prefer not to say" },
      ],
    },
    // ...
  ]}
/>
Programs affected by demographics:
DemographicPrograms
Elderly (60+)Medicare, Senior SNAP, Meals on Wheels
DisabledSSI, SSDI, Medicaid, Vocational Rehab
PregnantWIC, Medicaid, TANF
VeteranVA Healthcare, GI Bill, VA Pension
StudentPell Grant, Work-Study, SNAP student rules

Step 5: Current Benefits (Optional)

Purpose: Apply categorical eligibility and avoid duplicate suggestions
interface CurrentBenefitsStep {
  receivingBenefits: boolean;
  currentBenefits: string[];
}
UI Component: Checkbox list
<CurrentBenefitsStep
  options={[
    { value: "snap", label: "SNAP / Food Stamps" },
    { value: "medicaid", label: "Medicaid" },
    { value: "tanf", label: "TANF / Cash Assistance" },
    { value: "ssi", label: "SSI (Supplemental Security Income)" },
    { value: "ssdi", label: "SSDI (Social Security Disability)" },
    { value: "section8", label: "Section 8 / Housing Voucher" },
    { value: "wic", label: "WIC" },
    { value: "liheap", label: "LIHEAP / Utility Assistance" },
  ]}
/>
Categorical eligibility triggers:
  • SNAP → Free school meals, Lifeline, WIC
  • TANF → SNAP, Free meals, Medicaid
  • SSI → SNAP, Medicaid, Lifeline
  • Medicaid → WIC, Lifeline

Step 6: Results

Purpose: Display matched programs with actions
<ScreenerResults
  results={matchResults}
  onSaveProgram={(programId) => saveToUser(programId)}
  onStartApplication={(programId) => startApplication(programId)}
/>
Result grouping:
<ResultsDisplay>
  <ResultGroup
    title="Likely Eligible"
    description="Based on your answers, you appear to qualify"
    programs={results.filter(r => r.eligibility === "likely")}
    variant="success"
  />

  <ResultGroup
    title="Possibly Eligible"
    description="You may qualify - check program details"
    programs={results.filter(r => r.eligibility === "possible")}
    variant="info"
  />

  <ResultGroup
    title="Worth Exploring"
    description="Programs in your area you might qualify for later"
    programs={results.filter(r => r.eligibility === "unlikely")}
    variant="muted"
    collapsed
  />
</ResultsDisplay>

State Management

Screener state persists across sessions using localStorage and database sync.
interface ScreenerState {
  // Current progress
  currentStep: number;
  completedSteps: number[];

  // Collected data
  data: Partial<HouseholdData>;

  // Match results (cached)
  matchResults?: MatchResult[];
  lastMatchedAt?: Date;

  // Metadata
  startedAt: Date;
  lastUpdatedAt: Date;
}

// Hook for screener state
function useScreener() {
  const [state, setState] = useState<ScreenerState>(() => {
    // Load from localStorage on mount
    const saved = localStorage.getItem("pathfinder_screener");
    return saved ? JSON.parse(saved) : initialState;
  });

  // Auto-save on changes
  useEffect(() => {
    localStorage.setItem("pathfinder_screener", JSON.stringify(state));
  }, [state]);

  // Sync to database when logged in
  useEffect(() => {
    if (user) {
      syncScreenerToProfile(user.id, state.data);
    }
  }, [user, state.data]);

  return { state, updateScreener, resetScreener };
}

Real-Time Match Updates

Matches update after each step with debouncing.
function useMatchUpdates(screenerData: Partial<HouseholdData>) {
  const [matches, setMatches] = useState<MatchResult[]>([]);
  const [isLoading, setIsLoading] = useState(false);

  // Debounced match calculation
  const debouncedMatch = useMemo(
    () =>
      debounce(async (data: Partial<HouseholdData>) => {
        setIsLoading(true);
        const results = await matchPrograms(data);
        setMatches(results);
        setIsLoading(false);
      }, 300),
    []
  );

  useEffect(() => {
    debouncedMatch(screenerData);
  }, [screenerData]);

  return { matches, isLoading };
}

Benefits Calculator

Shows estimated annual value of matched programs.
function BenefitsCalculator({ matches }: { matches: MatchResult[] }) {
  const likelyMatches = matches.filter(m => m.eligibility === "likely");

  const monthlyTotal = likelyMatches.reduce((sum, match) => {
    const monthly = match.program.benefitValueCents
      ? match.program.benefit_frequency === "annual"
        ? match.program.benefitValueCents / 12
        : match.program.benefitValueCents
      : 0;
    return sum + monthly;
  }, 0);

  const annualTotal = monthlyTotal * 12;

  return (
    <Card>
      <CardHeader>
        <CardTitle>Estimated Benefits</CardTitle>
      </CardHeader>
      <CardContent>
        <div className="text-3xl font-bold text-emerald-600">
          ${(annualTotal / 100).toLocaleString()}/year
        </div>
        <p className="text-sm text-stone-500">
          Based on {likelyMatches.length} programs you likely qualify for
        </p>

        <div className="mt-4 space-y-2">
          {likelyMatches.slice(0, 5).map(match => (
            <div key={match.programId} className="flex justify-between">
              <span>{match.program.shortName || match.program.name}</span>
              <span className="text-emerald-600">
                ${(match.program.benefitValueCents / 100).toLocaleString()}
                /{match.program.benefit_frequency}
              </span>
            </div>
          ))}
        </div>
      </CardContent>
    </Card>
  );
}

Accessibility

The screener follows WCAG 2.1 AA guidelines:
  • Keyboard navigation: All steps navigable via Tab/Enter
  • Screen reader support: ARIA labels on all inputs
  • Focus management: Focus moves to first input on step change
  • Error handling: Inline validation with clear error messages
  • Progress indication: Step progress announced to screen readers
<form
  aria-label="Benefits eligibility screener"
  aria-describedby="screener-description"
>
  <div id="screener-description" className="sr-only">
    Answer questions about your household to find benefits you may qualify for.
    Step {currentStep} of {totalSteps}.
  </div>

  {/* Step content */}
</form>

Analytics Events

Track screener usage for optimization.
const SCREENER_EVENTS = {
  STARTED: "screener_started",
  STEP_COMPLETED: "screener_step_completed",
  STEP_SKIPPED: "screener_step_skipped",
  COMPLETED: "screener_completed",
  ABANDONED: "screener_abandoned",
  PROGRAM_SAVED: "screener_program_saved",
};

// Track step completion
analytics.track(SCREENER_EVENTS.STEP_COMPLETED, {
  step: currentStep,
  stepName: STEP_NAMES[currentStep],
  timeOnStep: Date.now() - stepStartTime,
  matchCount: matches.length,
});

Next Steps