Modal — Complete Guide
Every modal consists of up to three files:
- Store — Zustand store to control open state and typed data
- Modal — The modal UI component
- Open Button — (optional) Only create this when explicitly requested
Path Resolution
Before creating any files, invoke the finstreet-fe:path-resolver skill with your input parameters (featureName, subFeatureName, featureType, product, role) to resolve the correct paths. Use the returned Feature Path as {parentDirectory} in the directory structure below.
Directory Structure
{parentDirectory}/
├── store.ts
├── {ModalName}Modal.tsx
└── Open{ModalName}ModalButton.tsx ← only if requested
1. Store
File: store.ts
import { create } from "zustand";
type {ModalName}ModalData = {
financingCaseId: string;
} | null;
interface {ModalName}ModalStore {
isOpen: boolean;
data: {ModalName}ModalData;
setIsOpen: (isOpen: boolean) => void;
setData: (data: {ModalName}ModalData) => void;
}
export const use{ModalName}Modal = create<{ModalName}ModalStore>((set) => ({
isOpen: false,
data: null,
setIsOpen: (isOpen) => set({ isOpen }),
setData: (data) => set({ data, isOpen: true }),
}));
dataholds the typed payload the modal needs (e.g., IDs passed from the trigger)setDataalways setsisOpen: true— opening and setting data happen together- Shape the
{ModalName}ModalDatatype to match exactly what the modal content needs
2. Modal Component
File: {ModalName}Modal.tsx
"use client";
import {
Modal,
ModalContent,
ModalTitle,
} from "@finstreet/ui/components/patterns/Modal";
import { use{ModalName}Modal } from "./store";
import { Suspense } from "react";
import { useExtracted } from "next-intl";
import { Headline } from "@finstreet/ui/components/base/Headline";
import { Typography } from "@finstreet/ui/components/base/Typography";
import { VStack } from "@styled-system/jsx";
export const {ModalName}Modal = () => {
const { isOpen, data, setIsOpen } = use{ModalName}Modal();
const t = useExtracted();
if (!data) {
return null;
}
const { financingCaseId } = data;
return (
<Modal open={isOpen} onClose={() => setIsOpen(false)}>
<ModalTitle>
{/* See title patterns below */}
</ModalTitle>
<ModalContent>
{/* Modal content here */}
</ModalContent>
</Modal>
);
};
ModalTitle variants
Use exactly one of these based on whether a subheading is present in the context:
With title and subheading:
<ModalTitle>
<VStack gap={1} alignItems={"flex-start"}>
<Headline>{t("{German title}")}</Headline>
<Typography color={"text.dark"}>{t("{German subheading}")}</Typography>
</VStack>
</ModalTitle>
Title only (no subheading):
<ModalTitle>
{t("{German title}")}
</ModalTitle>
Rules
- Always guard with
if (!data) return nullbefore destructuring data - Wrap async content (e.g., forms with server actions) in
<Suspense> - Hardcoded strings are acceptable — translations are cleaned up separately
3. Open Button (optional)
File: Open{ModalName}ModalButton.tsx
Only create this file when explicitly asked to.
"use client";
import { use{ModalName}Modal } from "./store";
import { Button } from "@finstreet/ui/components/base/Button";
import { useExtracted } from "next-intl";
type Open{ModalName}ModalButtonProps = {
financingCaseId: string;
};
export const Open{ModalName}ModalButton = ({
financingCaseId,
}: Open{ModalName}ModalButtonProps) => {
const { setData } = use{ModalName}Modal();
const t = useExtracted();
return (
<Button onClick={() => setData({ financingCaseId })}>
{t("{German button label}")}
</Button>
);
};
- Call
setData(notsetIsOpen) from the button —setDataalready opens the modal - Props must match the
{ModalName}ModalDatatype from the store