DESIGN.md
This document captures the v3 visual language and product voice used across Infisical. It is the single reference for engineers, designers, and AI coding agents producing new UI or user-visible copy.
Source of truth for tokens: frontend/src/index.css (@theme block).
Canonical semantic reference: Badge.stories.tsx.
Canonical page references: OverviewPage and AccessControlPage.
Component usage reference: Every v3 generic component has a sibling <Name>.stories.tsx at frontend/src/components/v3/generic/<Name>/. Read it before producing UI with that component — the stories carry the variants, compositions, and use-when guidance the source does not.
Infisical is a security tool for operators. The interface reads like
infrastructure: dense, calm, and legible — never ornamental. Dark is the native
medium; the page canvas is --color-background, and light themes are not part
of the system yet.
Color carries meaning before brand. A danger badge is red because the action is destructive, not because red is the accent. A project-colored button signals project scope, not visual variety. Designers pick intent; hex values follow.
Depth is drawn with borders and surface tones, not shadows. Motion is restrained — 200ms ease-in-out, no springs, no decorative animation. Secret values are masked by default; revealing one is an intentional act.
Key characteristics:
--color-background page canvasAll colors are defined as CSS custom properties in
frontend/src/index.css and consumed via Tailwind v4
utilities (bg-org, text-danger, etc.). Never introduce a hex that is not
in this file.
Used to signal the scope a surface, badge, or action belongs to.
| Scope | Token |
|---|---|
| Organization | --color-org |
| Sub-Organization | --color-sub-org |
| Project | --color-project |
| Admin | --color-admin |
| Intent | Token | Use |
|---|---|---|
| Success | --color-success | Healthy states, completed rotations |
| Info | --color-info | Informational states, external documentation |
| Warning | --color-warning | Attention-warranting states, stale items |
| Danger | --color-danger | Destructive actions, errors, expired access |
| Neutral | --color-neutral | Disabled, muted, "empty" states |
| Role | Token |
|---|---|
| Page background | --color-background |
| Foreground text | --color-foreground |
| Card surface | --color-card |
| Popover / Sheet | --color-popover |
| Container | --color-container |
| Container (hover) | --color-container-hover |
| Border | --color-border |
| Focus ring | --color-ring |
| Accent text | --color-accent |
| Muted text | --color-muted |
| Label text | --color-label |
The mineshaft-* scale (50–900) is the underlying neutral ramp; see
index.css for the full list. Prefer semantic tokens (card, border,
accent) over raw mineshaft values.
Reserved for resource types in the secret management product:
--color-folder, --color-secret, --color-dynamic-secret,
--color-import, --color-secret-rotation, --color-override.
Do not repurpose these for generic UI.
Colored variants always layer as tinted backgrounds with matching borders — never as solid fills. The two canonical recipes:
bg-<c>/15 border-<c>/10 text-<c>, hover bg-<c>/35
(see Badge.tsx)bg-<c>/10 border-<c>/25 text-foreground, hover bg-<c>/15 border-<c>/30
(see Button.tsx)Inter is the only font family (--font-inter). All weights and sizes use
Tailwind's default scale.
| Role | Class | Notes |
|---|---|---|
| Page title (h1) | text-2xl font-medium underline underline-offset-4 decoration-<scope>/90 | In PageHeader; scope icon (size 26) sits inline before the title |
| Page description | text-mineshaft-300 | Sits under the title, mt-1.5 |
| Card title | text-lg font-semibold leading-none | flex gap-1.5 so badges can sit inline |
| Card description | text-sm text-accent | |
| Body | text-sm | Default for table cells, form values, dialog content |
| Label / meta | text-xs text-accent | Field labels, table column captions, metadata |
| Badge | text-xs (auto, via Badge) | Never override |
| Button | text-sm (md/sm/lg), text-xs (xs) | Auto via Button sizing |
Sentence case for descriptions, helper text, and empty states. Title Case for page titles and button labels. See §8 for voice rules on copy itself.
New UI must use v3 components from frontend/src/components/v3/.
The v2 library is legacy; only fall back when no v3 equivalent exists.
PageHeader is the notable exception — still v2, still canonical for page titles.
For exact tokens, class lists, and every variant, read the component source
and its *.stories.tsx — this doc cites them rather than duplicating them.
Every component's .stories.tsx follows the same shape:
Variant: X stories — one per prop-driven variant (e.g. Variant: Outline).Example: X stories — composition recipes (e.g. Example: With Header,
Example: Inside Card / Sheet / Dialog).parameters.docs.description.story is the use-when guidance.When picking a component, find the Example: story closest to your need and
mirror it. When picking a variant, the Variant: story descriptions are the
canonical "use this when..." guidance.
Run Storybook with cd frontend && npm run storybook (port 6006) to preview.
Use these tables to find the component for a given intent. For props,
variants, sizes, and class lists, open the source or its *.stories.tsx
— the stories are canonical.
| Component | Reach for this when… |
|---|---|
Button | A text-bearing button — primary or secondary action. |
IconButton | A square icon-only button — toolbars, row actions, compact triggers. Always aria-label. |
ButtonGroup | Visually join related controls — toolbars, segmented controls, split buttons, key-value chips. |
Dropdown | An action menu — overflow ⋯, split-button alternates, contextual lists. |
| Component | Reach for this when… |
|---|---|
Field | Wrap every form control — label + control + description + error. Never render a bare control in a form. |
Label | Standalone form label outside a Field. |
Input / TextArea | Single-line / multi-line text entry. |
InputGroup | Input with left/right addons — search bars, prefixed values. |
Select / ReactSelect | Native-style dropdown / async or searchable dropdown. |
Switch / Checkbox | Boolean toggle / multi-select boolean. |
Calendar | Date / multi-date / range picker primitive. |
DateRangeFilter | Date-range filter with presets — for filter bars. |
SecretInput | Secret-value editor with mask toggle and ${var} highlighting. |
PasswordGenerator | Generate a password against project secret-validation rules. |
| Component | Reach for this when… |
|---|---|
Card | Default section container — tables, filters, forms, empty states all live in a Card. |
Sheet | Right-side panel — use for create/edit forms (not Dialog). |
Dialog | Centered modal — short interactive prompts. Prefer Sheet for forms. |
AlertDialog | Confirm an action (destructive included). Replaces confirm(). |
Popover | Anchored floating panel — filters, pickers, contextual UI. |
Tooltip | Small floating annotation on hover/focus. |
Accordion | Collapsible sections. |
| Component | Reach for this when… |
|---|---|
Table | Read-mostly list of records with sortable columns. Pair with Empty + Pagination. |
DataGrid | Editable spreadsheet-style grid — copy/paste, multi-cell selection, keyboard nav. Use only when Table isn't enough. |
Pagination | Page controls under a Table or list. |
Item | Vertically-stacked list rows with shared spacing — when a Table is too heavy. |
Detail | Read-only label/value pairs in a detail view. |
Badge | Small label or chip — status, scope tag, key/value pair. |
| Component | Reach for this when… |
|---|---|
Sidebar | Scope-aware product navigation panel. |
Breadcrumb | Hierarchical location trail at the top of a page. |
Command | Search-driven command palette / typeahead list. |
| Component | Reach for this when… |
|---|---|
Alert | Inline message banner inside a page or Card. |
Toast | Transient post-action feedback. Replaces alert(). |
Empty | Zero-state placeholder — pair with Table, list, or empty filter. |
Skeleton | Shimmer placeholder while data is loading. |
PageLoader | Centered Lottie spinner for full-page loading. |
| Component | Reach for this when… |
|---|---|
Separator | Horizontal/vertical divider. |
ScopeIcons | OrgIcon / SubOrgIcon / ProjectIcon / InstanceIcon — use when intent is scope. |
DocumentationLinkBadge | Inline "Documentation" link badge in CardTitle. |
Icons — lucide-react. Sizing is bound by the
host component; don't override unless necessary.
max-w-8xl (88rem) centered, bg-bunker-800.PageHeader with scope icon + underlined h1 + description. See PageHeader.tsx. Always set scope to the correct hierarchy level.Card per logical section. Title + optional DocumentationLinkBadge in CardHeader; primary action in CardAction (top-right).CardHeader above the table; pagination sits in the CardFooter or bottom of CardContent.gap-1.5 (intra-element), gap-2 / gap-3 (adjacent elements), p-4 / p-5 (section padding). Card = p-5 gap-5; Sheet header/footer = p-4.Depth is conveyed by layered surface tones and borders. Shadows are reserved for elements that float (Popover, DropdownMenu, Sheet).
| Layer | Surface | Border |
|---|---|---|
| Page | bg-bunker-800 | — |
| Card | bg-card | border-border |
| Popover / Sheet | bg-popover | border-border + shadow-lg |
| Row hover | bg-container-hover | — |
| Focus | — | 3px ring, --color-ring |
| Disabled | opacity-50 / 75, pointer-events-none | — |
Never add a box-shadow to a Card, Table row, or Badge; it breaks the border-defined system.
org, sub-org, project, admin) to reinforce
hierarchy — the scope of a page, a primary button, a scope-link badge.bg-card) over hex (#xxxxxx) in new code.index.css @theme, it
doesn't belong.project yellow, org blue, or sub-org green as generic
accents. They are scope signals; repurposing them creates false hierarchy.Copy should read as if written by an engineer for another engineer: direct, technical, specific. The domain is serious — secrets, access, compliance — and the voice reflects that.
rotate, revoke,
import).secrets:write permission."Never include a secret's value in any user-visible copy — UI, logs, toasts, errors, audit trails, or analytics. Refer to secrets by key only. Mask tokens and keys in screenshots and docs as well.
Use DocumentationLinkBadge (info variant, external-link icon). Label it
"Documentation" — not "Learn more", "Read docs", "See more".
Pasteable prompt fragments for AI coding agents producing new UI.
Before generating UI for any component:
frontend/src/components/v3/generic/<Name>/<Name>.stories.tsx.Example: story closest to your need; mirror its composition exactly.Variant: story's description —
not by color preference.Adding a section to an existing page:
Wrap the section in a
Cardfrom@app/components/v3. UseCardHeaderwithCardTitle+ optionalCardDescription+CardActionfor the top-right primary button (variantprojecton a project page). Put the table or content inCardContent.
A new create/edit form:
Put the form in a right-side
Sheet(Sheet,SheetContent,SheetHeaderwithSheetTitle+SheetDescription,SheetFooterwith the action buttons). Usereact-hook-formwith a Zod resolver. Each input is wrapped inField+FieldLabel+FieldContent+FieldError. Primary button is variant is scope dependentproject, secondary isoutline, cancel isghost.
A status indicator:
Use
Badgefrom@app/components/v3. Pick the variant by intent:dangerfor errors or expired access,warningfor stale or attention-warranting,successfor healthy / completed,infofor informational,neutralfor disabled / empty,project/org/sub-orgfor scope references. Include a matching Lucide icon as the first child.
A destructive confirmation:
Use
AlertDialog. Title: "Delete<resource-name>". Description: one sentence naming the consequence, ending with "This cannot be undone." Confirm button is variantdanger. Cancel button is variantoutline.
A documentation link in a section:
Use
DocumentationLinkBadgefrom@app/components/v3/platform. Place it in theCardTitlenext to the section name.
Refer to:
Badge.stories.tsx — canonical semantic reference for variant choice.OverviewPage — full-page reference (PageHeader, Card-with-table, Create Secret Sheet, filters, DropdownMenu + ButtonGroup).AccessControlPage — full-page reference (permission-gated actions, DocumentationLinkBadge, role badges with ClockAlertIcon for expired access).cd frontend && npm run storybook (port 6006). Open
Badge, Button, Card, Table, Sheet first.OverviewPage and AccessControlPage
render the full v3 vocabulary in production.index.css — @theme block, lines 56–214. Never
introduce a hex that is not here.cva() block in the component and add
a story. Keep the tint pattern (bg-<c>/15 border-<c>/10 for Badge,
bg-<c>/10 border-<c>/25 for Button).PageHeader is the notable v2 exception still used by all pages.make reviewable-ui (lint + type-check).OverviewPage.