Jay-HTML Component Imports
Jay-HTML Component Imports
Designer Agent Kit — documentation written for AI agents, readable by humans.
Headless Components
Headless components provide data and interactions with no UI. The page or headfull component provides the template.
Pattern 1: Key-Based Import
Data merged into parent ViewState under a key. Use when you have one instance of a component per page.
Declare in <head> with a key attribute:
<head>
<script
type="application/jay-headless"
plugin="wix-stores"
contract="product-page"
key="productPage"
></script>
</head>
Access data and refs with the key prefix:
<h1>{productPage.productName}</h1>
<span>{productPage.price}</span>
<button ref="productPage.addToCartButton">Add to Cart</button>
<!-- Nested repeated sub-contracts -->
<div forEach="productPage.options" trackBy="_id">
<h3>{name}</h3>
<div forEach="choices" trackBy="choiceId">
<button ref="choiceButton">{name}</button>
</div>
</div>
Key-based imports are only available in pages (not in headfull FS components).
Important: Do NOT use <jay:keyName> for key-based imports. The key is for ViewState access ({key.field}), not for inline elements. <jay:> tags use the contract name, not the key.
Pattern 2: Instance-Based (jay: prefix)
Multiple instances with props and inline templates. Use when you need multiple instances or need to pass props.
Declare in <head> without a key:
<head>
<script
type="application/jay-headless"
plugin="product-widget"
contract="product-widget"
></script>
</head>
Use <jay:contract-name> tags with props:
<!-- Static props — each instance renders independently -->
<jay:product-widget productId="prod-1">
<h3>{name}</h3>
<div>${price}</div>
<button ref="addToCart">Add</button>
</jay:product-widget>
<jay:product-widget productId="prod-2">
<h3>{name}</h3>
<button ref="addToCart">Add</button>
</jay:product-widget>
With bindings from page data (props from keyed components or page ViewState):
<!-- p is a keyed headless component providing product data -->
<jay:category-products categorySlug="{p.categorySlug}" limit="4">
<div class="product-card">
<h3>{name}</h3>
<span>{price}</span>
</div>
</jay:category-products>
Use {path} syntax to bind props to values from the page's ViewState. The binding is resolved at render time — works with both slow and fast phase data.
With forEach (dynamic props from parent data):
<div forEach="featuredProducts" trackBy="_id">
<jay:product-widget productId="{_id}">
<h3>{name}</h3>
<div>${price}</div>
<button ref="addToCart">Add</button>
</jay:product-widget>
</div>
Inside <jay:...>, bindings resolve to that instance's contract tags (not the parent).
Choosing between patterns
| Need | Pattern | Key? | Tag? |
|---|---|---|---|
| One component per page, data across the whole template | Key-based | key="product" |
No <jay:> — use {product.field} bindings |
| Multiple instances, each with own props and template | Instance-based | No key | <jay:contract-name prop="..."> |
| One instance but with custom inline template | Instance-based | No key | <jay:contract-name> |
Never combine both: a component imported with key cannot also be used as <jay:>. These are mutually exclusive patterns.
Prop binding summary
| Syntax | Resolves to | Example |
|---|---|---|
prop="literal" |
Literal string value | productId="prod-1" |
prop="{field}" |
Page ViewState field | slug="{p.categorySlug}" |
prop="{field}" (inside forEach) |
ForEach item field | productId="{_id}" |
Prop phase constraints
Contract props can declare a phase (defaults to slow). The binding source must be available at that phase:
- A slow prop (default) must bind to a literal, a route param, or a slow-phase tag
- A fast prop can also bind to fast-phase tags
If a slow prop binds to a fast-phase field, jay-stack validate flags an error — the component's slow render would receive an empty value.
# In the component's contract:
props:
- name: categorySlug
type: string
phase: slow # Must be available at build time
- name: filter
type: string
phase: fast # Only needs to be available at request time
Headfull Components
In Jay Stack, headfull components are full-stack. They must have a .jay-contract file and are created using makeJayStackComponent in their .ts file. They support server rendering (slow/fast/interactive phases) and must include a contract attribute in the import.
Each headfull component lives in its own subdirectory under src/components/ with three files: .ts, .jay-html, and .jay-contract. The production build only discovers server-side component modules from src/components/ and src/plugins/. Placing them inside page directories will work in dev mode but fail in production.
Note: In Jay (without Jay Stack), headfull components use
makeJayComponentand do not require a contract. However,makeJayComponentcomponents should not be used in Jay Stack because they do not support server rendering.
Import Declaration
<head>
<script
type="application/jay-headfull"
src="../components/shared-header/shared-header"
names="SharedHeader"
contract="../components/shared-header/shared-header.jay-contract"
></script>
</head>
Attributes:
src— Path to the component file (must include the filename, not just the directory)names— Component name to importcontract— Path to the component's.jay-contractfile (required in Jay Stack)
Usage
<jay:SharedHeader logoUrl="/logo.png" />
Route params: Headfull and instance-based headless components do not receive route params directly. To pass a route param, expose it through the page's ViewState and bind it as a prop:
<jay:SideNav activePage="{activePage}" />. See routing.md for the full pattern.
Component Structure
Each headfull component needs three files in its subdirectory under src/components/:
.jay-contract — declares props. Tags are optional (use tags: [] or omit for structural components):
# components/site-header/site-header.jay-contract
name: SiteHeader
props:
- name: logoUrl
type: string
required: true
.ts — component code. Must use makeJayStackComponent with .withProps() matching the contract props:
// components/site-header/site-header.ts
import { makeJayStackComponent, phaseOutput } from '@jay-framework/fullstack-component';
import type { SiteHeaderContract, SiteHeaderProps } from './site-header.jay-html';
export const siteHeader = makeJayStackComponent<SiteHeaderContract>()
.withProps<SiteHeaderProps>()
.withFastRender(async (props) => phaseOutput({ logoUrl: props.logoUrl }, {}));
For a structural component with only props and no data logic, .withFastRender passes props through as ViewState.
.jay-html — the template:
<!-- components/site-header/site-header.jay-html -->
<html>
<head>
<script type="application/jay-data" contract="./site-header.jay-contract"></script>
</head>
<body>
<header>
<img src="{logoUrl}" />
<nav>Navigation here</nav>
</header>
</body>
</html>
Nesting Components
Headfull Inside Headfull
A layout component imports a header component:
<!-- layout/layout.jay-html -->
<html>
<head>
<script
type="application/jay-headfull"
src="../header/header"
contract="../header/header.jay-contract"
names="header"
></script>
<script type="application/jay-data">
data:
sidebarLabel: string
</script>
</head>
<body>
<div class="layout">
<jay:header logoUrl="/logo.png" />
<aside>{sidebarLabel}</aside>
</div>
</body>
</html>
Headless Inside Headfull
A header component uses a headless plugin widget:
<!-- header/header.jay-html -->
<html>
<head>
<script type="application/jay-headless" plugin="my-plugin" contract="cart-indicator"></script>
<script type="application/jay-data">
data:
logoUrl: string
</script>
</head>
<body>
<header>
<img src="{logoUrl}" />
<jay:cart-indicator>
<span class="count">{itemCount}</span>
</jay:cart-indicator>
</header>
</body>
</html>
Nesting depth is unlimited. Circular imports are detected as errors. Key-based headless imports (key="...") are not allowed inside headfull FS components — use instance-based imports instead.
Nesting Rules
| Parent component | Can import headfull FS? | Can import headless (instance)? | Can import keyed headless? |
|---|---|---|---|
| Page | Yes | Yes | Yes |
| Headfull FS | Yes (recursive) | Yes (in its own head) | No |
| Headless | No (no template) | No (no template) | No (no template) |
Complete Example
A homepage with key-based, instance-based, and headfull components:
<html>
<head>
<script
type="application/jay-headless"
plugin="mood-tracker"
contract="mood-tracker"
key="mt"
></script>
<script
type="application/jay-headless"
plugin="product-widget"
contract="product-widget"
></script>
<script
type="application/jay-headfull"
src="../components/shared-header/shared-header"
names="SharedHeader"
contract="../components/shared-header/shared-header.jay-contract"
></script>
<script type="application/jay-data" contract="./page.jay-contract"></script>
<style>
.section {
margin: 20px 0;
padding: 10px;
}
.product-card {
border: 1px solid #ccc;
padding: 10px;
display: inline-block;
}
</style>
</head>
<body>
<jay:SharedHeader logoUrl="/logo.png" />
<h1>Homepage</h1>
<!-- Key-based: mood tracker -->
<div class="section">
<div>Happy: {mt.happy} <button ref="mt.happy">more</button></div>
<span if="mt.currentMood === happy">:)</span>
<span if="mt.currentMood === sad">:(</span>
</div>
<!-- Instance-based: static product widgets -->
<div class="section">
<jay:product-widget productId="1">
<h3>{name}</h3>
<div>${price}</div>
<span if="inStock">In Stock</span>
<button ref="addToCart">Add</button>
</jay:product-widget>
</div>
<!-- Instance-based: dynamic from forEach -->
<div class="section">
<div forEach="featuredProducts" trackBy="_id">
<div class="product-card">
<jay:product-widget productId="{_id}">
<h3>{name}</h3>
<div>${price}</div>
<button ref="addToCart">Add</button>
</jay:product-widget>
</div>
</div>
</div>
</body>
</html>
About this document
This page is part of the Jay Stack Agent Kit — documentation generated from the framework source and written primarily for AI agents. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation. Learn more about the Agent Kit →