Skip to main content

styling components

This section looks at how to style the my-button component. It covers the following items:

Basic styles

Here is a quick reminder of how to create a custom element using @customElement. Note the css tag that is imported from @genesislcap/web-core along with html:

import {
css, customElement, html} from "@genesislcap/web-core";

@customElement({
name: "my-button",
template,
styles // focusing on styles,
})

So far we have seen how we can attach styles using the styles property in the @customElement decorator. But let's dive deeper and explore how to define styles.

Defining styles

The example below shows how the css tagged template helper enables you to create and re-use CSS for custom elements. These styles are encapsulated in the Shadow DOM and attached via the styles property in the @customElement decorator.

import {
html,
css,
customElement,
GenesisElement,
} from "@genesislcap/web-core";

const styles = css`
:host { /* The :host selector applies styles to the custom element itself (<my-button>).*/
display: inline-block;
padding: 10px;
background-color: gray;
}

:host([primary]) { /* :host([primary]): Styles the element with blue background if it has the primary attribute. */
background-color: blue;
}

:host([disabled]) { /* :host([disabled]): Styles the element with reduced opacity and disabled interaction if the disabled attribute is present.*/
opacity: 0.5;
pointer-events: none;
}
`;

@customElement({
name: "my-button",
template: html<MyButton>`<button>${(x) => x.label}</button>`,
styles,
})
export class MyButton extends GenesisElement {
@attr primary: boolean = false;
@attr disabled: boolean = false;
}
important

Even if the primary and disabled properties default to false, you can add the attributes in the HTML template to apply the corresponding styles. Boolean attributes are present or absent.

Usage in HTML

With the styles defined in the example above, you can now use <my-button> in your HTML:

<my-button> label="Click Me" primary></my-button>
<my-button> label="Disabled" disabled></my-button>

In the example above:

  • The first button will have a blue background (primary attribute applied).
  • The second button will have reduced opacity and disabled pointer events (disabled attribute applied).

To achieve this, we have:

  • used the styles property in the @customElement decorator
  • ensured that styles are encapsulated within the Shadow DOM.
  • used :host and :host([attribute]) to apply styles based on the custom element's attributes.

Composing styles

You can also compose reusable styles and utility styles in your component. This approach encourages consistency across components and reduces duplication by enabling shared styles.

For example, you could compose your own utility style (called normalize), which ensures consistent baseline styles across browsers. This can include setting consistent 'margin', 'padding', 'line-height', and other foundational properties.

// normalize.ts
import { css } from "@genesislcap/web-core";

export const normalize = css`
html, body, * {
margin: 0;
padding: 0;
box-sizing: border-box;
}

button {
all: unset;
cursor: pointer;
}
`;

Here's how you can now use the normalize utility along with additional reusable style blocks to style my-button component. This example:

  • shows how to reuse and combine shared style blocks, in this case, normalize and buttonBaseStyles
  • combines multiple styles using the css helper
import {
css,
html,
customElement,
GenesisElement,
} from "@genesislcap/web-core";
import { normalize } from "./normalize"; // Import normalize styles

// Define reusable base button styles
const buttonBaseStyles = css`
button {
font-family: Arial, sans-serif;
font-size: 16px;
padding: 10px 20px;
border-radius: 4px;
text-align: center;
border: 1px solid transparent;
}
`;

@customElement({
name: "my-button",
template: html<MyButton>` <button>${(x) => x.label}</button> `,
styles: css`
${normalize} /* Include normalize styles */
${buttonBaseStyles} /* Include reusable button styles */
/*normalize and buttonBaseStyles combined for consistent design across components*/
:host {
display: inline-block;
}

button {
background-color: blue;
color: white;
transition: background-color 0.3s ease-in-out;
}

button:hover {
background-color: darkblue;
}

button:active {
background-color: navy;
}
`,
})
export class MyButton extends GenesisElement {
label: string = "Click Me";
}

Partial CSS

In addition to reusing styles dynamically, you can also organize styles into separate files. This is when 'partial CSS' becomes useful.

To achieve this, you can use cssPartial. The example below:

  • uses cssPartial to define reusable snippets of CSS
  • reuses blocks of CSS properties that are not standalone styles
import { css, cssPartial } from "@genesislcap/web-core";

const partial = cssPartial`padding: 10px 20px;`; // We've created partial styles using cssPartial
const styles = css`
:host {
${partial} /* we can attach partial into host */
}
`;

@customElement({
name: "my-button",
template: html<MyButton>`<button>${(x) => x.label}</button>`,
styles: styles,
})

CSSDirective

A CSSDirective enables you to create dynamic CSS styles and behaviours. Instead of using static styles, you can programmatically control how the styles are applied to your component. This is useful for things like animation, dynamic layouts, or user interactions.

You can achieve this in a few simple steps:

Import the required modules

import { CSSDirective } from "@genesislcap/web-core";

Define the custom directive

The example below:

  • creates a new class, RandomWidth, which extends CSSDirective to make it a directive that can generate styles dynamically.
  • uses private property = "--button-width" to define a CSS custom property (--button-width) that holds the dynamically generated width value.
class RandomWidth extends CSSDirective {
private property = "--button-width";
}

Generate CSS dynamically

In the example below, the createCSS() method specifies what CSS should be added to the element.

Specifically, it generates a rule to set the width of the element based on the value of the --button-width custom property (which is updated dynamically).

  createCSS() {
return `width: var(${this.property});`; // value of --button-width
}

Define the behaviour

In the example below, the createBehavior() method defines the dynamic behaviour of the directive:

  • bind(el): When the element is connected to the DOM, it sets the --button-width property to a random value between 100px and 200px.
  • unbind(el): When the element is disconnected, it removes the --button-width property to clean up.
  createBehavior() {
return {
bind(el) {
el.style.setProperty(this.property, `${Math.random() * 100 + 100}px`);
},
unbind(el) {
el.style.removeProperty(this.property);
},
};
}

Attach the directive to styles

In the example below, the RandomWidth directive is used in the styles property for the my-button component.

When the button is rendered:

  • The RandomWidth directive dynamically sets the width of the button using the random value generated by bind().
  • The button also gets a blue background and white text from the other static styles.
import {
css,
html,
customElement,
GenesisElement,
} from "@genesislcap/web-core";

@customElement({
name: "my-button",
template: html<MyButton>` <button>${(x) => x.label}</button> `,
styles: css`
button {
${new RandomWidth()}
background-color: blue;
color: white;
}
`,
})
export class MyButton extends GenesisElement {
label: string = "Click Me";
}

When the code runs

From the above examples, this is what will happen when the code runs:

  • When the my-button component is created and added to the DOM, the bind() method runs.
  • A random width is calculated (e.g., 150px) and applied to the --button-width property.
  • The createCSS() method uses this property to set the button's width.
  • The button element will have a random width (e.g. 150px) and other static styles, such as blue background and white text.
  • If the element is removed from the DOM, the unbind() method is called, which removes the --button-width property, ensuring there are no leftover styles.

Summary

In this section we have applied dynamic styles by adding behaviour-driven styles to custom elements. We used two key methods:

  • createCSS()to generate CSS dynamically
  • createBehavior() to attach dynamic behaviour to an element

Shadow DOM styling

Shadow DOM encapsulation ensures styles do not affect the global DOM. They apply only to your custom element, and do not "leak" into the global styles.

Equally, styles from the global scope cannot accidentally modify the appearance of your custom element.

Here are some considerations for Shadow DOM styling:

  • By default, custom elements behave as inline elements. You might want to define a specific display property, such as inline-block or block, to control their layout behaviour.
  • Setting contain: content improves performance by signalling to the browser that the element's layout, painting, and size calculations are self-contained.
    This ensures the browser doesn't unnecessarily re-calculate styles or layout changes for the entire document if something inside the component changes.
  • Adding support for the hidden attribute ensures your component properly respects the display: none behaviour when hidden is applied. This is a common practice for ensuring that components behave predictably.

Applying styles directly

The :host selector enables you to apply styles directly to your custom element.

In the example below,

  • :host ensures that styles such as display and contain apply only to the <my-button> element. This prevents global CSS rules from interfering with the component, and vice versa.
  • When <my-button hidden> is used, the component is visually removed using display: none. This is a common convention for enabling/disabling components.
  • The button inside the Shadow DOM is styled with "padding", "background color", and "rounded corners". These styles are scoped to the Shadow DOM and won't affect other buttons outside the my-button component.
import {
css,
html,
customElement,
GenesisElement,
} from "@genesislcap/web-core";

const styles = css`
/* Encapsulation and default display */
:host {
display: inline-block; /* Ensure consistent layout behavior */
contain: content; /* Optimize performance */
}

/* Support for the "hidden" attribute */
:host([hidden]) {
display: none; /* Completely remove the component from the visual flow */
}
/* Styles for the internal button */
button {
background-color: blue;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
`;
@customElement({
name: "my-button",
template: html<MyButton>`
<button><slot></slot></button>
<!-- Slot allows flexible content -->
`,
styles,
})
export class MyButton extends GenesisElement {}

Slotted content

A slot is a placeholder where users of the component can insert their own content. This means that components can be flexible and reusable. For example, if the my-button component has a slot, you can pass content like a label or an icon into the button when using it.

The ::slotted() CSS element enables you to style content that is projected into a slot in your component's Shadow DOM. However, you can only style the direct children of the slot (not descendants of those children).

In the example below, the <span> inside the slot has yellow text in bold.

The styles applied to button and ::slotted(span) are scoped to the my-button Shadow DOM. These will not affect buttons or spans elsewhere in the document.

@customElement({
name: "my-button",
template: html<MyButton>`
<button><slot></slot></button>
// Slot for user content
`,
styles: css`
button {
background-color: blue; /* Styles for the button */
color: white; /* Button text color */
}

::slotted(span) {
color: yellow; /* Styles for <span> elements inside the slot */
font-weight: bold; /* Make slotted <span> text bold */
}
`,
})
export class MyButton extends GenesisElement {}

Usage in HTML

Once you have defined slotted content for your custom element, as above, you can now use <my-button> in your HTML:

<my-button>
<button style="background-color: blue; color: white;">
<span style="color: yellow; font-weight: bold;">Click Me</span>
</button>
</my-button>

Key limitations

::slotted() can only style the immediate children of the <slot>. If the content inside the slot has nested elements, those cannot be styled directly.

Styles defined in the Shadow DOM (such as button { /* button styles */ }) do not affect the slotted content directly. Slotted content is styled using the ::slotted() selector.

<my-button>
<div>
<span>Cannot style this span</span>
</div>
</my-button>

Style lifecycle management

In most cases, you define styles when you create a custom element and attach it to the DOM. However, if you want the component's appearance to adapt dynamically when it's rendered, you can also create dynamic styles that depend on the element's attributes or properties at the time it's connected to the DOM.

To create a custom element that adapts dynamically, you can use the resolveStyles() method. This enables you to generate and apply styles dynamically during the connectedCallback` phase of the custom element's lifecycle.

The resolveStyles() method is called when the element is attached to the DOM (in the connectedCallback phase).

In the example below:

  • resolveStyles() is called when the element is connected to the DOM.
  • The method checks the label property. If the label is "Click Me", the button will have a blue background. Otherwise, it will have a red background.
@customElement({
name: "my-button",
template: html<MyButton>` <button>${(x) => x.label}</button> `,
styles: (x) => x.resolveStyles(), // Use resolveStyles to dynamically resolve styles
})
export class MyButton extends GenesisElement {
label: string = "Click Me";

resolveStyles() {
// Dynamically return styles based on the label value
return this.label === "Click Me"
? css`
button {
background-color: blue;
}
`
: css`
button {
background-color: red;
}
`;
}
}

You can further extend this example to update styles dynamically based on attributes or properties, such as a type or theme:

In the example below, the type attribute determines the button's style:

  • <my-button type="primary"></my-button> → Blue button.
  • <my-button type="secondary"></my-button> → Grey button.
  • <my-button type="danger"></my-button> → Red button.
  • resolveStyles() is automatically called during connectedCallback and applies the correct styles based on the value of the type attribute.
  • You can dynamically update the type attribute, and the styles will be updated accordingly.
@customElement({
name: "my-button",
template: html<MyButton>` <button>${(x) => x.label}</button> `,
styles: (x) => x.resolveStyles(), // Use resolveStyles to dynamically resolve styles
})
export class MyButton extends GenesisElement {
@attr type: string = "primary"; // Attribute controlling style
label: string = "Click Me";

resolveStyles() {
switch (this.type) {
case "primary":
return css`
button {
background-color: blue;
color: white;
}
`;
case "secondary":
return css`
button {
background-color: gray;
color: black;
}
`;
case "danger":
return css`
button {
background-color: red;
color: white;
}
`;
default:
return css`
button {
background-color: white;
color: black;
}
`;
}
}
}

Hiding undefined elements

When using custom elements, the browser could render them before they are fully defined or styled. This can cause a "flash of unstyled content" (FOUC), where the element appears in its default, unstyled form for a brief moment.

To prevent FOUC, you can use the CSS :not(:defined) pseudo-class to hide custom elements that haven’t been upgraded yet.

  • :not(:defined) matches any element that has not yet been defined (i.e., upgraded by the browser).
  • visibility: hidden hides these elements from view until their definition and styles are applied.

You can see this in the implementation below:

:not(:defined) {
visibility: hidden; /* Hides all custom elements until defined */
}
note

Using visibility: hidden ensures that the element is still part of the document flow, but it is not visible to the user. This prevents layout shifts when the element is styled.