TSX in a design
The TSX a design can hold, and what Knighn changes when it cannot.
Knighn reads the TSX of a design. It does not run it. It turns the source into a tree of layers that people and agents can edit. For that, the source must stay in a subset of TSX. The subset has what a design needs: elements, classes, text, components and props. It has no logic.
When the source goes outside the subset, Knighn gives a diagnostic with a code, a message, a line, a column and often a fix. A component with refused code is locked: Knighn keeps its source byte for byte and the file still renders it, but the editor and the agent helpers cannot change it. Fix each error.
Top level
The source can have only these statements:
import type { ReactNode } from 'react'. Knighn refuses other imports.- Props types, as
typeorinterface. - Variant maps, such as
const cardTone = { plain: '…', accent: '…' }. - Components:
export function Name(…) { return (…) }. - One
export const canvas = { pages: […] }. - Comments.
Knighn keeps any other statement as raw text and gives a warning. The editor cannot change a raw statement.
This source uses each part:
import type { ReactNode } from 'react'
type CardProps = {
title?: string
tone?: 'plain' | 'accent'
icon?: ReactNode
children?: ReactNode
className?: string
}
const cardTone = {
plain: 'bg-surface text-foreground ring-1 ring-border',
accent: 'bg-accent text-accent-foreground'
}
// A card with an optional icon and any content.
export function Card({
title = 'Title',
tone = 'plain',
icon,
children,
className = ''
}: CardProps) {
return (
<section className={`flex flex-col gap-3 rounded-xl p-6 ${cardTone[tone]} ${className}`}>
{icon}
<h3 className="text-lg font-semibold">{title}</h3>
{children}
</section>
)
}
export const canvas = {
pages: [{ name: 'Components', kind: 'components', items: [{ render: Card, x: 0, y: 0 }] }]
}
Knighn keeps a comment with the statement or the layer that comes after it.
Components
- Export each component:
export function Pricing(…). The name is PascalCase. - The parameter destructures the props. Give each optional prop a literal default:
{ label = 'Save', tone = 'primary', className = '' }. - Put the props type after the parameter:
: ButtonProps, or an inline object type. - The body is only
return (…), with one root element, one instance or one fragment. - A component with no other props still takes
className.
The className prop
Each component takes className?: string with the default ''. It puts ${className} last in the class list of its root element. An instance uses it to add layout classes, such as <Button className="w-full" />. Knighn adds the prop when it makes a component.
className adds classes. It cannot reliably replace a class that the component sets, because the order of the CSS rules decides, not the order of the classes. To change a class that the component sets, add a variant prop. See components.md.
Props types
A prop has one of these types:
| Type | Example | Use |
|---|---|---|
string |
label?: string |
Text, links and image URLs |
number |
count?: number |
Numbers that you show as text |
boolean |
selected?: boolean |
Show or hide a part, or switch classes |
| Union of string literals | tone?: 'primary' | 'secondary' |
A variant |
ReactNode |
icon?: ReactNode |
A slot for one instance |
ReactNode named children |
children?: ReactNode |
The content between the tags of an instance |
Knighn refuses other types: arrays, objects, functions, unions of numbers, any and generic types.
A default is a literal: a string, a number, true or false. A ReactNode prop has no default, or one instance as its default: icon = <IconStar />.
Variant maps
A variant map gives the classes for each value of a variant prop.
- Write it at the top level:
const buttonTone = { primary: 'bg-accent text-accent-foreground', secondary: 'bg-surface text-foreground' }. - Give it one key for each value of the union. Each value is a string literal of classes.
- Use it in a class list as
${buttonTone[tone]}. - Name it after the component and the prop:
buttonTone,buttonSize.
JSX
Tags
- Use lowercase HTML and SVG tags:
section,h1,img,svg,path. - Use components of the same file:
<Button label="Save" />. - Knighn refuses the tags
script,iframe,object,embed,style,link,metaandbase.
className
className is a string of classes:
<div className="flex items-center gap-3 rounded-lg bg-surface p-4" />
In a component, className can also be a template literal with parts that come from props:
${map[prop]}: the classes of a variant map, such as${buttonTone[tone]}.${prop ? 'classes' : 'classes'}: classes for a boolean prop. Both branches are string literals. One branch can be''. The test can also be!prop,prop === 'value'orprop !== 'value'.${className}: theclassNameprop. Put it last, on the root element.
One part alone needs no template literal: className={buttonTone[tone]}, className={active ? 'font-medium' : ''} and className={className} are correct.
A nested ternary on one union prop, such as className={size === 'sm' ? 'h-8' : size === 'lg' ? 'h-12' : 'h-10'}, becomes a variant map. Knighn adds the map, uses ${map[size]} in its place and gives a note (subset/rewrite). Each value of the union that no test names gets the classes of the last branch.
type TabProps = { label?: string; active?: boolean; className?: string }
export function Tab({ label = 'Overview', active = false, className = '' }: TabProps) {
return (
<a
href="#"
className={`h-9 rounded-md px-3 text-sm ${active ? 'bg-muted font-medium text-foreground' : 'text-muted-foreground'} ${className}`}
>
{label}
</a>
)
}
Rules for class lists:
- Put spaces between classes and parts.
cn(...),clsx(...),cx(...)andtwMerge(...)join string literals, variant lookups and prop conditions. Knighn writes them as a template literal. Knighn refuses other calls,+, nested template literals and lookups with a literal key.- The order of the classes is not important. Knighn sorts them in the Tailwind order when it saves.
Attributes
- Literals:
type="email",rows={4},disabled. - A prop:
href={link},src={image},alt={caption}. - Use the JSX spelling:
className,htmlFor,strokeWidth,aria-label,data-name. - Use
defaultValueanddefaultCheckedto show values in form controls. style={{ … }}with literal values stays in the file, but the editor shows it as read only. Use classes.- Knighn refuses
key,ref, event handlers such asonClick, spreads such as{...props}anddangerouslySetInnerHTML. Give{autofix: true}toDefine,AddScreen,AddScreens,InsertorReplace, and Knighn removeskey,ref, event handlers and hooks, with a note for each change.
Children
A child is one of these:
- Text.
{prop}for a string or number prop,{children}, or{slot}for aReactNodeprop.{flag && <X />}. The condition isflag,!flag,tone === 'danger'ortone !== 'danger', or these joined with&&,||and!(…), such as{(open || pinned) && <X />}.- A template literal of text and props:
{`Build ${build}`}is the same asBuild {build}. Knighn writes it asBuild {build}. .map()over a literal array, written in place or as aconstof literals in the file or in the component:{plans.map((plan) => <Plan name={plan.name} />)}. Knighn writes each item out as JSX, with the values of the item in place ofplan.name, and removeskey. It gives a note (subset/rewrite). After that, each item is a layer that you can edit.- A comment:
{/* Pricing */}. - A fragment:
<>…</>.
A condition can hold one element, one instance or one fragment. Knighn reads a ternary as two conditions. {dark ? <A /> : <B />} is {dark && <A />} and {!dark && <B />}. A ternary in a prop or attribute value, with literals or props in its branches, makes two copies of the element: <Btn tone={dark ? 'white' : 'brand'} /> is {dark && <Btn tone="white" />} and {!dark && <Btn tone="brand" />}. null or undefined in a branch leaves the prop out of that copy. The condition of a ternary has the same forms as the condition of &&. Knighn writes the file with the two conditions.
type StatusProps = { online?: boolean; className?: string }
export function Status({ online = true, className = '' }: StatusProps) {
return (
<span className={`inline-flex items-center gap-2 text-sm ${className}`}>
{online && <span className="size-2 rounded-full bg-emerald-500" />}
{!online && <span className="size-2 rounded-full bg-muted-foreground" />}
{online && <>Online</>}
{!online && <>Offline</>}
</span>
)
}
Text
- Write
{,},<and>in text as{,},<and>. - JSX removes white space that has a line break between tags. Keep inline text on one line when the spaces are important:
<p>Read the <a href="#">guide</a> first.</p>. - Use
<br />for a hard line break.
Layer names
Give sections and important layers a name with data-name:
<section data-name="Pricing" className="flex flex-col gap-8 px-6 py-24" />
Knighn shows the name in the layers panel and in search. Agents find the layer with Find(page, '[data-name="Pricing"]'). Knighn writes data-name only for layers that have a name. The tag and the classes identify the other layers.
The canvas
export const canvas puts components on pages. It has only literals and component names.
export function Home({
headline = 'Ship it',
className = ''
}: {
headline?: string
className?: string
}) {
return (
<main
className={`flex size-full flex-col items-center justify-center bg-background text-foreground ${className}`}
>
<h1 className="text-5xl font-semibold">{headline}</h1>
</main>
)
}
export const canvas = {
pages: [
{
name: 'Web',
items: [
{ render: Home, x: 0, y: 0, width: 1440, height: 900 },
{
render: Home,
props: { headline: 'Ship it faster' },
x: 1540,
y: 0,
width: 390,
height: 844
},
{ render: Home, x: 2030, y: 0, width: 390, height: 844, modes: { theme: 'dark' } }
]
},
{ name: 'Components', kind: 'components', items: [] }
]
}
- A page has a
nameanditems. An older design can have a page withkind: 'components'for its mains. New mains go on the canvas page. - An item is a frame.
renderis the component.xandyare canvas pixels. widthsets the width of the frame. The frame is then the container for container variants such as@3xl:.heightsets the height of the frame. Without it, the frame hugs its content.propsgives literal props to the component.modesputs a mode on the frame, such as{ theme: 'dark' }. See tokens.md.
Refused code
Knighn refuses these, with an error:
| Refused | Fix |
|---|---|
Hooks and state: useState, useEffect |
Make a variant prop for each state, or a frame for each state. |
Event handlers: onClick, onChange |
Delete them. Use href for links. |
.map() over data that is not a literal array, for, while, .filter() |
Write each item as JSX, or one instance for each item. |
Calls: format(), price.toFixed(2) |
Write the result as a literal. |
Spreads: {...props}, ...rest |
Write each prop. |
Ternaries with a condition outside the subset: {count > 2 ? <A /> : <B />} |
Make a boolean or union prop, then write the ternary with it. |
key, ref, dangerouslySetInnerHTML |
Delete them. |
| Value imports | Define the component in the file. Use <Icon name="…" /> for icons. |
The tags script, iframe, object, embed, style, link, meta, base |
Delete them. Put theme CSS in tokens. |
With {autofix: true}, Knighn removes key, ref, event handlers and hooks in place of an error. A useState value with a literal default becomes a prop with that default.
The subset applies to the TSX only. An execute snippet is JavaScript, so it can use .map() and loops to write the TSX that it passes to Insert. See execute.md.
Diagnostics
Each diagnostic has these fields:
severity:error,warningorinfo.code: what is wrong, such assubset/map.message: what is wrong and how to fix it.lineandcolumn: where it starts, from 1. For the source, they count in the.htmlfile. For a fragment that you gave toInsertorReplace, they count in the fragment.fix: a replacement, when Knighn has one.
Errors stop a fragment. Insert, Replace, AddScreen and Define throw on an error, and the call changes nothing. Warnings do not stop anything, but fix them too.
syntax
The source is not valid TSX. Look at the line and the column.
- Close each tag:
<p>…</p>, or<img … />. - Write
{and}in text as{and}. - Close each
(,{and template literal.
export function Note({ className = '' }: { className?: string }) {
return <p className={`text-sm ${className}`}>Saved
}
subset/import
The source imports a value. Only import type { … } from 'react' is allowed.
- Define other components in the file.
- For icons, use
InsertIcon, or define an icon component with an inline<svg>. See assets.md.
import { ArrowRight } from 'lucide-react'
export function Next({ className = '' }: { className?: string }) {
return (
<a href="#" className={`inline-flex items-center gap-2 ${className}`}>
Next <ArrowRight />
</a>
)
}
export function Next({ className = '' }: { className?: string }) {
return (
<a href="#" className={`inline-flex items-center gap-2 ${className}`}>
Next
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="size-4"
>
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
</a>
)
}
subset/statement
A top-level statement is outside the subset: let, var, a function that is not exported, a class, export default, a call, or a const that is not a variant map. This is a warning. Knighn keeps the statement as raw text, and the editor cannot change it.
- Make data into literals in the JSX.
- Make a helper function into a component.
- Make a lookup table into a variant map.
subset/hook
A component calls a hook, such as useState. A design shows one state at a time.
- Make a variant prop for each state:
state?: 'idle' | 'loading' | 'done'. - Or make one frame for each state, with
propson each canvas item. - Or give
{autofix: true}. Knighn makesconst [count] = useState(0)the propcount = 0and removes the other hooks.
export function Counter({ className = '' }: { className?: string }) {
const [count] = useState(0)
return <p className={`text-2xl ${className}`}>{count}</p>
}
type CounterProps = { count?: number; className?: string }
export function Counter({ count = 0, className = '' }: CounterProps) {
return <p className={`text-2xl ${className}`}>{count}</p>
}
subset/handler
An element has an event handler, such as onClick. A design does not run code.
- Delete the handler, or give
{autofix: true}and Knighn deletes it. - Use
hreffor links. - Show hover, open and selected states as variant props.
export function Save({ className = '' }: { className?: string }) {
return (
<button type="button" onClick={() => {}} className={`h-10 rounded-lg px-4 ${className}`}>
Save
</button>
)
}
subset/map
The source uses a loop, an array method other than .map(), or .map() over data that is not a literal array. Knighn writes out .map() over a literal array (see Children), but it cannot edit items that other code makes.
- Write each item as JSX, or make the array a literal.
- For repeated items with the same shape, make a component and write one instance for each item.
- In an
executesnippet, build the TSX string with.map()in JavaScript, then pass the string.
export function Plans({ className = '' }: { className?: string }) {
return (
<ul className={`flex flex-col gap-2 ${className}`}>
{['Starter', 'Pro', 'Team'].filter(Boolean).map((plan) => (
<li>{plan}</li>
))}
</ul>
)
}
export function Plans({ className = '' }: { className?: string }) {
return (
<ul className={`flex flex-col gap-2 ${className}`}>
<li>Starter</li>
<li>Pro</li>
<li>Team</li>
</ul>
)
}
subset/expression
A JSX value is an expression outside the subset: a call, arithmetic, a ternary whose branches are not literals, props or JSX, a template literal with a part that is not a prop, props.label, key, ref or dangerouslySetInnerHTML.
- Write the result as a literal:
<p>$288</p>, not<p>{price * 12}</p>. - Destructure the prop:
{label}, not{props.label}. - Give a ternary literals, props or JSX in its branches.
- Delete
keyandref.
subset/spread
The source uses a spread: {...props} in JSX, or ...rest in the props parameter.
- Write each prop and each attribute.
type FieldProps = { className?: string; placeholder?: string }
export function Field({ className = '', ...rest }: FieldProps) {
return <input {...rest} className={`h-10 rounded-md px-3 ${className}`} />
}
type FieldProps = { placeholder?: string; className?: string }
export function Field({ placeholder = 'you@example.com', className = '' }: FieldProps) {
return (
<input type="email" placeholder={placeholder} className={`h-10 rounded-md px-3 ${className}`} />
)
}
subset/class-expression
Knighn cannot read a className expression: a call other than cn(), clsx(), cx() or twMerge(), strings joined with +, a part glued to other text, or a nested ternary that is not on one union prop.
- Use a template literal with variant lookups and prop conditions, or
cn(). - Do not join strings with
+. Write whole class names. - For a nested ternary, use a union prop and test it with
===in each branch, or write a variant map.
type TagProps = { active?: boolean; className?: string }
export function Tag({ active = false, className = '' }: TagProps) {
return <span className={'rounded-full px-2 text-xs ' + className}>New</span>
}
type TagProps = { active?: boolean; className?: string }
export function Tag({ active = false, className = '' }: TagProps) {
return (
<span
className={`rounded-full px-2 text-xs ${active ? 'bg-accent text-accent-foreground' : ''} ${className}`}
>
New
</span>
)
}
subset/condition
A condition in {… && …} is not a prop test. The allowed tests are flag, !flag, prop === 'value' and prop !== 'value'. Join them with &&, || and !(…).
- Add a boolean prop for the condition.
- Or compare a variant prop with one of its values.
type CartProps = { count?: number; className?: string }
export function Cart({ count = 0, className = '' }: CartProps) {
return (
<div className={`flex items-center gap-2 ${className}`}>
Cart
{count > 0 && (
<span className="rounded-full bg-accent px-1.5 text-xs text-accent-foreground">
{count}
</span>
)}
</div>
)
}
type CartProps = { count?: number; showCount?: boolean; className?: string }
export function Cart({ count = 3, showCount = true, className = '' }: CartProps) {
return (
<div className={`flex items-center gap-2 ${className}`}>
Cart
{showCount && (
<span className="rounded-full bg-accent px-1.5 text-xs text-accent-foreground">
{count}
</span>
)}
</div>
)
}
subset/component
A component or a tag is outside the subset. Causes:
- The body has statements before
return. - The function is not exported, or its name is not PascalCase.
- The parameter is not a destructured object, for example
(props). - An instance names a component that the file does not define.
- The tag is one of
script,iframe,object,embed,style,link,metaorbase.
Move values into props with defaults, export the function, define the component in the file, and delete refused tags.
subset/prop-type
A prop has a type outside the subset: an array, an object, a function, a union of numbers, any or a generic type.
- Split an object into one prop for each field.
- Make a list into separate instances, one for each item.
- Make a union of numbers into a union of strings, such as
'1' | '2' | '3'.
type MenuProps = { items?: string[]; className?: string }
export function Menu({ className = '' }: MenuProps) {
return <nav className={`flex gap-4 ${className}`} />
}
subset/default
A default is not a literal.
- Use a string, a number,
trueorfalse. - Give a
ReactNodeprop no default, or one instance.
type PriceProps = { amount?: number; className?: string }
export function Price({ amount = 20 * 12, className = '' }: PriceProps) {
return <p className={`text-2xl font-semibold ${className}`}>{amount}</p>
}
subset/rewrite
A note, not an error. Knighn changed the source to a form of the subset, and the call worked. The result notes of execute show each one. Causes:
- Knighn wrote out the items of a
.map()over a literal array. - Knighn made a variant map from a nested ternary in
className. {autofix: true}removedkey,ref, an event handler or a hook, or made auseStatevalue a prop.
Read the note. Then read the design again with Tree or Get, because the source that Knighn keeps is the changed source.
canvas/missing
The source has no export const canvas. Knighn needs it to show frames.
- Add
export const canvas = { pages: [{ name: 'Web', items: [] }] }, then place frames on the page.
canvas/item
An item of the canvas is wrong. Causes:
rendernames no exported component.xoryis missing, or is not a number.propshas a prop that the component does not have, or a value that is not a literal.modesnames a mode that no mode block defines.
Name an exported component, use number literals, use props from the props type, and add the mode block with SetTokens.
lint/viewport-variant
A class uses a viewport variant, such as md:. All frames share one document, so a viewport variant follows the editor window, not the frame. Use the container variant:
| Viewport variant | Container variant | Starts at |
|---|---|---|
sm: |
@min-[40rem]: |
640 px |
md: |
@3xl: |
768 px |
lg: |
@5xl: |
1024 px |
xl: |
@7xl: |
1280 px |
2xl: |
@min-[96rem]: |
1536 px |
max-md: |
@max-3xl: |
below 768 px |
max-lg: |
@max-5xl: |
below 1024 px |
export function Hero({ className = '' }: { className?: string }) {
return <section className={`px-6 py-16 md:px-16 lg:py-24 ${className}`} />
}
export function Hero({ className = '' }: { className?: string }) {
return <section className={`px-6 py-16 @3xl:px-16 @5xl:py-24 ${className}`} />
}
lint/screen-unit
A class uses the viewport size: h-screen, min-h-screen, w-screen, h-dvh, or vh, vw, dvh or svh in an arbitrary value. In the editor, the viewport is the editor window.
- Fill the frame with
size-fullormin-h-fullon the root. - Give the frame a height on the canvas, such as 900 for a desktop app screen.
- Use a fixed size:
h-160is 640 px. - Use
cqwfor a width that follows the frame:w-[50cqw].
export function Hero({ className = '' }: { className?: string }) {
return <section className={`flex min-h-screen items-center ${className}`} />
}
lint/fixed
A class uses fixed. A fixed element sticks to the editor window, not to the frame.
- Use
absolutein a root withrelative, for an overlay or a floating button. - For a header or a tab bar, keep it in the flow at the top or the bottom of the screen.
lint/img-alt
An img has no alt.
- Describe what the image shows:
alt="Two people at a desk with a laptop". - Use
alt=""for an image that is only decoration.
lint/status-bar
A row looks like a phone status bar: it starts with a time such as 9:41 and has only shapes after it, such as signal, Wi-Fi or battery icons. The device draws the status bar, so a design must not draw one.
- Remove the row.
- Do not keep an empty band for it: start the content at the top of the screen.
export function StatusBar({ className = '' }: { className?: string }) {
return (
<div className={`flex h-[59px] items-center justify-between px-8 ${className}`}>
<span>9:41</span>
<span className="h-3 w-6 rounded-sm bg-foreground" />
</div>
)
}
lint/top-band
The root of a screen starts with an empty band of 40 to 64 px: pt-[59px] on the root, or an empty spacer such as h-[54px] as its first child. It is space for a status bar, and it shows as blank padding over the design.
- Remove the padding or the spacer.
- Start the content at the top: the navigation bar or the large title first, with
pt-3orpt-4.
export function Home({ className = '' }: { className?: string }) {
return (
<div className={`flex size-full flex-col pt-[59px] ${className}`}>
<h1 className="px-4 text-[34px] font-bold">Home</h1>
</div>
)
}
lint/unknown-class
Tailwind does not know the class, or one of its variants. The class stays in the file, but it does nothing on the canvas. The severity is info, because the class can be for your own CSS.
- Correct the spelling. The message often gives the class that you meant, and
fixhas it. - Use the Tailwind v4 name:
bg-linear-to-r, notbg-gradient-to-r;shrink-0, notflex-shrink-0;grow, notflex-grow;bg-black/50, notbg-opacity-50;text-ellipsis, notoverflow-ellipsis. - Use an arbitrary value for a value that has no class:
tracking-[-0.03em].
lint/unknown-token
A class names a token that the theme does not define, such as bg-primary when there is no --color-primary. A var(--name) in an arbitrary value that the theme does not define gives the same warning.
- Use a token that the theme defines.
GetTokens()lists them. - Or add the token with
SetTokens({ '--color-primary': '#2563eb' }).
lint/theme
The theme block does not compile, so Knighn checks the classes against the default Tailwind theme. This is an error.
- Write each token as
--name: value;in the@themeblock. - Close each
{of the theme block and of each mode block. - Read the message: it gives the error of the Tailwind compiler.