Skip to content
MocklyDocs

Build

Components and styling

Split markup into components, pass props, and style with utility classes.

Components are functions that return markup. Split a page into components, keep the ones you share in components/, and import them where you need them. A component renders to HTML when you publish, and sends no JavaScript to visitors. A component with state or event handlers also becomes an island, a small script that runs in the browser; see Interactive parts.

Defining a component#

components/hero.tsx
export function Hero({ title, cta = "Start free" }: { title: string; cta?: string }) {  return (    <section class="px-6 py-24 text-center">      <h1 class="text-5xl font-bold tracking-tight">{title}</h1>      <a href="/" class="mt-8 inline-block rounded-full bg-gray-900 px-6 py-3 font-semibold text-white">        {cta}      </a>    </section>  );}
  • A function declaration with a PascalCase name. A lowercase name isn't a component (MK5001), and a component can't be an arrow function (MK1010). Names are unique within a file (MK5004).
  • One parameter at most: props, or an object pattern with optional defaults, like cta above (MK5002).
  • Types are optional. When you write them, the type checker uses them: a missing prop, or a value of the wrong type, is a TypeScript error, with a TS code, where the component is used.
  • Exports are named, like export function Hero. A file can also have one default export, which a page imports as import Logo from "../components/logo".
  • No recursion. A component can't render itself, directly or through other components (MK5003).

Using components#

A page imports components with a relative path. The .tsx extension is optional.

pages/index.tsx
import { Page } from "mockly";import { Hero } from "../components/hero"; export default function Home() {  return (    <Page title="Acme">      <Hero title="Launch in an afternoon" />      <Hero title="Ship every week" cta="See pricing" />    </Page>  );}
  • Imports come from "mockly", for built-ins such as Page, or from other files of the site, by relative path. Anything else, such as an npm package, is MK1003.
  • Component files live in components/, in nested folders if you like. Their names use letters, digits, dashes, and underscores. They have no route.
  • Named, default, and namespace imports all work, and so do re-exports such as export { Hero } from "./hero". A namespace import lets you write a component as a member:
TSX
import * as icons from "../components/icons"; <icons.Bolt class="size-5 text-indigo-600" />

An import cycle is MK5005, a file that doesn't exist is MK5006, importing a page is MK5007, and a name the file doesn't export is MK5008.

Props#

Pass strings as attributes and other values in braces:

TSX
<PlanCard plan="team" seats={5} yearly={true} features={["SSO", "Audit log"]} />

Children and markup props#

Markup between a component's tags arrives as children. Any other prop can take markup too:

components/card.tsx
export function Card({ title, icon, children }: { title: string; icon?: unknown; children?: unknown }) {  return (    <div class="rounded-2xl border border-gray-200 p-6">      {icon}      <h3 class="mt-4 font-semibold">{title}</h3>      <p class="mt-2 text-sm text-gray-600">{children}</p>    </div>  );}
TSX
<Card title="No flicker" icon={<icons.Bolt />}>  Every variant is HTML before anyone visits.</Card>

What a component contains#

A component's body holds const declarations, then one return with its markup:

components/price.tsx
export function Price({ amount, yearly }: { amount: number; yearly: boolean }) {  const monthly = yearly ? Math.round((amount * 10) / 12) : amount;  const price = `$${monthly}`;  return (    <p>      <span class="text-4xl font-bold">{price}</span> per month    </p>  );}

Nothing else goes in the body. if, loops, and other statements are MK1001, let and var are MK1005, and a component without a final return is MK1006.

Markup goes in the return, in children, and in props. A const can't hold markup, and markup can't appear inside an expression such as {open && <p>Open</p>} or {items.map((item) => <li>{item}</li>)} (both MK1010). For conditions and lists, use <Show> and <For>:

TSX
<Show when={plan.popular} fallback={<span>Standard</span>}>  <span class="font-semibold text-indigo-600">Most popular</span></Show><ul>  <For each={plan.features}>{(feature) => <li>{feature}</li>}</For></ul>

Both are built-ins you import from "mockly". Inside an island they follow state in the browser; see Interactive parts.

Top-level constants#

Data lives in const declarations at the top of a file, outside any component. A constant can hold literals, lists, objects, template literals, other constants, and any allowed expression, but not markup. Constants are worked out when you publish. Export one to use it in other files:

components/plans.tsx
export const plans = [  { id: "hobby", name: "Hobby", monthly: 0 },  { id: "team", name: "Team", monthly: 10 },  { id: "scale", name: "Scale", monthly: 49 },]; export const paidPlans = plans.filter((p) => p.monthly > 0);

The top level of a file holds only imports, const declarations, components, and type and interface declarations. Anything else is MK1002. For data from an API, see Data from APIs.

Markup#

Markup is JSX, with HTML's names for elements and attributes.

Elements#

  • A lowercase tag is an HTML or SVG element. A tag that isn't one is MK2002.
  • A capitalized tag is a component: a built-in from "mockly", or a component the file defines or imports.
  • Not allowed (MK2001): script, style, link, meta, base, title, head, html, body, object, embed, frame, frameset, template, slot, and any tag that starts with mk-. <Page> writes the document and its head, and styles come from utility classes.
  • Fragments (<>…</>) and JSX comments ({/* … */}) are fine. Elements that can't have content close themselves, such as <img /> and <br />.

Attributes#

  • HTML names: class, for, tabindex, aria-*, and data-*. React's names, such as className and htmlFor, are MK2004, with a quick fix that renames them.
  • Values are a string (class="card"), an expression (class={tone}), or a bare name for true (disabled).
  • style takes a string: style="color: red". An object is MK1010.
  • Spread attributes ({...props}) are MK2005. Pass each one.
  • Not allowed (MK2003): dangerouslySetInnerHTML, innerHTML, srcdoc, key, attributes that start with data-mk- (Mockly's own), and event handlers written as strings, such as onclick="…".
  • URLs in href, src, and other URL attributes can be relative or use http, https, mailto, or tel. A string with another scheme, such as javascript:, is MK2007, and a computed value with one renders as about:blank#blocked.

Event handlers such as onClick are for islands; see Interactive parts. The track attribute records a goal when an element is clicked; see Goals.

Whitespace follows JSX's rules: each line of text is trimmed, and line breaks between elements are dropped. Write {" "} where you need a space. HTML entities in text and string attributes, such as &amp;, are decoded, then escaped again when the page renders.

Expressions#

Expressions go in braces in markup, and in const declarations. You can use:

  • Literals and names: strings, numbers, true, false, null, undefined, untagged template literals, and anything in scope: props, constants, <For> parameters, and state.
  • Member access: a.b, a?.b, a[0], and a[key].
  • Operators: arithmetic, comparisons with === and !==, &&, ||, ??, !, ? :, and typeof.
  • Lists and objects, with spread: [...items, item], { ...base, b: 2 }.
  • Type syntax, which is removed: x as T, x!, and x satisfies T.
  • A few functions: String, Number, Boolean, encodeURIComponent, Math.round and its relatives (floor, ceil, min, max, abs, pow, sqrt, trunc, sign), Object.keys, Object.values, Object.entries, Object.fromEntries, Array.isArray, Number.isInteger and the like, Date.now, Date.UTC, and new Date(…).
  • List methods that take a callback: map, filter, find, findIndex, findLast, findLastIndex, some, every, reduce, flatMap, and toSorted, plus replace and replaceAll on strings.
  • Other methods that return a new value: slice, includes, indexOf, at, concat, and join, string methods such as trim, split, padStart, and toUpperCase, list methods such as flat, toReversed, and with, and number and date methods such as toFixed, toLocaleString, toLocaleDateString, and getFullYear.

A callback is an arrow function whose body is one expression, such as plans.filter((p) => p.monthly > 0). Its parameters are names, or an object or list pattern one level deep without defaults: ([key, value]) => ….

These aren't allowed:

  • Other calls (MK1008), including methods that change a list in place: sort, reverse, splice, push, pop, shift, unshift, fill, and copyWithin. The hint names the method that returns a copy instead, such as toSorted.
  • == and != (MK1011). Use === and !==; the quick fix swaps them.
  • Assignment, ++, and -- outside an event handler (MK1012).
  • Globals other than those listed above, such as window, document, fetch, localStorage, or setTimeout (MK1007).
  • Other syntax (MK1010): new with anything but Date, this, await, classes, regular expressions, tagged templates, and functions other than handlers, <For> children, and callbacks.
  • __proto__, prototype, and constructor, in any form (MK1009).

These limits are what make pages safe to build and run: compiled code can only call what this list allows.

new Date() and Date.now() give the time the page renders: when you publish, for everything outside islands. Date methods such as toLocaleDateString use the publishing computer's time zone unless you pass one, so pass timeZone for dates written like "2026-10-14":

TSX
{new Date(post.date).toLocaleDateString("en-US", { month: "long", day: "numeric", timeZone: "UTC" })}

How values render#

Text, such as {value}:

  • Strings are escaped, so <b> in a string shows as text.
  • Numbers render as String() writes them.
  • null, undefined, true, and false render nothing.
  • Markup passed in a prop, such as children, renders as markup.
  • Anything else, such as an object or a list, renders nothing.

Attributes, such as title={value}:

  • null, undefined, and false leave the attribute out.
  • true writes the attribute with an empty value.
  • Anything else is written as text, escaped.
  • aria-* and data-* attributes are the exception: true and false write "true" and "false".

URL attributes always pass the scheme check above, and root-relative URLs, such as /pricing, get the site's base path when the site is served under one.

Styling with utility classes#

Style elements with utility classes in class. Mockly supports the class names of Tailwind CSS version 3 and generates them with UnoCSS when you publish. The site's stylesheet holds Tailwind's reset and a rule for each class the site uses, so it stays small.

components/badge.tsx
const TONES: Record<string, string> = {  new: "bg-green-50 text-green-700 ring-green-600/20",  fixed: "bg-blue-50 text-blue-700 ring-blue-600/20",}; export function Badge({ kind, label }: { kind: string; label: string }) {  return (    <span class={`rounded-full px-2 py-0.5 text-xs font-semibold ring-1 ${TONES[kind] ?? ""}`}>      {label}    </span>  );}
  • Variants and modifiers work as in Tailwind: hover:, focus:, disabled:, first:, last:, group-hover:, motion-reduce:, breakpoints such as sm:, md:, and lg:, and opacity such as bg-indigo-500/40.
  • Arbitrary values work too, such as w-[40rem] or top-[3px], and so do arbitrary properties, such as [text-wrap:balance].
  • Classes are found in the text of every file. A class name counts wherever it's written: in class, in a constant, or in a ternary. Write each name in full. A name pieced together, such as `bg-${color}-600`, isn't found, so it gets no rule. Use a lookup like TONES above instead.
  • No <style> elements (MK2001). For a one-off value, use an arbitrary value or a style attribute.
  • Images stay within their container, because the reset gives them max-width: 100% and height: auto.

Fonts you declare on <Page> add a class for each family, such as font-inter; see Images, fonts, and files.