Getting Started
Installation
Into an existing project:
npx kevlar initScaffold a new Next.js project with Kevlar:
npx kevlar create my-appBoth commands generate the kevlar/ folder in your project root. Everything inside is your code, in your repo, reviewed in PRs.
What Gets Created
your-project/
kevlar/
design.config.ts # one file — your entire design language
base/ # 9 base components
BaseInteractive.tsx
BaseInput.tsx
BaseStatic.tsx
BaseOverlay.tsx
BaseContainer.tsx
BaseFeedback.tsx
BaseNavigation.tsx
BaseDisclosure.tsx
BaseMedia.tsx
components/ # 108 component files
Button.tsx
ActionIcon.tsx
TextInput.tsx
Modal.tsx
Image.tsx
... (108 files total)
index.ts # re-exports everythingEvery file ships full of MUST_BE_DEFINED markers. That is the point. You open these files and make conscious decisions about every interaction in your app.
The Flow
Day 0: npx kevlar init
└─ Everything is MUST_BE_DEFINED
Day 1-3: Tech lead fills kevlar/design.config.ts
└─ Colors, sounds, haptics, animation curves,
breakpoints, touch targets, timing defaults
Day 3-7: Tech lead fills kevlar/base/*.tsx
└─ Goes through each base, replaces every MUST_BE_DEFINED
└─ References config everywhere: config.shadows.low,
config.sounds.click, config.focusRing
└─ Uses targets inline: isTV(), prefersReducedMotion(),
isSilentMode()
└─ Leaves STRING_MUST_BE_DEFINED for per-instance things
(announcements, labels)
Day 7+: Devs use components
└─ import { Button } from '../kevlar'
└─ Fill remaining blanks via props (announce, onKevlarAction)
└─ Override any base decision if their instance needs
something different
└─ Any surviving MUST_BE_DEFINED at render → error with
exact instructionsKevlarProvider Setup
Wrap your app with KevlarProvider at the root. This sets up target detection (viewport, network, input method, accessibility preferences) and the sensory budget system.
// app/layout.tsx (Next.js App Router)
import { KevlarProvider } from '../kevlar';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<KevlarProvider
colorBlind={false} // set true if user has indicated color blindness
userSegment="normal" // 'first_time' | 'normal' | 'power'
>
{children}
</KevlarProvider>
</body>
</html>
);
}Your First Button
Every Kevlar component requires you to fill the blanks that survived from the base. For Button, the two required props are onKevlarAction and announce.
import { Button } from '../kevlar';
function AddTodoButton() {
return (
<Button
onKevlarAction={async (ctx) => {
const res = await fetch('/api/todos', { method: 'POST', body: JSON.stringify({ text: 'New todo' }) });
if (!res.ok) throw new Error('Failed to add todo');
}}
announce={{
loading: 'Adding todo...',
success: 'Todo added successfully',
error: 'Failed to add todo',
}}
>
Add Todo
</Button>
);
}onKevlarActionis your async action. Kevlar handles the state machine: idle, pressed, loading, success/error.announcefills theSTRING_MUST_BE_DEFINEDmarkers that survived from the base. These are what screen readers announce during each state.
You can override any base decision at the instance level:
<Button
onKevlarAction={handleSubmit}
announce={{ loading: 'Submitting...', success: 'Submitted!', error: 'Submission failed' }}
network={{
onOffline: (ctx) => {
ctx.setState('disabled');
ctx.setText('No internet — try again later');
},
}}
animation={{
enter: () => config.animationPresets.slideUp,
}}
>
Submit
</Button>What Happens When MUST_BE_DEFINED Survives
If you forget to fill a required slot, the component will not render. Instead, it throws with an error that tells you exactly what is missing, which slot it is, and where to fill it:
Kevlar: Button "Add Todo" cannot render.
states.loading.announcement = STRING_MUST_BE_DEFINED
— What should the screen reader announce when this button is loading?
— Pass `announce={{ loading: 'Adding todo...' }}` to this Button instance.
network.onOffline = FUNCTION_MUST_BE_DEFINED
— What happens when the user clicks this button with no internet?
— Define `network.onOffline` in kevlar/base/BaseInteractive.tsx for all buttons,
or in kevlar/components/Button.tsx for all buttons,
or pass `network={{ onOffline: (ctx) => { ... } }}` to this instance.This is intentional. Kevlar will not let you ship a component where you have not decided what happens when the user is offline, or what the screen reader says during loading, or what happens on a TV. Every blank is a question. Every question needs an answer.