, meaning they have to have a correspoonding closing tag.
\\n\\n\\n\\nSince custom elements are not built-in elements, they are undefined by default — and being undefined can be a useful thing! That means we can use them as containers with default properties. For example, they are display: inline
by default and inherit the current font-family
, which can be useful to pass down to the contents. We can also use them as styling hooks since they can be selected in CSS. Or maybe they can be used for accessibility hints. The bottom line is that they do not require JavaScript in order to make them immediately useful.
Working with JavaScript. If there is one <my-button>
on the page, we can query it and set a click handler on it with an event listener. But if we were to insert more instances on the page later, we would need to query it when it’s appended and re-run the function since it is not part of the original document rendering.
This defines and registers the custom element. It teaches the browser that this is an instance of the Custom Elements API and extends the same class that makes other HTML elements valid HTML elements:
\\n\\n\\n\\n<my-element>My Element</my-element>\\n\\n<script>\\n customElements.define(\\"my-element\\", class extends HTMLElement {});\\n</script>
\\n\\n\\n\\nCheck out the methods we get immediate access to:
\\n\\n\\n\\ncustomElements\\n .define(\\n \\"my-element\\",\\n class extends HTMLElement {}\\n );\\n\\t\\n// Functionally the same as:\\nclass MyElement extends HTMLElement {}\\ncustomElements.define(\\"my-element\\", MyElement);\\nexport default myElement\\n\\n// ...which makes it importable by other elements:\\nimport MyElement from \'./MyElement.js\';\\nconst myElement = new MyElement();\\ndocument.body.appendChild(myElement);\\n\\n// <body>\\n// <my-element></my-element>\\n// </body>\\n\\n// Or simply pull it into a page\\n// Don\'t need to `export default` but it doesn\'t hurt to leave it\\n// <my-element>My Element</my-element>\\n// <script type=\\"module\\" src=\\"my-element.js\\"></script>
\\n\\n\\n\\nIt’s possible to define a custom element by extending a specific HTML element. The specification documents this, but Scott is focusing on the primary way.
\\n\\n\\n\\nclass WordCount extends HTMLParagraphElement\\ncustomElements.define(\\"word-count\\", WordCount, { extends: \\"p\\" });\\n\\n// <p is=\\"word-count\\">This is a custom paragraph!</p>
\\n\\n\\n\\nScott says do not use this because WebKit is not going to implement it. We would have to polyfill it forever, or as long as WebKit holds out. Consider it a dead end.
\\n\\n\\nA component has various moments in its “life” span:
\\n\\n\\n\\nconstructor
)connectedCallback
)adoptedCallback
)attributeChangedCallback
)disconnectedCallback
)We can hook into these to define the element’s behavior.
\\n\\n\\n\\nclass myElement extends HTMLElement {\\n constructor() {}\\n connectedCallback() {}\\n adoptedCallback() {}\\n attributeChangedCallback() {}\\n disconnectedCallback() {}\\n}\\n\\ncustomElements.define(\\"my-element\\", MyElement);
\\n\\n\\nconstructor()
class myElement extends HTMLElement {\\n constructor() {\\n // provides us with the `this` keyword\\n super()\\n \\n // add a property\\n this.someProperty = \\"Some value goes here\\";\\n // add event listener\\n this.addEventListener(\\"click\\", () => {});\\n }\\n}\\n\\ncustomElements.define(\\"my-element\\", MyElement);
\\n\\n\\n\\n“When the constructor is called, do this…” We don’t have to have a constructor when working with custom elements, but if we do, then we need to call super()
because we’re extending another class and we’ll get all of those properties.
Constructor is useful, but not for a lot of things. It’s useful for setting up initial state, registering default properties, adding event listeners, and even creating Shadow DOM (which Scott will get into in a later module). For example, we are unable to sniff out whether or not the custom element is in another element because we don’t know anything about its parent container yet (that’s where other lifecycle methods come into play) — we’ve merely defined it.
\\n\\n\\nconnectedCallback()
class myElement extends HTMLElement {\\n // the constructor is unnecessary in this example but doesn\'t hurt.\\n constructor() {\\n super()\\n }\\n // let me know when my element has been found on the page.\\n connectedCallback() {\\n console.log(`${this.nodeName} was added to the page.`);\\n }\\n}\\n\\ncustomElements.define(\\"my-element\\", MyElement);
\\n\\n\\n\\nNote that there is some strangeness when it comes to timing things. Sometimes isConnected
returns true
during the constructor. connectedCallback()
is our best way to know when the component is found on the page. This is the moment it is connected to the DOM. Use it to attach event listeners.
If the <script>
tag comes before the DOM is parsed, then it might not recognize childNodes
. This is not an uncommon situation. But if we add type=\\"module\\"
to the <script>
, then the script is deferred and we get the child nodes. Using setTimeout
can also work, but it looks a little gross.
disconnectedCallback
class myElement extends HTMLElement {\\n // let me know when my element has been found on the page.\\n disconnectedCallback() {\\n console.log(`${this.nodeName} was removed from the page.`);\\n }\\n}\\n\\ncustomElements.define(\\"my-element\\", MyElement);
\\n\\n\\n\\nThis is useful when the component needs to be cleaned up, perhaps like stopping an animation or preventing memory links.
\\n\\n\\nadoptedCallback()
This is when the component is adopted by another document or page. Say you have some iframes on a page and move a custom element from the page into an iframe, then it would be adopted in that scenario. It would be created, then added, then removed, then adopted, then added again. That’s a full lifecycle! This callback is adopted automatically simply by picking it up and dragging it between documents in the DOM.
\\n\\n\\nUnlike React, HTML attributes are strings (not props!). Global attributes work as you’d expect, though some global attributes are reflected as properties. You can make any attribute do that if you want, just be sure to use care and caution when naming because, well, we don’t want any conflicts.
\\n\\n\\n\\nAvoid standard attributes on a custom element as well, as that can be confusing particularly when handing a component to another developer. Example: using type as an attribute which is also used by <input>
elements. We could say data-type
instead. (Remember that Chris has a comprehensive guide on using data attributes.)
Here’s a quick example showing how to get a greeting
attribute and set it on the custom element:
class MyElement extends HTMLElement {\\n get greeting() {\\n return this.getAttribute(\'greeting\');\\n // return this.hasAttribute(\'greeting\');\\n }\\n set greeting(val) {\\n if(val) {\\n this.setAttribute(\'greeting\', val);\\n // this setAttribute(\'greeting\', \'\');\\n } else {\\n this.removeAttribute(\'greeting\');\\n }\\n }\\n}\\ncustomElements.define(\\"my-element\\", MyElement);
\\n\\n\\n\\nAnother example, this time showing a callback for when the attribute has changed, which prints it in the element’s contents:
\\n\\n\\n\\n<my-element greeting=\\"hello\\">hello</my-element>\\n\\n<!-- Change text greeting when attribite greeting changes --\x3e\\n<script>\\n class MyElement extends HTMLElement {\\n static observedAttributes = [\\"greeting\\"];\\n \\n attributeChangedCallback(name, oldValue, newValue) {\\n if (name === \'greeting\' && oldValue && oldValue !== newValue) {\\n console.log(name + \\" changed\\");\\n this.textContent = newValue;\\n }\\n }\\n }\\n \\n customElements.define(\\"my-element\\", MyElement);\\n</script>
\\n\\n\\n\\nA few more custom element methods:
\\n\\n\\n\\ncustomElements.get(\'my-element\');\\n// returns MyElement Class\\n\\ncustomElements.getName(MyElement);\\n// returns \'my-element\'\\n\\ncustomElements.whenDefined(\\"my-element\\");\\n// waits for custom element to be defined\\n\\nconst el = document.createElement(\\"spider-man\\");\\nclass SpiderMan extends HTMLElement {\\n constructor() {\\n super();\\n console.log(\\"constructor!!\\");\\n }\\n}\\ncustomElements.define(\\"spider-man\\", SpiderMan);\\n\\ncustomElements.upgrade(el);\\n// returns \\"constructor!!\\"
\\n\\n\\n\\nCustom methods and events:
\\n\\n\\n\\n<my-element><button>My Element</button></my-element>\\n\\n<script>\\n customElements.define(\\"my-element\\", class extends HTMLElement {\\n connectedCallback() {\\n const btn = this.firstElementChild;\\n btn.addEventListener(\\"click\\", this.handleClick)\\n }\\n handleClick() {\\n console.log(this);\\n }\\n });\\n</script>
\\n\\n\\n\\nBring your own base class, in the same way web components frameworks like Lit do:
\\n\\n\\n\\nclass BaseElement extends HTMLElement {\\n $ = this.querySelector;\\n}\\n// extend the base, use its helper\\nclass myElement extends BaseElement {\\n firstLi = this.$(\\"li\\");\\n}
\\n\\n\\nCreate a custom HTML element called <say-hi>
that displays the text “Hi, World!” when added to the page:
Enhance the element to accept a name
attribute, displaying \\"Hi, [Name]!\\"
instead:
The <template>
element is not for users but developers. It is not exposed visibly by browsers.
<template>The browser ignores everything in here.</template>
\\n\\n\\n\\nTemplates are designed to hold HTML fragments:
\\n\\n\\n\\n<template>\\n <div class=\\"user-profile\\">\\n <h2 class=\\"name\\">Scott</h2>\\n <p class=\\"bio\\">Author</p>\\n </div>\\n</template>
\\n\\n\\n\\nA template is selectable in CSS; it just doesn’t render. It’s a document fragment. The inner document is a #document-fragment
. Not sure why you’d do this, but it illustrates the point that templates are selectable:
template { display: block; }` /* Nope */\\ntemplate + div { height: 100px; width: 100px; } /* Works */
\\n\\n\\ncontent
propertyNo, not in CSS, but JavaScript. We can query the inner contents of a template and print them somewhere else.
\\n\\n\\n\\n<template>\\n <p>Hi</p>\\n</template>\\n\\n<script>\\n const myTmpl = documenty.querySelector(\\"template\\").content;\\n console.log(myTmpl);\\n</script>
\\n\\n\\n<template>
const myFrag = document.createDocumentFragment();\\nmyFrag.innerHTML = \\"<p>Test</p>\\"; // Nope\\n\\nconst myP = document.createElement(\\"p\\"); // Yep\\nmyP.textContent = \\"Hi!\\";\\nmyFrag.append(myP);\\n\\n// use the fragment\\ndocument.body.append(myFrag);
\\n\\n\\n<template>\\n <p>Hi</p>\\n</template>\\n\\n<script>\\n const myTmpl = documenty.querySelector(\\"template\\").content;\\n console.log(myTmpl);\\n \\n // Oops, only works one time! We need to clone it.\\n</script>
\\n\\n\\n\\nOops, the component only works one time! We need to clone it if we want multiple instances:
\\n\\n\\n\\n<template>\\n <p>Hi</p>\\n</template>\\n\\n<script>\\n const myTmpl = document.querySelector(\\"template\\").content;\\n document.body.append(myTmpl.cloneNode(true)); // true is necessary\\n document.body.append(myTmpl.cloneNode(true));\\n document.body.append(myTmpl.cloneNode(true));\\n document.body.append(myTmpl.cloneNode(true));\\n</script>
\\n\\n\\nLet’s stub out a template for a list item and then insert them into an unordered list:
\\n\\n\\n\\n<template id=\\"tmpl-user\\"><li><strong></strong>: <span></span></li></template>\\n\\n<ul id=\\"users\\"></ul>\\n\\n<script>\\n const usersElement = document.querySelector(\\"#users\\");\\n const userTmpl = document.querySelector(\\"#tmpl-user\\").content;\\n const users = [{name: \\"Bob\\", title: \\"Artist\\"}, {name: \\"Jane\\", title: \\"Doctor\\"}];\\n users.forEach(user => {\\n let thisLi = userTmpl.cloneNode(true);\\n thisLi.querySelector(\\"strong\\").textContent = user.name;\\n thisLi.querySelector(\\"span\\").textContent = user.title;\\n usersElement.append(thisLi);\\n });\\n</script>
\\n\\n\\n\\nThe other way to use templates that we’ll get to in the next module: Shadow DOM
\\n\\n\\n\\n<template shadowroot=open>\\n <p>Hi, I\'m in the Shadow DOM</p>\\n</template>
\\n\\n\\nHere we go, this is a heady chapter! The Shadow DOM reminds me of playing bass in a band: it’s easy to understand but incredibly difficult to master. It’s easy to understand that there are these nodes in the DOM that are encapsulated from everything else. They’re there, we just can’t really touch them with regular CSS and JavaScript without some finagling. It’s the finagling that’s difficult to master. There are times when the Shadow DOM is going to be your best friend because it prevents outside styles and scripts from leaking in and mucking things up. Then again, you’re most certainly going go want to style or apply scripts to those nodes and you have to figure that part out.
\\n\\n\\n\\nThat’s where web components really shine. We get the benefits of an element that’s encapsulated from outside noise but we’re left with the responsibility of defining everything for it ourselves.
\\n\\n\\n\\nWe covered the <template>
element in the last chapter and determined that it renders in the Shadow DOM without getting displayed on the page.
<template shadowrootmode=\\"closed\\">\\n <p>This will render in the Shadow DOM.</p>\\n</template>
\\n\\n\\n\\nIn this case, the <template>
is rendered as a #shadow-root
without the <template>
element’s tags. It’s a fragment of code. So, while the paragraph inside the template is rendered, the <template>
itself is not. It effectively marks the Shadow DOM’s boundaries. If we were to omit the shadowrootmode
attribute, then we simply get an unrendered template. Either way, though, the paragraph is there in the DOM and it is encapsulated from other styles and scripts on the page.
There are times you’re going to want to “pierce” the Shadow DOM to allow for some styling and scripts. The content is relatively protected but we can open the shadowrootmode
and allow some access.
<div>\\n <template shadowrootmode=\\"open\\">\\n <p>This will render in the Shadow DOM.</p>\\n </template>\\n</div>
\\n\\n\\n\\nNow we can query the div
that contains the <template>
and select the #shadow-root
:
document.querySelector(\\"div\\").shadowRoot\\n// #shadow-root (open)\\n// <p>This will render in the Shadow DOM.</p>
\\n\\n\\n\\nWe need that <div>
in there so we have something to query in the DOM to get to the paragraph. Remember, the <template>
is not actually rendered at all.
<!-- should this root stay with a parent clone? --\x3e\\n<template shadowrootcloneable>\\n<!-- allow shadow to be serialized into a string object — can forget about this --\x3e\\n<template shadowrootserializable>\\n<!-- click in element focuses first focusable element --\x3e\\n<template shadowrootdelegatesfocus>
\\n\\n\\nWhen you add a shadow root, it becomes the only rendered root in that shadow host. Any elements after a shadow root node in the DOM simply don’t render. If a DOM element contains more than one shadow root node, the ones after the first just become template tags. It’s sort of like the Shadow DOM is a monster that eats the siblings.
\\n\\n\\n\\nSlots bring those siblings back!
\\n\\n\\n\\n<div>\\n <template shadowroot=\\"closed\\">\\n <slot></slot>\\n <p>I\'m a sibling of a shadow root, and I am visible.</p>\\n </template>\\n</div>
\\n\\n\\n\\nAll of the siblings go through the slots and are distributed that way. It’s sort of like slots allow us to open the monster’s mouth and see what’s inside.
\\n\\n\\nUsing templates is the declarative way to define the Shadow DOM. We can also define the Shadow DOM imperatively using JavaScript. So, this is doing the exact same thing as the last code snippet, only it’s done programmatically in JavaScript:
\\n\\n\\n\\n<my-element>\\n <template shadowroot=\\"open\\">\\n <p>This will render in the Shadow DOM.</p>\\n </template>\\n</my-element>\\n\\n<script>\\n customElements.define(\'my-element\', class extends HTMLElement {\\n constructor() {\\n super();\\n // attaches a shadow root node\\n this.attachShadow({mode: \\"open\\"});\\n // inserts a slot into the template\\n this.shadowRoot.innerHTML = \'<slot></slot>\';\\n }\\n });\\n</script>
\\n\\n\\n\\nAnother example:
\\n\\n\\n\\n<my-status>available</my-status>\\n\\n<script>\\n customElements.define(\'my-status\', class extends HTMLElement {\\n constructor() {\\n super();\\n this.attachShadow({mode: \\"open\\"});\\n this.shadowRoot.innerHTML = \'<p>This item is currently: <slot></slot></p>\';\\n }\\n });\\n</script>
\\n\\n\\n\\nSo, is it better to be declarative or imperative? Like the weather where I live, it just depends.
\\n\\n\\n\\nWe can set the shadow mode via Javascript as well:
\\n\\n\\n\\n// open\\nthis.attachShadow({mode: open});\\n// closed\\nthis.attachShadow({mode: closed});\\n// cloneable\\nthis.attachShadow({cloneable: true});\\n// delegateFocus\\nthis.attachShadow({delegatesFocus: true});\\n// serialized\\nthis.attachShadow({serializable: true});\\n\\n// Manually assign an element to a slot\\nthis.attachShadow({slotAssignment: \\"manual\\"});
\\n\\n\\n\\nAbout that last one, it says we have to manually insert the <slot>
elements in JavaScript:
<my-element>\\n <p>This WILL render in shadow DOM but not automatically.</p>\\n</my-element>\\n\\n<script>\\n customElements.define(\'my-element\', class extends HTMLElement {\\n constructor() {\\n super();\\n this.attachShadow({\\n mode: \\"open\\",\\n slotAssignment: \\"manual\\"\\n });\\n this.shadowRoot.innerHTML = \'<slot></slot>\';\\n }\\n connectedCallback(){\\n const slotElem = this.querySelector(\'p\');\\n this.shadowRoot.querySelector(\'slot\').assign(slotElem);\\n }\\n });\\n</script>
\\n\\n\\nScott spent a great deal of time sharing examples that demonstrate different sorts of things you might want to do with the Shadow DOM when working with web components. I’ll rapid-fire those in here.
\\n\\n\\nthis.shadowRoot.querySelector(\'slot\')\\n .assignedElements();\\n\\n// get an array of all nodes in a slot, text too\\nthis.shadowRoot.querySelector(\'slot\')\\n .assignedNodes();
\\n\\n\\nlet slot = document.querySelector(\'div\')\\n .shadowRoot.querySelector(\\"slot\\");\\n \\n slot.addEventListener(\\"slotchange\\", (e) => {\\n console.log(`Slot \\"${slot.name}\\" changed`);\\n // > Slot \\"saying\\" changed\\n })
\\n\\n\\nBack to this example:
\\n\\n\\n\\n<my-status>available</my-status>\\n\\n<script>\\n customElements.define(\'my-status\', class extends HTMLElement {\\n constructor() {\\n super();\\n this.attachShadow({mode: \\"open\\"});\\n this.shadowRoot.innerHTML = \'<p>This item is currently: <slot></slot></p>\';\\n }\\n });\\n</script>
\\n\\n\\n\\nLet’s get that string out of our JavaScript with reusable imperative shadow HTML:
\\n\\n\\n\\n<my-status>available</my-status>\\n\\n<template id=\\"my-status\\">\\n <p>This item is currently: \\n <slot></slot>\\n </p>\\n</template>\\n\\n<script>\\n customElements.define(\'my-status\', class extends HTMLElement {\\n constructor(){\\n super();\\n this.attachShadow({mode: \'open\'});\\n const template = document.getElementById(\'my-status\');\\n\\t\\t\\n this.shadowRoot.append(template.content.cloneNode(true));\\n }\\n });\\n</script>
\\n\\n\\n\\nSlightly better as it grabs the component’s name programmatically to prevent name collisions:
\\n\\n\\n\\n<my-status>available</my-status>\\n\\n<template id=\\"my-status\\">\\n <p>This item is currently: \\n <slot></slot>\\n </p>\\n</template>\\n\\n<script>\\n customElements.define(\'my-status\', class extends HTMLElement {\\n constructor(){\\n super();\\n this.attachShadow({mode: \'open\'});\\n const template = document.getElementById( this.nodeName.toLowerCase() );\\n\\n this.shadowRoot.append(template.content.cloneNode(true));\\n }\\n });\\n</script>
\\n\\n\\nLong story, cut short: maybe don’t create custom form controls as web components. We get a lot of free features and functionalities — including accessibility — with native form controls that we have to recreate from scratch if we decide to roll our own.
\\n\\n\\n\\nIn the case of forms, one of the oddities of encapsulation is that form submissions are not automatically connected. Let’s look at a broken form that contains a web component for a custom input:
\\n\\n\\n\\n<form>\\n <my-input>\\n <template shadowrootmode=\\"open\\">\\n <label>\\n <slot></slot>\\n <input type=\\"text\\" name=\\"your-name\\">\\n </label>\\n </template>\\n Type your name!\\n </my-input>\\n <label><input type=\\"checkbox\\" name=\\"remember\\">Remember Me</label>\\n <button>Submit</button>\\n</form>\\n\\n<script>\\ndocument.forms[0].addEventListener(\'input\', function(){\\n let data = new FormData(this);\\n console.log(new URLSearchParams(data).toString());\\n});\\n</script>
\\n\\n\\n\\nThis input’s value won’t be in the submission! Also, form validation and states are not communicated in the Shadow DOM. Similar connectivity issues with accessibility, where the shadow boundary can interfere with ARIA. For example, IDs are local to the Shadow DOM. Consider how much you really need the Shadow DOM when working with forms.
\\n\\n\\nThe moral of the last section is to tread carefully when creating your own web components for form controls. Scott suggests avoiding that altogether, but he continued to demonstrate how we could theoretically fix functional and accessibility issues using element internals.
\\n\\n\\n\\nLet’s start with an input value that will be included in the form submission.
\\n\\n\\n\\n<form>\\n <my-input name=\\"name\\"></my-input>\\n <button>Submit</button>\\n</form>
\\n\\n\\n\\nNow let’s slot this imperatively:
\\n\\n\\n\\n<script>\\n customElements.define(\'my-input\', class extends HTMLElement {\\n constructor() {\\n super();\\n this.attachShadow({mode: \'open\'});\\n this.shadowRoot.innerHTML = \'<label><slot></slot><input type=\\"text\\"></label>\'\\n }\\n });\\n</script>
\\n\\n\\n\\nThe value is not communicated yet. We’ll add a static formAssociated
variable with internals attached:
<script>\\n customElements.define(\'my-input\', class extends HTMLElement {\\n static formAssociated = true;\\n constructor() {\\n super();\\n this.attachShadow({mode: \'open\'});\\n this.shadowRoot.innerHTML = \'<label><slot></slot><input type=\\"text\\"></label>\'\\n this.internals = this.attachedInternals();\\n }\\n });\\n</script>
\\n\\n\\n\\nThen we’ll set the form value as part of the internals when the input’s value changes:
\\n\\n\\n\\n<script>\\n customElements.define(\'my-input\', class extends HTMLElement {\\n static formAssociated = true;\\n constructor() {\\n super();\\n this.attachShadow({mode: \'open\'});\\n this.shadowRoot.innerHTML = \'<label><slot></slot><input type=\\"text\\"></label>\'\\n this.internals = this.attachedInternals();\\n \\n this.addEventListener(\'input\', () => {\\n this-internals.setFormValue(this.shadowRoot.querySelector(\'input\').value);\\n });\\n }\\n });\\n</script>
\\n\\n\\n\\nHere’s how we set states with element internals:
\\n\\n\\n\\n// add a checked state\\nthis.internals.states.add(\\"checked\\");\\n\\n// remove a checked state\\nthis.internals.states.delete(\\"checked\\");
\\n\\n\\n\\nLet’s toggle a “add” or “delete” a boolean state:
\\n\\n\\n\\n<form>\\n <my-check name=\\"remember\\">Remember Me?</my-check>\\n</form>\\n\\n<script>\\n customElements.define(\'my-check\', class extends HTMLElement {\\n static formAssociated = true;\\n constructor(){\\n super();\\n this.attachShadow({mode: \'open\'});\\n this.shadowRoot.innerHTML = \'<slot></slot>\';\\n this.internals = this.attachInternals();\\n let addDelete = false;\\n this.addEventListener(\\"click\\", ()=> {\\n addDelete = !addDelete;\\n this.internals.states[addDelete ? \\"add\\" : \\"delete\\"](\\"checked\\");\\n } );\\n }\\n });\\n</script>
\\n\\n\\n\\nLet’s refactor this for ARIA improvements:
\\n\\n\\n\\n<form>\\n <style>\\n my-check { display: inline-block; inline-size: 1em; block-size: 1em; background: #eee; }\\n my-check:state(checked)::before { content: \\"[x]\\"; }\\n </style>\\n <my-check name=\\"remember\\" id=\\"remember\\"></my-check><label for=\\"remember\\">Remember Me?</label>\\n</form>\\n\\n<script>\\n customElements.define(\'my-check\', class extends HTMLElement {\\n static formAssociated = true;\\n constructor(){\\n super();\\n this.attachShadow({mode: \'open\'});\\n\\n\\n this.internals = this.attachInternals();\\n this.internals.role = \'checkbox\';\\n this.setAttribute(\'tabindex\', \'0\');\\n let addDelete = false;\\n this.addEventListener(\\"click\\", ()=> {\\n addDelete = !addDelete;\\n this.internals.states[addDelete ? \\"add\\" : \\"delete\\"](\\"checked\\");\\n this[addDelete ? \\"setAttribute\\" : \\"removeAttribute\\"](\\"aria-checked\\", true);\\n });\\n }\\n });\\n</script>
\\n\\n\\n\\nPhew, that’s a lot of work! And sure, this gets us a lot closer to a more functional and accessible custom form input, but there’s still a long way’s to go to achieve what we already get for free from using native form controls. Always question whether you can rely on a light DOM form instead.
\\n\\n\\nStyling web components comes in levels of complexity. For example, we don’t need any JavaScript at all to slap a few styles on a custom element.
\\n\\n\\n\\n<my-element theme=\\"suave\\" class=\\"priority\\">\\n <h1>I\'m in the Light DOM!</h1>\\n</my-element>\\n\\n<style>\\n /* Element, class, attribute, and complex selectors all work. */\\n my-element {\\n display: block; /* custom elements are inline by default */\\n }\\n .my-element[theme=suave] {\\n color: #fff;\\n }\\n .my-element.priority {\\n background: purple;\\n }\\n .my-element h1 {\\n font-size: 3rem;\\n }\\n</style>
\\n\\n\\n\\nclosed
to open
doesn’t change CSS. It allows JavaScript to pierce the Shadow DOM but CSS isn’t affected.<style>\\np { color: red; }\\n</style>\\n\\n<p>Hi</p>\\n\\n<div>\\n <template shadowrootmode=\\"open\\">\\n <p>Hi</p>\\n </template>\\n</div>\\n\\n<p>Hi</p>
\\n\\n\\n\\n<template>
, even if the shadow root’s mode is set to open
.Let’s poke at it from the other direction:
\\n\\n\\n\\n<style>\\np { color: red; }\\n</style>\\n\\n<p>Hi</p>\\n\\n<div>\\n <template shadowrootmode=\\"open\\">\\n <style> p { color: blue;} </style>\\n <p>Hi</p>\\n </template>\\n</div>\\n\\n<p>Hi</p>
\\n\\n\\n\\n<style>
declarations in the <template>
are encapsulated and do not leak out to the other paragraphs, even though it is declared later in the cascade.Same idea, but setting the color on the <body>
:
<style>\\nbody { color: red; }\\n</style>\\n\\n<p>Hi</p>\\n\\n<div>\\n <template shadowrootmode=\\"open\\">\\n <p>Hi</p>\\n </template>\\n</div>\\n\\n<p>Hi</p>
\\n\\n\\n\\ncolor
. The <body>
is the parent and everything in it is a child that inherits these styles, including custom elements.We can target the paragraph in the <template>
style block to override the styles set on the <body>
. Those won’t leak back to the other paragraphs.
<style>\\n body {\\n color: red;\\n font-family: fantasy;\\n font-size: 2em;\\n }\\n</style>\\n \\n<p>Hi</p>\\n<div>\\n <template shadowrootmode=\\"open\\">\\n <style> \\n /* reset the light dom styles */\\n p {\\n color: initial; \\n font-family: initial; \\n font-size: initial;\\n }\\n </style>\\n <p>Hi</p>\\n </template>\\n</div>\\n \\n<p>Hi</p>
\\n\\n\\n\\nall: initital
as a defensive strategy against future inheritable styles. But what if we add more elements to the custom element? It’s a constant fight.We can scope things to the shadow root’s :host
selector to keep things protected.
<style>\\n body {\\n color: red;\\n font-family: fantasy;\\n font-size: 2em;\\n }\\n</style>\\n \\n<p>Hi</p>\\n<div>\\n <template shadowrootmode=\\"open\\">\\n <style> \\n /* reset the light dom styles */\\n :host { all: initial; }\\n </style>\\n <p>Hi</p>\\n <a href=\\"#\\">Click me</a>\\n </template>\\n</div>\\n \\n<p>Hi</p>
\\n\\n\\n\\nNew problem! What if the Light DOM styles are scoped to the universal selector instead?
\\n\\n\\n\\n<style>\\n * {\\n color: red;\\n font-family: fantasy;\\n font-size: 2em;\\n }\\n</style>\\n\\n<p>Hi</p>\\n<div>\\n <template shadowrootmode=\\"open\\">\\n <style> \\n /* reset the light dom styles */\\n :host { all: initial; }\\n </style>\\n <p>Hi</p>\\n <a href=\\"#\\">Click me</a>\\n </template>\\n</div>\\n\\n<p>Hi</p>
\\n\\n\\n\\nThis breaks the custom element’s styles. But that’s because Shadow DOM styles are applied before Light DOM styles. The styles scoped to the universal selector are simply applied after the :host
styles, which overrides what we have in the shadow root. So, we’re still locked in a brutal fight over inheritance and need stronger specificity.
According to Scott, !important
is one of the only ways we have to apply brute force to protect our custom elements from outside styles leaking in. The keyword gets a bad rap — and rightfully so in the vast majority of cases — but this is a case where it works well and using it is an encouraged practice. It’s not like it has an impact on the styles outside the custom element, anyway.
<style>\\n * {\\n color: red;\\n font-family: fantasy;\\n font-size: 2em;\\n }\\n</style>\\n\\n<p>Hi</p>\\n<div>\\n <template shadowrootmode=\\"open\\">\\n <style> \\n /* reset the light dom styles */\\n :host { all: initial; !important }\\n </style>\\n <p>Hi</p>\\n <a href=\\"#\\">Click me</a>\\n </template>\\n</div>\\n\\n<p>Hi</p>
\\n\\n\\nThere are some useful selectors we have to look at components from the outside, looking in.
\\n\\n\\n:host()
We just looked at this! But note how it is a function in addition to being a pseudo-selector. It’s sort of a parent selector in the sense that we can pass in the <div>
that contains the <template>
and that becomes the scoping context for the entire selector, meaning the !important
keyword is no longer needed.
<style>\\n * {\\n color: red;\\n font-family: fantasy;\\n font-size: 2em;\\n }\\n</style>\\n\\n<p>Hi</p>\\n<div>\\n <template shadowrootmode=\\"open\\">\\n <style> \\n /* reset the light dom styles */\\n :host(div) { all: initial; }\\n </style>\\n <p>Hi</p>\\n <a href=\\"#\\">Click me</a>\\n </template>\\n</div>\\n\\n<p>Hi</p>
\\n\\n\\n:host-context()
<header>\\n <my-element>\\n <template shadowrootmode=\\"open\\">\\n <style>\\n :host-context(header) { ... } /* matches the host! */\\n </style>\\n </template>\\n </my-element>\\n</header>
\\n\\n\\n\\nThis targets the shadow host but only if the provided selector is a parent node anywhere up the tree. This is super helpful for styling custom elements where the layout context might change, say, from being contained in an <article>
versus being contained in a <header>
.
:defined
Defining an element occurs when it is created, and this pseudo-selector is how we can select the element in that initially-defined state. I imagine this is mostly useful for when a custom element is defined imperatively in JavaScript so that we can target the very moment that the element is constructed, and then set styles right then and there.
\\n\\n\\n\\n<style>\\n simple-custom:defined { display: block; background: green; color: #fff; }\\n</style>\\n<simple-custom></simple-custom>\\n\\n<script>\\ncustomElements.define(\'simple-custom\', class extends HTMLElement {\\n constructor(){\\n super();\\n this.attachShadow({mode: \'open\'});\\n this.shadowRoot.innerHTML = \\"<p>Defined!</p>\\";\\n }\\n});\\n</script>
\\n\\n\\n\\nMinor note about protecting against a flash of unstyled content (FOUC)… or unstyled element in this case. Some elements are effectively useless until JavsScript has interacted with it to generate content. For example, an empty custom element that only becomes meaningful once JavaScript runs and generates content. Here’s how we can prevent the inevitable flash that happens after the content is generated:
\\n\\n\\n\\n<style>\\n js-dependent-element:not(:defined) {\\n visibility: hidden;\\n }\\n</style>\\n\\n<js-dependent-element></js-dependent-element>
\\n\\n\\n\\nWarning zone! It’s best for elements that are empty and not yet defined. If you’re working with a meaningful element up-front, then it’s best to style as much as you can up-front.
\\n\\n\\nThis does not style the paragraph green
as you might expect:
<div>\\n <template shadowrootmode=\\"open\\">\\n <style>\\n p { color: green; }\\n </style>\\n <slot></slot>\\n </template>\\n \\n <p>Slotted Element</p>\\n</div>
\\n\\n\\n\\nThe Shadow DOM cannot style this content directly. The styles would apply to a paragraph in the <template>
that gets rendered in the Light DOM, but it cannot style it when it is slotted into the <template>
.
Slots are part of the Light DOM. So, this works:
\\n\\n\\n\\n<style>\\n p { color: green; }\\n</style>\\n\\n<div>\\n <template shadowrootmode=\\"open\\">\\n <slot></slot>\\n </template>\\n \\n <p>Slotted Element</p>\\n</div>
\\n\\n\\n\\nThis means that slots are easier to target when it comes to piercing the shadow root with styles, making them a great method of progressive style enhancement.
\\n\\n\\n\\nWe have another special selected, the ::slotted()
pseudo-element that’s also a function. We pass it an element or class and that allows us to select elements from within the shadow root.
<div>\\n <template shadowrootmode=\\"open\\">\\n <style> ::slotted(p) { color: red; } </style>\\n <slot></slot>\\n </template>\\n \\n <p>Slotted Element</p>\\n</div>
\\n\\n\\n\\nUnfortunately, ::slotted()
is a weak selected when compared to global selectors. So, if we were to make this a little more complicated by introducing an outside inheritable style, then we’d be hosed again.
<style>\\n /* global paragraph style... */\\n p { color: green; }\\n</style>\\n\\n<div>\\n <template shadowrootmode=\\"open\\">\\n <style>\\n /* ...overrides the slotted style */\\n ::slotted(p) { color: red; }\\n </style>\\n <slot></slot>\\n </template>\\n \\n <p>Slotted Element</p>\\n</div>
\\n\\n\\n\\nThis is another place where !important
could make sense. It even wins if the global style is also set to !important
. We could get more defensive and pass the universal selector to ::slotted
and set everything back to its initial value so that all slotted content is encapsulated from outside styles leaking in.
<style>\\n /* global paragraph style... */\\n p { color: green; }\\n</style>\\n\\n<div>\\n <template shadowrootmode=\\"open\\">\\n <style>\\n /* ...can\'t override this important statement */\\n ::slotted(*) { all: initial !important; }\\n </style>\\n <slot></slot>\\n </template>\\n \\n <p>Slotted Element</p>\\n</div>
\\n\\n\\n:parts
A part is a way of offering up Shadow DOM elements to the parent document for styling. Let’s add a part to a custom element:
\\n\\n\\n\\n<div>\\n <template shadowrootmode=\\"open\\">\\n <p part=\\"hi\\">Hi there, I\'m a part!</p>\\n </template>\\n</div>
\\n\\n\\n\\nWithout the part
attribute, there is no way to write styles that reach the paragraph. But with it, the part is exposed as something that can be styled.
<style>\\n ::part(hi) { color: green; }\\n ::part(hi) b { color: green; } /* nope! */\\n</style>\\n\\n<div>\\n <template shadowrootmode=\\"open\\">\\n <p part=\\"hi\\">Hi there, I\'m a <b>part</b>!</p>\\n </template>\\n</div>
\\n\\n\\n\\nWe can use this to expose specific “parts” of the custom element that are open to outside styling, which is almost like establishing a styling API with specifications for what can and can’t be styled. Just note that ::part
cannot be used as part of a complex selector, like a descendant selector:
A bit in the weeds here, but we can export parts in the sense that we can nest elements within elements within elements, and so on. This way, we include parts within elements.
\\n\\n\\n\\n<my-component>\\n <!-- exposes three parts to the nested component --\x3e\\n <nested-component exportparts=\\"part1, part2, part5\\"></nested-component>\\n</my-component>
\\n\\n\\nWe discussed this when going over element internals in the chapter about the Shadow DOM. But it’s worth revisiting that now that we’re specifically talking about styling. We have a :state
pseudo-function that accepts our defined states.
<script>\\n this.internals.states.add(\\"checked\\");\\n</script>\\n\\n<style>\\n my-checkbox:state(checked) {\\n /* ... */\\n }\\n</style>
\\n\\n\\n\\nWe also have access to the :invalid
pseudo-class.
<style>\\n:root {\\n --text-primary: navy;\\n --bg-primary: #abe1e1;\\n --padding: 1.5em 1em;\\n}\\n\\np {\\n color: var(--text-primary);\\n background: var(--bg-primary);\\n padding: var(--padding);\\n}\\n</style>
\\n\\n\\n\\nCustom properties cross the Shadow DOM barrier!
\\n\\n\\n\\n<my-elem></my-elem>\\n\\n<script>\\n customElements.define(\'my-elem\', class extends HTMLElement {\\n constructor(){\\n super();\\n this.attachShadow({mode: \'open\'});\\n this.shadowRoot.innerHTML = `\\n <style>\\n p {\\n color: var(--text-primary);\\n background: var(--bg-primary);\\n padding: var(--padding);\\n }\\n </style>\\n \\n <p>Hi there!</p>`;\\n }\\n })\\n</script>
\\n\\n\\n\\nThere’s the classic ol’ external <link>
way of going about it:
<simple-custom>\\n <template shadowrootmode=\\"open\\">\\n <link rel=\\"stylesheet\\" href=\\"../../assets/external.css\\">\\n <p>This one\'s in the shadow Dom.</p>\\n <slot></slot>\\n </template>\\n\\n <p>Slotted <b>Element</b></p>\\n\\n</simple-custom>
\\n\\n\\n\\nIt might seem like an anti-DRY approach to call the same external stylesheet at the top of all web components. To be clear, yes, it is repetitive — but only as far as writing it. Once the sheet has been downloaded once, it is available across the board without any additional requests, so we’re still technically dry in the sense of performance.
\\n\\n\\n\\nCSS imports also work:
\\n\\n\\n\\n<style>\\n @import url(\\"../../assets/external.css\\");\\n</style>\\n\\n<simple-custom>\\n <template shadowrootmode=\\"open\\">\\n <style>\\n @import url(\\"../../assets/external.css\\");\\n </style>\\n <p>This one\'s in the shadow Dom.</p>\\n <slot></slot>\\n </template>\\n\\n <p>Slotted <b>Element</b></p>\\n\\n</simple-custom>
\\n\\n\\n\\nOne more way using a JavaScript-based approach. It’s probably better to make CSS work without a JavaScript dependency, but it’s still a valid option.
\\n\\n\\n\\n<my-elem></my-elem>\\n\\n<script type=\\"module\\">\\n import sheet from \'../../assets/external.css\' with { type: \'css\' };\\n\\n customElements.define(\'my-elem\', class extends HTMLElement {\\n constructor(){\\n super();\\n this.attachShadow({mode: \'open\'});\\n this.shadowRoot.innerHTML = \'<p>Hi there</p>\';\\n this.shadowRoot.adoptedStyleSheets = [sheet];\\n }\\n })\\n</script>
\\n\\n\\n\\nWe have a JavaScript module and import CSS into a string that is then adopted by the shadow root using shadowRoort.adoptedStyleSheets
. And since adopted stylesheets are dynamic, we can construct one, share it across multiple instances, and update styles via the CSSOM that ripple across the board to all components that adopt it.
Container queries are nice to pair with components, as custom elements and web components are containers and we can query them and adjust things as the container changes.
\\n\\n\\n\\n<div>\\n<template shadowrootmode=\\"open\\">\\n <style>\\n :host {\\n container-type: inline-size;\\n background-color: tan;\\n display: block;\\n padding: 2em;\\n }\\n \\n ul {\\n display: block;\\n list-style: none;\\n margin: 0;\\n }\\n \\n li {\\n padding: .5em;\\n margin: .5em 0;\\n background-color: #fff;\\n }\\n \\n @container (min-width: 50em) {\\n ul {\\n display: flex;\\n justify-content: space-between;\\n gap: 1em;\\n }\\n li {\\n flex: 1 1 auto;\\n }\\n }\\n </style>\\n \\n <ul>\\n <li>First Item</li>\\n <li>Second Item</li>\\n </ul>\\n \\n</template>\\n</div>
\\n\\n\\n\\nIn this example, we’re setting styles on the :host()
to define a new container, as well as some general styles that are protected and scoped to the shadow root. From there, we introduce a container query that updates the unordered list’s layout when the custom element is at least 50em
wide.
How web component features are used together!
\\n\\n\\nIn this chapter, Scott focuses on how other people are using web components in the wild and highlights a few of the more interesting and smart patterns he’s seen.
\\n\\n\\nIt’s often the very first example used in React tutorials.
\\n\\n\\n\\n<counter-element></counter-element>\\n\\n<script type=\\"module\\">\\n customElements.define(\'counter-element\', class extends HTMLElement {\\n #count = 0;\\n connectedCallback() {\\n this.innerHTML = `<button id=\\"dec\\">-</button><p id=\\"count\\">${this.#count}</p><button id=\\"inc\\">+</button>`;\\n this.addEventListener(\'click\', e => this.update(e) );\\n }\\n update(e) {\\n if( e.target.nodeName !== \'BUTTON\' ) { return }\\n this.#count = e.target.id === \'inc\' ? this.#count + 1 : this.#count - 1;\\n this.querySelector(\'#count\').textContent = this.#count;\\n }\\n });\\n</script>
\\n\\n\\nReef is a tiny library by Chris Ferdinandi that weighs just 2.6KB minified and zipped yet still provides DOM diffing for reactive state-based UIs like React, which weighs significantly more. An example of how it works in a standalone way:
\\n\\n\\n\\n<div id=\\"greeting\\"></div>\\n\\n<script type=\\"module\\">\\n import {signal, component} from \'.../reef.es..min.js\';\\n // Create a signal\\n let data = signal({\\n greeting: \'Hello\',\\n name: \'World\'\\n });\\n component(\'#greeting\', () => `<p>${data.greeting}, ${data.name}!</p>`);\\n</script>
\\n\\n\\n\\nThis sets up a “signal” that is basically a live-update object, then calls the component()
method to select where we want to make the update, and it injects a template literal in there that passes in the variables with the markup we want.
So, for example, we can update those values on setTimeout
:
<div id=\\"greeting\\"></div>\\n\\n<script type=\\"module\\">\\n import {signal, component} from \'.../reef.es..min.js\';\\n // Create a signal\\n let data = signal({\\n greeting: \'Hello\',\\n name: \'World\'\\n });\\n component(\'#greeting\', () => `<p>${data.greeting}, ${data.name}!</p>`);\\n \\n setTimeout(() => {\\n data.greeting = \'¡Hola\'\\n data,name = \'Scott\'\\n }, 3000)\\n</script>
\\n\\n\\n\\nWe can combine this sort of library with a web component. Here, Scott imports Reef and constructs the data outside the component so that it’s like the application state:
\\n\\n\\n\\n<my-greeting></my-greeting>\\n\\n<script type=\\"module\\">\\n import {signal, component} from \'https://cdn.jsdelivr.net/npm/reefjs@13/dist/reef.es.min.js\';\\n \\n window.data = signal({\\n greeting: \'Hi\',\\n name: \'Scott\'\\n });\\n \\n customElements.define(\'my-greeting\', class extends HTMLElement {\\n connectedCallback(){\\n component(this, () => `<p>${data.greeting}, ${data.name}!</p>` );\\n }\\n });\\n</script>
\\n\\n\\n\\nIt’s the virtual DOM in a web component! Another approach that is more reactive in the sense that it watches for changes in attributes and then updates the application state in response which, in turn, updates the greeting.
\\n\\n\\n\\n<my-greeting greeting=\\"Hi\\" name=\\"Scott\\"></my-greeting>\\n\\n<script type=\\"module\\">\\n import {signal, component} from \'https://cdn.jsdelivr.net/npm/reefjs@13/dist/reef.es.min.js\';\\n customElements.define(\'my-greeting\', class extends HTMLElement {\\n static observedAttributes = [\\"name\\", \\"greeting\\"];\\n constructor(){\\n super();\\n this.data = signal({\\n greeting: \'\',\\n name: \'\'\\n });\\n }\\n attributeChangedCallback(name, oldValue, newValue) {\\n this.data[name] = newValue;\\n }\\n connectedCallback(){\\n component(this, () => `<p>${this.data.greeting}, ${this.data.name}!</p>` );\\n }\\n });\\n</script>
\\n\\n\\n\\nIf the attribute changes, it only changes that instance. The data is registered at the time the component is constructed and we’re only changing string attributes rather than objects with properties.
\\n\\n\\nThis describes web components that are not empty by default like this:
\\n\\n\\n\\n<my-greeting></my-greeting>
\\n\\n\\n\\nThis is a “React” mindset where all the functionality, content, and behavior comes from JavaScript. But Scott reminds us that web components are pretty useful right out of the box without JavaScript. So, “HTML web components” refers to web components that are packed with meaningful content right out of the gate and Scott points to Jeremy Keith’s 2023 article coining the term.
\\n\\n\\n\\n\\n\\n\\n\\n\\n[…] we could call them “HTML web components.” If your custom element is empty, it’s not an HTML web component. But if you’re using a custom element to extend existing markup, that’s an HTML web component.
\\n
Jeremy cites something Robin Rendle mused about the distinction:
\\n\\n\\n\\n\\n\\n\\n\\n\\n[…] I’ve started to come around and see Web Components as filling in the blanks of what we can do with hypertext: they’re really just small, reusable chunks of code that extends the language of HTML.
\\n
The “React” way:
\\n\\n\\n\\n<UserAvatar\\n src=\\"https://example.com/path/to/img.jpg\\"\\n alt=\\"...\\"\\n/>
\\n\\n\\n\\nThe props look like HTML but they’re not. Instead, the props provide information used to completely swap out the <UserAvatar />
tag with the JavaScript-based markup.
Web components can do that, too:
\\n\\n\\n\\n<user-avatar\\n src=\\"https://example.com/path/to/img.jpg\\"\\n alt=\\"...\\"\\n></user-avatar>
\\n\\n\\n\\nSame deal, real HTML. Progressive enhancement is at the heart of an HTML web component mindset. Here’s how that web component might work:
\\n\\n\\n\\nclass UserAvatar extends HTMLElement {\\n connectedCallback() {\\n const src = this.getAttribute(\\"src\\");\\n const name = this.getAttribute(\\"name\\");\\n this.innerHTML = `\\n <div>\\n <img src=\\"${src}\\" alt=\\"Profile photo of ${name}\\" width=\\"32\\" height=\\"32\\" />\\n <!-- Markup for the tooltip --\x3e\\n </div>\\n `;\\n }\\n}\\ncustomElements.define(\'user-avatar\', UserAvatar);
\\n\\n\\n\\nBut a better starting point would be to include the <img>
directly in the component so that the markup is immediately available:
<user-avatar>\\n <img src=\\"https://example.com/path/to/img.jpg\\" alt=\\"...\\" />\\n</user-avatar>
\\n\\n\\n\\nThis way, the image is downloaded and ready before JavaScript even loads on the page. Strive for augmentation over replacement!
\\n\\n\\nresizeasaurus
This helps developers test responsive component layouts, particularly ones that use container queries.
\\n\\n\\n\\n<resize-asaurus>\\n Drop any HTML in here to test.\\n</resize-asaurus>\\n\\n<!-- for example: --\x3e\\n\\n<resize-asaurus>\\n <div class=\\"my-responsive-grid\\">\\n <div>Cell 1</div> <div>Cell 2</div> <div>Cell 3</div> <!-- ... --\x3e\\n </div>\\n</resize-asaurus>
\\n\\n\\n\\nlite-youtube-embed
This is like embedding a YouTube video, but without bringing along all the baggage that YouTube packs into a typical embed snippet.
\\n\\n\\n\\n<lite-youtube videoid=\\"ogYfd705cRs\\" style=\\"background-image: url(...);\\">\\n <a href=\\"https://youtube.com/watch?v=ogYfd705cRs\\" class=\\"lyt-playbtn\\" title=\\"Play Video\\">\\n <span class=\\"lyt-visually-hidden\\">Play Video: Keynote (Google I/O \'18)</span>\\n </a>\\n</lite-youtube>\\n\\n<link rel=\\"stylesheet\\" href=\\"./src.lite-yt-embed.css\\" />\\n<script src=\\"./src.lite-yt-embed.js\\" defer></script>
\\n\\n\\n\\nIt starts with a link which is a nice fallback if the video fails to load for whatever reason. When the script runs, the HTML is augmented to include the video <iframe>
.
Lit extends the base class and then extends what that class provides, but you’re still working directly on top of web components. There are syntax shortcuts for common patterns and a more structured approach.
\\n\\n\\n\\nThe package includes all this in about 5-7KB:
\\n\\n\\n\\n<simple-greeting name=\\"Geoff\\"></simple-greeting>\\n\\n<script>\\n import {html, css, LitElement} from \'lit\';\\n \\n export class SimpleGreeting extends LitElement {\\n state styles = css`p { color: blue }`;\\n static properties = {\\n name: {type = String},\\n };\\n\\t\\t\\n constructor() {\\n super();\\n this.name = \'Somebody\';\\n }\\n\\t\\t\\n render() {\\n return html`<p>Hello, ${this.name}!</p>`;\\n }\\n }\\n\\t\\n customElements.define(\'simple-greeting\', SimpleGreeting);\\n</script>
\\n\\n\\n\\nPros | Cons |
---|---|
Ecosystem | No official SSR story (but that is changing) |
Community | |
Familiar ergonomics | |
Lightweight | |
Industry-proven |
This is part of the 11ty project. It allows you to define custom elements as files, writing everything as a single file component.
\\n\\n\\n\\n<!-- starting element / index.html --\x3e\\n<my-element></my-element>\\n\\n<!-- ../components/my-element.webc --\x3e\\n<p>This is inside the element</p>\\n\\n<style>\\n /* etc. */\\n</style>\\n\\n<script>\\n // etc.\\n</script>
\\n\\n\\n\\nPros | Cons |
---|---|
Community | Geared toward SSG |
SSG progressive enhancement | Still in early stages |
Single file component syntax | |
Zach Leatherman! |
This is Scott’s favorite! It renders web components on the server. Web components can render based on application state per request. It’s a way to use custom elements on the server side.
\\n\\n\\n\\nPros | Cons |
---|---|
Ergonomics | Still in early stages |
Progressive enhancement | |
Single file component syntax | |
Full-stack stateful, dynamic SSR components |
This is a super short module simply highlighting a few of the more notable libraries for web components that are offered by third parties. Scott is quick to note that all of them are closer in spirit to a React-based approach where custom elements are more like replaced elements with very little meaningful markup to display up-front. That’s not to throw shade at the libraries, but rather to call out that there’s a cost when we require JavaScript to render meaningful content.
\\n\\n\\n<sp-button variant=\\"accent\\" href=\\"components/button\\">\\n Use Spectrum Web Component buttons\\n</sp-button>
\\n\\n\\n\\nMost components are not exactly HTML-first. The pattern is closer to replaced elements. There’s plenty of complexity, but that makes sense for a system that drives an application like Photoshop and is meant to drop into any project. But still, there is a cost when it comes to delivering meaningful content to users up-front. An all-or-nothing approach like this might be too stark for a small website project.
\\n\\n\\n<fast-checkbox>Checkbox</fast-checkbox>
\\n\\n\\n\\n<sl-button>Click Me</sl-button>
\\n\\n\\n\\nScott covers what the future holds for web components as far as he is aware.
\\n\\n\\nDefine an element in HTML alone that can be used time and again with a simpler syntax. There’s a GitHub issue that explains the idea, and Zach Leatherman has a great write-up as well.
\\n\\n\\n\\nMake it easier to pair custom elements with other elements in the Light DOM as well as other custom elements through ARIA.
\\n\\n\\n\\nHow can we use container queries without needing an extra wrapper around the custom element?
\\n\\n\\nThis was one of the web components’ core features but was removed at some point. They can define HTML in an external place that could be used over and over.
\\n\\n\\n\\nThis is also known as “open styling.”
\\n\\n\\n\\nThis would be a templating feature that allows for JSX-string-literal-like syntax where variables inject data.
\\n\\n\\n\\n<section>\\n <h1 id=\\"name\\">{name}</h1>\\n Email: <a id=\\"link\\" href=\\"mailto:{email}\\">{email}</a>\\n</section>
\\n\\n\\n\\nAnd the application has produced a template with the following content:
\\n\\n\\n\\n<template>\\n <section>\\n <h1 id=\\"name\\">{{}}</h1>\\n Email: <a id=\\"link\\" href=\\"{{}}\\">{{}}</a>\\n </section>\\n</template>
\\n\\n\\n\\nUsing variations of the same web component without name collisions.
\\n\\n\\n\\nWeb Components Demystified originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
","description":"Scott Jehl released a course called Web Components Demystified. I love that name because it says what the course is about right on the tin: you’re going to learn about web components and clear up any confusion you may already have about them. And there’s plenty of confusion to…","guid":"https://css-tricks.com/?p=383422","author":"Geoff Graham","authorUrl":null,"authorAvatar":null,"insertedAt":"2025-03-14T13:19:36.892Z","publishedAt":"2025-03-14T12:51:59.963Z","media":[{"url":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/01/Screenshot-2025-01-13-at-11.22.04%E2%80%AFAM.png?resize=2058%2C912&ssl=1","type":"photo","width":2058,"height":912},{"url":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/01/Screenshot-2025-01-23-at-9.02.01%E2%80%AFAM.png?resize=1024%2C485&ssl=1","type":"photo","width":1024,"height":485},{"url":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/01/Screenshot-2025-01-28-at-12.37.56%E2%80%AFPM.png?resize=1740%2C988&ssl=1","type":"photo","width":1740,"height":988},{"url":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/01/Screenshot-2025-01-28-at-12.41.24%E2%80%AFPM.png?resize=1724%2C828&ssl=1","type":"photo","width":1724,"height":828},{"url":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/01/Screenshot-2025-01-27-at-9.56.48%E2%80%AFAM.png?resize=1204%2C662&ssl=1","type":"photo","width":1204,"height":662},{"url":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/01/Screenshot-2025-01-27-at-10.24.26%E2%80%AFAM.png?resize=1300%2C616&ssl=1","type":"photo","width":1300,"height":616},{"url":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/01/Screenshot-2025-01-27-at-11.16.58%E2%80%AFAM.png?resize=1024%2C463&ssl=1","type":"photo","width":1024,"height":463},{"url":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/02/Screenshot-2025-01-29-at-8.12.45%E2%80%AFAM.png?resize=1532%2C928&ssl=1","type":"photo","width":1532,"height":928},{"url":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/02/Screenshot-2025-01-29-at-10.01.34%E2%80%AFAM.png?resize=1710%2C944&ssl=1","type":"photo","width":1710,"height":944},{"url":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/02/Screenshot-2025-01-30-at-2.26.38%E2%80%AFPM.png?resize=1668%2C846&ssl=1","type":"photo","width":1668,"height":846}],"categories":["Notes","web components"],"attachments":null,"extra":null,"language":null,"feeds":{"type":"feed","id":"41719081557593116","url":"https://css-tricks.com/feed/","title":"CSS-Tricks","description":"Tips, Tricks, and Techniques on using Cascading Style Sheets.","siteUrl":"https://css-tricks.com/","image":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1","errorMessage":null,"errorAt":null,"ownerUserId":null}},{"feedId":"42579624844251149","id":"123021931613851648","title":"How To Prevent WordPress SQL Injection Attacks","url":"https://smashingmagazine.com/2025/03/how-prevent-wordpress-sql-injection-attacks/","content":"Did you know that your WordPress site could be a target for hackers right now? That’s right! Today, WordPress powers over 43% of all websites on the internet. That kind of public news makes WordPress sites a big target for hackers.
\\nOne of the most harmful ways they attack is through an SQL injection. A SQL injection may break your website, steal data, and destroy your content. More than that, they can lock you out of your website! Sounds scary, right? But don’t worry, you can protect your site. That is what this article is about.
\\nWhat Is SQL?\\nSQL stands for Structured Query Language. It is a way to talk to databases, which store and organize a lot of data, such as user details, posts, or comments on a website. SQL helps us ask the database for information or give it new data to store.
\\nWhen writing an SQL query, you ask the database a question or give it a task. For example, if you want to see all users on your site, an SQL query can retrieve that list.
\\nSQL is powerful and vital since all WordPress sites use databases to store content.
\\nWhat Is An SQL Injection Attack?\\nWordPress SQL injection attacks try to gain access to your site’s database. An SQL injection (SQLi) lets hackers exploit a vulnerable SQL query to run a query they made. The attack occurs when a hacker tricks a database into running harmful SQL commands.
\\nHackers can send these commands via input fields on your site, such as those in login forms or search bars. If the website does not check input carefully, a command can grant access to the database. Imagine a hacker typing an SQL command instead of typing a username. It may fool the database and show private data such as passwords and emails. The attacker could use it to change or delete database data.
\\nYour database holds all your user-generated data and content. It stores pages, posts, links, comments, and users. For the “bad” guys, it is a goldmine of valuable data.
\\nSQL injections are dangerous as they let hackers steal data or take control of a website. A WordPress firewall prevents SQL injection attacks. Those attacks can compromise and hack sites very fast.
\\nSQL Injections: Three Main Types\\nThere are three main kinds of SQL injection attacks. Every type works in various ways, but they all try to fool the database. We’re going to look at every single type.
\\nThis is perhaps the most common type of attack. A hacker sends the command and gets the results using the same communication method. It is to make a request and get the answer right away.
\\nThere are two types of In-band SQLi injection attacks:
\\nWith error-based SQLi, the hacker causes the database to give an error message. This message may reveal crucial data, such as database structure and settings.
\\nWhat about union-based SQLi attacks? The hacker uses the SQL UNION statement to combine their request with a standard query. It can give them access to other data stored in the database.
\\nWith inferential SQLi, the hacker will not see the results at once. Instead, they ask for database queries that give “yes” and “no” answers. Hackers can reveal the database structure or data by how the site responds.
\\nThey do that in two common ways:
\\nThrough Boolean-based SQLi, the hacker sends queries that can only be “true” or “false.” For example, is this user ID more than 100? This allows hackers to gather more data about the site based on how it reacts.
\\nIn time-based SQLi, the hacker asks a query that makes the database take longer to reply if the answer is “yes.” They can figure out what they need to know due to the delay.
\\nOut-of-band SQLi is a less common but equally dangerous type of attack. Hackers use various ways to get results. Usually, they connect the database to a server they control.
\\nThe hacker does not see the results all at once. However, they can get the data sent somewhere else via email or a network connection. This method applies when the site blocks ordinary SQL injection methods.
\\nWhy Preventing SQL Injection Is Crucial\\nSQL injections are a giant risk for websites. They can lead to various harms — stolen data, website damage, legal issues, loss of trust, and more.
\\nHackers can steal data like usernames, passwords, and emails. They may cause damage by deleting and changing your data. Besides, it messes up your site structure, making it unusable.
\\nIs your user data stolen? You might face legal troubles if your site treats sensitive data. People may lose trust in you if they see that your site gets hacked. As a result, the reputation of your site can suffer.
\\nThus, it is so vital to prevent SQL injections before they occur.
\\n11 Ways To Prevent WordPress SQL Injection Attacks\\nOK, so we know what SQL is and that WordPress relies on it. We also know that attackers take advantage of SQL vulnerabilities. I’ve collected 11 tips for keeping your WordPress site free of SQL injections. The tips limit your vulnerability and secure your site from SQL injection attacks.
\\nSQL injection attacks usually occur via forms or input fields on your site. It could be inside a login form, a search box, a contact form, or a comment section. Does a hacker enter bad SQL commands into one of these fields? They may fool your site, giving them access to your database by running those commands.
\\nHence, always sanitize and validate all input data on your site. Users should not be able to submit data if it does not follow a specific format. The easiest way to avoid this is to use a plugin like Formidable Forms, an advanced builder for adding forms. That said, WordPress has many built-in functions to sanitize and validate input on your own. It includes sanitize_text_field()
, sanitize_email()
, and sanitize_url()
.
The validation cleans up user inputs before they get sent to your database. These functions strip out unwanted characters and ensure the data is safe to store.
\\nDynamic SQL allows you to create SQL statements on the fly at runtime. How does dynamic SQL work compared to static SQL? You can create flexible and general SQL queries adjusted to various conditions. As a result, dynamic SQL is typically slower than static SQL, as it demands runtime parsing.
\\nDynamic SQL can be more vulnerable to SQL injection attacks. It occurs when the bad guy alters a query by injecting evil SQL code. The database may respond and run this harmful code. As a result, the attacker can access data, corrupt it, or even hack your entire database.
\\nHow do you keep your WordPress site safe? Use prepared statements, stored procedures or parameterized queries.
\\nKeeping WordPress and all plugins updated is the first step in keeping your site safe. Hackers often look for old software versions with known security issues.
\\nThere are regular security updates for WordPress, themes, and plugins. They fix security issues. You leave your site open to attacks as you ignore these updates.
\\nTo stay safe, set up automatic updates for minor WordPress versions. Check for theme and plugin updates often. Only use trusted plugins from the official WordPress source or well-known developers.
\\nBy updating often, you close many ways hackers could attack.
\\nA firewall is one of the best ways to keep your WordPress website safe. It is a shield for your WordPress site and a security guard that checks all incoming traffic. The firewall decides who can enter your site and who gets blocked.
\\nThere are five main types of WordPress firewalls:
\\nPlugin-based firewalls you install on your WordPress site. They work from within your website to block the bad traffic. Web application firewalls filter, check and block the traffic to and from a web service. They detect and defend against risky security flaws that are most common in web traffic. Cloud-based firewalls work from outside your site. They block the bad traffic before it even reaches your site. DNS-level firewalls send your site traffic via their cloud proxy servers, only letting them direct real traffic to your web server. Finally, application-level firewalls check the traffic as it reaches your server. That means before loading most of the WordPress scripts.
\\nStable security plugins like Sucuri and Wordfence can also act as firewalls.
\\nOlder WordPress versions display the WordPress version in the admin footer. It’s not always a bad thing to show your version of WordPress. But revealing it does provide virtual ammo to hackers. They want to exploit vulnerabilities in outdated WordPress versions.
\\nAre you using an older WordPress version? You can still hide your WordPress version:
\\nfunctions.php
file.function hide_wordpress_version() {\\n return \'\';\\n}\\nadd_filter(\'the_generator\', \'hide_wordpress_version\');\\n
\\n\\nThis code stops your WordPress version number from showing in the theme’s header.php
file and RSS feeds. It adds a small but helpful layer of security. Thus, it becomes more difficult for hackers to detect.
Bad guys can see how your database is set up via error notices. Ensure creating a custom database error notice that users see to stop it. Hackers will find it harder to detect weak spots in your site when you hide error details. The site will stay much safer when you show less data on the front end.
\\nTo do that, copy and paste the code into a new db-error.php
file. Jeff Starr has a classic article on the topic from 2009 with an example:
<?php // Custom WordPress Database Error Page\\n header(\'HTTP/1.1 503 Service Temporarily Unavailable\');\\n header(\'Status: 503 Service Temporarily Unavailable\');\\n header(\'Retry-After: 600\'); // 1 hour = 3600 seconds\\n\\n// If you want to send an email to yourself upon an error\\n// mail(\\"your@email.com\\", \\"Database Error\\", \\"There is a problem with the database!\\", \\"From: Db Error Watching\\");\\n?>
<!DOCTYPE HTML>\\n<html>\\n <head>\\n <title>Database Error</title>\\n <style>\\n body { padding: 50px; background: #04A9EA; color: #fff; font-size: 30px; }\\n .box { display: flex; align-items: center; justify-content: center; }\\n </style>\\n</head>\\n\\n <body>\\n <div class=\\"box\\">\\n <h1>Something went wrong</h1>\\n </div>\\n </body>\\n</html>\\n
\\nNow save the file in the root of your /wp-content/
folder for it to take effect.
Assign only the permissions that each role demands to do its tasks. For example, Editors may not need access to the WordPress database or plugin settings. Improve site security by giving only the admin role full dashboard access. Limiting access to features for fewer roles reduces the odds of an SQL injection attack.
\\nA great way to protect your WordPress site is to apply two-factor authentication (2FA). Why? Since it adds an extra layer of security to your login page. Even if a hacker cracks your password, they still won’t be able to log in without getting access to the 2FA code.
\\nSetting up 2FA on WordPress goes like this:
\\nAssure erasing tables you no longer use and delete junk or unapproved comments. Your database will be more resistant to hackers who try to exploit sensitive data.
\\nWatch for unusual activity on your site. You can check for actions like many failed login attempts or strange traffic spikes. Security plugins such as Wordfence or Sucuri alert you when something seems odd. That helps to catch issues before they get worse.
\\nRunning regular backups is crucial. With a backup, you can quickly restore your site to its original state if it gets hacked. You want to do this anytime you execute a significant update on your site. Also, it regards updating your theme and plugins.
\\nBegin to create a plan for your backups so it suits your needs. For example, if you publish new content every day, then it may be a good idea to back up your database and files daily.
\\nMany security plugins offer automated backups. Of course, you can also use backup plugins like UpdraftPlus or Solid Security. You should store backup copies in various locations, such as Dropbox and Google Drive. It will give you peace of mind.
\\nHow To Remove SQL Injection From Your Site\\nLet’s say you are already under attack and are dealing with an active SQL injection on your site. It’s not like any of the preventative measures we’ve covered will help all that much. Here’s what you can do to fight back and defend your site:
\\nHackers love weak sites. They look for easy ways to break in, steal data, and cause harm. One of the tricks they often use is SQL injection. If they find a way in, they can steal private data, alter your content, or even take over your site. That’s bad news both for you and your visitors.
\\nBut here is the good news: You can stop them! It is possible to block these attacks before they happen by taking the correct steps. And you don’t need to be a tech freak.
\\nMany people ignore website security until it’s too late. They think, “Why would a hacker target my site?” But hackers don’t attack only big sites. They attack any site with weak security. So, even small blogs and new websites are in danger. Once a hacker gets in, this person can cause you lots of damage. Fixing a hacked site takes time, effort, and money. But stopping an attack before it happens? That’s much easier.
\\nHackers don’t sit and wait, so why should you? Thousands of sites get attacked daily, so don’t let yours be the next one. Update your site, add a firewall, enable 2FA, and check your security settings. These small steps can help prevent giant issues in the future.
\\nYour site needs protection against the bad guys. You have worked hard to build it. Never neglect to update and protect it. After that, your site will be safer and sounder.
","description":"Did you know that your WordPress site could be a target for hackers right now? That’s right! Today, WordPress powers over 43% of all websites on the internet. That kind of public news makes WordPress sites a big target for hackers. One of the most harmful ways they attack is…","guid":"https://smashingmagazine.com/2025/03/how-prevent-wordpress-sql-injection-attacks/","author":"hello@smashingmagazine.com (Anders Johansson)","authorUrl":null,"authorAvatar":null,"insertedAt":"2025-03-13T14:48:49.099Z","publishedAt":"2025-03-13T08:00:00.629Z","media":[{"url":"http://files.smashing.media/articles/how-prevent-wordpress-sql-injection-attacks/how-prevent-wordpress-sql-injection-attacks.jpg","type":"photo","width":1200,"height":675,"blurhash":"LARC*+?wx[~q_1-Bxvt6vPENNYRj"}],"categories":null,"attachments":[{"url":"http://files.smashing.media/articles/how-prevent-wordpress-sql-injection-attacks/how-prevent-wordpress-sql-injection-attacks.jpg","mime_type":"image/jpg","size_in_bytes":"0"}],"extra":null,"language":null,"feeds":{"type":"feed","id":"42579624844251149","url":"https://www.smashingmagazine.com/feed/","title":"Articles on Smashing Magazine — For Web Designers And Developers","description":"Recent content in Articles on Smashing Magazine — For Web Designers And Developers","siteUrl":"https://www.smashingmagazine.com/","image":"https://www.smashingmagazine.com/images/favicon/app-icon-512x512.png","errorMessage":null,"errorAt":null,"ownerUserId":null}},{"feedId":"55793257860504606","id":"123201746784029696","title":"Making a Browser Based Game With Vanilla JS and CSS","url":"https://www.sitepoint.com/browser-game-with-vanilla-js-and-css/?utm_source=rss","content":"\\n\\n Continue reading\\n Making a Browser Based Game With Vanilla JS and CSS\\n on SitePoint.\\n
","description":"Learn how to build a flag guessing game using pure JavaScript and CSS without any other frameworks or libraries. Continue reading Making a Browser Based Game With Vanilla JS and CSS on…","guid":"/?p=502647","author":"Eoin McGrath","authorUrl":null,"authorAvatar":null,"insertedAt":"2025-03-14T02:43:20.328Z","publishedAt":"2025-03-12T22:30:59.932Z","media":[{"url":"https://uploads.sitepoint.com/wp-content/uploads/2025/03/1741883256Creating-a-Browser-Based-Game-With-Vanilla-JS-and-CSS.jpg","type":"photo","width":1920,"height":1008,"blurhash":"LSQIOdr]X5--.4x?RlV]_HX7nlaM"}],"categories":["CSS","JavaScript","Vanilla JavaScript"],"attachments":null,"extra":null,"language":null,"feeds":{"type":"feed","id":"55793257860504606","url":"https://www.sitepoint.com/sitepoint.rss","title":"SitePoint","description":"Learn CSS | HTML5 | JavaScript | Wordpress | Tutorials-Web Development | Reference | Books and More","siteUrl":"https://www.sitepoint.com/","image":null,"errorMessage":null,"errorAt":null,"ownerUserId":null}},{"feedId":"41719081557593116","id":"122321007244067840","title":"Powering Search With Astro Actions and Fuse.js","url":"https://css-tricks.com/powering-search-with-astro-actions-and-fuse-js/","content":"Static sites are wonderful. I’m a big fan.
\\n\\n\\n\\nThey also have their issues. Namely, static sites either are purely static or the frameworks that generate them completely lose out on true static generation when you just dip your toes in the direction of server routes.
\\n\\n\\n\\nAstro has been watching the front-end ecosystem and is trying to keep one foot firmly embedded in pure static generation, and the other in a powerful set of server-side functionality.
\\n\\n\\n\\nWith Astro Actions, Astro brings a lot of the power of the server to a site that is almost entirely static. A good example of this sort of functionality is dealing with search. If you have a content-based site that can be purely generated, adding search is either going to be something handled entirely on the front end, via a software-as-a-service solution, or, in other frameworks, converting your entire site to a server-side application.
\\n\\n\\n\\nWith Astro, we can generate most of our site during our build, but have a small bit of server-side code that can handle our search functionality using something like Fuse.js.
\\n\\n\\n\\nIn this demo, we’ll use Fuse to search through a set of personal “bookmarks” that are generated at build time, but return proper results from a server call.
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\nTo get started, we’ll just set up a very basic Astro project. In your terminal, run the following command:
\\n\\n\\n\\nnpm create astro@latest
\\n\\n\\n\\nAstro’s adorable mascot Houston is going to ask you a few questions in your terminal. Here are the basic responses, you’ll need:
\\n\\n\\n\\n./astro-search
This will create a directory in the location specified and install everything you need to start an Astro project. Open the directory in your code editor of choice and run npm run dev
in your terminal in the directory.
When you run your project, you’ll see the default Astro project homepage.
\\n\\n\\n\\nWe’re ready to get our project rolling!
\\n\\n\\nTo get started, let’s remove the default content from the homepage. Open the /src/pages/index.astro
file.
This is a fairly barebones homepage, but we want it to be even more basic. Remove the <Welcome />
component, and we’ll have a nice blank page.
For styling, let’s add Tailwind and some very basic markup to the homepage to contain our site.
\\n\\n\\n\\nnpx astro add tailwind
\\n\\n\\n\\nThe astro add
command will install Tailwind and attempt to set up all the boilerplate code for you (handy!). The CLI will ask you if you want it to add the various components, I recommend letting it, but if anything fails, you can copy the code needed from each of the steps in the process. As the last step for getting to work with Tailwind, the CLI will tell you to import the styles into a shared layout. Follow those instructions, and we can get to work.
Let’s add some very basic markup to our new homepage.
\\n\\n\\n\\n---\\n// ./src/pages/index.astro\\nimport Layout from \'../layouts/Layout.astro\';\\n---\\n\\n<Layout>\\n <div class=\\"max-w-3xl mx-auto my-10\\">\\n <h1 class=\\"text-3xl text-center\\">My latest bookmarks</h1>\\n <p class=\\"text-xl text-center mb-5\\">This is only 10 of A LARGE NUMBER THAT WE\'LL CHANGE LATER</p>\\n </div>\\n</Layout>
\\n\\n\\n\\nYour site should now look like this.
\\n\\n\\n\\nNot exactly winning any awards yet! That’s alright. Let’s get our bookmarks loaded in.
\\n\\n\\nSince not everyone runs their own application for bookmarking interesting items, you can borrow my data. Here’s a small subset of my bookmarks, or you can go get 110 items from this link on GitHub. Add this data as a file in your project. I like to group data in a data
directory, so my file lives in /src/data/bookmarks.json
.
[\\n {\\n \\"pageTitle\\": \\"Our Favorite Sandwich Bread | King Arthur Baking\\",\\n \\"url\\": \\"<https://www.kingarthurbaking.com/recipes/our-favorite-sandwich-bread-recipe>\\",\\n \\"description\\": \\"Classic American sandwich loaf, perfect for French toast and sandwiches.\\",\\n \\"id\\": \\"007y8pmEOvhwldfT3wx1MW\\"\\n },\\n {\\n \\"pageTitle\\": \\"Chris Coyier\'s discussion of Automatic Social Share Images | CSS-Tricks \\",\\n \\"url\\": \\"<https://css-tricks.com/automatic-social-share-images/>\\",\\n \\"description\\": \\"It\'s a pretty low-effort thing to get a big fancy link preview on social media. Toss a handful of specific <meta> tags on a URL and you get a big image-title-description thing \\",\\n \\"id\\": \\"04CXDvGQo19m0oXERL6bhF\\"\\n },\\n {\\n \\"pageTitle\\": \\"Automatic Social Share Images | ryanfiller.com\\",\\n \\"url\\": \\"<https://www.ryanfiller.com/blog/automatic-social-share-images/>\\",\\n \\"description\\": \\"Setting up automatic social share images with Puppeteer and Netlify Functions. \\",\\n \\"id\\": \\"04CXDvGQo19m0oXERLoC10\\"\\n },\\n {\\n \\"pageTitle\\": \\"Emma Wedekind: Foundations of Design Systems / React Boston 2019 - YouTube\\",\\n \\"url\\": \\"<https://m.youtube.com/watch?v=pXb2jA43A6k>\\",\\n \\"description\\": \\"Emma Wedekind: Foundations of Design Systems / React Boston 2019 Presented by: Emma Wedekind – LogMeIn Design systems are in the world around us, from street...\\",\\n \\"id\\": \\"0d56d03e-aba4-4ebd-9db8-644bcc185e33\\"\\n },\\n {\\n \\"pageTitle\\": \\"Editorial Design Patterns With CSS Grid And Named Columns — Smashing Magazine\\",\\n \\"url\\": \\"<https://www.smashingmagazine.com/2019/10/editorial-design-patterns-css-grid-subgrid-naming/>\\",\\n \\"description\\": \\"By naming lines when setting up our CSS Grid layouts, we can tap into some interesting and useful features of Grid — features that become even more powerful when we introduce subgrids.\\",\\n \\"id\\": \\"13ac1043-1b7d-4a5b-a3d8-b6f5ec34cf1c\\"\\n },\\n {\\n \\"pageTitle\\": \\"Netlify pro tip: Using Split Testing to power private beta releases - DEV Community 👩💻👨💻\\",\\n \\"url\\": \\"<https://dev.to/philhawksworth/netlify-pro-tip-using-split-testing-to-power-private-beta-releases-a7l>\\",\\n \\"description\\": \\"Giving users ways to opt in and out of your private betas. Video and tutorial.\\",\\n \\"id\\": \\"1fbabbf9-2952-47f2-9005-25af90b0229e\\"\\n },\\n {\\n \\"pageTitle\\": \\"Netlify Public Folder, Part I: What? Recreating the Dropbox Public Folder With Netlify | Jim Nielsen’s Weblog\\",\\n \\"url\\": \\"<https://blog.jim-nielsen.com/2019/netlify-public-folder-part-i-what/>\\",\\n\\n \\"id\\": \\"2607e651-7b64-4695-8af9-3b9b88d402d5\\"\\n },\\n {\\n \\"pageTitle\\": \\"Why Is CSS So Weird? - YouTube\\",\\n \\"url\\": \\"<https://m.youtube.com/watch?v=aHUtMbJw8iA&feature=youtu.be>\\",\\n \\"description\\": \\"Love it or hate it, CSS is weird! It doesn\'t work like most programming languages, and it doesn\'t work like a design tool either. But CSS is also solving a v...\\",\\n \\"id\\": \\"2e29aa3b-45b8-4ce4-85b7-fd8bc50daccd\\"\\n },\\n {\\n \\"pageTitle\\": \\"Internet world despairs as non-profit .org sold for $$$$ to private equity firm, price caps axed • The Register\\",\\n \\"url\\": \\"<https://www.theregister.co.uk/2019/11/20/org_registry_sale_shambles/>\\",\\n\\n \\"id\\": \\"33406b33-c453-44d3-8b18-2d2ae83ee73f\\"\\n },\\n {\\n \\"pageTitle\\": \\"Netlify Identity for paid subscriptions - Access Control / Identity - Netlify Community\\",\\n \\"url\\": \\"<https://community.netlify.com/t/netlify-identity-for-paid-subscriptions/1947/2>\\",\\n \\"description\\": \\"I want to limit certain functionality on my website to paying users. Now I’m using a payment provider (Mollie) similar to Stripe. My idea was to use the webhook fired by this service to call a Netlify function and give…\\",\\n \\"id\\": \\"34d6341c-18eb-4744-88e1-cfbf6c1cfa6c\\"\\n },\\n {\\n \\"pageTitle\\": \\"SmashingConf Freiburg 2019: Videos And Photos — Smashing Magazine\\",\\n \\"url\\": \\"<https://www.smashingmagazine.com/2019/10/smashingconf-freiburg-2019/>\\",\\n \\"description\\": \\"We had a lovely time at SmashingConf Freiburg. This post wraps up the event and also shares the video of all of the Freiburg presentations.\\",\\n \\"id\\": \\"354cbb34-b24a-47f1-8973-8553ed1d809d\\"\\n },\\n {\\n \\"pageTitle\\": \\"Adding Google Calendar to your JAMStack\\",\\n \\"url\\": \\"<https://www.raymondcamden.com/2019/11/18/adding-google-calendar-to-your-jamstack>\\",\\n \\"description\\": \\"A look at using Google APIs to add events to your static site.\\",\\n \\"id\\": \\"361b20c4-75ce-46b3-b6d9-38139e03f2ca\\"\\n },\\n {\\n \\"pageTitle\\": \\"How to Contribute to an Open Source Project | CSS-Tricks\\",\\n \\"url\\": \\"<https://css-tricks.com/how-to-contribute-to-an-open-source-project/>\\",\\n \\"description\\": \\"The following is going to get slightly opinionated and aims to guide someone on their journey into open source. As a prerequisite, you should have basic\\",\\n \\"id\\": \\"37300606-af08-4d9a-b5e3-12f64ebbb505\\"\\n },\\n {\\n \\"pageTitle\\": \\"Functions | Netlify\\",\\n \\"url\\": \\"<https://www.netlify.com/docs/functions/>\\",\\n \\"description\\": \\"Netlify builds, deploys, and hosts your front end. Learn how to get started, see examples, and view documentation for the modern web platform.\\",\\n \\"id\\": \\"3bf9e31b-5288-4b3b-89f2-97034603dbf6\\"\\n },\\n {\\n \\"pageTitle\\": \\"Serverless Can Help You To Focus - By Simona Cotin\\",\\n \\"url\\": \\"<https://hackernoon.com/serverless-can-do-that-7nw32mk>\\",\\n\\n \\"id\\": \\"43b1ee63-c2f8-4e14-8700-1e21c2e0a8b1\\"\\n },\\n {\\n \\"pageTitle\\": \\"Nuxt, Next, Nest?! My Head Hurts. - DEV Community 👩💻👨💻\\",\\n \\"url\\": \\"<https://dev.to/laurieontech/nuxt-next-nest-my-head-hurts-5h98>\\",\\n \\"description\\": \\"I clearly know what all of these things are. Their names are not at all similar. But let\'s review, just to make sure we know...\\",\\n \\"id\\": \\"456b7d6d-7efa-408a-9eca-0325d996b69c\\"\\n },\\n {\\n \\"pageTitle\\": \\"Consuming a headless CMS GraphQL API with Eleventy - Webstoemp\\",\\n \\"url\\": \\"<https://www.webstoemp.com/blog/headless-cms-graphql-api-eleventy/>\\",\\n \\"description\\": \\"With Eleventy, consuming data coming from a GraphQL API to generate static pages is as easy as using Markdown files.\\",\\n \\"id\\": \\"4606b168-21a6-49df-8536-a2a00750d659\\"\\n },\\n]
\\n\\n\\nNow that the data is in the project, we need for Astro to incorporate the data into its build process. To do this, we can use Astro’s new(ish) Content Layer API. The Content Layer API adds a content configuration file to your src
directory that allows you to run and collect any number of content pieces from data in your project or external APIs. Create the file /src/content.config.ts
(the name of this file matters, as this is what Astro is looking for in your project).
import { defineCollection, z } from \\"astro:content\\";\\nimport { file } from \'astro/loaders\';\\n\\nconst bookmarks = defineCollection({\\n schema: z.object({\\n pageTitle: z.string(),\\n url: z.string(),\\n description: z.string().optional()\\n }),\\n loader: file(\\"src/data/bookmarks.json\\"),\\n});\\n\\nexport const collections = { bookmarks };
\\n\\n\\n\\nIn this file, we import a few helpers from Astro. We can use defineCollection
to create the collection, z
as Zod, to help define our types, and file
is a specific content loader meant to read data files.
The defineCollection
method takes an object as its argument with a required loader and optional schema. The schema will help make our content type-safe and make sure our data is always what we expect it to be. In this case, we’ll define the three data properties each of our bookmarks has. It’s important to define all your data in your schema, otherwise it won’t be available to your templates.
We provide the loader
property with a content loader. In this case, we’ll use the file
loader that Astro provides and give it the path to our JSON.
Finally, we need to export the collections
variable as an object containing all the collections that we’ve defined (just bookmarks
in our project). You’ll want to restart the local server by re-running npm run dev
in your terminal to pick up the new data.
Now that we have data, we can use it in our homepage to show the most recent bookmarks that have been added. To get the data, we need to access the content collection with the getCollection
method from astro:content
. Add the following code to the frontmatter for ./src/pages/index.astro
.
---\\nimport Layout from \'../layouts/Layout.astro\';\\nimport { getCollection } from \'astro:content\';\\n\\nconst bookmarks = await getCollection(\'bookmarks\');\\n---
\\n\\n\\n\\nThis code imports the getCollection
method and uses it to create a new variable that contains the data in our bookmarks
collection. The bookmarks
variable is an array of data, as defined by the collection, which we can use to loop through in our template.
---\\nimport Layout from \'../layouts/Layout.astro\';\\nimport { getCollection } from \'astro:content\';\\n\\nconst bookmarks = await getCollection(\'bookmarks\');\\n---\\n\\n<Layout>\\n <div class=\\"max-w-3xl mx-auto my-10\\">\\n <h1 class=\\"text-3xl text-center\\">My latest bookmarks</h1>\\n <p class=\\"text-xl text-center mb-5\\">\\n This is only 10 of {bookmarks.length}\\n </p>\\n\\n <h2 class=\\"text-2xl mb-3\\">Latest bookmarks</h2>\\n <ul class=\\"grid gap-4\\">\\n {\\n bookmarks.slice(0, 10).map((item) => (\\n <li>\\n <a\\n href={item.data?.url}\\n class=\\"block p-6 bg-white border border-gray-200 rounded-lg shadow-sm hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700\\">\\n <h3 class=\\"mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white\\">\\n {item.data?.pageTitle}\\n </h3>\\n <p class=\\"font-normal text-gray-700 dark:text-gray-400\\">\\n {item.data?.description}\\n </p>\\n </a>\\n </li>\\n ))\\n }\\n </ul>\\n </div>\\n</Layout>
\\n\\n\\n\\nThis should pull the most recent 10 items from the array and display them on the homepage with some Tailwind styles. The main thing to note here is that the data structure has changed a little. The actual data for each item in our array actually resides in the data
property of the item. This allows Astro to put additional data on the object without colliding with any details we provide in our database. Your project should now look something like this.
Now that we have data and display, let’s get to work on our search functionality.
\\n\\n\\nTo start, we’ll want to scaffold out a new Astro component. In our example, we’re going to use vanilla JavaScript, but if you’re familiar with React or other frameworks that Astro supports, you can opt for client Islands to build out your search. The Astro actions will work the same.
\\n\\n\\nWe need to make a new component to house a bit of JavaScript and the HTML for the search field and results. Create the component in a ./src/components/Search.astro
file.
<form id=\\"searchForm\\" class=\\"flex mb-6 items-center max-w-sm mx-auto\\">\\n <label for=\\"simple-search\\" class=\\"sr-only\\">Search</label>\\n <div class=\\"relative w-full\\">\\n <input\\n type=\\"text\\"\\n id=\\"search\\"\\n class=\\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\\"\\n placeholder=\\"Search Bookmarks\\"\\n required\\n />\\n </div>\\n <button\\n type=\\"submit\\"\\n class=\\"p-2.5 ms-2 text-sm font-medium text-white bg-blue-700 rounded-lg border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800\\">\\n <svg\\n class=\\"w-4 h-4\\"\\n aria-hidden=\\"true\\"\\n xmlns=\\"<http://www.w3.org/2000/svg>\\"\\n fill=\\"none\\"\\n viewBox=\\"0 0 20 20\\">\\n <path\\n stroke=\\"currentColor\\"\\n stroke-linecap=\\"round\\"\\n stroke-linejoin=\\"round\\"\\n stroke-width=\\"2\\"\\n d=\\"m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z\\"></path>\\n </svg>\\n <span class=\\"sr-only\\">Search</span>\\n </button>\\n</form>\\n\\n<div class=\\"grid gap-4 mb-10 hidden\\" id=\\"results\\">\\n <h2 class=\\"text-xl font-bold mb-2\\">Search Results</h2>\\n</div>\\n\\n<script>\\n const form = document.getElementById(\\"searchForm\\");\\n const search = document.getElementById(\\"search\\");\\n const results = document.getElementById(\\"results\\");\\n\\n form?.addEventListener(\\"submit\\", async (e) => {\\n e.preventDefault();\\n console.log(\\"SEARCH WILL HAPPEN\\");\\n });\\n</script>
\\n\\n\\n\\nThe basic HTML is setting up a search form, input, and results area with IDs that we’ll use in JavaScript. The basic JavaScript finds those elements, and for the form, adds an event listener that fires when the form is submitted. The event listener is where a lot of our magic is going to happen, but for now, a console log will do to make sure everything is set up properly.
\\n\\n\\nIn order for Actions to work, we need our project to allow for Astro to work in server or hybrid mode. These modes allow for all or some pages to be rendered in serverless functions instead of pre-generated as HTML during the build. In this project, this will be used for the Action and nothing else, so we’ll opt for hybrid mode.
\\n\\n\\n\\nTo be able to run Astro in this way, we need to add a server integration. Astro has integrations for most of the major cloud providers, as well as a basic Node implementation. I typically host on Netlify, so we’ll install their integration. Much like with Tailwind, we’ll use the CLI to add the package and it will build out the boilerplate we need.
\\n\\n\\n\\nnpx astro add netlify
\\n\\n\\n\\nOnce this is added, Astro is running in Hybrid mode. Most of our site is pre-generated with HTML, but when the Action gets used, it will run as a serverless function.
\\n\\n\\nNext, we need an Astro Action to handle our search functionality. To create the action, we need to create a new file at ./src/actions/index.js
. All our Actions live in this file. You can write the code for each one in separate files and import them into this file, but in this example, we only have one Action, and that feels like premature optimization.
In this file, we’ll set up our search Action. Much like setting up our content collections, we’ll use a method called defineAction
and give it a schema and in this case a handler. The schema will validate the data it’s getting from our JavaScript is typed correctly, and the handler will define what happens when the Action runs.
import { defineAction } from \\"astro:actions\\";\\nimport { z } from \\"astro:schema\\";\\nimport { getCollection } from \\"astro:content\\";\\n\\nexport const server = {\\n search: defineAction({\\n schema: z.object({\\n query: z.string(),\\n }),\\n handler: async (query) => {\\n const bookmarks = await getCollection(\\"bookmarks\\");\\n const results = await bookmarks.filter((bookmark) => {\\n return bookmark.data.pageTitle.includes(query);\\n });\\n return results;\\n },\\n }),\\n};
\\n\\n\\n\\nFor our Action, we’ll name it search
and expect a schema of an object with a single property named query
which is a string. The handler function will get all of our bookmarks from the content collection and use a native JavaScript .filter()
method to check if the query is included in any bookmark titles. This basic functionality is ready to test with our front-end.
When the user submits the form, we need to send the query to our new Action. Instead of figuring out where to send our fetch request, Astro gives us access to all of our server Actions with the actions
object in astro:actions
. This means that any Action we create is accessible from our client-side JavaScript.
In our Search component, we can now import our Action directly into the JavaScript and then use the search action when the user submits the form.
\\n\\n\\n\\n<script>\\nimport { actions } from \\"astro:actions\\";\\n\\nconst form = document.getElementById(\\"searchForm\\");\\nconst search = document.getElementById(\\"search\\");\\nconst results = document.getElementById(\\"results\\");\\n\\nform?.addEventListener(\\"submit\\", async (e) => {\\n e.preventDefault();\\n results.innerHTML = \\"\\";\\n\\n const query = search.value;\\n const { data, error } = await actions.search(query);\\n if (error) {\\n results.innerHTML = `<p>${error.message}</p>`;\\n return;\\n }\\n // create a div for each search result\\n data.forEach(( item ) => {\\n const div = document.createElement(\\"div\\");\\n div.innerHTML = `\\n <a href=\\"${item.data?.url}\\" class=\\"block p-6 bg-white border border-gray-200 rounded-lg shadow-sm hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700\\">\\n <h3 class=\\"mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white\\">\\n ${item.data?.pageTitle}\\n </h3>\\n <p class=\\"font-normal text-gray-700 dark:text-gray-400\\">\\n ${item.data?.description}\\n </p>\\n </a>`;\\n // append the div to the results container\\n results.appendChild(div);\\n });\\n // show the results container\\n results.classList.remove(\\"hidden\\");\\n});\\n</script>
\\n\\n\\n\\nWhen results are returned, we can now get search results!
\\n\\n\\n\\nThough, they’re highly problematic. This is just a simple JavaScript filter, after all. You can search for “Favorite” and get my favorite bread recipe, but if you search for “favorite” (no caps), you’ll get an error… Not ideal.
\\n\\n\\n\\nThat’s why we should use a package like Fuse.js.
\\n\\n\\nFuse.js is a JavaScript package that has utilities to make “fuzzy” search much easier for developers. Fuse will accept a string and based on a number of criteria (and a number of sets of data) provide responses that closely match even when the match isn’t perfect. Depending on the settings, Fuse can match “Favorite”, “favorite”, and even misspellings like “favrite” all to the right results.
\\n\\n\\n\\nIs Fuse as powerful as something like Algolia or ElasticSearch? No. Is it free and pretty darned good? Absolutely! To get Fuse moving, we need to install it into our project.
\\n\\n\\n\\nnpm install fuse.js
\\n\\n\\n\\nFrom there, we can use it in our Action by importing it in the file and creating a new instance of Fuse based on our bookmarks collection.
\\n\\n\\n\\nimport { defineAction } from \\"astro:actions\\";\\nimport { z } from \\"astro:schema\\";\\nimport { getCollection } from \\"astro:content\\";\\nimport Fuse from \\"fuse.js\\";\\n\\nexport const server = {\\n search: defineAction({\\n schema: z.object({\\n query: z.string(),\\n }),\\n handler: async (query) => {\\n const bookmarks = await getCollection(\\"bookmarks\\");\\n const fuse = new Fuse(bookmarks, {\\n threshold: 0.3,\\n keys: [\\n { name: \\"data.pageTitle\\", weight: 1.0 },\\n { name: \\"data.description\\", weight: 0.7 },\\n { name: \\"data.url\\", weight: 0.3 },\\n ],\\n });\\n\\n const results = await fuse.search(query);\\n return results;\\n },\\n }),\\n};
\\n\\n\\n\\nIn this case, we create the Fuse instance with a few options. We give it a threshold value between 0 and 1 to decide how “fuzzy” to make the search. Fuzziness is definitely something that depends on use case and the dataset. In our dataset, I’ve found 0.3
to be a great threshold.
The keys
array allows you to specify which data should be searched. In this case, I want all the data to be searched, but I want to allow for different weighting for each item. The title should be most important, followed by the description, and the URL should be last. This way, I can search for keywords in all these areas.
Once there’s a new Fuse instance, we run fuse.search(query)
to have Fuse check the data, and return an array of results.
When we run this with our front-end, we find we have one more issue to tackle.
\\n\\n\\n\\nThe structure of the data returned is not quite what it was with our simple JavaScript. Each result now has a refIndex
and an item
. All our data lives on the item, so we need to destructure the item off of each returned result.
To do that, adjust the front-end forEach
.
// create a div for each search result\\ndata.forEach(({ item }) => {\\n const div = document.createElement(\\"div\\");\\n div.innerHTML = `\\n <a href=\\"${item.data?.url}\\" class=\\"block p-6 bg-white border border-gray-200 rounded-lg shadow-sm hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700\\">\\n <h3 class=\\"mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white\\">\\n ${item.data?.pageTitle}\\n </h3>\\n <p class=\\"font-normal text-gray-700 dark:text-gray-400\\">\\n ${item.data?.description}\\n </p>\\n </a>`;\\n // append the div to the results container\\n results.appendChild(div);\\n});
\\n\\n\\n\\nNow, we have a fully working search for our bookmarks.
\\n\\n\\n\\nThis just scratches the surface of what you can do with Astro Actions. For instance, we should probably add additional error handling based on the error we get back. You can also experiment with handling this at the page-level and letting there be a Search page where the Action is used as a form action and handles it all as a server request instead of with front-end JavaScript code. You could also refactor the JavaScript from the admittedly low-tech vanilla JS to something a bit more robust with React, Svelte, or Vue.
\\n\\n\\n\\nOne thing is for sure, Astro keeps looking at the front-end landscape and learning from the mistakes and best practices of all the other frameworks. Actions, Content Layer, and more are just the beginning for a truly compelling front-end framework.
\\nPowering Search With Astro Actions and Fuse.js originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
","description":"Static sites are wonderful. I’m a big fan. They also have their issues. Namely, static sites either are purely static or the frameworks that generate them completely lose out on true static generation when you just dip your toes in the direction of server routes.\\n\\nAstro has…","guid":"https://css-tricks.com/?p=384941","author":"Bryan Robinson","authorUrl":null,"authorAvatar":null,"insertedAt":"2025-03-11T16:23:35.731Z","publishedAt":"2025-03-11T15:26:10.040Z","media":[{"url":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/02/image-1.png?resize=2796%2C1060&ssl=1","type":"photo","width":2796,"height":1060},{"url":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/02/image-2.png?resize=1874%2C732&ssl=1","type":"photo","width":1874,"height":732},{"url":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/02/image-3.png?resize=2010%2C1554&ssl=1","type":"photo","width":2010,"height":1554},{"url":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/02/image-4.png?resize=1816%2C1388&ssl=1","type":"photo","width":1816,"height":1388},{"url":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/02/image-5.png?resize=1832%2C898&ssl=1","type":"photo","width":1832,"height":898},{"url":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/02/image-6.png?resize=1658%2C1600&ssl=1","type":"photo","width":1658,"height":1600}],"categories":["Articles","astro","static site generators"],"attachments":null,"extra":null,"language":null,"feeds":{"type":"feed","id":"41719081557593116","url":"https://css-tricks.com/feed/","title":"CSS-Tricks","description":"Tips, Tricks, and Techniques on using Cascading Style Sheets.","siteUrl":"https://css-tricks.com/","image":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1","errorMessage":null,"errorAt":null,"ownerUserId":null}},{"feedId":"42579624844251149","id":"122351868489391104","title":"How To Build Confidence In Your UX Work","url":"https://smashingmagazine.com/2025/03/how-to-build-confidence-in-your-ux-work/","content":"When I start any UX project, typically, there is very little confidence in the successful outcome of my UX initiatives. In fact, there is quite a lot of reluctance and hesitation, especially from teams that have been burnt by empty promises and poor delivery in the past.
\\n\\nGood UX has a huge impact on business. But often, we need to build up confidence in our upcoming UX projects. For me, an effective way to do that is to address critical bottlenecks and uncover hidden deficiencies — the ones that affect the people I’ll be working with.
\\n\\nLet’s take a closer look at what this can look like.
\\n\\n\\n\\nThis article is part of our ongoing series on UX. You can find more details on design patterns and UX strategy in Smart Interface Design Patterns 🍣 — with live UX training coming up soon. Free preview.
\\n\\nUX Doesn’t Disrupt, It Solves Problems\\n\\nBottlenecks are usually the most disruptive part of any company. Almost every team, every unit, and every department has one. It’s often well-known by employees as they complain about it, but it rarely finds its way to senior management as they are detached from daily operations.
\\n\\nThe bottleneck can be the only senior developer on the team, a broken legacy tool, or a confusing flow that throws errors left and right — there’s always a bottleneck, and it’s usually the reason for long waiting times, delayed delivery, and cutting corners in all the wrong places.
\\n\\nWe might not be able to fix the bottleneck. But for a smooth flow of work, we need to ensure that non-constraint resources don’t produce more than the constraint can handle. All processes and initiatives must be aligned to support and maximize the efficiency of the constraint.
\\n\\nSo before doing any UX work, look out for things that slow down the organization. Show that it’s not UX work that disrupts work, but it’s internal disruptions that UX can help with. And once you’ve delivered even a tiny bit of value, you might be surprised how quickly people will want to see more of what you have in store for them.
\\n\\nThe Work Is Never Just “The Work”\\n\\nMeetings, reviews, experimentation, pitching, deployment, support, updates, fixes — unplanned work blocks other work from being completed. Exposing the root causes of unplanned work and finding critical bottlenecks that slow down delivery is not only the first step we need to take when we want to improve existing workflows, but it is also a good starting point for showing the value of UX.
\\n\\nTo learn more about the points that create friction in people’s day-to-day work, set up 1:1s with the team and ask them what slows them down. Find a problem that affects everyone. Perhaps too much work in progress results in late delivery and low quality? Or lengthy meetings stealing precious time?
\\n\\nOne frequently overlooked detail is that we can’t manage work that is invisible. That’s why it is so important that we visualize the work first. Once we know the bottleneck, we can suggest ways to improve it. It could be to introduce 20% idle times if the workload is too high, for example, or to make meetings slightly shorter to make room for other work.
\\n\\nThe Theory Of Constraints\\n\\nThe idea that the work is never just “the work” is deeply connected to the Theory of Constraints discovered by Dr. Eliyahu M. Goldratt. It showed that any improvements made anywhere beside the bottleneck are an illusion.
\\nAny improvement after the bottleneck is useless because it will always remain starved, waiting for work from the bottleneck. And any improvements made before the bottleneck result in more work piling up at the bottleneck.
\\n\\nTo improve flow, sometimes we need to freeze the work and bring focus to one single project. Just as important as throttling the release of work is managing the handoffs. The wait time for a given resource is the percentage of time that the resource is busy divided by the percentage of time it’s idle. If a resource is 50% utilized, the wait time is 50/50, or 1 unit.
\\n\\nIf the resource is 90% utilized, the wait time is 90/10, or 9 times longer. And if it’s 99% of time utilized, it’s 99/1, so 99 times longer than if that resource is 50% utilized. The critical part is to make wait times visible so you know when your work spends days sitting in someone’s queue.
\\n\\nThe exact times don’t matter, but if a resource is busy 99% of the time, the wait time will explode.
\\n\\nOur goal is to maximize flow: that means exploiting the constraint but creating idle times for non-constraint to optimize system performance.
\\n\\nOne surprising finding for me was that any attempt to maximize the utilization of all resources — 100% occupation across all departments — can actually be counterproductive. As Goldratt noted, “An hour lost at a bottleneck is an hour out of the entire system. An hour saved at a non-bottleneck is worthless.”
\\n\\nI can only wholeheartedly recommend The Phoenix Project, an absolutely incredible book that goes into all the fine details of the Theory of Constraints described above.
\\n\\nIt’s not a design book but a great book for designers who want to be more strategic about their work. It’s a delightful and very real read about the struggles of shipping (albeit on a more technical side).
\\n\\nWrapping Up\\n\\nPeople don’t like sudden changes and uncertainty, and UX work often disrupts their usual ways of working. Unsurprisingly, most people tend to block it by default. So before we introduce big changes, we need to get their support for our UX initiatives.
\\n\\nWe need to build confidence and show them the value that UX work can have — for their day-to-day work. To achieve that, we can work together with them. Listening to the pain points they encounter in their workflows, to the things that slow them down.
\\n\\nOnce we’ve uncovered internal disruptions, we can tackle these critical bottlenecks and suggest steps to make existing workflows more efficient. That’s the foundation to gaining their trust and showing them that UX work doesn’t disrupt but that it’s here to solve problems.
\\n\\nNew: How To Measure UX And Design Impact\\n\\nMeet Measure UX & Design Impact (8h), a practical guide for designers and UX leads to measure and show your UX impact on business. Watch the free preview or jump to the details.
\\n\\n\\n \\n25 video lessons (8h) + Live UX Training.
100 days money-back-guarantee.
25 video lessons (8h). Updated yearly.
Also available as a UX Bundle with 2 video courses.
The videos from Smashing Magazine’s recent event on accessibility were just posted the other day. I was invited to host the panel discussion with the speakers, including a couple of personal heroes of mine, Stéphanie Walter and Sarah Fossheim. But I was just as stoked to meet Kardo Ayoub who shared his deeply personal story as a designer with a major visual impairment.
\\n\\n\\n\\n\\n\\n\\n\\nI’ll drop the video here:
\\n\\n\\n\\n\\n\\n\\n\\nI’ll be the first to admit that I had to hold back my emotions as Kardo detailed what led to his impairment, the shock that came of it, and how he has beaten the odds to not only be an effective designer, but a real leader in the industry. It’s well worth watching his full presentation, which is also available on YouTube alongside the full presentations from Stéphanie and Sarah.
\\nSmashing Meets Accessibility originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
","description":"The videos from Smashing Magazine’s recent event on accessibility were just posted the other day. I was invited to host the panel discussion with the speakers, including a couple of personal heroes of mine, Stéphanie Walter and Sarah Fossheim. But I was just as stoked to meet Kar…","guid":"https://css-tricks.com/?p=385112","author":"Geoff Graham","authorUrl":null,"authorAvatar":null,"insertedAt":"2025-03-10T16:49:23.334Z","publishedAt":"2025-03-10T15:08:47.613Z","media":null,"categories":["Links","accessibility"],"attachments":null,"extra":null,"language":null,"feeds":{"type":"feed","id":"41719081557593116","url":"https://css-tricks.com/feed/","title":"CSS-Tricks","description":"Tips, Tricks, and Techniques on using Cascading Style Sheets.","siteUrl":"https://css-tricks.com/","image":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1","errorMessage":null,"errorAt":null,"ownerUserId":null}}]}')