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 className and 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 of hover: 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

  • children is the content between the tags of an instance: <Card>…</Card>. The component shows it where it puts {children}.
  • A ReactNode prop other than children is 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.
  • className cannot 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. Update changes a layer, and each instance shows the change.
  • Get('Card') returns the whole component source. Change it and give it to Define to 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 Define on 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.