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#
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, likectaabove (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
TScode, where the component is used. - Exports are named, like
export function Hero. A file can also have one default export, which a page imports asimport 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.
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 asPage, 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:
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:
<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:
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> );}<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:
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>:
<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:
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 withmk-.<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-*, anddata-*. React's names, such asclassNameandhtmlFor, are MK2004, with a quick fix that renames them. - Values are a string (
class="card"), an expression (class={tone}), or a bare name fortrue(disabled). styletakes 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 withdata-mk-(Mockly's own), and event handlers written as strings, such asonclick="…". - URLs in
href,src, and other URL attributes can be relative or usehttp,https,mailto, ortel. A string with another scheme, such asjavascript:, is MK2007, and a computed value with one renders asabout: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 &, 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], anda[key]. - Operators: arithmetic, comparisons with
===and!==,&&,||,??,!,? :, andtypeof. - Lists and objects, with spread:
[...items, item],{ ...base, b: 2 }. - Type syntax, which is removed:
x as T,x!, andx satisfies T. - A few functions:
String,Number,Boolean,encodeURIComponent,Math.roundand its relatives (floor,ceil,min,max,abs,pow,sqrt,trunc,sign),Object.keys,Object.values,Object.entries,Object.fromEntries,Array.isArray,Number.isIntegerand the like,Date.now,Date.UTC, andnew Date(…). - List methods that take a callback:
map,filter,find,findIndex,findLast,findLastIndex,some,every,reduce,flatMap, andtoSorted, plusreplaceandreplaceAllon strings. - Other methods that return a new value:
slice,includes,indexOf,at,concat, andjoin, string methods such astrim,split,padStart, andtoUpperCase, list methods such asflat,toReversed, andwith, and number and date methods such astoFixed,toLocaleString,toLocaleDateString, andgetFullYear.
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, andcopyWithin. The hint names the method that returns a copy instead, such astoSorted. ==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, orsetTimeout(MK1007). - Other syntax (MK1010):
newwith anything butDate,this,await, classes, regular expressions, tagged templates, and functions other than handlers,<For>children, and callbacks. __proto__,prototype, andconstructor, 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":
{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, andfalserender 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, andfalseleave the attribute out.truewrites the attribute with an empty value.- Anything else is written as text, escaped.
aria-*anddata-*attributes are the exception:trueandfalsewrite"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.
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 assm:,md:, andlg:, and opacity such asbg-indigo-500/40. - Arbitrary values work too, such as
w-[40rem]ortop-[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 likeTONESabove instead. - No
<style>elements (MK2001). For a one-off value, use an arbitrary value or astyleattribute. - Images stay within their container, because the reset gives them
max-width: 100%andheight: auto.
Fonts you declare on <Page> add a class for each family, such as font-inter; see Images, fonts, and files.