{"id":50455176,"url":"https://github.com/bcomnes/fragtml","last_synced_at":"2026-06-01T02:03:14.396Z","repository":{"id":360844824,"uuid":"1251847634","full_name":"bcomnes/fragtml","owner":"bcomnes","description":"Tagged template html literals with fragment support (for htmx)","archived":false,"fork":false,"pushed_at":"2026-05-28T04:45:03.000Z","size":47,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-05-28T06:16:21.515Z","etag":null,"topics":["htmx"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/fragtml","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bcomnes.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/funding.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["bcomnes"],"custom":["https://bret.io"]}},"created_at":"2026-05-28T00:59:53.000Z","updated_at":"2026-05-28T04:45:06.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bcomnes/fragtml","commit_stats":null,"previous_names":["bcomnes/fragtml"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/bcomnes/fragtml","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bcomnes%2Ffragtml","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bcomnes%2Ffragtml/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bcomnes%2Ffragtml/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bcomnes%2Ffragtml/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bcomnes","download_url":"https://codeload.github.com/bcomnes/fragtml/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bcomnes%2Ffragtml/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33756582,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-01T02:00:06.963Z","response_time":115,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["htmx"],"created_at":"2026-06-01T02:03:09.825Z","updated_at":"2026-06-01T02:03:14.384Z","avatar_url":"https://github.com/bcomnes.png","language":"JavaScript","funding_links":["https://github.com/sponsors/bcomnes","https://bret.io"],"categories":[],"sub_categories":[],"readme":"# fragtml\n\n[![latest version](https://img.shields.io/npm/v/fragtml.svg)](https://www.npmjs.com/package/fragtml)\n[![Actions Status](https://github.com/bcomnes/fragtml/workflows/tests/badge.svg)](https://github.com/bcomnes/fragtml/actions)\n\n[![downloads](https://img.shields.io/npm/dm/fragtml.svg)](https://npmtrends.com/fragtml)\n![Types in JS](https://img.shields.io/badge/types_in_js-yes-brightgreen)\n[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-7fffff?style=flat\u0026labelColor=ff80ff)](https://github.com/neostandard/neostandard)\n[![Socket Badge](https://socket.dev/api/badge/npm/package/fragtml)](https://socket.dev/npm/package/fragtml)\n\nA safe-by-default, string-generating HTML tagged template library with inline fragment support for server-rendered hypermedia apps.\n\n`fragtml` is inspired by `common-tags` HTML formatting behavior and by htmx-style [template fragments](https://htmx.org/essays/template-fragments/). It lets you keep a full template and its partial update fragments together in one JavaScript template function.\n\nThe practical benefits of inline fragments are still being assessed against function-based composition. Fragments can reduce indirection in large templates by keeping related partial update targets in place, but they also increase type complexity compared with function-based partials.\n\n## Why fragments?\n\nFragments are useful when a page has several partial update targets, but those targets are easiest to understand in the structure of the full template:\n\n```ts\nimport html, { render } from 'fragtml'\nimport type { FragmentTemplateTypes } from 'fragtml/types.js'\n\nfunction dashboardTemplate ({\n  account,\n  feed,\n  fragmentId\n}: DashboardTemplate['templateArgs']) {\n  return html\u003cDashboardTemplate['fragmentId']\u003e(fragmentId)/* html */`\n    \u003cmain\u003e\n      \u003ch1\u003eWorkspace\u003c/h1\u003e\n\n      ${html.fragment.start('account-root')}\n      \u003csection id=\"account\"\u003e\n        \u003ch2\u003e${account.title}\u003c/h2\u003e\n        \u003cp\u003e${account.name}\u003c/p\u003e\n\n        ${html.fragment.start('account-primary-action')}\n        \u003cbutton hx-patch=\"/accounts/${account.id}\"\u003e\n          ${account.archived ? 'Restore' : 'Archive'}\n        \u003c/button\u003e\n        ${html.fragment.end}\n      \u003c/section\u003e\n      ${html.fragment.end}\n\n      ${html.fragment.start('feed-root')}\n      \u003csection id=\"feed\"\u003e\n        \u003ch2\u003e${feed.title}\u003c/h2\u003e\n        \u003carticle\u003e\n          ${feed.latest.title}\n\n          ${html.fragment.start('feed-item-menu')}\n          \u003cmenu\u003e\n            \u003cbutton hx-get=\"/feed/${feed.latest.id}/menu\"\u003eOpen\u003c/button\u003e\n          \u003c/menu\u003e\n          ${html.fragment.end}\n        \u003c/article\u003e\n      \u003c/section\u003e\n      ${html.fragment.end}\n    \u003c/main\u003e\n  `\n}\n\nexport function dashboard (args: DashboardTemplate['args']) {\n  return render(dashboardTemplate(args))\n}\n\n// Rendering different fragments, with type safety\ndashboard({\n  account: {\n    id: 'acct_123',\n    title: 'Account',\n    name: 'Acme',\n    archived: false\n  },\n  feed: {\n    title: 'Feed',\n    latest: {\n      id: 'item_123',\n      title: 'New signup'\n    }\n  }\n})\n\ndashboard({\n  fragmentId: 'account-primary-action',\n  account: {\n    id: 'acct_123',\n    archived: false\n  }\n})\n\ndashboard({\n  fragmentId: 'feed-item-menu',\n  feed: {\n    latest: {\n      id: 'item_123'\n    }\n  }\n})\n\ntype AccountActionContext = {\n  account: {\n    id: string\n    archived: boolean\n  }\n}\n\ntype AccountRootContext = AccountActionContext \u0026 {\n  account: AccountActionContext['account'] \u0026 {\n    title: string\n    name: string\n  }\n}\n\ntype FeedItemMenuContext = {\n  feed: {\n    latest: {\n      id: string\n    }\n  }\n}\n\ntype FeedRootContext = FeedItemMenuContext \u0026 {\n  feed: FeedItemMenuContext['feed'] \u0026 {\n    title: string\n    latest: FeedItemMenuContext['feed']['latest'] \u0026 {\n      title: string\n    }\n  }\n}\n\ntype DashboardTemplate = FragmentTemplateTypes\u003c{\n  fragments: {\n    'account-root': AccountRootContext\n    'account-primary-action': AccountActionContext\n    'feed-root': FeedRootContext\n    'feed-item-menu': FeedItemMenuContext\n  }\n  full: AccountRootContext \u0026 FeedRootContext\n}\u003e\n```\n\nThe same template can render the full dashboard, `account-root`, `account-primary-action`, `feed-root`, or `feed-item-menu`. The public `dashboard(args)` wrapper enforces the required context fields for each target, while the full-page structure stays visible and each htmx target remains named next to the markup it updates.\n\nFor comparison, the same page can be built with function composition. This keeps each partial reusable, but the full-page structure is now spread across several functions. The types are much simpler though and there are less moving parts.\n\n```ts\nfunction accountPrimaryAction (context: AccountActionContext) {\n  return html`\n    \u003cbutton hx-patch=\"/accounts/${context.account.id}\"\u003e\n      ${context.account.archived ? 'Restore' : 'Archive'}\n    \u003c/button\u003e\n  `\n}\n\nfunction accountRoot (context: AccountRootContext) {\n  return html`\n    \u003csection id=\"account\"\u003e\n      \u003ch2\u003e${context.account.title}\u003c/h2\u003e\n      \u003cp\u003e${context.account.name}\u003c/p\u003e\n      ${accountPrimaryAction(context)}\n    \u003c/section\u003e\n  `\n}\n\nfunction feedItemMenu (context: FeedItemMenuContext) {\n  return html`\n    \u003cmenu\u003e\n      \u003cbutton hx-get=\"/feed/${context.feed.latest.id}/menu\"\u003eOpen\u003c/button\u003e\n    \u003c/menu\u003e\n  `\n}\n\nfunction feedRoot (context: FeedRootContext) {\n  return html`\n    \u003csection id=\"feed\"\u003e\n      \u003ch2\u003e${context.feed.title}\u003c/h2\u003e\n      \u003carticle\u003e\n        ${context.feed.latest.title}\n        ${feedItemMenu(context)}\n      \u003c/article\u003e\n    \u003c/section\u003e\n  `\n}\n\nfunction composedDashboardTemplate (context: AccountRootContext \u0026 FeedRootContext) {\n  return html`\n    \u003cmain\u003e\n      \u003ch1\u003eWorkspace\u003c/h1\u003e\n      ${accountRoot(context)}\n      ${feedRoot(context)}\n    \u003c/main\u003e\n  `\n}\n\nexport function composedDashboard (context: AccountRootContext \u0026 FeedRootContext) {\n  return render(composedDashboardTemplate(context))\n}\n```\n\nUse fragments when preserving the full template structure is the point. Use composed functions when these pieces need to be reused by other templates.\n\n## Install\n\n```sh\nnpm install fragtml\n```\n\n## Basic usage\n\n```js\nimport html, { render } from 'fragtml'\n\nconst name = '\u003cBret\u003e'\nconst result = html`\u003cp\u003eHello ${name}\u003c/p\u003e`\n\nrender(result)\n// '\u003cp\u003eHello \u0026lt;Bret\u0026gt;\u003c/p\u003e'\n```\n\n`html` returns an intermediate result object, not a primitive string. Use `render()` at route-handler boundaries:\n\n```js\nreturn render(html`\u003ch1\u003e${title}\u003c/h1\u003e`)\n```\n\nThe returned object also supports direct string coercion:\n\n```js\nconst result = html`\u003cp\u003e${'Hello'}\u003c/p\u003e`\n\nString(result)\nresult.toString()\n`${result}`\n```\n\n## Safe interpolation\n\nStatic template HTML is left as-is. Ordinary substitutions are escaped:\n\n```js\nrender(html`\u003cp\u003e${'\u003cscript\u003ealert(1)\u003c/script\u003e'}\u003c/p\u003e`)\n// '\u003cp\u003e\u0026lt;script\u0026gt;alert(1)\u0026lt;/script\u0026gt;\u003c/p\u003e'\n```\n\nThe following non-printing values are omitted:\n\n- `null`\n- `undefined`\n- booleans\n- `NaN`\n\n```js\nrender(html`\u003cp\u003e${null}${false}${Number.NaN}${0}\u003c/p\u003e`)\n// '\u003cp\u003e0\u003c/p\u003e'\n```\n\n## Trusted raw HTML\n\nUse `raw()` for trusted HTML that should not be escaped:\n\n```js\nimport html, { raw, render } from 'fragtml'\n\nrender(html`\u003cp\u003e${raw('\u003cstrong\u003etrusted\u003c/strong\u003e')}\u003c/p\u003e`)\n// '\u003cp\u003e\u003cstrong\u003etrusted\u003c/strong\u003e\u003c/p\u003e'\n```\n\nThe tag also exposes the same helper as `.raw`:\n\n```js\nhtml.raw === raw\n\nrender(html`\u003cp\u003e${html.raw('\u003cem\u003etrusted\u003c/em\u003e')}\u003c/p\u003e`)\n// '\u003cp\u003e\u003cem\u003etrusted\u003c/em\u003e\u003c/p\u003e'\n```\n\nOnly pass trusted HTML to `raw()`. User input should be interpolated normally so it is escaped.\n\nThere is no public `unsafeHtml` tag in v1. Prefer local, explicit trust boundaries with `raw()`.\n\n## Composition\n\nNested `html` results are treated as trusted `fragtml` output, while their own substitutions remain escaped:\n\n```js\nconst button = html`\u003cbutton\u003e${'\u003cArchive\u003e'}\u003c/button\u003e`\n\nrender(html`\n  \u003cdiv hx-target=\"this\"\u003e\n    ${button}\n  \u003c/div\u003e\n`)\n// '\u003cdiv hx-target=\"this\"\u003e\\n  \u003cbutton\u003e\u0026lt;Archive\u0026gt;\u003c/button\u003e\\n\u003c/div\u003e'\n```\n\nNested results render in their own fragment scope. A parent template does not see fragment IDs declared by child templates; pass a `fragmentId` to the child template when you want the child to render one of its own fragments.\n\nArrays are inlined with indentation-aware formatting:\n\n```js\nconst items = ['one', 'two'].map((item) =\u003e html`\u003cli\u003e${item}\u003c/li\u003e`)\n\nrender(html`\n  \u003cul\u003e\n    ${items}\n  \u003c/ul\u003e\n`)\n// '\u003cul\u003e\\n  \u003cli\u003eone\u003c/li\u003e\\n  \u003cli\u003etwo\u003c/li\u003e\\n\u003c/ul\u003e'\n```\n\nString substitutions containing newlines are split and aligned to the surrounding indentation.\n\n## Boolean attributes\n\nUse `?name=${condition}` to toggle boolean attributes. When the value is truthy, `fragtml` renders the bare attribute. When the value is falsey, it omits the attribute.\n\n```js\nrender(html`\u003cbutton ?disabled=${loading}\u003eSave\u003c/button\u003e`)\n```\n\nWhen `loading` is truthy:\n\n```html\n\u003cbutton disabled\u003eSave\u003c/button\u003e\n```\n\nWhen `loading` is falsey:\n\n```html\n\u003cbutton\u003eSave\u003c/button\u003e\n```\n\nThis syntax is useful for native HTML boolean attributes such as `disabled`, `checked`, `selected`, `readonly`, `required`, `multiple`, `autofocus`, `hidden`, and `open`.\n\nOnly the unquoted form is supported:\n\n```js\nhtml`\u003cbutton ?disabled=${loading}\u003eSave\u003c/button\u003e`\n```\n\nQuoted forms are intentionally unsupported in v1:\n\n```js\nhtml`\u003cbutton ?disabled=\"${loading}\"\u003eSave\u003c/button\u003e`\nhtml`\u003cbutton ?disabled='${loading}'\u003eSave\u003c/button\u003e`\n```\n\n## Fragments\n\nFragments mark named ranges inside a larger template. Calling `html(fragmentId)` on that template renders either the full template or one selected fragment:\n\n- `html()` / `html(undefined)` renders the full template.\n- `html('archive-ui')` renders only the `archive-ui` fragment.\n- `html({ fragmentId: 'archive-ui' })` is the options-object form.\n\nThis lets one view function serve both full-page requests and htmx-style fragment requests by passing the requested fragment ID through to `html(fragmentId)`.\n\nThis mirrors the htmx article’s idea:\n\n```txt\n#fragment archive-ui\n  ...\n#end\n```\n\nIn `fragtml`, use boundary tokens:\n\n```js\n${html.fragment.start('archive-ui')}\n...\n${html.fragment.end}\n```\n\n### Example\n\n```js\nimport html, { render } from 'fragtml'\n\nfunction contactDetailTemplate ({ contact, fragmentId }) {\n  return html(fragmentId)`\n    \u003chtml\u003e\n      \u003cbody\u003e\n        \u003cdiv hx-target=\"this\"\u003e\n          ${html.fragment.start('archive-ui')}\n          ${contact.archived\n            ? html`\u003cbutton hx-patch=\"/contacts/${contact.id}/unarchive\"\u003eUnarchive\u003c/button\u003e`\n            : html`\u003cbutton hx-delete=\"/contacts/${contact.id}\"\u003eArchive\u003c/button\u003e`}\n          ${html.fragment.end}\n        \u003c/div\u003e\n\n        \u003ch3\u003eContact\u003c/h3\u003e\n        \u003cp\u003e${contact.email}\u003c/p\u003e\n      \u003c/body\u003e\n    \u003c/html\u003e\n  `\n}\n\nexport function contactDetail (args) {\n  return render(contactDetailTemplate(args))\n}\n```\n\nRender the whole page:\n\n```js\ncontactDetail({ contact })\n```\n\nRender only the archive button fragment:\n\n```js\ncontactDetail({ contact, fragmentId: 'archive-ui' })\n```\n\nFragment boundary tokens are not included in either output.\n\nIf you want a simple local tag name for editor highlighting or repeated use, `frag` is an alias of `html`:\n\n```js\nimport { frag, render } from 'fragtml'\n\nfunction contactDetailTemplate ({ contact, fragmentId }) {\n  const html = frag(fragmentId)\n\n  return html`\n    \u003carticle\u003e\n      \u003ch3\u003e${contact.name}\u003c/h3\u003e\n      \u003cp\u003e${contact.email}\u003c/p\u003e\n\n      \u003cdiv hx-target=\"this\"\u003e\n        ${html.fragment.start('archive-ui')}\n        \u003cbutton\u003e${contact.archived ? 'Unarchive' : 'Archive'}\u003c/button\u003e\n        ${html.fragment.end}\n      \u003c/div\u003e\n    \u003c/article\u003e\n  `\n}\n\nexport function contactDetail (args) {\n  return render(contactDetailTemplate(args))\n}\n```\n\nCalling `html('archive-ui')` directly before a template can break editor HTML highlighting because many highlighters only recognize a simple tag identifier before the backtick. Assign the fragment tag to a local variable for highlighting, or add a `/* html */` marker. Editors such as Sublime Text and Zed understand this marker:\n\n```js\nconst h = frag(fragmentId)\n\nreturn h/* html */`\n  \u003carticle\u003e\n    \u003ch3\u003e${contact.name}\u003c/h3\u003e\n\n    \u003cdiv hx-target=\"this\"\u003e\n      ${h.fragment.start('archive-ui')}\n      \u003cbutton\u003eArchive\u003c/button\u003e\n      ${h.fragment.end}\n    \u003c/div\u003e\n  \u003c/article\u003e\n`\n```\n\nIn TypeScript, you can use an explicit fragment-name union to type-check both incoming fragment IDs and declared fragment boundaries:\n\n```ts\nimport { frag, render } from 'fragtml'\nimport type { RenderOptions } from 'fragtml/types.js'\n\ntype ContactFragment = 'archive-ui' | 'details'\n\nfunction contactDetailTemplate ({\n  contact,\n  fragmentId\n}: {\n  contact: Contact\n} \u0026 RenderOptions\u003cContactFragment\u003e) {\n  const html = frag\u003cContactFragment\u003e(fragmentId)\n\n  return html`\n    \u003carticle\u003e\n      \u003ch3\u003e${contact.name}\u003c/h3\u003e\n      \u003cp\u003e${contact.email}\u003c/p\u003e\n\n      \u003cdiv hx-target=\"this\"\u003e\n        ${html.fragment.start('archive-ui')}\n        \u003cbutton\u003e${contact.archived ? 'Unarchive' : 'Archive'}\u003c/button\u003e\n        ${html.fragment.end}\n      \u003c/div\u003e\n    \u003c/article\u003e\n  `\n}\n\nexport function contactDetail (args: {\n  contact: Contact\n} \u0026 RenderOptions\u003cContactFragment\u003e) {\n  return render(contactDetailTemplate(args))\n}\n```\n\n### Fragment context typing\n\nFragment markers keep the whole template in one expression. JavaScript evaluates every `${...}` substitution before `fragtml` selects a fragment, so the context type must cover the whole template even when a smaller fragment is requested. If different fragments need different fields, those fields usually have to be optional or otherwise guarded:\n\n```ts\nimport { frag, render } from 'fragtml'\nimport type {\n  FragmentTemplateTypes\n} from 'fragtml/types.js'\n\ntype InnerPageContext = {\n  text: string\n}\n\ntype OuterPageContext = InnerPageContext \u0026 {\n  title: string\n}\n\ntype FullPageContext = OuterPageContext \u0026 {\n  foo: string\n}\n\ntype PageTemplate = FragmentTemplateTypes\u003c{\n  // Fragment IDs and their required context types.\n  fragments: {\n    inner: InnerPageContext\n    outer: OuterPageContext\n  }\n  // Context required to render the full template.\n  full: FullPageContext\n}\u003e\n\ntype PageFragment = PageTemplate['fragmentId']\n// Resolves to:\n// 'inner' | 'outer'\n\ntype PageArgs = PageTemplate['args']\n// Resolves to:\n// | { fragmentId: 'inner', context: InnerPageContext \u0026 Record\u003cstring, unknown\u003e }\n// | { fragmentId: 'outer', context: OuterPageContext \u0026 Record\u003cstring, unknown\u003e }\n// | { fragmentId?: undefined, context: FullPageContext \u0026 Record\u003cstring, unknown\u003e }\n\ntype PageTemplateArgs = PageTemplate['templateArgs']\n// Resolves to:\n// {\n//   fragmentId?: 'inner' | 'outer' | undefined\n//   context: {\n//     foo?: string\n//     title?: string\n//     text?: string\n//   }\n// }\n\nfunction pageTemplate ({\n  context,\n  fragmentId\n}: PageTemplateArgs) {\n  const html = frag\u003cPageFragment\u003e(fragmentId)\n\n  return html`\n    \u003cdiv\u003e${context.foo}\u003c/div\u003e\n\n    ${html.fragment.start('outer')}\n    \u003csection\u003e\n      \u003ch2\u003e${context.title}\u003c/h2\u003e\n\n      ${html.fragment.start('inner')}\n      \u003cbutton\u003eInner update target\u003c/button\u003e\n      \u003cdiv\u003e${context.text}\u003c/div\u003e\n      ${html.fragment.end}\n    \u003c/section\u003e\n    ${html.fragment.end}\n  `\n}\n\nexport function page (args: PageArgs) {\n  return render(pageTemplate(args))\n}\n\n// These calls are still type-safe: PageArgs enforces the required context\n// fields for each fragment target at the public call boundary.\npage({\n  fragmentId: 'inner',\n  context: { text: 'Updated body text' }\n})\n\npage({\n  fragmentId: 'outer',\n  context: {\n    // Extra already-loaded data is allowed.\n    foo: 'Full page field',\n    title: 'Outer fragment title',\n    text: 'Updated body text'\n  }\n})\n\npage({\n  context: {\n    foo: 'Full page field',\n    title: 'Outer fragment title',\n    text: 'Updated body text'\n  }\n})\n\n// @ts-expect-error outer fragments require both title and text.\npage({\n  fragmentId: 'outer',\n  context: { text: 'Missing the outer title' }\n})\n```\n\nThis pattern keeps one large template while hiding most of the type complexity in `FragmentTemplateTypes` and the small `page(args: PageArgs)` wrapper. The wrapper enforces the required fields for each fragment target, while `PageTemplateArgs` gives the shared template implementation a looser context type because every `${...}` expression is still evaluated before fragment selection.\n\nIf you want exact input types per render target, split the fragments into typed template functions and compose them without fragment markers. This also makes the smaller template functions reusable across multiple callsites or larger templates:\n\n```ts\nimport html, { render } from 'fragtml'\n\ntype InnerContext = {\n  text: string\n}\n\ntype OuterContext = InnerContext \u0026 {\n  title: string\n}\n\ntype FullContext = OuterContext \u0026 {\n  foo: string\n}\n\nexport function inner (context: InnerContext) {\n  return html`\n    \u003cbutton\u003eInner update target\u003c/button\u003e\n    \u003cdiv\u003e${context.text}\u003c/div\u003e\n  `\n}\n\nexport function outer (context: OuterContext) {\n  return html`\n    \u003csection\u003e\n      \u003ch2\u003e${context.title}\u003c/h2\u003e\n      ${inner(context)}\n    \u003c/section\u003e\n  `\n}\n\nexport function full (context: FullContext) {\n  return html`\n    \u003cdiv\u003e${context.foo}\u003c/div\u003e\n    ${outer(context)}\n  `\n}\n\nrender(inner({ text: 'Updated body text' }))\n\nrender(outer({\n  title: 'Outer section title',\n  text: 'Updated body text'\n}))\n\nrender(full({\n  foo: 'Full page field',\n  title: 'Outer section title',\n  text: 'Updated body text'\n}))\n\n// @ts-expect-error outer requires both title and text.\nrender(outer({ text: 'Missing the outer title' }))\n```\n\nUse fragments to preserve the structure of a larger template while still rendering named pieces of it.\nUse function composition when those pieces need to be reused across multiple parent templates, similar to React components or partials in other template languages.\n\n### Fragment context helpers\n\n`FragmentTemplateTypes\u003cConfig\u003e` bundles the public and internal types for one fragment-marked template:\n\n```ts\ntype PageTemplate = FragmentTemplateTypes\u003c{\n  fragments: {\n    inner: InnerPageContext\n    outer: OuterPageContext\n  }\n  full: FullPageContext\n}\u003e\n\ntype PageArgs = PageTemplate['args']\ntype PageTemplateArgs = PageTemplate['templateArgs']\n```\n\n`fragments` maps fragment IDs to their required context types. `full` is the context required to render the full template.\n\n`PageTemplate['args']` is the public call boundary that enforces the required fields for the selected render target. Extra context fields are allowed, so callers can pass already-loaded full-page data to smaller fragment renders. `PageTemplate['templateArgs']` is the looser implementation type for the shared template body; its `context` is an `OptionalMerge` of every render target context because the full template expression is evaluated before fragment selection.\n\n`FragmentArgs\u003cFragments, FullContext\u003e` builds the public argument union from a map of fragment contexts and the full-page context:\n\n```ts\ntype PageArgs = FragmentArgs\u003c{\n  inner: InnerPageContext\n  outer: OuterPageContext\n}, FullPageContext\u003e\n\n// Equivalent to:\n// | { fragmentId: 'inner', context: InnerPageContext \u0026 Record\u003cstring, unknown\u003e }\n// | { fragmentId: 'outer', context: OuterPageContext \u0026 Record\u003cstring, unknown\u003e }\n// | { fragmentId?: undefined, context: FullPageContext \u0026 Record\u003cstring, unknown\u003e }\n```\n\n`FragmentTemplateArgs\u003cArgs\u003e` derives the full argument type for the shared template implementation. `FragmentTemplateContext\u003cArgs\u003e` derives only its looser `context` field. Both make fields from every render target optional:\n\n```ts\ntype PageTemplateArgs = FragmentTemplateArgs\u003cPageArgs\u003e\n\n// Equivalent to:\n// {\n//   fragmentId?: 'inner' | 'outer' | undefined\n//   context: {\n//     foo?: string\n//     title?: string\n//     text?: string\n//   }\n// }\n```\n\nMost users should start with `FragmentTemplateTypes`. Use `FragmentArgs` and `FragmentTemplateArgs` directly if you prefer to assemble the public and implementation types yourself. Lower-level utility types live in `fragtml/lib/html-types.js` for advanced cases, but they are not part of the recommended surface.\n\n### Fragment rules\n\n- Fragment IDs must be unique within a rendered template.\n- Nested template results have their own fragment scope; parent templates do not select or conflict with child fragment IDs.\n- Missing fragments throw `FragmentNotFoundError`.\n- Duplicate fragment IDs throw `DuplicateFragmentError`.\n- `html.fragment.end` without a matching start throws `FragmentBoundaryError`.\n- An unclosed `html.fragment.start(id)` throws `FragmentBoundaryError`.\n\n### Fragment antipatterns\n\nDo not wrap the entire template in an outer fragment. Rendering without a `fragmentId` already renders the whole template, so a fragment that covers everything adds a fake target without changing the output.\n\nAvoid:\n\n```js\nfunction pageTemplate ({ fragmentId }) {\n  return html(fragmentId)`\n    ${html.fragment.start('page')}\n    \u003cmain\u003e\n      \u003ch1\u003e${title}\u003c/h1\u003e\n      \u003cp\u003e${body}\u003c/p\u003e\n    \u003c/main\u003e\n    ${html.fragment.end}\n  `\n}\n```\n\nPrefer:\n\n```js\nfunction pageTemplate ({ fragmentId }) {\n  return html(fragmentId)`\n    \u003cmain\u003e\n      \u003ch1\u003e${title}\u003c/h1\u003e\n      \u003cp\u003e${body}\u003c/p\u003e\n    \u003c/main\u003e\n  `\n}\n```\n\nOnly mark fragments that represent real partial update targets inside the full template.\n\n## Nested fragments\n\nNested fragments are supported with stack semantics. This is useful when a larger region can be re-rendered as a whole, but a smaller region inside it is also a valid htmx update target. A single template can contain multiple independent nested fragment groups, each with its own root fragment.\n\n```js\nimport html, { render } from 'fragtml'\n\nfunction pageTemplate ({ fragmentId }) {\n  return html(fragmentId)`\n    ${html.fragment.start('profile')}\n    \u003csection\u003e\n      \u003ch2\u003eProfile\u003c/h2\u003e\n\n      ${html.fragment.start('profile-actions')}\n      \u003cbutton\u003eEdit profile\u003c/button\u003e\n      ${html.fragment.end}\n    \u003c/section\u003e\n    ${html.fragment.end}\n\n    ${html.fragment.start('activity')}\n    \u003csection\u003e\n      \u003ch2\u003eActivity\u003c/h2\u003e\n\n      ${html.fragment.start('activity-row')}\n      \u003carticle\u003eRecent activity\u003c/article\u003e\n      ${html.fragment.end}\n    \u003c/section\u003e\n    ${html.fragment.end}\n  `\n}\n\nexport function page (args) {\n  return render(pageTemplate(args))\n}\n```\n\nRendering a root fragment includes its nested fragment content:\n\n```js\npage({ fragmentId: 'profile' })\n// '\u003csection\u003e\\n  \u003ch2\u003eProfile\u003c/h2\u003e\\n\\n  \u003cbutton\u003eEdit profile\u003c/button\u003e\\n\u003c/section\u003e'\n\npage({ fragmentId: 'activity' })\n// '\u003csection\u003e\\n  \u003ch2\u003eActivity\u003c/h2\u003e\\n\\n  \u003carticle\u003eRecent activity\u003c/article\u003e\\n\u003c/section\u003e'\n```\n\nRendering a nested fragment returns only that nested fragment:\n\n```js\npage({ fragmentId: 'profile-actions' })\n// '\u003cbutton\u003eEdit profile\u003c/button\u003e'\n\npage({ fragmentId: 'activity-row' })\n// '\u003carticle\u003eRecent activity\u003c/article\u003e'\n```\n\nUse nested fragments sparingly. Prefer flat fragments unless you actually need both a parent region and a child region as independently renderable update targets.\n\n## API\n\n### `html`\n\nSafe-by-default template tag.\n\n```js\nhtml`\u003cp\u003e${value}\u003c/p\u003e`\n```\n\nPass a fragment ID before the tagged template to render a selected fragment from that template:\n\n```js\nhtml('name')`...`\nhtml({ fragmentId: 'name' })`...`\n```\n\n### `frag`\n\nAlias of `html`, useful when you want a local tag name for editor highlighting or repeated use:\n\n```js\nimport { frag } from 'fragtml'\n\nconst html = frag(fragmentId)\n```\n\nCalling `html('name')` directly before a template can break editor HTML highlighting because the tag expression is no longer a simple identifier. Assign the result to a local tag name, or use the `/* html */` marker that Sublime Text and Zed understand:\n\n```js\nconst h = frag(fragmentId)\n\nreturn h/* html */`\u003cp\u003e${value}\u003c/p\u003e`\n```\n\nThis is kind of hit or miss per editor. Play around and see what works. You can usually figure something out.\n\n### `render(value)`\n\nConverts a `fragtml` result to a primitive string.\n\n```js\nrender(html`\u003cp\u003e${value}\u003c/p\u003e`)\n```\n\n### `raw(value)` / `html.raw(value)`\n\nMarks trusted HTML so it is inserted without escaping.\n\n```js\nhtml`\u003cp\u003e${raw('\u003cstrong\u003etrusted\u003c/strong\u003e')}\u003c/p\u003e`\n```\n\n### `HtmlResult`\n\nClass returned by `html` and `frag` tagged templates.\n\n```js\nimport html, { HtmlResult } from 'fragtml'\n\nconst result = html`\u003cp\u003eHello\u003c/p\u003e`\n\nresult instanceof HtmlResult\n```\n\n### `RawHtml`\n\nClass returned by `raw(value)` and `html.raw(value)`.\n\n```js\nimport { RawHtml, raw } from 'fragtml'\n\nconst trusted = raw('\u003cstrong\u003etrusted\u003c/strong\u003e')\n\ntrusted instanceof RawHtml\n```\n\n### Type guards\n\nUse the public type guards to narrow unknown values without importing from internal `lib/` paths:\n\n```js\nimport {\n  isFragmentBoundary,\n  isHtmlResult,\n  isRawHtml\n} from 'fragtml'\n```\n\n### Boolean attributes\n\nUse unquoted `?name=${condition}` syntax to toggle a boolean attribute.\n\n```js\nhtml`\u003cbutton ?disabled=${loading}\u003eSave\u003c/button\u003e`\n```\n\n### `html.fragment.start(id)`\n\nStarts a named fragment range.\n\n```js\n${html.fragment.start('archive-ui')}\n```\n\n### `html.fragment.end`\n\nEnds the most recently opened fragment range.\n\n```js\n${html.fragment.end}\n```\n\n### Error classes\n\n```js\nimport {\n  DuplicateFragmentError,\n  FragmentBoundaryError,\n  FragmentNotFoundError\n} from 'fragtml'\n```\n\n## TypeScript\n\n`fragtml` is written in typed JavaScript and ships generated declaration files.\n\nRuntime classes such as `HtmlResult` and `RawHtml` are exported from the package root. Type-only aliases are exported from `fragtml/types.js`:\n\n```ts\nimport type {\n  FragmentArgs,\n  FragmentIdOf,\n  FragmentTemplateArgs,\n  FragmentTemplateTypes,\n  HtmlRenderable,\n  HtmlResult,\n  HtmlTag,\n  RawHtml,\n  RenderOptions\n} from 'fragtml/types.js'\n```\n\n`HtmlResult` is both a runtime class from the package root and an importable type from `fragtml/types.js`:\n\n```ts\nimport { HtmlResult } from 'fragtml'\nimport type { HtmlResult as HtmlResultType } from 'fragtml/types.js'\n\nfunction sendHtml (result: HtmlResultType) {\n  return result.toString()\n}\n\nfunction isHtmlResultValue (value: unknown): value is HtmlResultType {\n  return value instanceof HtmlResult\n}\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbcomnes%2Ffragtml","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbcomnes%2Ffragtml","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbcomnes%2Ffragtml/lists"}