Custom components
Author React components for themes — the injected scope API, styling rules, and TypeScript with npm imports.
A custom component is a node with type: "custom" whose code string contains a React
component. Lithium precompiles it server-side and evaluates it with react-live, injecting
a rich scope — so your code needs no imports at runtime and can still use the SDK, cart,
routing and i18n.
Authoring shape
You author flat TSX files; the kit compiles them into the code
string. The contract:
const propsSchema = {
heading: { type: "string", name: "Heading" },
} as const;
export const meta = {
type: "custom",
title: "Hero",
order: 0,
propsSchema,
props: { heading: "" },
} as const satisfies FeeefComponentMeta<typeof propsSchema>;
type Props = FeeefLivePropsOf<typeof propsSchema>;
function App() {
const p = props as Props;
return <h1>{p.heading || t("home.heroHeading")}</h1>;
}Rules that matter:
- The entry point is always
function App(). - Use
React.useState/React.useEffect(namespaced) rather than bare hooks. - Metadata lives in
export const meta— no separate JSON manifest. - The published code string contains no
import/export; the build strips or bundles them. Your source may import freely (see below). - Type props with
FeeefLivePropsOf<typeof propsSchema>and cast:const p = props as Props.
Injected scope
Everything available inside App() without imports:
| Symbol | Purpose |
|---|---|
React | Hooks, Fragment |
props | Instance props (plus props.slots) |
useStore() | { store } or null — always null-check |
useFeeefCart() | { cart, count, store } or null — the cart service |
useCurrentProduct() | { product } on product pages |
useFeeef() | SDK instance or null — call it as a hook |
useTemplate() | Read-only template state |
useTheme() | next-themes (dark/light/system) |
t(key, params?), useFeeefT(), useFeeefLocale() | Theme i18n from locales/*.json |
Link, useRouter, usePathname, useSearchParams | next/navigation — never <a> for in-app nav |
RouterNav | Clickable block with stopPropagation for nested cards |
SlotContextBridge, useSlotContext(key) | Pass context between a shell and its slot children |
SlotsLayout | Renders props.slotsLayout with props.slots |
dartColorToCssColor | Convert Flutter color ints coming from props |
Slots in JSX
{props.slots?.header}
<main>{props.slots?.body}</main>
{props.slots?.footer}Prefer <SlotsLayout /> when the node has slotsLayout — merchants can then rearrange
responsive slot order in the editor without code changes.
Styling rules
- Brand:
hsl(var(--primary)),hsl(var(--primary-foreground)),hsl(var(--background))… - Corners:
var(--corners-card),var(--corners-buttons-large)… - Dark mode rides the native tokens (
:root/.dark) — never build a parallel palette. - Never read
store.decoration.primarydirectly (it's a Flutter0xAARRGGBBint). - Store chrome from store fields:
store.logoUrl,store.name,store.contacts.
Working with data
// Cart
const cartCtx = useFeeefCart();
if (!cartCtx) return null;
cartCtx.cart.addItem({ product, quantity: 1, variantPath: "Color/Red" });
// Product media: product.photoUrl is a string, product.media is string[] of URLs
// Product lists: server-side filters via flat params or the filterator
const ff = useFeeef();
const res = await ff.products.list({
params: { store_id: store.id, in_stock: true, order_by: "sold:desc", limit: 8 },
});
const products = res?.data ?? res ?? [];Product/category listing recipes (related products, price ranges, search grids) follow the
filterator conventions. Categories accept
flat params only (store_id, parent_id).
Orders from theme code are submitted through the storefront's server route
(POST /api/orders/create), never straight from the browser to the API — the built-in order
form components handle this for you.
TypeScript and npm imports
Your source files are real modules — local imports and npm packages both work:
import clsx from "clsx";
import { bannerLabel } from "./labels";
import { FoButton } from "../../../shared/ds/ui";
function App() {
return <div className={clsx("hero")}>{bannerLabel("Feeef")}</div>;
}The build bundles everything into the single code string:
- Install deps at the theme root (
npm install clsx) — the compiler resolves from the theme'snode_modules. react/react-domstay external — the runtime injectsReact. Don'timport React from "react"; add@types/reactonly as a dev dependency for types.- No Node builtins or server-only packages — the code runs in the shopper's browser.
- Keep
export const metain the entry file.
When to use a folder
Only when the component needs slots/, children/, or colocated helper modules:
pages/product/components/product-shell/
product-shell.tsx # export const meta + App (includes slotsSchema)
slots/header/header.json # $ref or leaf node
slots/body/gallery.tsx
slots/form/order.json # built-in type placementNever create both hero.tsx and hero/ for the same id.
Anti-patterns
code/propsSchema/slots*nested insideprops— breaks renderer and editor.- Bare
useStateinstead ofReact.useState. <a href>for internal navigation — useLink/RouterNav.- Hardcoded colors instead of CSS variables.
- Regenerating
instanceIds on edit. - Treating
product.mediaas{ url }[]— it isstring[].