CINXE.COM
Exploring Ember Polaris: A Fresh Take on the Component Format
<!DOCTYPE html> <html lang="en-US" class="auto" > <head> <title>Exploring Ember Polaris: A Fresh Take on the Component Format</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="HandheldFriendly" content="True"/> <link rel="apple-touch-icon" href="https://yehudakatz.com/content/images/2024/08/yehuda-square.png"> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet"> <link rel="stylesheet" type="text/css" href="https://yehudakatz.com/assets/built/screen.min.css?v=b917a20540"> <script> function colored(e){document.querySelectorAll("."+e).forEach(function(e){e.textContent.includes("✦")&&(e.innerHTML=e.innerHTML.replace("✦","<span>✦</span>"))})} const htmlElement=document.documentElement,classList=htmlElement.classList; window.matchMedia("(prefers-color-scheme: dark)").matches?(classList.add("dark"),localStorage.setItem("theme","dark")):(classList.remove("dark"),localStorage.setItem("theme","light")); document.addEventListener("DOMContentLoaded",()=>{"dark"===localStorage.getItem("theme")?classList.add("dark"):classList.remove("dark")}),window.addEventListener("storage",()=>{"dark"===localStorage.getItem("theme")?classList.add("dark"):classList.remove("dark")}); window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",e=>{e.matches?(classList.add("dark"),localStorage.setItem("theme","dark")):(classList.remove("dark"),localStorage.setItem("theme","light"))});function initializeDarkMode(){var e=document.querySelectorAll(".light-logo"),o=document.querySelectorAll(".footer-light-logo");document.querySelector("html");var r=window.matchMedia("(prefers-color-scheme: dark)");function t(){r.matches?(e.forEach(function(e){e.src=""}),o.forEach(function(e){e.src=""})):(e.forEach(function(e){e.src="https://yehudakatz.com/content/images/2024/08/wycats--2-.png"}),o.forEach(function(e){e.src="https://yehudakatz.com/content/images/2024/08/wycats--2-.png"}))}t(),r.addEventListener("change",t)} </script> <meta name="description" content="What if you could put multiple "single-file components" into one file? Without JSX Spaghetti? Ember's <template> syntax has already landed, and it's kind of the best of both worlds."> <link rel="icon" href="https://yehudakatz.com/content/images/size/w256h256/2024/08/yehuda-square.png" type="image/png"> <link rel="canonical" href="https://yehudakatz.com/2024/09/09/exploring-ember-polaris-a-fresh-take-on-the-component-format/"> <meta name="referrer" content="no-referrer-when-downgrade"> <link rel="amphtml" href="https://yehudakatz.com/2024/09/09/exploring-ember-polaris-a-fresh-take-on-the-component-format/amp/"> <meta property="og:site_name" content="Katz Got Your Tongue"> <meta property="og:type" content="article"> <meta property="og:title" content="Exploring Ember Polaris: A Fresh Take on the Component Format"> <meta property="og:description" content="What if you could put multiple "single-file components" into one file? Without JSX Spaghetti? Ember's <template> syntax has already landed, and it's kind of the best of both worlds."> <meta property="og:url" content="https://yehudakatz.com/2024/09/09/exploring-ember-polaris-a-fresh-take-on-the-component-format/"> <meta property="article:published_time" content="2024-09-09T04:18:44.000Z"> <meta property="article:modified_time" content="2024-09-11T21:44:22.000Z"> <meta property="article:tag" content="Polaris"> <meta name="twitter:card" content="summary"> <meta name="twitter:title" content="Exploring Ember Polaris: A Fresh Take on the Component Format"> <meta name="twitter:description" content="What if you could put multiple "single-file components" into one file? Without JSX Spaghetti? Ember's <template> syntax has already landed, and it's kind of the best of both worlds."> <meta name="twitter:url" content="https://yehudakatz.com/2024/09/09/exploring-ember-polaris-a-fresh-take-on-the-component-format/"> <meta name="twitter:label1" content="Written by"> <meta name="twitter:data1" content="Yehuda Katz"> <meta name="twitter:label2" content="Filed under"> <meta name="twitter:data2" content="Polaris"> <meta name="twitter:site" content="@wycats"> <meta name="twitter:creator" content="@wycats"> <script type="application/ld+json"> { "@context": "https://schema.org", "@type": "Article", "publisher": { "@type": "Organization", "name": "Katz Got Your Tongue", "url": "https://yehudakatz.com/", "logo": { "@type": "ImageObject", "url": "https://yehudakatz.com/content/images/2024/08/wycats--2-.png" } }, "author": { "@type": "Person", "name": "Yehuda Katz", "image": { "@type": "ImageObject", "url": "https://yehudakatz.com/content/images/2021/04/Profile---Correct.png", "width": 500, "height": 500 }, "url": "https://yehudakatz.com/author/wycats/", "sameAs": [ "https://twitter.com/wycats" ] }, "headline": "Exploring Ember Polaris: A Fresh Take on the Component Format", "url": "https://yehudakatz.com/2024/09/09/exploring-ember-polaris-a-fresh-take-on-the-component-format/", "datePublished": "2024-09-09T04:18:44.000Z", "dateModified": "2024-09-11T21:44:22.000Z", "keywords": "Polaris", "description": "What if you could put multiple "single-file components" into one file? Without JSX Spaghetti? Ember's <template> syntax has already landed, and it's kind of the best of both worlds.", "mainEntityOfPage": "https://yehudakatz.com/2024/09/09/exploring-ember-polaris-a-fresh-take-on-the-component-format/" } </script> <meta name="generator" content="Ghost 5.101"> <link rel="alternate" type="application/rss+xml" title="Katz Got Your Tongue" href="https://yehudakatz.com/rss/"> <script defer src="https://cdn.jsdelivr.net/ghost/portal@~2.46/umd/portal.min.js" data-i18n="true" data-ghost="https://yehudakatz.com/" data-key="7d10c674ca44f7fe0a608ff2fb" data-api="https://yehudakatz.ghost.io/ghost/api/content/" data-locale="en-US" crossorigin="anonymous"></script><style id="gh-members-styles">.gh-post-upgrade-cta-content, .gh-post-upgrade-cta { display: flex; flex-direction: column; align-items: center; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; text-align: center; width: 100%; color: #ffffff; font-size: 16px; } .gh-post-upgrade-cta-content { border-radius: 8px; padding: 40px 4vw; } .gh-post-upgrade-cta h2 { color: #ffffff; font-size: 28px; letter-spacing: -0.2px; margin: 0; padding: 0; } .gh-post-upgrade-cta p { margin: 20px 0 0; padding: 0; } .gh-post-upgrade-cta small { font-size: 16px; letter-spacing: -0.2px; } .gh-post-upgrade-cta a { color: #ffffff; cursor: pointer; font-weight: 500; box-shadow: none; text-decoration: underline; } .gh-post-upgrade-cta a:hover { color: #ffffff; opacity: 0.8; box-shadow: none; text-decoration: underline; } .gh-post-upgrade-cta a.gh-btn { display: block; background: #ffffff; text-decoration: none; margin: 28px 0 0; padding: 8px 18px; border-radius: 4px; font-size: 16px; font-weight: 600; } .gh-post-upgrade-cta a.gh-btn:hover { opacity: 0.92; }</style><script async src="https://js.stripe.com/v3/"></script> <script defer src="https://cdn.jsdelivr.net/ghost/sodo-search@~1.5/umd/sodo-search.min.js" data-key="7d10c674ca44f7fe0a608ff2fb" data-styles="https://cdn.jsdelivr.net/ghost/sodo-search@~1.5/umd/main.css" data-sodo-search="https://yehudakatz.ghost.io/" data-locale="en-US" crossorigin="anonymous"></script> <script defer src="https://cdn.jsdelivr.net/ghost/announcement-bar@~1.1/umd/announcement-bar.min.js" data-announcement-bar="https://yehudakatz.com/" data-api-url="https://yehudakatz.com/members/api/announcement/" crossorigin="anonymous"></script> <link href="https://yehudakatz.com/webmentions/receive/" rel="webmention"> <script defer src="/public/cards.min.js?v=b917a20540"></script> <link rel="stylesheet" type="text/css" href="/public/cards.min.css?v=b917a20540"> <script defer src="/public/member-attribution.min.js?v=b917a20540"></script><style>:root {--ghost-accent-color: #ff9b36;}</style> <style> :root { --vp-code-line-height: 1.7; --vp-code-font-size: .875em; --vp-code-color: var(--vp-c-brand-1); --vp-code-link-color: var(--vp-c-brand-1); --vp-code-link-hover-color: var(--vp-c-brand-2); --vp-code-bg: var(--vp-c-default-soft); --vp-code-block-color: var(--vp-c-text-2); --vp-code-block-bg: var(--vp-c-bg-alt); --vp-code-block-divider-color: var(--vp-c-gutter); --vp-code-lang-color: var(--vp-c-text-3); --vp-code-line-highlight-color: var(--vp-c-default-soft); --vp-code-line-number-color: var(--vp-c-text-3); --vp-code-line-diff-add-color: var(--vp-c-success-soft); --vp-code-line-diff-add-symbol-color: var(--vp-c-success-1); --vp-code-line-diff-remove-color: var(--vp-c-danger-soft); --vp-code-line-diff-remove-symbol-color: var(--vp-c-danger-1); --vp-code-line-warning-color: var(--vp-c-warning-soft); --vp-code-line-error-color: var(--vp-c-danger-soft); --vp-code-copy-code-border-color: var(--vp-c-divider); --vp-code-copy-code-bg: var(--vp-c-bg-soft); --vp-code-copy-code-hover-border-color: var(--vp-c-divider); --vp-code-copy-code-hover-bg: var(--vp-c-bg); --vp-code-copy-code-active-text: var(--vp-c-text-2); --vp-code-copy-copied-text-content: "Copied"; --vp-code-tab-divider: var(--vp-code-block-divider-color); --vp-code-tab-text-color: var(--vp-c-text-2); --vp-code-tab-bg: var(--vp-code-block-bg); --vp-code-tab-hover-text-color: var(--vp-c-text-1); --vp-code-tab-active-text-color: var(--vp-c-text-1); --vp-code-tab-active-bar-color: var(--vp-c-brand-1); } :root { --vp-c-white: #ffffff; --vp-c-black: #000000; --vp-c-neutral: var(--vp-c-black); --vp-c-neutral-inverse: var(--vp-c-white) } .dark { --vp-c-neutral: var(--vp-c-white); --vp-c-neutral-inverse: var(--vp-c-black) } :root { --vp-c-gray-1: #dddde3; --vp-c-gray-2: #e4e4e9; --vp-c-gray-3: #ebebef; --vp-c-gray-soft: rgba(142, 150, 170, .14); --vp-c-indigo-1: #3451b2; --vp-c-indigo-2: #3a5ccc; --vp-c-indigo-3: #5672cd; --vp-c-indigo-soft: rgba(100, 108, 255, .14); --vp-c-purple-1: #6f42c1; --vp-c-purple-2: #7e4cc9; --vp-c-purple-3: #8e5cd9; --vp-c-purple-soft: rgba(159, 122, 234, .14); --vp-c-green-1: #18794e; --vp-c-green-2: #299764; --vp-c-green-3: #30a46c; --vp-c-green-soft: rgba(16, 185, 129, .14); --vp-c-yellow-1: #915930; --vp-c-yellow-2: #946300; --vp-c-yellow-3: #9f6a00; --vp-c-yellow-soft: rgba(234, 179, 8, .14); --vp-c-red-1: #b8272c; --vp-c-red-2: #d5393e; --vp-c-red-3: #e0575b; --vp-c-red-soft: rgba(244, 63, 94, .14); --vp-c-sponsor: #db2777 } .dark { --vp-c-gray-1: #515c67; --vp-c-gray-2: #414853; --vp-c-gray-3: #32363f; --vp-c-gray-soft: rgba(101, 117, 133, .16); --vp-c-indigo-1: #a8b1ff; --vp-c-indigo-2: #5c73e7; --vp-c-indigo-3: #3e63dd; --vp-c-indigo-soft: rgba(100, 108, 255, .16); --vp-c-purple-1: #c8abfa; --vp-c-purple-2: #a879e6; --vp-c-purple-3: #8e5cd9; --vp-c-purple-soft: rgba(159, 122, 234, .16); --vp-c-green-1: #3dd68c; --vp-c-green-2: #30a46c; --vp-c-green-3: #298459; --vp-c-green-soft: rgba(16, 185, 129, .16); --vp-c-yellow-1: #f9b44e; --vp-c-yellow-2: #da8b17; --vp-c-yellow-3: #a46a0a; --vp-c-yellow-soft: rgba(234, 179, 8, .16); --vp-c-red-1: #f66f81; --vp-c-red-2: #f14158; --vp-c-red-3: #b62a3c; --vp-c-red-soft: rgba(244, 63, 94, .16) } :root { --vp-c-bg: #ffffff; --vp-c-bg-alt: #f6f6f7; --vp-c-bg-elv: #ffffff; --vp-c-bg-soft: #f6f6f7 } .dark { --vp-c-bg: #1b1b1f; --vp-c-bg-alt: #161618; --vp-c-bg-elv: #202127; --vp-c-bg-soft: #202127 } :root { --vp-c-border: #c2c2c4; --vp-c-divider: #e2e2e3; --vp-c-gutter: #e2e2e3 } .dark { --vp-c-border: #3c3f44; --vp-c-divider: #2e2e32; --vp-c-gutter: #000000 } :root { --vp-c-text-1: rgba(60, 60, 67); --vp-c-text-2: rgba(60, 60, 67, .78); --vp-c-text-3: rgba(60, 60, 67, .56) } :root { --vp-c-default-1: var(--vp-c-gray-1); --vp-c-default-2: var(--vp-c-gray-2); --vp-c-default-3: var(--vp-c-gray-3); --vp-c-default-soft: var(--vp-c-gray-soft); --vp-c-brand-1: var(--vp-c-indigo-1); --vp-c-brand-2: var(--vp-c-indigo-2); --vp-c-brand-3: var(--vp-c-indigo-3); --vp-c-brand-soft: var(--vp-c-indigo-soft); --vp-c-brand: var(--vp-c-brand-1); --vp-c-tip-1: var(--vp-c-brand-1); --vp-c-tip-2: var(--vp-c-brand-2); --vp-c-tip-3: var(--vp-c-brand-3); --vp-c-tip-soft: var(--vp-c-brand-soft); --vp-c-note-1: var(--vp-c-brand-1); --vp-c-note-2: var(--vp-c-brand-2); --vp-c-note-3: var(--vp-c-brand-3); --vp-c-note-soft: var(--vp-c-brand-soft); --vp-c-success-1: var(--vp-c-green-1); --vp-c-success-2: var(--vp-c-green-2); --vp-c-success-3: var(--vp-c-green-3); --vp-c-success-soft: var(--vp-c-green-soft); --vp-c-important-1: var(--vp-c-purple-1); --vp-c-important-2: var(--vp-c-purple-2); --vp-c-important-3: var(--vp-c-purple-3); --vp-c-important-soft: var(--vp-c-purple-soft); --vp-c-warning-1: var(--vp-c-yellow-1); --vp-c-warning-2: var(--vp-c-yellow-2); --vp-c-warning-3: var(--vp-c-yellow-3); --vp-c-warning-soft: var(--vp-c-yellow-soft); --vp-c-danger-1: var(--vp-c-red-1); --vp-c-danger-2: var(--vp-c-red-2); --vp-c-danger-3: var(--vp-c-red-3); --vp-c-danger-soft: var(--vp-c-red-soft); --vp-c-caution-1: var(--vp-c-red-1); --vp-c-caution-2: var(--vp-c-red-2); --vp-c-caution-3: var(--vp-c-red-3); --vp-c-caution-soft: var(--vp-c-red-soft) } .content pre { max-height: initial; } .content pre.shiki, .content pre.shiki code, code[class*=language-], pre:has(code[class*=language-]) { --font-size: 0.8rem; font-size: var(--font-size) !important; line-height: calc(var(--font-size) * 1.5) !important; } pre.shiki { position: relative; z-index: 1; margin: 0; padding: 20px 20px; background: transparent; overflow-x: auto } pre.shiki code { padding: 0; width: fit-content; min-width: 100%; line-height: var(--vp-code-line-height); font-size: var(--vp-code-font-size); color: var(--vp-code-block-color); transition: color .5s } pre.shiki code .highlighted { background-color: var(--vp-code-line-highlight-color); transition: background-color .5s; margin: 0 -24px; padding: 0 24px; width: calc(100% + 44px); display: inline-block } pre.shiki code .highlighted.error { background-color: var(--vp-code-line-error-color) } pre.shiki code .highlighted.warning { background-color: var(--vp-code-line-warning-color) } pre.shiki code .diff { transition: background-color .5s; margin: 0 -24px; padding: 0 24px; width: calc(100% + 44px); display: inline-block } pre.shiki code .diff:before { position: absolute; left: 10px } pre.shiki code .diff.remove { background-color: var(--vp-code-line-diff-remove-color); opacity: .7 } pre.shiki code .diff.remove:before { content: "-"; color: var(--vp-code-line-diff-remove-symbol-color) } pre.shiki code .diff.add { background-color: var(--vp-code-line-diff-add-color) } pre.shiki code .diff.add:before { content: "+"; color: var(--vp-code-line-diff-add-symbol-color) } a[title="Home"] img { border-radius: 100px; } .post-image:not(:has(img)) { display: none !important; } section.post-content details { margin-bottom: 2em; } section.post-content details[open] { margin-bottom: 0; } .kg-toggle-heading-text { padding-block-start: 0 !important; } </style> <script type="comment"> import hljs from 'https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.10.0/build/es/highlight.min.js'; // and it's easy to individually load additional languages import {setup} from 'https://cdn.jsdelivr.net/npm/highlightjs-glimmer@2.2.1/dist/glimmer.esm.min.js'; setup(hljs); hljs.addPlugin(new LineFocusPlugin({ focusedStyle: { borderLeft: "2px solid #78e08f55", background: "#78e08f05", padding: "2px", paddingLeft: "10px", }, unfocusedStyle: { borderLeft: "2px solid transparent", paddingLeft: "10px", opacity: "0.5", filter: "grayscale(1)" } })); </script> <script type='module'> import { codeToHtml } from 'https://esm.sh/shiki@1.17.0' import {transformerNotationHighlight,transformerNotationDiff} from 'https://esm.sh/@shikijs/transformers@1.17.0' const containers = document.querySelectorAll('code[class*=language-]'); console.log (containers); for (const container of containers) { console.log(container.innerHTML); const className = [...container.classList].find(name => name.startsWith("language-")); const lang = className.split("-").at(-1); const html = await codeToHtml(container.innerText, { lang, theme: 'github-light-high-contrast', transformers: [transformerNotationHighlight(), transformerNotationDiff()] }); container.parentElement.outerHTML = html; } </script> <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/cferdinandi/tabby@12/dist/css/tabby-ui.min.css"> <script src="https://cdn.jsdelivr.net/gh/cferdinandi/tabby@12/dist/js/tabby.polyfills.min.js"></script> </head> <body class="post-template tag-polaris bg-white dark:bg-zinc-950"> <nav class="main-navbar relative h-16 sm:h-20 min-h-max w-full px-4 xl:px-0 !z-40"> <div class="container mx-auto flex h-full max-w-screen-lg items-center justify-between"> <div class="main-navbar-logo flex-[1] justify-start"> <a href="https://yehudakatz.com" class="c-logo-light flex h-fit w-fit items-center"> <img class="c-logo-light-img light-logo min-h-[18px] !max-h-12 w-fit object-contain object-left " style=" height:48px" src="" alt="Katz Got Your Tongue" /> </a> <script> initializeDarkMode(); </script> </div> <div class="main-navbar-links main-nav mx-auto h-full w-fit items-center gap-6 px-4 hidden lg:flex select-none justify-center"> <a href="http://www.yehudakatz.com/" class="main-nav-item nv py-2 text-base lg:py-1 lg:text-md leading-none font-medium text-two hover:opacity-80 transition-opacity duration-200 ">Home</a> <a href="https://yehudakatz.com/about/" class="main-nav-item nv py-2 text-base lg:py-1 lg:text-md leading-none font-medium text-two hover:opacity-80 transition-opacity duration-200 ">About</a> <a href="https://yehudakatz.com/projects/" class="main-nav-item nv py-2 text-base lg:py-1 lg:text-md leading-none font-medium text-two hover:opacity-80 transition-opacity duration-200 ">Projects</a> <a href="https://yehudakatz.com/talks/" class="main-nav-item nv py-2 text-base lg:py-1 lg:text-md leading-none font-medium text-two hover:opacity-80 transition-opacity duration-200 ">Talks</a> <script>colored("nv")</script> </div> <div class="site-login h-full flex w-fit min-w-fit items-center gap-x-3 xs:gap-x-4 flex-[1] justify-end"> <a href="javascript:" data-ghost-search class="main-navbar-search-button primary-button p-0 min-w-[34px] min-h-[34px] w-[34px] h-[34px]" aria-label="Search"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="13" height="13"><path d="M14.5 14.5l-4-4m-4 2a6 6 0 110-12 6 6 0 010 12z" stroke="currentColor"></path></svg> </a> <a data-portal="signin" href="javascript:" aria-label="Sign in" class="main-navbar-sign-button secondary-button text-opacity-80 xs:text-opacity-100 p-0 xs:p-1 xs:px-3 xs:pr-3.5 xs:py-2 min-w-[34px] max-w-[34px] xs:max-w-fit "> <span class="block xs:hidden font-medium"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="14" height="14"> <path d="M4.5 6.5v-3a3 3 0 016 0V4m-8 2.5h10a1 1 0 011 1v6a1 1 0 01-1 1h-10a1 1 0 01-1-1v-6a1 1 0 011-1z" stroke="currentColor" stroke-width="1.3px"></path> </svg> </span> <span class="main-navbar-sign-button-text hidden xs:block font-medium">✦ Sign in</span> </a> <a href="javascript:" aria-label="mobile-menu" class="main-navbar-mobile-icon primary-button p-0 min-w-[34px] max-w-[34px] min-h-[34px] hamburger lg:hidden"> <svg width="16px" height="16px" stroke-width="1.6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="currentColor"><path d="M3 5H21" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path><path d="M3 12H21" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path><path d="M3 19H21" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path></svg> </a> </div> </div> </nav> <div class="mobile-menu mobile-menu-section fixed hidden flex-col top-16 lg:hidden h-[calc(100svh-64px)] w-full bg-white dark:bg-zinc-950 !z-[9999]"> <div class="mobile-menu flex flex-col text-center w-full !overflow-y-auto scroll-two h-[calc(100%-60px)]"> <a href="http://www.yehudakatz.com/" class="mobile-menu-links py-4 text-lg leading-none font-medium text-one focus:bg-zinc-50 dark:focus:bg-zinc-900 border-b border-zinc-50 dark:border-zinc-900/[0.7] last:border-none first:border-t"> Home </a> <a href="https://yehudakatz.com/about/" class="mobile-menu-links py-4 text-lg leading-none font-medium text-one focus:bg-zinc-50 dark:focus:bg-zinc-900 border-b border-zinc-50 dark:border-zinc-900/[0.7] last:border-none first:border-t"> About </a> <a href="https://yehudakatz.com/projects/" class="mobile-menu-links py-4 text-lg leading-none font-medium text-one focus:bg-zinc-50 dark:focus:bg-zinc-900 border-b border-zinc-50 dark:border-zinc-900/[0.7] last:border-none first:border-t"> Projects </a> <a href="https://yehudakatz.com/talks/" class="mobile-menu-links py-4 text-lg leading-none font-medium text-one focus:bg-zinc-50 dark:focus:bg-zinc-900 border-b border-zinc-50 dark:border-zinc-900/[0.7] last:border-none first:border-t"> Talks </a> </div> <div class="mobile-social-accounts flex flex-wrap justify-center items-center w-full h-[60px] bg-zinc-50 dark:bg-zinc-900 border-t border-zinc-100/[0.8] dark:border-zinc-800/[0.7] px-6"> <div class="mobile-social-accounts-items flex flex-wrap flex-row gap-5 items-center justify-center sm:justify-end text-three py-2"> <a href="https://twitter.com/@wycats" target="_blank" rel="noopener" aria-label="Twitter X Link" class="mobile-social-accounts-item footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 16 16" width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.52217 6.77491L15.4785 0H14.0671L8.89516 5.88256L4.76437 0H0L6.24656 8.89547L0 16H1.41155L6.87321 9.78782L11.2356 16H16L9.52183 6.77491H9.52217ZM7.58887 8.97384L6.95596 8.08805L1.92015 1.03974H4.0882L8.15216 6.72795L8.78507 7.61374L14.0677 15.0075H11.8997L7.58887 8.97418V8.97384Z" fill="currentColor"/></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Instagram Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M11 3.5h1M4.5.5h6a4 4 0 014 4v6a4 4 0 01-4 4h-6a4 4 0 01-4-4v-6a4 4 0 014-4zm3 10a3 3 0 110-6 3 3 0 010 6z" stroke="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Instagram Link" class="mobile-social-accounts-item footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M4.5 6v5m6 0V8.5a2 2 0 10-4 0V11 6M4 4.5h1M1.5.5h12a1 1 0 011 1v12a1 1 0 01-1 1h-12a1 1 0 01-1-1v-12a1 1 0 011-1z" stroke="currentColor"></path></svg> </a> <a href="https://github.com/hedwik" target="_blank" rel="noopener" aria-label="Github Link" class="mobile-social-accounts-item footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M5.65 12.477a.5.5 0 10-.3-.954l.3.954zm-3.648-2.96l-.484-.128-.254.968.484.127.254-.968zM9 14.5v.5h1v-.5H9zm.063-4.813l-.054-.497a.5.5 0 00-.299.852l.352-.354zM12.5 5.913h.5V5.91l-.5.002zm-.833-2.007l-.466-.18a.5.5 0 00.112.533l.354-.353zm-.05-2.017l.456-.204a.5.5 0 00-.319-.276l-.137.48zm-2.173.792l-.126.484a.5.5 0 00.398-.064l-.272-.42zm-3.888 0l-.272.42a.5.5 0 00.398.064l-.126-.484zM3.383 1.89l-.137-.48a.5.5 0 00-.32.276l.457.204zm-.05 2.017l.354.353a.5.5 0 00.112-.534l-.466.181zM2.5 5.93H3v-.002l-.5.002zm3.438 3.758l.352.355a.5.5 0 00-.293-.851l-.06.496zM5.5 11H6l-.001-.037L5.5 11zM5 14.5v.5h1v-.5H5zm.35-2.977c-.603.19-.986.169-1.24.085-.251-.083-.444-.25-.629-.49a4.8 4.8 0 01-.27-.402c-.085-.139-.182-.302-.28-.447-.191-.281-.473-.633-.929-.753l-.254.968c.08.02.184.095.355.346.082.122.16.252.258.412.094.152.202.32.327.484.253.33.598.663 1.11.832.51.168 1.116.15 1.852-.081l-.3-.954zm4.65-.585c0-.318-.014-.608-.104-.878-.096-.288-.262-.51-.481-.727l-.705.71c.155.153.208.245.237.333.035.105.053.254.053.562h1zm-.884-.753c.903-.097 1.888-.325 2.647-.982.78-.675 1.237-1.729 1.237-3.29h-1c0 1.359-.39 2.1-.892 2.534-.524.454-1.258.653-2.099.743l.107.995zM13 5.91a3.354 3.354 0 00-.98-2.358l-.707.706c.438.44.685 1.034.687 1.655l1-.003zm-.867-1.824c.15-.384.22-.794.21-1.207l-1 .025a2.12 2.12 0 01-.142.82l.932.362zm.21-1.207a3.119 3.119 0 00-.27-1.195l-.913.408c.115.256.177.532.184.812l1-.025zm-.726-.99c.137-.481.137-.482.136-.482h-.003l-.004-.002a.462.462 0 00-.03-.007 1.261 1.261 0 00-.212-.024 2.172 2.172 0 00-.51.054c-.425.091-1.024.317-1.82.832l.542.84c.719-.464 1.206-.634 1.488-.694a1.2 1.2 0 01.306-.03l-.008-.001a.278.278 0 01-.01-.002l-.006-.002h-.003l-.002-.001c-.001 0-.002 0 .136-.482zm-2.047.307a8.209 8.209 0 00-4.14 0l.252.968a7.209 7.209 0 013.636 0l.252-.968zm-3.743.064c-.797-.514-1.397-.74-1.822-.83a2.17 2.17 0 00-.51-.053 1.259 1.259 0 00-.241.03l-.004.002h-.003l.136.481.137.481h-.001l-.002.001-.003.001a.327.327 0 01-.016.004l-.008.001h.008a1.19 1.19 0 01.298.03c.282.06.769.23 1.488.694l.543-.84zm-2.9-.576a3.12 3.12 0 00-.27 1.195l1 .025a2.09 2.09 0 01.183-.812l-.913-.408zm-.27 1.195c-.01.413.06.823.21 1.207l.932-.362a2.12 2.12 0 01-.143-.82l-1-.025zm.322.673a3.354 3.354 0 00-.726 1.091l.924.38c.118-.285.292-.545.51-.765l-.708-.706zm-.726 1.091A3.354 3.354 0 002 5.93l1-.003c0-.31.06-.616.177-.902l-.924-.38zM2 5.93c0 1.553.458 2.597 1.239 3.268.757.65 1.74.88 2.64.987l.118-.993C5.15 9.09 4.416 8.89 3.89 8.438 3.388 8.007 3 7.276 3 5.928H2zm3.585 3.404c-.5.498-.629 1.09-.584 1.704L6 10.963c-.03-.408.052-.683.291-.921l-.705-.709zM5 11v3.5h1V11H5zm5 3.5V13H9v1.5h1zm0-1.5v-2.063H9V13h1z" fill="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="mobile-social-accounts-item footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M.5.5V0a.5.5 0 00-.5.5h.5zm14 0h.5a.5.5 0 00-.5-.5v.5zm0 8l.354.354A.5.5 0 0015 8.5h-.5zm-3 3v.5a.5.5 0 00.354-.146L11.5 11.5zm-5 0V11a.5.5 0 00-.325.12l.325.38zm-3.5 3h-.5a.5.5 0 00.825.38L3 14.5zm0-3h.5A.5.5 0 003 11v.5zm-2.5 0H0a.5.5 0 00.5.5v-.5zM.5 1h14V0H.5v1zM14 .5v8h1v-8h-1zm.146 7.646l-3 3 .708.708 3-3-.708-.708zM11.5 11h-5v1h5v-1zm-5.325.12l-3.5 3 .65.76 3.5-3-.65-.76zM3.5 14.5v-3h-1v3h1zM3 11H.5v1H3v-1zm-2 .5V.5H0v11h1zM10 3v5h1V3h-1zM7 3v5h1V3H7z" fill="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="mobile-social-accounts-item footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M9.5 0v11A3.5 3.5 0 116 7.5m8-2A4.5 4.5 0 019.5 1" stroke="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="mobile-social-accounts-item footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M1.61 12.738l-.104.489.105-.489zm11.78 0l.104.489-.105-.489zm0-10.476l.104-.489-.105.489zm-11.78 0l.106.489-.105-.489zM6.5 5.5l.277-.416A.5.5 0 006 5.5h.5zm0 4H6a.5.5 0 00.777.416L6.5 9.5zm3-2l.277.416a.5.5 0 000-.832L9.5 7.5zM0 3.636v7.728h1V3.636H0zm15 7.728V3.636h-1v7.728h1zM1.506 13.227c3.951.847 8.037.847 11.988 0l-.21-.978a27.605 27.605 0 01-11.568 0l-.21.978zM13.494 1.773a28.606 28.606 0 00-11.988 0l.21.978a27.607 27.607 0 0111.568 0l.21-.978zM15 3.636c0-.898-.628-1.675-1.506-1.863l-.21.978c.418.09.716.458.716.885h1zm-1 7.728a.905.905 0 01-.716.885l.21.978A1.905 1.905 0 0015 11.364h-1zm-14 0c0 .898.628 1.675 1.506 1.863l.21-.978A.905.905 0 011 11.364H0zm1-7.728c0-.427.298-.796.716-.885l-.21-.978A1.905 1.905 0 000 3.636h1zM6 5.5v4h1v-4H6zm.777 4.416l3-2-.554-.832-3 2 .554.832zm3-2.832l-3-2-.554.832 3 2 .554-.832z" fill="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="mobile-social-accounts-item footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M7.5 1c-1.155 0-2.174.412-2.894 1.281-.642.775-1.006 2.35-1.066 3.666l-.073.01-.022.004a8.68 8.68 0 00-.368.059c-.465.089-1.346.326-1.543 1.277-.093.445.011.833.247 1.134.211.269.497.429.708.53.09.041.181.08.274.117-.21.584-.579 1.184-.987 1.728-.382.508-.28 1.153-.083 1.573.197.421.57.402 1.192.43.352.015.722.051 1.09.12.166.03.362.098.606.2.142.06.283.123.423.187.113.052.235.106.374.167.573.25 1.276.517 2.056.517s1.483-.267 2.055-.517c.14-.06.26-.115.375-.167l.025-.012c.135-.06.26-.117.398-.174.243-.103.44-.17.606-.201a7.951 7.951 0 011.09-.12c.622-.028 1.104-.009 1.303-.43.197-.42.298-1.065-.084-1.573-.406-.54-.772-1.136-.983-1.716a5.24 5.24 0 00.305-.127c.216-.098.518-.261.73-.543.245-.326.315-.739.175-1.184-.28-.886-1.092-1.122-1.568-1.216a6.857 6.857 0 00-.355-.058l-.012-.002-.056-.009c-.065-1.234-.41-2.795-1.036-3.581C9.695 1.485 8.682 1 7.5 1z" stroke="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="mobile-social-accounts-item footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M4.5 13.5l3-7m-3.236 3a2.989 2.989 0 01-.764-2V7A3.5 3.5 0 017 3.5h1A3.5 3.5 0 0111.5 7v.5a3 3 0 01-3 3 2.081 2.081 0 01-1.974-1.423L6.5 9m1 5.5a7 7 0 110-14 7 7 0 010 14z" stroke="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="mobile-social-accounts-item footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M7.5 1.5l.121-.485A.5.5 0 007 1.5h.5zm5.5 8c0 .774-.55 1.641-1.583 2.343C10.4 12.533 8.998 13 7.5 13v1c1.696 0 3.294-.525 4.479-1.33C13.148 11.876 14 10.743 14 9.5h-1zM7.5 13c-1.498 0-2.9-.466-3.917-1.157C2.551 11.14 2 10.273 2 9.5H1c0 1.243.852 2.376 2.021 3.17C4.206 13.475 5.804 14 7.5 14v-1zM2 9.5c0-.774.55-1.641 1.583-2.343C4.6 6.467 6.002 6 7.5 6V5c-1.696 0-3.294.525-4.479 1.33C1.852 7.124 1 8.257 1 9.5h1zM7.5 6c1.498 0 2.9.467 3.917 1.157C12.449 7.86 13 8.727 13 9.5h1c0-1.243-.852-2.376-2.021-3.17C10.794 5.525 9.196 5 7.5 5v1zm2.306 4.54c-.69.29-1.32.46-2.306.46v1c1.136 0 1.898-.204 2.694-.54l-.388-.92zM7.5 11c-.987 0-1.617-.17-2.306-.46l-.388.92c.796.336 1.558.54 2.694.54v-1zM8 5.5v-4H7v4h1zm-.621-3.515l4 1 .242-.97-4-1-.242.97zM3.974 6.841c-.286-.855-1.12-1.297-1.952-1.297v1c.51 0 .886.261 1.004.615l.948-.318zM2.022 5.544A2.022 2.022 0 000 7.566h1a1.02 1.02 0 011.022-1.022v-1zM0 7.566C0 8.589.76 9.424 1.74 9.56l.139-.99A1.016 1.016 0 011 7.565H0zm11.974-.407c.118-.354.493-.615 1.004-.615v-1c-.832 0-1.666.442-1.952 1.297l.948.318zm1.004-.615A1.02 1.02 0 0114 7.566h1a2.022 2.022 0 00-2.022-2.022v1zM14 7.566c0 .511-.38.934-.879 1.004l.139.99A2.016 2.016 0 0015 7.567h-1zM12.5 3a.5.5 0 01-.5-.5h-1A1.5 1.5 0 0012.5 4V3zm.5-.5a.5.5 0 01-.5.5v1A1.5 1.5 0 0014 2.5h-1zm-.5-.5a.5.5 0 01.5.5h1A1.5 1.5 0 0012.5 1v1zm0-1A1.5 1.5 0 0011 2.5h1a.5.5 0 01.5-.5V1zM5 9h1V8H5v1zm4 0h1V8H9v1z" fill="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="mobile-social-accounts-item footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M11.5 13.5l-.326.379a.5.5 0 00.342.12L11.5 13.5zm-1.066-1.712a.5.5 0 00-.785.62l.785-.62zm.398-.41l-.174-.468a.672.672 0 00-.02.007l.194.461zm-1.738.516L9.01 11.4l-.008.001.092.492zm-3.104-.012l-.095.49.003.001.092-.491zm-1.762-.515l-.182.465.182-.466zm-.875-.408l-.278.415a.46.46 0 00.033.021l.245-.436zm-.108-.06l.277-.416a.491.491 0 00-.054-.031l-.223.447zm-.048-.036l.353-.354a.502.502 0 00-.11-.083l-.243.437zm2.154 1.52a.5.5 0 00-.785-.62l.785.62zM3.5 13.5l-.016.5a.5.5 0 00.347-.125L3.5 13.5zm-3-2.253H0a.5.5 0 00.006.08l.494-.08zm1.726-8.488l-.3-.4a.5.5 0 00-.168.225l.468.175zM5.594 1.5l.498-.047A.5.5 0 005.605 1l-.01.5zm-.378 1.306a.5.5 0 00.996-.095l-.996.095zm3.526-.063a.5.5 0 00.992.127l-.992-.127zM9.406 1.5L9.395 1a.5.5 0 00-.485.436l.496.064zm3.368 1.259l.468-.175a.5.5 0 00-.168-.225l-.3.4zm1.726 8.488l.494.08a.497.497 0 00.006-.08h-.5zM6.481 8.8l-.5-.008V8.8h.5zm5.019 4.7l.326-.379-.002-.002a.794.794 0 01-.044-.038 21.355 21.355 0 01-.536-.48c-.325-.298-.66-.622-.81-.813l-.785.62c.208.264.603.64.918.93a29.109 29.109 0 00.593.53l.01.008.003.002.327-.378zm.436-3.246c-.46.303-.894.513-1.278.656l.348.937a7.352 7.352 0 001.48-.758l-.55-.835zm-1.297.663a7.387 7.387 0 01-1.629.484l.168.986a8.39 8.39 0 001.848-.548l-.387-.922zm-1.637.485a7.895 7.895 0 01-2.92-.012l-.184.983a8.896 8.896 0 003.288.012l-.184-.983zm-2.917-.011a9.57 9.57 0 01-1.675-.49l-.364.931c.512.2 1.13.402 1.849.54l.19-.981zm-1.675-.49a6.536 6.536 0 01-.813-.378l-.489.872c.326.183.648.324.938.437l.364-.931zm-.78-.358a.802.802 0 00-.108-.061c-.02-.01-.011-.007 0 .001l-.555.832a.87.87 0 00.108.061c.021.01.012.007 0-.002l.556-.83zm-.162-.091a.332.332 0 01.082.058l-.707.707c.023.023.081.08.178.13l.447-.895zm-.028-.026a4.697 4.697 0 01-.28-.168l-.011-.008a.025.025 0 00-.001 0l-.287.41-.286.409.001.001.002.002.007.004.021.014.075.049c.064.04.156.096.273.161l.486-.874zm1.126 1.338c-.152.193-.489.525-.813.829a30.38 30.38 0 01-.538.491l-.034.031-.01.008-.001.002h-.001l.331.375.331.375.001-.001.003-.002.01-.009.036-.032a38.039 38.039 0 00.555-.508c.315-.296.708-.677.915-.94l-.785-.62zM3.516 13c-1.166-.037-1.778-.521-2.11-.96a2.394 2.394 0 01-.4-.82 1.1 1.1 0 01-.013-.056v.002l-.493.08c-.494.08-.494.08-.493.081v.006a1.367 1.367 0 00.028.127 3.394 3.394 0 00.573 1.183c.505.667 1.393 1.31 2.876 1.357l.032-1zM1 11.247c0-1.867.42-3.94.847-5.564a35.45 35.45 0 01.776-2.552 16.43 16.43 0 01.067-.186l.004-.01v-.001l-.468-.175-.469-.175v.001l-.001.003-.004.011a9.393 9.393 0 00-.072.2 36.445 36.445 0 00-.8 2.629C.443 7.083 0 9.253 0 11.247h1zm1.526-8.088c.8-.6 1.577-.89 2.15-1.03a4.764 4.764 0 01.86-.128A1.48 1.48 0 015.585 2h-.001l.01-.5.01-.5H5.6a.848.848 0 00-.028 0h-.068a3.973 3.973 0 00-.24.016 5.763 5.763 0 00-.825.141 6.938 6.938 0 00-2.513 1.2l.6.8zm2.57-1.612l.12 1.259.996-.095-.12-1.258-.996.094zM9.734 2.87l.168-1.306-.992-.128-.168 1.307.992.127zM9.406 1.5l.01.5h-.001a.497.497 0 01.049 0c.038.002.1.005.179.013.16.014.394.047.681.117a5.94 5.94 0 012.15 1.029l.6-.8a6.937 6.937 0 00-2.513-1.2 5.76 5.76 0 00-.825-.142A3.98 3.98 0 009.399 1h-.003l.01.5zm3.368 1.259l-.469.174.001.003.004.009.013.037.053.149a35.482 35.482 0 01.777 2.552c.428 1.624.847 3.697.847 5.564h1c0-1.994-.444-4.164-.88-5.819a36.512 36.512 0 00-.8-2.629 15.246 15.246 0 00-.057-.158l-.015-.042-.004-.01-.001-.004-.47.174zm1.726 8.488l-.493-.08v-.003l-.002.008-.01.047c-.012.045-.03.113-.061.197-.062.17-.167.396-.34.624-.332.439-.944.923-2.11.96l.032 1c1.483-.047 2.37-.69 2.876-1.356a3.395 3.395 0 00.573-1.184 2.05 2.05 0 00.026-.118l.002-.01v-.004c0-.001 0-.002-.493-.081zM5.259 6.97c-1.002 0-1.723.867-1.723 1.83h1c0-.498.357-.83.723-.83v-1zM3.536 8.8c0 .967.736 1.83 1.723 1.83v-1c-.357 0-.723-.334-.723-.83h-1zm1.723 1.83c1 0 1.722-.866 1.722-1.83h-1c0 .5-.357.83-.722.83v1zM6.98 8.81c.016-.978-.728-1.84-1.722-1.84v1.001c.372 0 .73.338.722.822l1 .017zm2.653-1.84c-1.002.001-1.723.868-1.723 1.831h1c0-.498.357-.83.723-.83v-1zM7.91 8.802c0 .967.736 1.83 1.723 1.83v-1c-.357 0-.723-.334-.723-.83h-1zm1.723 1.83c1 0 1.722-.866 1.722-1.83h-1c0 .5-.357.83-.722.83v1zm1.722-1.83c0-.963-.721-1.83-1.722-1.83v1c.365 0 .722.332.722.83h1zM3.74 4.44c1.443-.787 2.619-1.154 3.763-1.155 1.145 0 2.318.365 3.758 1.154l.48-.876c-1.522-.835-2.865-1.279-4.238-1.278-1.373 0-2.717.445-4.241 1.277l.478.878z" fill="currentColor"></path></svg> </a> </div> </div> </div> <nav class="post-nav flex items-center justify-center -translate-y-[61px] sm:-translate-y-[71px] h-16 sm:h-[74px] transition-transform duration-300 ease-in-out fixed top-0 w-full bg-white dark:bg-zinc-900 !z-[999999] px-4 sm:px-6"> <div class="post-nav-content w-full flex justify-between items-center max-w-screen-lg gap-3"> <div class="post-nav-left flex items-center justify-start gap-3 xs:gap-4"> <h4 class="post-nav-title text-one font-semibold text-base sm:text-lg line-clamp-1">Exploring Ember Polaris: A Fresh Take on the Component Format</h4> </div> <button type="button" class="post-nav-button share-button size-9 min-w-9 max-w-9 sm:px-4 sm:h-9 sm:w-fit sm:max-w-fit flex items-center justify-center gap-2 text-two border border-zinc-200/70 bg-zinc-100 dark:bg-zinc-800 dark:border-zinc-700/70 hover:opacity-80 transition-opacity duration-300 rounded-full"> <svg class="copying" width="17px" height="17px" stroke-width="2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="currentColor"><path d="M19.4 20H9.6C9.26863 20 9 19.7314 9 19.4V9.6C9 9.26863 9.26863 9 9.6 9H19.4C19.7314 9 20 9.26863 20 9.6V19.4C20 19.7314 19.7314 20 19.4 20Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M15 9V4.6C15 4.26863 14.7314 4 14.4 4H4.6C4.26863 4 4 4.26863 4 4.6V14.4C4 14.7314 4.26863 15 4.6 15H9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg> <svg class="copied hidden" width="17px" height="17px" stroke-width="2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="currentColor"><path d="M5 13L9 17L19 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg> <span class="post-nav-button-text text-current font-medium text-sm hidden sm:block">Copy Link</span> </button> </div> <div class="scroll-line-content absolute bottom-0 left-0 w-full h-[3px] bg-zinc-200/30 dark:bg-zinc-200/5"> <div class="scroll-line fixed h-[3px] rounded-r-sm bg-ghost-accent bottom-0 before:absolute before:content-[''] before:inset-0 before:size-full before:opacity-30 before:shadow-[0px_0.5px_4px_var(--ghost-accent-color)]"></div> </div> </nav> <style> .scroll-line { transition: 0.5s cubic-bezier(0.075, 0.82, 0.165, 1) width; z-index: 1; } </style> <div class="native-post-content xs:my-16 mx-auto my-12 flex w-full flex-col items-center justify-start gap-2 px-4 sm:my-24 md:max-w-2xl lg:px-0"> <div class="post-heading post-heading-centered xs:gap-6 flex w-full flex-col items-start justify-start gap-4 sm:items-center md:gap-8"> <div class="post-info text-four flex flex-row items-center justify-start gap-3 sm:justify-center"> <span class="post-heading-centered-date text-md text-left font-normal sm:text-center">08 Sep 2024</span> <i class="separator cursor-default text-[11px] !text-zinc-400 dark:!text-zinc-600">•</i> <span class="post-heading-centered-reading-time text-md text-left font-normal sm:text-center">10 min read</span> </div> <h1 class="post-title xxs:text-3xl text-one w-full text-left text-[28px] font-bold !leading-[1.2] sm:text-center sm:text-4xl md:text-5xl">Exploring Ember Polaris: A Fresh Take on the Component Format</h1> <p class="post-excerpt text-two w-full text-left text-lg leading-paragraph sm:text-center md:text-xl">What if you could put multiple "single-file components" into one file? Without JSX Spaghetti? Ember's <template> syntax has already landed, and it's kind of the best of both worlds.</p> <div class="post-authors hidden sm:block"> <div class="c-author-area xs:flex -ml-1 hidden select-none flex-row items-center"> <a href="/author/wycats/" aria-label="Yehuda Katz" class="c-author-area-item flex flex-row rounded-full gap-0 first:order-1 first:mr-1 [&:nth-child(3)]:order-2 [&:nth-child(3)]:z-10 [&:nth-child(3)]:-ml-5 [&:nth-child(3)]:mr-1.5 "> <figure class="c-author-area-image aspect-[1/1] overflow-hidden rounded-full border-white bg-zinc-100 hover:z-20 dark:border-zinc-950 dark:bg-zinc-900 h-10 w-10 border-4 "> <img class="c-author-area-image-img lazyhedwik-image h-full w-full rounded-full object-cover object-center" src="/content/images/size/w90/format/webp/2021/04/Profile---Correct.png" data-src="/content/images/size/w180/format/webp/2021/04/Profile---Correct.png" alt="Yehuda Katz" /> </figure> </a> <a href="/author/wycats/" aria-label="Yehuda Katz" class="c-author-area-button author-1 wycats [&:nth-child(2)]:order-3 [&:nth-child(4)]:order-4 [&:nth-child(4)]:before:inline-block [&:nth-child(4)]:before:content-['&'] [&:nth-child(4)]:before:!no-underline [&:nth-child(4)]:before:hover:text-two [&:nth-child(4)]:before:mr-1.5 [&:nth-child(4)]:ml-1.5 text-base [&:nth-child(4)]:before:opacity-50 text-two hover:underline before:decoration-transparent underline-offset-6 decoration-dotted decoration-transparent hover:text-ghost-accent hover:decoration-ghost-accent font-medium transition-colors duration-300">Yehuda Katz</a> </div> </div> <div class="post-image aspect-[12/7] h-fit w-full select-none overflow-hidden bg-zinc-50 sm:rounded-2xl dark:bg-zinc-900"> </div> </div> <div class="content"> <p>For the most part, the component format in web frameworks falls into one of two camps.</p><ol><li><strong>Single File Components</strong>, which provide an HTML-based template syntax, a declarative way to handle conditionals and loops, and other syntax extensions (like directives) that feel right at home in HTML. <a href="https://vuejs.org/guide/scaling-up/sfc.html?ref=yehudakatz.com" rel="noreferrer"><em>Vue</em></a><strong> </strong>and <a href="https://svelte.dev/docs/svelte-components?ref=yehudakatz.com" rel="noreferrer"><em>Svelte</em></a> are two major frameworks that use Single File Components.</li><li><strong>JSX</strong>, which provides a way to build up fragments of HTML using an XML-like syntax embedded in JavaScript. </li></ol><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">💡</div><div class="kg-callout-text"><a href="https://www.solidjs.com/?ref=yehudakatz.com" rel="noreferrer">Solid</a> uses JSX syntax in a novel way, and <a href="https://angular.dev/?ref=yehudakatz.com" rel="noreferrer">Angular</a> attaches templates to classes via decorators. Both are interesting approaches, and outside the scope of this post. If there's interest, I may write a follow-up article that delves into other interesting approaches used by frameworks to combine the JavaScript implementation of a component with its HTML representation.</div></div><p>While only React-like frameworks use this approach, <em>React-like frameworks are extremely popular.</em> This can give the impression that there's a strong ecosystem consensus around JSX. In reality, unless a new framework is explicitly trying to visually resemble React, it <em>almost always chooses a templating language</em>.</p><p>When explaining the choice to use templates, frameworks usually give a sales pitch that has something to do with <em>performance</em>. But that's <em>not</em> the primary reason that frameworks are attracted to templating languages.</p><h2 id="template-languages-are-good-actually">Template Languages Are Good, Actually</h2><p>So why do new frameworks tend to choose templating engines when the conventional wisdom is that JSX proved that templates are old and busted?</p><p><strong>1. As a user of these frameworks, you can start with static HTML and layer dynamism on top, rather than learning and remembering a whole new syntax and semantics for static content.</strong></p> <figure class="kg-card kg-code-card"><pre><code class="language-html"><img class="hero" src="/assets/hero.png"> <form> <input value="hello world" style="--theme: red"> </form></code></pre><figcaption><p><span style="white-space: pre-wrap;">This just works. No lectures about eating your syntax vegetables required.</span></p></figcaption></figure><p>Templating languages are typically built on HTML, which means that you can start with valid HTML and layer dynamic bits on top. <em>In fact, you can tell that you're looking at a template engine if you can grab some valid, static HTML, paste it into your file, and expect it to work.</em></p><div class="kg-card kg-toggle-card" data-kg-toggle-state="close"> <div class="kg-toggle-heading"> <h4 class="kg-toggle-heading-text"><span style="white-space: pre-wrap;">🕵 A Closer Look: Attributes, Web Components and SVG</span></h4> <button class="kg-toggle-card-icon" aria-label="Expand toggle to read content"> <svg id="Regular" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path class="cls-1" d="M23.25,7.311,12.53,18.03a.749.749,0,0,1-1.06,0L.75,7.311"></path> </svg> </button> </div> <div class="kg-toggle-content"><p><span style="white-space: pre-wrap;">Because templating engines use HTML as their foundation, it is very easy to write an HTML attribute in a templating language (an attribute in a templating language is spelled... like an attribute).</span></p><p><span style="white-space: pre-wrap;">Web components and SVG tags </span><i><em class="italic" style="white-space: pre-wrap;">just work</em></i><span style="white-space: pre-wrap;">, and not because of a pile of brittle heuristics that a project whack-a-mole'd into existence to address bug reports that kept cropping up. </span></p><p><code spellcheck="false" style="white-space: pre-wrap;"><span>htmlFor</span></code><span style="white-space: pre-wrap;"> and </span><code spellcheck="false" style="white-space: pre-wrap;"><span>className</span></code><span style="white-space: pre-wrap;"> in React are glaring reminders of the fact that you're writing embedded XML rather than HTML, but they're just the tip of the iceberg. JSX adherents do a great job of convincing the world that "it doesn't make that much of a difference," but </span><a href="https://www.google.com/search?q=jsx+web+components+svg+react+github+issue&ref=yehudakatz.com" rel="noreferrer"><span style="white-space: pre-wrap;">it really does</span></a><span style="white-space: pre-wrap;">.</span></p></div> </div><p><strong>2. You can understand the whole component's structure by reading its template from top to bottom.</strong></p> <figure class="kg-card kg-code-card"><pre><code class="language-svelte">{#if itJustWorks} <p>It Just Works!</p> {/if} <ul> {#each thingsThatWork as thing} <li>{thing} also just works!</li> {/each} </ul></code></pre><figcaption><p><span style="white-space: pre-wrap;">Conditionals and loops are interleaved with your markup (using Svelte syntax as an example)</span></p></figcaption></figure><p>Conditionals and loops are fundamental concepts in programming, and templating engines <a href="https://svelte.dev/docs/logic-blocks?ref=yehudakatz.com#if" rel="noreferrer">always</a> <a href="https://svelte.dev/docs/logic-blocks?ref=yehudakatz.com#each" rel="noreferrer">have</a> <a href="https://vuejs.org/guide/essentials/conditional.html?ref=yehudakatz.com" rel="noreferrer">built-in</a> <a href="https://vuejs.org/guide/essentials/list.html?ref=yehudakatz.com" rel="noreferrer">syntax</a> <a href="https://angular.dev/essentials/conditionals-and-loops?ref=yehudakatz.com#for-block" rel="noreferrer">for</a> <a href="https://angular.dev/essentials/conditionals-and-loops?ref=yehudakatz.com#if-block" rel="noreferrer">conditionals</a> <a href="https://guides.emberjs.com/v5.11.0/components/conditional-content/?ref=yehudakatz.com" rel="noreferrer">and</a> <a href="https://guides.emberjs.com/v5.11.0/components/looping-through-lists/?ref=yehudakatz.com" rel="noreferrer">loops</a>. This makes it possible to look at a component's template and understand what it's doing in one shot.</p><p><strong>3. You write your component's setup logic in one place and it runs exactly one time.</strong> Your rendering logic takes the state that you set up and turns it into HTML that remains up to date as reactive data changes.</p> <figure class="kg-card kg-code-card"><pre><code class="language-html"><script setup> const count = ref(0) const increment = () => count.value++; </script> <template> <p>The count is {{count}}.</p> <button @click="count++">Increment</button> </template></code></pre><figcaption><p><span style="white-space: pre-wrap;">The setup is in one place. The rendering logic is in another. (using Vue syntax as an example)</span></p></figcaption></figure><p>In contrast, your setup logic in a hooks-based framework is a subtle consequence of the way each of the hooks that you use in your component behaves during setup, rendering and in other parts of the lifecycle.</p><figure class="kg-card kg-code-card"><pre><code class="language-jsx">function Counter() { // This _function_ runs over and over again, but the // initialization of `count` to `0` happens only once const [count, setCount] = useState(0); const increment = () => setCount(count + 1); return (<> <p>The count is {count}.</p> <button onClick={increment}>Increment</button> </>) }</code></pre><figcaption><p><span style="white-space: pre-wrap;">Pop Quiz: Did you need useCallback here? Is the useEvent RFC relevant?</span></p></figcaption></figure><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">🔍</div><div class="kg-callout-text">Did you spot the subtle bug in <code spellcheck="false" style="white-space: pre-wrap;">increment</code>? If you did, you're probably screaming 😡 at me at the top of your lungs. But maybe it would just be better if the setup logic was separate? 🤔</div></div><p>The charitable point of view is that hooks are <a href="https://github.com/facebook/react/issues/14514?ref=yehudakatz.com#issuecomment-450983895" rel="noreferrer">high-level, declarative building blocks</a>, and you're <a href="https://medium.com/@sahil90085/useeffect-double-mounting-in-strict-mode-1beb339f1919?ref=yehudakatz.com" rel="noreferrer">not supposed to think about</a> how they behave during each part of the lifecycle. </p><p>But people <strong>do</strong> <a href="https://github.com/facebook/react/issues/14514?ref=yehudakatz.com#issue-395105006" rel="noreferrer">want to think about</a> the "mount" phase of their components. They end up twisting themselves into pretzels to try to understand and control the setup phase.</p><p>At the end of the day, this mismatch makes hooks <em>feel </em>significantly more complex and <a href="https://stackoverflow.com/questions/59492626/stop-useeffect-from-running-on-mount?ref=yehudakatz.com" rel="noreferrer">leads to far more confusion</a> <a href="https://blog.logrocket.com/when-not-to-use-usememo-react-hook/?ref=yehudakatz.com" rel="noreferrer">and debate</a> than the designers of hooks intended.</p><h2 id="some-issues-with-single-file-components">Some Issues With Single-File Components</h2><p>While those benefits may seem like a slam dunk argument for SFCs, JSX proponents have some <a href="https://dev.to/ryansolid/why-i-m-not-a-fan-of-single-file-components-3bfl?ref=yehudakatz.com" rel="noreferrer">strong</a> <a href="https://dev.to/ryansolid/why-i-m-not-a-fan-of-single-file-components-3bfl?ref=yehudakatz.com" rel="noreferrer">arguments</a> against them.</p><p>The SFC format is, indeed, quite ergonomic. But it has some major downsides.</p><p><strong>1. Extracting a small helper component adds a lot of friction.</strong></p> <p>Even if you think that components should <em>ultimately</em> end up in their own files, it's really nice to be able to sketch out a few related components in a single file while developing them.</p><p><strong>2. The syntax for invoking components in tests is very different from the syntax for invoking them in an application.</strong></p> <p>This <a href="https://test-utils.vuejs.org/guide/essentials/passing-data.html?ref=yehudakatz.com" rel="noreferrer">isn't</a> <a href="https://testing-library.com/docs/svelte-testing-library/example/?ref=yehudakatz.com#basic" rel="noreferrer">so bad</a> if you're just invoking your component with props.</p><p>But if you want to test <a href="https://test-utils.vuejs.org/guide/advanced/slots.html?ref=yehudakatz.com" rel="noreferrer">advanced</a> features, like directives or <a href="https://testing-library.com/docs/svelte-testing-library/example/?ref=yehudakatz.com#slots" rel="noreferrer">slots</a> (especially if you want to test <a href="https://testing-library.com/docs/svelte-testing-library/faq/?ref=yehudakatz.com#why-isnt-onmount-called-when-rendering-components" rel="noreferrer">real-world</a> uses, like <a href="https://test-utils.vuejs.org/guide/advanced/slots.html?ref=yehudakatz.com#Scoped-Slots" rel="noreferrer">scoped</a> slots or passing <a href="https://test-utils.vuejs.org/guide/advanced/slots.html?ref=yehudakatz.com#Advanced-Usage" rel="noreferrer">dynamic content</a> to slots), it gets a lot uglier.</p><figure class="kg-card kg-code-card"><pre><code class="language-ts">test('layout full page layout', () => { const wrapper = mount(Layout, { slots: { header: Header, main: h('div', 'Main Content'), sidebar: { template: '<div>Sidebar</div>' }, footer: '<div>Footer</div>' } }) // ... })</code></pre><figcaption><p><a href="https://test-utils.vuejs.org/guide/advanced/slots.html?ref=yehudakatz.com#Advanced-Usage" rel="noreferrer"><span style="white-space: pre-wrap;">An example</span></a><span style="white-space: pre-wrap;"> from the Vue Test Utils docs. This isn't the end of the world, but it's pretty different from how you'd write this if you were invoking </span><code spellcheck="false" style="white-space: pre-wrap;"><span>Layout</span></code><span style="white-space: pre-wrap;"> in an app.</span></p></figcaption></figure><hr><p>Beyond the ugliness, the fact that <strong>you can't just use the syntax you already know</strong> to invoke your components in test files means that you need to learn a second syntax (with its own set of quirks) to test components robustly.</p><p>Ultimately, this means that the investment a framework makes into the ergonomics of Single File Components works <strong>really well</strong> in single file component files. But when you're not inside of a Single File Component file, all of that ergonomic investment disappears in a puff of smoke.</p><p>All things being equal, I would still personally prefer Single File Components to JSX any day of the week.</p><p>But what if we could combine the benefits of Single File Components with the ability to naturally have multiple components in a single file? </p><p><strong>That's the point of the </strong><code>.gjs</code><strong> format in the Polaris Edition of Ember.</strong></p><h2 id="components-in-polaris">Components in Polaris</h2><h3 id="starting-small-lets-add-some-imports">Starting Small: Let's Add Some Imports</h3><p>The design of the new Polaris component format in Polaris started out as a way to get <code>import</code>s into templates.</p><div class="kg-card kg-toggle-card" data-kg-toggle-state="close"> <div class="kg-toggle-heading"> <h4 class="kg-toggle-heading-text"><span style="white-space: pre-wrap;">📖 A Little Bit of History</span></h4> <button class="kg-toggle-card-icon" aria-label="Expand toggle to read content"> <svg id="Regular" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path class="cls-1" d="M23.25,7.311,12.53,18.03a.749.749,0,0,1-1.06,0L.75,7.311"></path> </svg> </button> </div> <div class="kg-toggle-content"><p><span style="white-space: pre-wrap;">In the Octane Edition of Ember, templates were in their own </span><code spellcheck="false" style="white-space: pre-wrap;"><span>.hbs</span></code><span style="white-space: pre-wrap;"> files. When you invoke the </span><code spellcheck="false" style="white-space: pre-wrap;"><span><Hello /></span></code><span style="white-space: pre-wrap;"> component, Ember auto-imports the component from </span><code spellcheck="false" style="white-space: pre-wrap;"><span>app/components/hello.hbs</span></code><span style="white-space: pre-wrap;">. </span></p><p><span style="white-space: pre-wrap;">We wanted a way to import </span><code spellcheck="false" style="white-space: pre-wrap;"><span>Hello</span></code><span style="white-space: pre-wrap;"> explicitly, and that ultimately evolved into the Polaris component format.</span></p><p><span style="white-space: pre-wrap;">This is why the library that implements this feature is called </span><a href="https://github.com/ember-template-imports/ember-template-imports?ref=yehudakatz.com" rel="noreferrer"><span style="white-space: pre-wrap;">ember-template-imports</span></a><span style="white-space: pre-wrap;"> and why people sometimes call the feature "Template Imports".</span></p></div> </div><p>The most basic way to use the new format looks exactly like that: a traditional Ember template plus some imports.</p><figure class="kg-card kg-code-card"><pre><code class="language-ts">import Accordion from "./accordion"; <template> <Accordion> <:header>I can expand and contract!</:header> <:body> Here's a lot of information that you can expand or contract. </:body> </Accordion> </template></code></pre><figcaption><p><span style="white-space: pre-wrap;">app/components/info.gjs</span></p></figcaption></figure><p>What's nice about this simple way of using <code><template></code> is that it looks very similar to a traditional Single File Component. But this is just the <em>beginning</em> of the story.</p><h3 id="local-helper-components">Local Helper Components</h3><p>The second way to use the <code><template></code> syntax is as a JavaScript expression.</p><figure class="kg-card kg-code-card"><pre><code class="language-ts"><template> <Hello>Tom</Hello> </template> const Hello = <template> <p>Hello, {{yield}}</p> </template></code></pre><figcaption><p><span style="white-space: pre-wrap;">The first </span><code spellcheck="false" style="white-space: pre-wrap;"><span><template></span></code><span style="white-space: pre-wrap;"> is the default export of the module. It uses the </span><code spellcheck="false" style="white-space: pre-wrap;"><span>Hello</span></code><span style="white-space: pre-wrap;"> component, which is defined by the second </span><code spellcheck="false" style="white-space: pre-wrap;"><span><template></span></code><span style="white-space: pre-wrap;">.</span></p></figcaption></figure><p>In Polaris-style components, the <code><Hello></code> invocation in a template refers to an <em>in-scope variable.</em> You can define a simple helper component by assigning a <code><template></code> to a variable.</p><div class="kg-card kg-toggle-card" data-kg-toggle-state="close"> <div class="kg-toggle-heading"> <h4 class="kg-toggle-heading-text"><span style="white-space: pre-wrap;">🕵 Local Variables: A Closer Look</span></h4> <button class="kg-toggle-card-icon" aria-label="Expand toggle to read content"> <svg id="Regular" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path class="cls-1" d="M23.25,7.311,12.53,18.03a.749.749,0,0,1-1.06,0L.75,7.311"></path> </svg> </button> </div> <div class="kg-toggle-content"><p><span style="white-space: pre-wrap;">You can even use lowercase names for your components: as long as the name after the </span><code spellcheck="false" style="white-space: pre-wrap;"><span><</span></code><span style="white-space: pre-wrap;"> refers to an in-scope variable, it's treated like a component. That said, </span><i><em class="italic" style="white-space: pre-wrap;">we recommend that you avoid using variable names that could be confused with HTML</em></i><span style="white-space: pre-wrap;">, and </span><code spellcheck="false" style="white-space: pre-wrap;"><span>ember-template-lint</span></code><span style="white-space: pre-wrap;">'s recommended config includes a rule that disallows it.</span></p></div> </div><hr><p>But there's more. You can also use <code><template></code> expressions in tests! Because of course you can.</p><figure class="kg-card kg-code-card"><pre><code class="language-ts">import Avatar from 'app/components/avatar'; test('renders name argument', async function (assert) { const initial = 'Zoey'; await render( <template> <Avatar @title="Picture of Zoey" @initial={{initial}} /> </template> ); assert.dom().hasText(initial); });</code></pre><figcaption><p><span style="white-space: pre-wrap;">An example from the </span><a href="https://guides.emberjs.com/release/components/template-tag-format/?ref=yehudakatz.com#toc_testing" rel="noreferrer"><span style="white-space: pre-wrap;">official Ember docs</span></a><span style="white-space: pre-wrap;">.</span></p></figcaption></figure><p>This also allows you to <em>define constants in your test file</em> and use them directly in your tests.</p><p>Most importantly, this use of <code><template></code> is literally identical to the way it's used in files implementing components. There's no special syntax for attributes, arguments or modifiers.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">🌟</div><div class="kg-callout-text"><b><strong style="white-space: pre-wrap;">If you can do it in a component implementation, you can do it in a test with the same syntax.</strong></b></div></div><h3 id="adding-state"><strong>Adding State</strong></h3><p>You can also use <code><template></code> to define the template for a <strong>stateful component</strong>.</p><pre><code class="language-ts">import Component from "@glimmer/components"; export default class extends Component { @tracked count = 0; increment = () => this.count++; <template> <p>{{this.count}}</p> <button {{on "click" this.increment}}>++</button> </template> }</code></pre><p>When you use <code><template></code> in this way, the template becomes the "render function" of the component, and <code>this</code> refers to an instance of the component class.</p><div class="kg-card kg-toggle-card" data-kg-toggle-state="close"> <div class="kg-toggle-heading"> <h4 class="kg-toggle-heading-text"><span style="white-space: pre-wrap;">🕵 Why Does Ember Use Classes Here?</span></h4> <button class="kg-toggle-card-icon" aria-label="Expand toggle to read content"> <svg id="Regular" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path class="cls-1" d="M23.25,7.311,12.53,18.03a.749.749,0,0,1-1.06,0L.75,7.311"></path> </svg> </button> </div> <div class="kg-toggle-content"><p><span style="white-space: pre-wrap;">Templating engines always provide a way to separate a component's setup logic ("instantiation") and per-instance state from the component's "render function," which converts component state into HTML.</span></p><p><span style="white-space: pre-wrap;">Since JavaScript classes are a built-in way to describe objects that can be instantiated, Ember uses classes to represent these sorts of stateful objects. </span></p><p><b><strong style="white-space: pre-wrap;">We think leaning on standard JavaScript features rather than inventing a custom syntax for setup logic is a big win.</strong></b></p><p><span style="white-space: pre-wrap;">In most cases, you use regular functions or standalone </span><code spellcheck="false" style="white-space: pre-wrap;"><span><template></span></code><span style="white-space: pre-wrap;"> when your code is not stateful. </span></p></div> </div><h2 id="smooth-interoperability">Smooth Interoperability</h2><p>If you lived through the migration from class-based components to hooks in the React ecosystem, you might be thinking "this stuff is cool, but there's no way I can use it, because it will take me forever to migrate all of my components to the new format, and I don't have the time for that."</p><p>That couldn't be further from the truth.</p><p>Because of the way Ember's rendering engine works, <strong>you'll be able to start using the new format right away, and it will interoperate seamlessly with all of your existing components.</strong></p><p>Some examples:</p><ol><li>You can import an <code>.hbs</code> component into a <code>.gjs</code> file and invoke it directly.</li><li>You can put a <code>.gjs</code> component in your <code>app/components</code> directory and it will be auto-imported into <code>.hbs</code> components.</li><li>You can put a <code><template></code> in a component with the classic <code>@ember/component</code> base class and it will work.</li><li><strong>You can interleave <code>.hbs</code> and <code>.gjs</code> components in any "zebra-striping" configuration you want.</strong> This allows you to write new components using <code>.gjs</code> and interoperate perfectly with existing components. <em>It also allows you to migrate any component in your hierarchy to <code>.gjs</code>. You don't need to think about whether your component is a "leaf" or anything like that before choosing to migrate.</em></li></ol><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">💡</div><div class="kg-callout-text">An example of "zebra striping": you can invoke a <code spellcheck="false" style="white-space: pre-wrap;">.gjs</code> component from an <code spellcheck="false" style="white-space: pre-wrap;">.hbs</code> component, yield back to the <code spellcheck="false" style="white-space: pre-wrap;">.hbs</code> component, have the yielded code invoke another <code spellcheck="false" style="white-space: pre-wrap;">.gjs</code> component, which invokes an <code spellcheck="false" style="white-space: pre-wrap;">.hbs</code> component. <b><strong style="white-space: pre-wrap;">Any combination works.</strong></b></div></div><hr><p>From the perspective of the Ember rendering engine, <code>.gjs</code> and <code>.hbs</code> components are just <em>different implementations of the same low-level component protocol</em>. </p><p>This is also true of components using <code>@ember/component</code> and <code>@glimmer/component</code>, which is why you can still interoperate classic components and Glimmer components today.</p><p>In the abstract, this may seem like a minor detail. But in practice, it means that <strong>you don't need to wait to have time for a big migration process to use the new <code>.gjs</code> format.</strong></p><p>You can follow the <a href="https://guides.emberjs.com/release/components/template-tag-format/?ref=yehudakatz.com#toc_installation" rel="noreferrer">installation instructions</a> on any Ember app running Ember 3.28 or newer, and begin using the new format in new code, without worrying about interop.</p><h2 id="subtle-ember-wins">Subtle Ember Wins</h2><p>If you're a long-term Ember user, something that might not immediately jump out at you is the fact that this change makes it possible to write "helpers" as simple functions in the same file as the template.</p><p>This is more than just a nice ergonomic win: it means that "helpers" aren't really a thing anymore.</p><p>If you want to use a function in your template, <strong>you write a function</strong> in your component and call it.</p><p>If you want to extract it into another file, <strong>you move it into another file and import it</strong>.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">💡</div><div class="kg-callout-text">This is more of an improvement to an Ember annoyance than a benefit of the <code spellcheck="false" style="white-space: pre-wrap;">.gjs</code> format. But it's such a big improvement to the Ember programming model that I wanted to call it out.</div></div><h2 id="coda-its-polish-time">Coda: It's Polish Time</h2><p>If the <code>.gjs </code>format works as far back as Ember 3.28, why are we calling it a "Polaris" feature?</p><p>The truth is: while <code>.gjs</code> has worked for quite some time now, the new format regressed on ergonomics when it first landed.</p><p>In order to make the <code>.gjs</code> format feel like a clear improvement, we needed to invest in solid developer tooling, clear documentation, and making it straight-forward to use most add-ons in a <code><template></code>.</p><p>It also took some effort to get <code>.gjs</code> and <code>.gts</code> support into GitHub's syntax highlighter and to implement support for other popular syntax highlighters.</p><p>The bulk of those problems are now resolved, and <em>I personally would recommend using <code>.gjs </code> for new components in modern Ember apps</em> without much reservation.</p><hr><p>The next step is to smooth out the experience of migrating existing components to the <code>.gjs </code>format.</p><p>This includes:</p><ul><li>solid codemods</li><li>inclusion in all popular syntax highlighters <em>by default</em></li><li>clear documentation of <code>.gjs</code> usage in the vast majority of maintained addons</li></ul><p>Coordinating that sort of polish effort across the ecosystem is why we have "editions," and the <code>.gjs</code> format is one of the primary pillars of the Polaris Edition.</p><p>But the syntax itself is already stable (and has been for years at this point). If you start using <code>.gjs</code> in your Ember app, you can trust that the syntax will remain stable.</p><p>Adopting <code>.gjs</code> right now means you're a <a href="https://youtu.be/9zc4DSTRGeM?si=js1hH2lqS_fhLTdj&ref=yehudakatz.com" rel="noreferrer">pioneer</a>. It means you're willing to put up with some ergonomic growing pains in exchange for being one of the first people who get to experience the future of Ember. It means you're willing to experience a feature that is not completely polished so that you can help make the future more hospitable for everyone else.</p><p><em>That's how I like to roll in my personal projects.</em></p><p>That said, I think it makes a lot of sense for projects with full time teams to wait for fully polished features before adopting them.</p><p>Either way, if you're still reading, you're almost certainly the kind of person who should <em>play around</em> with the new format, at least for a weekend project or with <a href="https://limber.glimdown.com/?ref=yehudakatz.com" rel="noreferrer">Limber</a>.</p><p>Have fun! And let's work together to build a better future for all of us.</p> </div> <div class="c-post-footer post-footer flex flex-col gap-9 items-start justify-start w-full"> <div class="c-post-footer-unless-member lazyhedwik-element relative flex flex-col sm:flex-row items-start sm:items-center py-6 px-7 gap-4 justify-between w-full h-fit bg-transparent after:absolute after:z-[-2] after:inset-0 after:w-full after:h-full after:opacity-[0.06] after:border-[1.6px] after:border-ghost-accent after:rounded-2xl before:absolute before:z-[-1] before:inset-0 before:w-full before:h-full before:bg-ghost-accent before:rounded-2xl before:opacity-[0.036] dark:before:bg-zinc-900 dark:before:opacity-70 dark:after:border-zinc-800 dark:after:opacity-80 rounded-xl select-none"> <div class="c-post-footer-unless-member-content flex w-full flex-col xs:flex-row items-center gap-3 xxs:gap-4 xs:gap-5 justify-center xs:justify-start"> <div class="cta-favicon"> <img src="https://yehudakatz.com/content/images/2024/08/yehuda-square.png" alt="Katz Got Your Tongue" class="aspect-[1/1] min-w-[64px] min-h-[64px] w-16 h-16"> </div> <div class="c-post-footer-texts flex flex-col items-center justify-start xs:items-start xs:justify-start gap-2"> <h5 class="c-post-footer-head text-lg font-bold leading-none text-one text-center xs:text-left line-clamp-1">Enjoy this Post?</h5> <span class="c-post-footer-description text-sm leading-subheading text-three text-center xs:text-left sm:line-clamp-1">Dive into the unlimited – upgrade to premium now!</span> </div> </div> <a href="javascript:" data-portal="signup" class="c-post-footer-button secondary-button w-full sm:max-w-[120px] h-10 min-h-10 max-h-10 font-medium rounded-full">✦ Sign up</a> </div> <div class="c-post-footer-tags lazyhedwik-element tag-cloud flex items-center justify-start flex-wrap gap-3 w-full h-fit"> <a href="/tag/polaris/" class="c-post-footer-tag-item tag-cloud-item active:scale-[0.98] shadow-sm border border-zinc-200 hover:border-zinc-300 dark:border-zinc-800 dark:hover:border-zinc-700 transition flex items-center justify-center pt-2 pb-[7px] px-[9px] text-xs font-medium text-four rounded-md uppercase tracking-widest leading-none no-underline">Polaris</a> </div> <hr class="c-post-footer-divider border-t border-zinc-100/[0.8] dark:border-zinc-800/[0.8] w-full"> <div class="related-posts-section lazyhedwik-element flex w-full flex-col items-start justify-start gap-4"> <h3 class="related-posts-head text-three text-[13px] font-semibold uppercase tracking-wider">You might also like</h3> <div class="related-posts-content xs:grid-cols-2 grid w-full grid-cols-1 gap-4 sm:grid-cols-3"> <a href="/2024/09/12/why-ember-is-different-running-browser-tests-in-a-browser/" class="related-posts-item flex h-auto max-h-fit w-full flex-col items-stretch gap-4 rounded-lg border border-zinc-100 bg-zinc-50 p-4 hover:border-zinc-200 hover:transition dark:border-zinc-900 dark:bg-zinc-900 dark:hover:border-zinc-800 post-public"> <div class="related-posts-item-content flex h-full w-full flex-col items-start justify-start gap-2.5"> <figure class="related-posts-item-image aspect-[1/1] size-12 max-h-fit max-w-fit overflow-hidden rounded-full"> <img class="related-posts-item-image-img lazyhedwik-image h-full w-full rounded-full object-cover object-center" alt="Why Ember is Different: Running Browser Tests in a Browser" src="/content/images/size/w90/format/webp/2024/09/Screenshot-from-2024-09-11-20-12-38-1.png" data-src="/content/images/size/w360/format/webp/2024/09/Screenshot-from-2024-09-11-20-12-38-1.png" /> </figure> <h2 class="related-posts-item-title text-one line-clamp-5 text-base font-semibold leading-subheading">Why Ember is Different: Running Browser Tests in a Browser</h2> </div> <span class="related-posts-item-reading-time text-four text-left text-[13px] font-normal leading-none">6 min read</span> </a> <a href="/2024/09/11/css-modules-in-ember-without-addons/" class="related-posts-item flex h-auto max-h-fit w-full flex-col items-stretch gap-4 rounded-lg border border-zinc-100 bg-zinc-50 p-4 hover:border-zinc-200 hover:transition dark:border-zinc-900 dark:bg-zinc-900 dark:hover:border-zinc-800 post-public"> <div class="related-posts-item-content flex h-full w-full flex-col items-start justify-start gap-2.5"> <figure class="related-posts-item-image aspect-[1/1] size-12 max-h-fit max-w-fit overflow-hidden rounded-full"> <img class="related-posts-item-image-img lazyhedwik-image h-full w-full rounded-full object-cover object-center" alt="Exploring Ember Polaris: CSS Modules Without Addons" src="/content/images/size/w90/format/webp/2024/09/Screenshot-2024-09-04-5.08.03-PM.png" data-src="/content/images/size/w360/format/webp/2024/09/Screenshot-2024-09-04-5.08.03-PM.png" /> </figure> <h2 class="related-posts-item-title text-one line-clamp-5 text-base font-semibold leading-subheading">Exploring Ember Polaris: CSS Modules Without Addons</h2> </div> <span class="related-posts-item-reading-time text-four text-left text-[13px] font-normal leading-none">5 min read</span> </a> </div> </div></div></div> <footer class="footer footer-primary lazyhedwik-element footer-minimal xs:px-6 xs:py-12 relative border-t !border-zinc-100/[0.7] dark:!border-zinc-900 flex h-fit w-full select-none flex-col items-center justify-center px-4 lg:px-0 py-10 sm:py-14"> <div class="footer-primary-content flex flex-col items-center justify-center gap-6"> <a href="https://yehudakatz.com" class="c-footer-light-logo flex h-fit w-fit items-center footer-logo"> <img class="c-footer-logo-img footer-light-logo !w-fit object-contain min-h-[24px] object-center" style=" height:36px" src="" alt="Katz Got Your Tongue" /> </a> <script> initializeDarkMode(); </script> <span class="footer-primary-description text-two max-w-md text-center text-lg font-medium leading-normal">Long-form writing by Yehuda Katz, co-creator of Ember.js and serial Open Sourcerer.</span> <div class="footer-social-accounts footer-primary-social-accounts flex flex-row gap-5 items-center justify-start sm:justify-end text-three py-2"> <a href="https://twitter.com/@wycats" target="_blank" rel="noopener" aria-label="Twitter X Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 16 16" width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.52217 6.77491L15.4785 0H14.0671L8.89516 5.88256L4.76437 0H0L6.24656 8.89547L0 16H1.41155L6.87321 9.78782L11.2356 16H16L9.52183 6.77491H9.52217ZM7.58887 8.97384L6.95596 8.08805L1.92015 1.03974H4.0882L8.15216 6.72795L8.78507 7.61374L14.0677 15.0075H11.8997L7.58887 8.97418V8.97384Z" fill="currentColor"/></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Instagram Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M11 3.5h1M4.5.5h6a4 4 0 014 4v6a4 4 0 01-4 4h-6a4 4 0 01-4-4v-6a4 4 0 014-4zm3 10a3 3 0 110-6 3 3 0 010 6z" stroke="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Instagram Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M4.5 6v5m6 0V8.5a2 2 0 10-4 0V11 6M4 4.5h1M1.5.5h12a1 1 0 011 1v12a1 1 0 01-1 1h-12a1 1 0 01-1-1v-12a1 1 0 011-1z" stroke="currentColor"></path></svg> </a> <a href="https://github.com/hedwik" target="_blank" rel="noopener" aria-label="Github Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M5.65 12.477a.5.5 0 10-.3-.954l.3.954zm-3.648-2.96l-.484-.128-.254.968.484.127.254-.968zM9 14.5v.5h1v-.5H9zm.063-4.813l-.054-.497a.5.5 0 00-.299.852l.352-.354zM12.5 5.913h.5V5.91l-.5.002zm-.833-2.007l-.466-.18a.5.5 0 00.112.533l.354-.353zm-.05-2.017l.456-.204a.5.5 0 00-.319-.276l-.137.48zm-2.173.792l-.126.484a.5.5 0 00.398-.064l-.272-.42zm-3.888 0l-.272.42a.5.5 0 00.398.064l-.126-.484zM3.383 1.89l-.137-.48a.5.5 0 00-.32.276l.457.204zm-.05 2.017l.354.353a.5.5 0 00.112-.534l-.466.181zM2.5 5.93H3v-.002l-.5.002zm3.438 3.758l.352.355a.5.5 0 00-.293-.851l-.06.496zM5.5 11H6l-.001-.037L5.5 11zM5 14.5v.5h1v-.5H5zm.35-2.977c-.603.19-.986.169-1.24.085-.251-.083-.444-.25-.629-.49a4.8 4.8 0 01-.27-.402c-.085-.139-.182-.302-.28-.447-.191-.281-.473-.633-.929-.753l-.254.968c.08.02.184.095.355.346.082.122.16.252.258.412.094.152.202.32.327.484.253.33.598.663 1.11.832.51.168 1.116.15 1.852-.081l-.3-.954zm4.65-.585c0-.318-.014-.608-.104-.878-.096-.288-.262-.51-.481-.727l-.705.71c.155.153.208.245.237.333.035.105.053.254.053.562h1zm-.884-.753c.903-.097 1.888-.325 2.647-.982.78-.675 1.237-1.729 1.237-3.29h-1c0 1.359-.39 2.1-.892 2.534-.524.454-1.258.653-2.099.743l.107.995zM13 5.91a3.354 3.354 0 00-.98-2.358l-.707.706c.438.44.685 1.034.687 1.655l1-.003zm-.867-1.824c.15-.384.22-.794.21-1.207l-1 .025a2.12 2.12 0 01-.142.82l.932.362zm.21-1.207a3.119 3.119 0 00-.27-1.195l-.913.408c.115.256.177.532.184.812l1-.025zm-.726-.99c.137-.481.137-.482.136-.482h-.003l-.004-.002a.462.462 0 00-.03-.007 1.261 1.261 0 00-.212-.024 2.172 2.172 0 00-.51.054c-.425.091-1.024.317-1.82.832l.542.84c.719-.464 1.206-.634 1.488-.694a1.2 1.2 0 01.306-.03l-.008-.001a.278.278 0 01-.01-.002l-.006-.002h-.003l-.002-.001c-.001 0-.002 0 .136-.482zm-2.047.307a8.209 8.209 0 00-4.14 0l.252.968a7.209 7.209 0 013.636 0l.252-.968zm-3.743.064c-.797-.514-1.397-.74-1.822-.83a2.17 2.17 0 00-.51-.053 1.259 1.259 0 00-.241.03l-.004.002h-.003l.136.481.137.481h-.001l-.002.001-.003.001a.327.327 0 01-.016.004l-.008.001h.008a1.19 1.19 0 01.298.03c.282.06.769.23 1.488.694l.543-.84zm-2.9-.576a3.12 3.12 0 00-.27 1.195l1 .025a2.09 2.09 0 01.183-.812l-.913-.408zm-.27 1.195c-.01.413.06.823.21 1.207l.932-.362a2.12 2.12 0 01-.143-.82l-1-.025zm.322.673a3.354 3.354 0 00-.726 1.091l.924.38c.118-.285.292-.545.51-.765l-.708-.706zm-.726 1.091A3.354 3.354 0 002 5.93l1-.003c0-.31.06-.616.177-.902l-.924-.38zM2 5.93c0 1.553.458 2.597 1.239 3.268.757.65 1.74.88 2.64.987l.118-.993C5.15 9.09 4.416 8.89 3.89 8.438 3.388 8.007 3 7.276 3 5.928H2zm3.585 3.404c-.5.498-.629 1.09-.584 1.704L6 10.963c-.03-.408.052-.683.291-.921l-.705-.709zM5 11v3.5h1V11H5zm5 3.5V13H9v1.5h1zm0-1.5v-2.063H9V13h1z" fill="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M.5.5V0a.5.5 0 00-.5.5h.5zm14 0h.5a.5.5 0 00-.5-.5v.5zm0 8l.354.354A.5.5 0 0015 8.5h-.5zm-3 3v.5a.5.5 0 00.354-.146L11.5 11.5zm-5 0V11a.5.5 0 00-.325.12l.325.38zm-3.5 3h-.5a.5.5 0 00.825.38L3 14.5zm0-3h.5A.5.5 0 003 11v.5zm-2.5 0H0a.5.5 0 00.5.5v-.5zM.5 1h14V0H.5v1zM14 .5v8h1v-8h-1zm.146 7.646l-3 3 .708.708 3-3-.708-.708zM11.5 11h-5v1h5v-1zm-5.325.12l-3.5 3 .65.76 3.5-3-.65-.76zM3.5 14.5v-3h-1v3h1zM3 11H.5v1H3v-1zm-2 .5V.5H0v11h1zM10 3v5h1V3h-1zM7 3v5h1V3H7z" fill="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M9.5 0v11A3.5 3.5 0 116 7.5m8-2A4.5 4.5 0 019.5 1" stroke="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M1.61 12.738l-.104.489.105-.489zm11.78 0l.104.489-.105-.489zm0-10.476l.104-.489-.105.489zm-11.78 0l.106.489-.105-.489zM6.5 5.5l.277-.416A.5.5 0 006 5.5h.5zm0 4H6a.5.5 0 00.777.416L6.5 9.5zm3-2l.277.416a.5.5 0 000-.832L9.5 7.5zM0 3.636v7.728h1V3.636H0zm15 7.728V3.636h-1v7.728h1zM1.506 13.227c3.951.847 8.037.847 11.988 0l-.21-.978a27.605 27.605 0 01-11.568 0l-.21.978zM13.494 1.773a28.606 28.606 0 00-11.988 0l.21.978a27.607 27.607 0 0111.568 0l.21-.978zM15 3.636c0-.898-.628-1.675-1.506-1.863l-.21.978c.418.09.716.458.716.885h1zm-1 7.728a.905.905 0 01-.716.885l.21.978A1.905 1.905 0 0015 11.364h-1zm-14 0c0 .898.628 1.675 1.506 1.863l.21-.978A.905.905 0 011 11.364H0zm1-7.728c0-.427.298-.796.716-.885l-.21-.978A1.905 1.905 0 000 3.636h1zM6 5.5v4h1v-4H6zm.777 4.416l3-2-.554-.832-3 2 .554.832zm3-2.832l-3-2-.554.832 3 2 .554-.832z" fill="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M7.5 1c-1.155 0-2.174.412-2.894 1.281-.642.775-1.006 2.35-1.066 3.666l-.073.01-.022.004a8.68 8.68 0 00-.368.059c-.465.089-1.346.326-1.543 1.277-.093.445.011.833.247 1.134.211.269.497.429.708.53.09.041.181.08.274.117-.21.584-.579 1.184-.987 1.728-.382.508-.28 1.153-.083 1.573.197.421.57.402 1.192.43.352.015.722.051 1.09.12.166.03.362.098.606.2.142.06.283.123.423.187.113.052.235.106.374.167.573.25 1.276.517 2.056.517s1.483-.267 2.055-.517c.14-.06.26-.115.375-.167l.025-.012c.135-.06.26-.117.398-.174.243-.103.44-.17.606-.201a7.951 7.951 0 011.09-.12c.622-.028 1.104-.009 1.303-.43.197-.42.298-1.065-.084-1.573-.406-.54-.772-1.136-.983-1.716a5.24 5.24 0 00.305-.127c.216-.098.518-.261.73-.543.245-.326.315-.739.175-1.184-.28-.886-1.092-1.122-1.568-1.216a6.857 6.857 0 00-.355-.058l-.012-.002-.056-.009c-.065-1.234-.41-2.795-1.036-3.581C9.695 1.485 8.682 1 7.5 1z" stroke="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M4.5 13.5l3-7m-3.236 3a2.989 2.989 0 01-.764-2V7A3.5 3.5 0 017 3.5h1A3.5 3.5 0 0111.5 7v.5a3 3 0 01-3 3 2.081 2.081 0 01-1.974-1.423L6.5 9m1 5.5a7 7 0 110-14 7 7 0 010 14z" stroke="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M7.5 1.5l.121-.485A.5.5 0 007 1.5h.5zm5.5 8c0 .774-.55 1.641-1.583 2.343C10.4 12.533 8.998 13 7.5 13v1c1.696 0 3.294-.525 4.479-1.33C13.148 11.876 14 10.743 14 9.5h-1zM7.5 13c-1.498 0-2.9-.466-3.917-1.157C2.551 11.14 2 10.273 2 9.5H1c0 1.243.852 2.376 2.021 3.17C4.206 13.475 5.804 14 7.5 14v-1zM2 9.5c0-.774.55-1.641 1.583-2.343C4.6 6.467 6.002 6 7.5 6V5c-1.696 0-3.294.525-4.479 1.33C1.852 7.124 1 8.257 1 9.5h1zM7.5 6c1.498 0 2.9.467 3.917 1.157C12.449 7.86 13 8.727 13 9.5h1c0-1.243-.852-2.376-2.021-3.17C10.794 5.525 9.196 5 7.5 5v1zm2.306 4.54c-.69.29-1.32.46-2.306.46v1c1.136 0 1.898-.204 2.694-.54l-.388-.92zM7.5 11c-.987 0-1.617-.17-2.306-.46l-.388.92c.796.336 1.558.54 2.694.54v-1zM8 5.5v-4H7v4h1zm-.621-3.515l4 1 .242-.97-4-1-.242.97zM3.974 6.841c-.286-.855-1.12-1.297-1.952-1.297v1c.51 0 .886.261 1.004.615l.948-.318zM2.022 5.544A2.022 2.022 0 000 7.566h1a1.02 1.02 0 011.022-1.022v-1zM0 7.566C0 8.589.76 9.424 1.74 9.56l.139-.99A1.016 1.016 0 011 7.565H0zm11.974-.407c.118-.354.493-.615 1.004-.615v-1c-.832 0-1.666.442-1.952 1.297l.948.318zm1.004-.615A1.02 1.02 0 0114 7.566h1a2.022 2.022 0 00-2.022-2.022v1zM14 7.566c0 .511-.38.934-.879 1.004l.139.99A2.016 2.016 0 0015 7.567h-1zM12.5 3a.5.5 0 01-.5-.5h-1A1.5 1.5 0 0012.5 4V3zm.5-.5a.5.5 0 01-.5.5v1A1.5 1.5 0 0014 2.5h-1zm-.5-.5a.5.5 0 01.5.5h1A1.5 1.5 0 0012.5 1v1zm0-1A1.5 1.5 0 0011 2.5h1a.5.5 0 01.5-.5V1zM5 9h1V8H5v1zm4 0h1V8H9v1z" fill="currentColor"></path></svg> </a> <a href="javascript:" target="_blank" rel="noopener" aria-label="Tiktok Link" class="footer-social-account-link transition-transform hover:scale-105 active:scale-100"> <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M11.5 13.5l-.326.379a.5.5 0 00.342.12L11.5 13.5zm-1.066-1.712a.5.5 0 00-.785.62l.785-.62zm.398-.41l-.174-.468a.672.672 0 00-.02.007l.194.461zm-1.738.516L9.01 11.4l-.008.001.092.492zm-3.104-.012l-.095.49.003.001.092-.491zm-1.762-.515l-.182.465.182-.466zm-.875-.408l-.278.415a.46.46 0 00.033.021l.245-.436zm-.108-.06l.277-.416a.491.491 0 00-.054-.031l-.223.447zm-.048-.036l.353-.354a.502.502 0 00-.11-.083l-.243.437zm2.154 1.52a.5.5 0 00-.785-.62l.785.62zM3.5 13.5l-.016.5a.5.5 0 00.347-.125L3.5 13.5zm-3-2.253H0a.5.5 0 00.006.08l.494-.08zm1.726-8.488l-.3-.4a.5.5 0 00-.168.225l.468.175zM5.594 1.5l.498-.047A.5.5 0 005.605 1l-.01.5zm-.378 1.306a.5.5 0 00.996-.095l-.996.095zm3.526-.063a.5.5 0 00.992.127l-.992-.127zM9.406 1.5L9.395 1a.5.5 0 00-.485.436l.496.064zm3.368 1.259l.468-.175a.5.5 0 00-.168-.225l-.3.4zm1.726 8.488l.494.08a.497.497 0 00.006-.08h-.5zM6.481 8.8l-.5-.008V8.8h.5zm5.019 4.7l.326-.379-.002-.002a.794.794 0 01-.044-.038 21.355 21.355 0 01-.536-.48c-.325-.298-.66-.622-.81-.813l-.785.62c.208.264.603.64.918.93a29.109 29.109 0 00.593.53l.01.008.003.002.327-.378zm.436-3.246c-.46.303-.894.513-1.278.656l.348.937a7.352 7.352 0 001.48-.758l-.55-.835zm-1.297.663a7.387 7.387 0 01-1.629.484l.168.986a8.39 8.39 0 001.848-.548l-.387-.922zm-1.637.485a7.895 7.895 0 01-2.92-.012l-.184.983a8.896 8.896 0 003.288.012l-.184-.983zm-2.917-.011a9.57 9.57 0 01-1.675-.49l-.364.931c.512.2 1.13.402 1.849.54l.19-.981zm-1.675-.49a6.536 6.536 0 01-.813-.378l-.489.872c.326.183.648.324.938.437l.364-.931zm-.78-.358a.802.802 0 00-.108-.061c-.02-.01-.011-.007 0 .001l-.555.832a.87.87 0 00.108.061c.021.01.012.007 0-.002l.556-.83zm-.162-.091a.332.332 0 01.082.058l-.707.707c.023.023.081.08.178.13l.447-.895zm-.028-.026a4.697 4.697 0 01-.28-.168l-.011-.008a.025.025 0 00-.001 0l-.287.41-.286.409.001.001.002.002.007.004.021.014.075.049c.064.04.156.096.273.161l.486-.874zm1.126 1.338c-.152.193-.489.525-.813.829a30.38 30.38 0 01-.538.491l-.034.031-.01.008-.001.002h-.001l.331.375.331.375.001-.001.003-.002.01-.009.036-.032a38.039 38.039 0 00.555-.508c.315-.296.708-.677.915-.94l-.785-.62zM3.516 13c-1.166-.037-1.778-.521-2.11-.96a2.394 2.394 0 01-.4-.82 1.1 1.1 0 01-.013-.056v.002l-.493.08c-.494.08-.494.08-.493.081v.006a1.367 1.367 0 00.028.127 3.394 3.394 0 00.573 1.183c.505.667 1.393 1.31 2.876 1.357l.032-1zM1 11.247c0-1.867.42-3.94.847-5.564a35.45 35.45 0 01.776-2.552 16.43 16.43 0 01.067-.186l.004-.01v-.001l-.468-.175-.469-.175v.001l-.001.003-.004.011a9.393 9.393 0 00-.072.2 36.445 36.445 0 00-.8 2.629C.443 7.083 0 9.253 0 11.247h1zm1.526-8.088c.8-.6 1.577-.89 2.15-1.03a4.764 4.764 0 01.86-.128A1.48 1.48 0 015.585 2h-.001l.01-.5.01-.5H5.6a.848.848 0 00-.028 0h-.068a3.973 3.973 0 00-.24.016 5.763 5.763 0 00-.825.141 6.938 6.938 0 00-2.513 1.2l.6.8zm2.57-1.612l.12 1.259.996-.095-.12-1.258-.996.094zM9.734 2.87l.168-1.306-.992-.128-.168 1.307.992.127zM9.406 1.5l.01.5h-.001a.497.497 0 01.049 0c.038.002.1.005.179.013.16.014.394.047.681.117a5.94 5.94 0 012.15 1.029l.6-.8a6.937 6.937 0 00-2.513-1.2 5.76 5.76 0 00-.825-.142A3.98 3.98 0 009.399 1h-.003l.01.5zm3.368 1.259l-.469.174.001.003.004.009.013.037.053.149a35.482 35.482 0 01.777 2.552c.428 1.624.847 3.697.847 5.564h1c0-1.994-.444-4.164-.88-5.819a36.512 36.512 0 00-.8-2.629 15.246 15.246 0 00-.057-.158l-.015-.042-.004-.01-.001-.004-.47.174zm1.726 8.488l-.493-.08v-.003l-.002.008-.01.047c-.012.045-.03.113-.061.197-.062.17-.167.396-.34.624-.332.439-.944.923-2.11.96l.032 1c1.483-.047 2.37-.69 2.876-1.356a3.395 3.395 0 00.573-1.184 2.05 2.05 0 00.026-.118l.002-.01v-.004c0-.001 0-.002-.493-.081zM5.259 6.97c-1.002 0-1.723.867-1.723 1.83h1c0-.498.357-.83.723-.83v-1zM3.536 8.8c0 .967.736 1.83 1.723 1.83v-1c-.357 0-.723-.334-.723-.83h-1zm1.723 1.83c1 0 1.722-.866 1.722-1.83h-1c0 .5-.357.83-.722.83v1zM6.98 8.81c.016-.978-.728-1.84-1.722-1.84v1.001c.372 0 .73.338.722.822l1 .017zm2.653-1.84c-1.002.001-1.723.868-1.723 1.831h1c0-.498.357-.83.723-.83v-1zM7.91 8.802c0 .967.736 1.83 1.723 1.83v-1c-.357 0-.723-.334-.723-.83h-1zm1.723 1.83c1 0 1.722-.866 1.722-1.83h-1c0 .5-.357.83-.722.83v1zm1.722-1.83c0-.963-.721-1.83-1.722-1.83v1c.365 0 .722.332.722.83h1zM3.74 4.44c1.443-.787 2.619-1.154 3.763-1.155 1.145 0 2.318.365 3.758 1.154l.48-.876c-1.522-.835-2.865-1.279-4.238-1.278-1.373 0-2.717.445-4.241 1.277l.478.878z" fill="currentColor"></path></svg> </a> </div> <span class="footer-primary-site-title text-two text-base font-normal">2024 <span class="text-sm opacity-70">•</span> <a href="https://yehudakatz.com" class="font-bold">Katz Got Your Tongue</a></span> </div> </footer> <script src="https://yehudakatz.com/assets/js/exclude/cards.js?v=b917a20540" defer></script> <script src="https://yehudakatz.com/assets/js/exclude/fslightbox.min.js?v=b917a20540" defer></script> <script src="https://yehudakatz.com/assets/js/lib/lazyhedwik.js?v=b917a20540"></script> <script src="https://yehudakatz.com/assets/built/main.min.js?v=b917a20540"></script> <script> function initPopover(){let e=document.getElementById("popoverButton"),s=document.getElementById("hedwikPopover");window.addEventListener("scroll",()=>{s.classList.contains("popover-show")&&(s.classList.remove("popover-show"),s.classList.add("popover-hide"),setTimeout(()=>{s.style.display="none"},150))}),e.addEventListener("click",()=>{s.classList.contains("popover-show")?(s.classList.remove("popover-show"),s.classList.add("popover-hide"),setTimeout(()=>{s.style.display="none"},150)):(s.style.display="block",s.classList.remove("popover-hide"),s.classList.add("popover-show"),s.style.left=`${e.offsetLeft+(e.offsetWidth-s.offsetWidth)/2}px`,s.style.top=`${e.offsetTop+e.offsetHeight+10}px`)}),document.addEventListener("click",o=>{e.contains(o.target)||s.contains(o.target)||(s.classList.remove("popover-show"),s.classList.add("popover-hide"),setTimeout(()=>{s.style.display="none"},150))})}initPopover(); </script> <script> const scrollLine = document.querySelector('.scroll-line'); function fillScrollLine() { const windowHeight = window.innerHeight; const fullHeight = document.body.clientHeight; const scrolled = window.scrollY; const percentScrolled = (scrolled / (fullHeight - windowHeight)) * 100; scrollLine.style.width = `${percentScrolled}%`; } window.addEventListener('scroll', debounce(fillScrollLine)); function debounce(func, wait = 15, immediate) { var timeout; return function() { var context = this, args = arguments; var later = function() { timeout = null; if (!immediate) func.apply(context, args); }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; } function copyPageLink(){let e=window.location.href,t=document.createElement("textarea");t.value=e,document.body.appendChild(t),t.select(),document.execCommand("copy"),document.body.removeChild(t);let o=document.querySelector(".share-button"),n=document.querySelector(".copying"),c=document.querySelector(".copied"),d=o.querySelector("span");n.classList.add("hidden"),c.classList.remove("hidden"),d.textContent="Copied",setTimeout(()=>{n.classList.remove("hidden"),c.classList.add("hidden"),d.textContent="Copy Link"},2e3)}const shareButton=document.querySelector(".share-button");shareButton.addEventListener("click",copyPageLink); </script> <script defer src="https://ghostboard.io/t/5cc01a81a803662c9f6bab1d.js" type="text/javascript" async></script><noscript><img src="https://ghostboard.io/api/noscript/5cc01a81a803662c9f6bab1d/pixel.gif" alt="" border="0" /></noscript> <script defer src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" referrerpolicy="no-referrer"></script> <script defer> var tabs = new Tabby('[data-tabs]'); </script> </body> </html>