Components
Components, props and variants in a design.
A component is an exported function in the source. A screen is a component, and so is each part that repeats: buttons, cards, list rows, nav items. An instance is a use of a component, such as <Button label="Save" />. When you change a component, each instance of it changes.
Make a component
- Give it a PascalCase name and export it.
- Write a props type. Give each optional prop a literal default.
- Take
classNameand put${className}last on the root element. - Return one root element.
type ButtonProps = {
label?: string
tone?: 'primary' | 'secondary' | 'ghost'
size?: 'sm' | 'md' | 'lg'
className?: string
}
const buttonTone = {
primary: 'bg-accent text-accent-foreground',
secondary: 'bg-surface text-foreground ring-1 ring-border',
ghost: 'text-foreground'
}
const buttonSize = {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4 text-sm',
lg: 'h-12 px-6 text-base'
}
export function Button({
label = 'Continue',
tone = 'primary',
size = 'md',
className = ''
}: ButtonProps) {
return (
<button
type="button"
className={`inline-flex items-center justify-center gap-2 rounded-lg font-medium ${buttonTone[tone]} ${buttonSize[size]} ${className}`}
>
{label}
</button>
)
}
export const canvas = {
pages: [
{
name: 'Button states',
items: [
{ render: Button, x: 0, y: 0 },
{ render: Button, props: { tone: 'secondary' }, x: 160, y: 0 },
{ render: Button, props: { tone: 'ghost', size: 'sm' }, x: 320, y: 0 }
]
},
{ name: 'Components', kind: 'components', items: [{ render: Button, x: 0, y: 0 }] }
]
}
Choose the props
| The instance changes | Use | Example |
|---|---|---|
| A text | A string prop | title?: string, then {title} |
| A number that you show | A number prop | count?: number, then {count} |
| A link or an image | A string prop in an attribute | href={link}, src={image} |
| A part that shows or hides | A boolean prop | {badge && <span>…</span>} |
| The look | A variant | tone?: 'primary' | 'secondary' |
| Content from outside | children or a slot |
{children}, {icon} |
Keep the number of props small. A prop is a promise that the instances can change this part.
Variants
A variant is a prop with a union of string literals. Its variant map gives the classes for each value.
- Write one map for each variant prop, with one key for each value.
- Put the map in the class list:
${buttonTone[tone]}. - A variant can also show or hide parts:
{tone === 'danger' && <IconAlert />}. - Show states as a variant, such as
state?: 'default' | 'hover' | 'disabled'. The canvas shows only the base state ofhover:classes.
type AlertProps = {
title?: string
body?: string
tone?: 'info' | 'danger'
className?: string
}
const alertTone = {
info: 'bg-muted text-foreground',
danger: 'bg-red-500/10 text-red-700'
}
export function Alert({
title = 'Heads up',
body = 'Your trial ends in 3 days.',
tone = 'info',
className = ''
}: AlertProps) {
return (
<div role="status" className={`flex gap-3 rounded-lg p-4 ${alertTone[tone]} ${className}`}>
{tone === 'danger' && (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="size-5 shrink-0"
>
<circle cx="12" cy="12" r="10" />
<path d="M12 8v4" />
<path d="M12 16h.01" />
</svg>
)}
<div className="flex flex-col gap-1">
<p className="font-medium">{title}</p>
<p className="text-sm opacity-80">{body}</p>
</div>
</div>
)
}
Booleans
A boolean prop shows or hides a part with &&, or switches classes with a ternary of two strings.
type NavItemProps = { label?: string; active?: boolean; badge?: boolean; className?: string }
export function NavItem({
label = 'Inbox',
active = false,
badge = false,
className = ''
}: NavItemProps) {
return (
<a
href="#"
className={`flex h-9 items-center gap-2 rounded-md px-3 text-sm ${active ? 'bg-muted font-medium text-foreground' : 'text-muted-foreground'} ${className}`}
>
{label}
{badge && <span className="ml-auto size-2 rounded-full bg-accent" />}
</a>
)
}
Children and slots
childrenis the content between the tags of an instance:<Card>…</Card>. The component shows it where it puts{children}.- A
ReactNodeprop other thanchildrenis a slot. An instance gives it one instance:action={<IconMore />}. - Make small components for slot content, such as icons.
import type { ReactNode } from 'react'
type CardProps = { title?: string; action?: ReactNode; children?: ReactNode; className?: string }
export function Card({ title = 'Card title', action, children, className = '' }: CardProps) {
return (
<section
className={`flex flex-col gap-4 rounded-xl bg-surface p-6 ring-1 ring-border ${className}`}
>
<header className="flex items-center justify-between">
<h3 className="font-semibold">{title}</h3>
{action}
</header>
{children}
</section>
)
}
export function IconMore({ className = '' }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
className={`size-5 text-muted-foreground ${className}`}
>
<circle cx="5" cy="12" r="2" />
<circle cx="12" cy="12" r="2" />
<circle cx="19" cy="12" r="2" />
</svg>
)
}
export function Overview({ className = '' }: { className?: string }) {
return (
<main
className={`grid size-full grid-cols-2 gap-6 bg-background p-8 text-foreground ${className}`}
>
<Card title="Revenue" action={<IconMore />}>
<p className="text-3xl font-semibold tracking-tight">$48,210</p>
</Card>
<Card title="Team">
<p className="text-muted-foreground">Six people and two open roles.</p>
</Card>
</main>
)
}
Instances change props only
An instance sets props, children and slots. It cannot change the classes or the text of the elements in the component.
- To change the layout of one instance, give it classes with
className:<Card className="col-span-2" />. Use it for width, margin and grid or flex placement. classNamecannot reliably replace a class that the component sets, because the order of the CSS rules decides. For that, add a variant.- To change a part inside one instance, expose it as a prop in the component: a string prop for text, a boolean prop to hide it, or a variant for its look. Then set the prop on the instance.
- To make an instance into plain layers, replace it with the TSX of the component and change the copy. In the app, this is "Detach instance".
Edit a component
Tree('Card')shows the layers of the component with their ids.Updatechanges a layer, and each instance shows the change.Get('Card')returns the whole component source. Change it and give it toDefineto change the props or the structure.- In the app, double-click an instance to edit its component in place. Press Esc to stop.
Print(Tree('Card', { depth: 3 }))
const [title] = Find('Card', 'h3')
Update(title, { classes: 'text-lg' })
Mains on the canvas
Each component shows one time on the canvas, with its default props: its main. The main is beside the screens of the page, in a column to their right.
- Knighn puts the main of each component that you make with
Defineon the page in view. - An older design can have a page with
kind: 'components'. Its mains stay there. - Screens use instances. An edit of the main changes each instance.
- In the app, a click in an instance selects inside it. An edit there changes the main and each instance.
Make a component from layers
When the same group of layers repeats, make it a component, then replace each copy with an instance:
Define(`
type FeatureProps = { title?: string; body?: string; className?: string }
export function Feature({ title = 'Fast', body = 'Pages load in under a second.', className = '' }: FeatureProps) {
return (
<div className={\`flex flex-col gap-2 \${className}\`}>
<h3 className="text-lg font-semibold">{title}</h3>
<p className="text-muted-foreground">{body}</p>
</div>
)
}
`)
const copies = Find(page, '[data-name="Feature"]')
Replace(copies[0], `<Feature title="Fast" body="Pages load in under a second." />`)
Replace(copies[1], `<Feature title="Private" body="Your data stays in your account." />`)
Component kits
UseKit('shadcn') or UseKit('heroui') defines the common components of shadcn/ui or HeroUI: Button, Badge, Card, Input, Switch, Checkbox, Tabs with Tab, Avatar, Separator and Tooltip. They have the variants and the default look of the library, and they use the tokens of the design. Use a kit when the person names the library, or when the code will use it. Then write instances such as <Button variant="secondary" label="Cancel" /> (shadcn) or <Button color="primary" variant="flat" label="Save" /> (HeroUI).
Screens are components
A screen is a component with only a className prop, placed on a page as a frame. A frame can also give props to its component. A deck of slides is one Slide component placed many times with different props. See guide/slides.md.