Build a Style-Isolated Widget

Build a Shadow DOM feedback card with slots and controlled style overrides.

Introduction

30 Second Summary

A feedback form can look polished on its own. On someone else's website, broad page styles can turn its buttons or headings into a mess.

In this project, you will build a feedback card as a custom element with a protective Shadow DOM boundary. Named slots let the page supply content while named parts permit one deliberate CSS override.

What You'll Build

Picture opening one page where aggressive global styles reshape every heading or button while your feedback card keeps its own polished appearance.

By the end of this project, you'll have:

  • An isolated feedback card that keeps its internal layout or button styling when the host page applies aggressive global rules.
  • A side-by-side demonstration showing that the page can style its own elements without changing the card's matching elements.
  • Controlled extension points that accept page content through named slots or allow one selected element to be restyled with ::part.
  • Secret Mission: Add a custom accent variant while keeping the rest of the component's design isolated.

Are there any prerequisites?

You need a text editor. You also need a modern browser.

There is no account or installation to set up.

Before We Start

A widget can look correct on one page. The host page's CSS can change its appearance without warning.

Your workspace needs an empty folder in a code editor. Your browser also needs developer tools ready for inspecting the Shadow DOM boundary later.

In this step, get ready to:
  • Create an empty shadow-widget folder on your Desktop.
  • Open the folder in your code editor.
  • Confirm that your browser can display its developer tools.
Create the project folder

A dedicated folder keeps the page and widget files together. The empty folder gives every project file one clear home.

  • Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
  • Search for Finder on macOS or File Explorer on Windows.
  • Press Enter to open the file manager.
  • Select Desktop in the sidebar.
  • Create a folder using the file manager's new-folder control.
  • Name the folder shadow-widget.
Open the folder in your editor

A code editor treats an opened folder as a workspace. This keeps every file you create inside shadow-widget.

  • Return to your operating system's search bar with Cmd+Space on macOS or the Windows key on Windows.
  • Type the name of your code editor into the search field.
  • Press Enter to start the editor.
  • Choose the folder-opening command from the editor's file menu.
  • Select shadow-widget from your Desktop.
  • Confirm the folder selection.
  • Approve the folder if your editor asks whether you trust it.

Good start. Your editor now shows an empty shadow-widget workspace in its sidebar.

Can't See the Folder?

Use the editor's folder-opening command again if you see a single file or a welcome screen. Select the shadow-widget folder from your Desktop.

Ask for help with opening the project folder in your editor.

Prepare the browser for inspection

Browser developer tools expose a page's document structure and applied styles. Later, they let you see the boundary between the page and the protected widget.

  • Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
  • Type the name of your modern web browser into the search field.
  • Press Enter to start the browser.
  • Use the browser's main menu to open its developer tools.
  • Choose the panel that displays the page's document elements.

Before you do the final check, which side of your setup handles editing and which side handles inspection?

  • Arrange your code editor beside your browser.
  • Confirm that the editor displays the shadow-widget folder.
  • Confirm that the browser displays its developer tools.

You're set. You can now build in the editor while checking the result and its structure in the browser.

Developer Tools Not Opening?

  • Make the browser the active window before opening its developer tools.
  • Look in the browser's main menu for its developer tools or web development controls.

Ask for help with finding developer tools in your browser.

Your local workspace is ready. Next, you'll build a plain feedback card that exposes how global page styles can break a widget.

Break a Plain Feedback Card

An embedded widget has to survive page styles it does not control. A normal HTML card shares the page's DOM.

In this step, you will build an ordinary feedback card with CSS. You will add aggressive global rules that visibly break it.

In this step, get ready to:
  • Create a demo page with a plain feedback card.
  • Confirm the card looks tidy before the style collision.
  • Add aggressive global selectors that visibly break the card.
Build the plain feedback card

The first version uses ordinary page markup. Its elements sit beside the host page elements inside the same document.

  • Create index.html inside the local project folder in your code editor.
  • Fill index.html with the demo page by pasting this code:
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Feedback Card Style Leak</title>
  <style>
    /* Give the host page a simple layout for comparison */
    body { font-family: Arial, sans-serif; margin: 0; background: #eef2ff; color: #1f2937; }
    .page-shell { width: min(720px, 90%); margin: 3rem auto; }
    /* Style the normal-DOM card as an embedded widget */
    .feedback-card { margin-top: 2rem; padding: 1.5rem; border: 1px solid #cbd5e1; border-radius: 0.75rem; background: white; }
    .feedback-card p { color: #475569; }
  </style>
</head>
<body>
  <main class="page-shell">
    <!-- Keep page-owned content beside the widget for comparison -->
    <h2>Widget host page</h2>
    <p>This content belongs to the page that hosts the feedback card.</p>
    <button class="page-button">Page action</button>
    <!-- Build the first widget with ordinary document elements -->
    <section class="feedback-card">
      <h2>How was your experience?</h2>
      <p>Tell us what worked and what we could improve.</p>
      <button>Send feedback</button>
    </section>
  </main>
</body></html>

What does this code do?

  • The page-shell content represents the website that owns the page.
  • The feedback-card section represents an embedded widget.
  • Both sets of elements belong to the same document styling system.
  • Save index.html.
  • Load index.html through your browser's open-file control.

You'll see a page heading above a page-owned button. A white feedback card appears underneath with its own heading and button.

Does the page look blank?

  • Confirm the file is named index.html.
  • Check that the closing </section> tag appears below the feedback button.

Help me fix a local HTML page that does not display its feedback card.

Break every heading

A global selector applies wherever it finds a matching element in the document. The page heading and card heading are both second-level headings.

  • Find </style> near the top of index.html.
  • Insert this global heading rule directly above it:
    /* Host page styles intentionally reach every second-level heading */
    h2 {
      color: #ff006e;
      font-family: "Courier New", monospace;
      text-transform: uppercase;
      letter-spacing: 0.18em;
    }

What does this code do?

  • The h2 selector matches the host page heading.
  • The same selector matches the card heading because it shares the document.
  • The exaggerated typography makes the collision easy to spot.

Before you refresh, which headings do you think the new selector can reach?

  • Save index.html.
  • Refresh the page in your browser.

You'll see both headings turn pink. Their letters also become uppercase with wide spacing.

Are the headings unchanged?

  • Confirm the h2 rule sits inside the existing <style> element.
  • Check that both visible headings use <h2> tags.

Help me find why my global heading styles are not appearing.

Break every button

The heading collision proves that broad page selectors can reach the card. A global button rule makes the same weakness visible in an interactive control.

  • Find the closing brace for the global h2 rule in index.html.
  • Insert this global button rule below it:
    /* Host page styles intentionally reach every button */
    button {
      background: #111827;
      color: #fef08a;
      border: 6px double #ff006e;
      border-radius: 0;
      padding: 1rem 1.5rem;
      font-size: 1.1rem;
      font-weight: 800;
      text-transform: uppercase;
      transform: rotate(-2deg);
    }

What does this code do?

  • The button selector reaches every button in the document.
  • The thick border makes the host page's visual influence obvious.
  • The rotation makes the feedback button look misplaced inside its tidy card.

Before you refresh, do you expect only the page-owned button to change?

  • Save index.html.
  • Refresh the page in your browser.

You'll see both buttons become dark rectangles with yellow text. Each button also gains a thick pink border plus a slight tilt.

That's your failure baseline captured: the host page now controls the feedback card's presentation.

Is only one button changing?

  • Confirm the card control uses a plain <button> element.
  • Check that the global button rule sits before </style>.

Help me identify why my global button rule reaches only one button.

Your finished index.html should now match one of the paths below.

✔️ Awesome, I've got everything!

Your demo page now shows the global style collision clearly.

ⓧ I'd like to double check the full code

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Feedback Card Style Leak</title>
  <style>
    /* Give the host page a simple layout for comparison */
    body { font-family: Arial, sans-serif; margin: 0; background: #eef2ff; color: #1f2937; }
    .page-shell { width: min(720px, 90%); margin: 3rem auto; }
    /* Style the normal-DOM card as an embedded widget */
    .feedback-card { margin-top: 2rem; padding: 1.5rem; border: 1px solid #cbd5e1; border-radius: 0.75rem; background: white; }
    .feedback-card p { color: #475569; }
    /* Host page styles intentionally reach every second-level heading */
    h2 {
      color: #ff006e;
      font-family: "Courier New", monospace;
      text-transform: uppercase;
      letter-spacing: 0.18em;
    }
    /* Host page styles intentionally reach every button */
    button {
      background: #111827;
      color: #fef08a;
      border: 6px double #ff006e;
      border-radius: 0;
      padding: 1rem 1.5rem;
      font-size: 1.1rem;
      font-weight: 800;
      text-transform: uppercase;
      transform: rotate(-2deg);
    }
  </style>
</head>
<body>
  <main class="page-shell">
    <!-- Keep page-owned content beside the widget for comparison -->
    <h2>Widget host page</h2>
    <p>This content belongs to the page that hosts the feedback card.</p>
    <button class="page-button">Page action</button>
    <!-- Build the first widget with ordinary document elements -->
    <section class="feedback-card">
      <h2>How was your experience?</h2>
      <p>Tell us what worked and what we could improve.</p>
      <button>Send feedback</button>
    </section>
  </main>
</body></html>

Your broken card proves that ordinary page styles can reach an embedded widget. Next up, you'll move the card into a custom element with a shadow root.

Move the Card Into Shadow DOM

Your plain card proved how easily host-page CSS can reshape embedded markup. The page's broad heading and button rules now give you a clear baseline.

You will turn the card into a custom element backed by Shadow DOM. Its internal heading and button will sit behind a selector boundary that stops the page's broad rules.

In this step, get ready to:
  • Define the FeedbackCard custom element in feedback-card.js.
  • Replace the plain card with a feedback-card element.
  • Verify that the global h2 and button selectors stop at the shadow boundary.
Define the custom element

A custom element gives the browser a reusable HTML tag backed by a JavaScript class. The class creates a shadow root that holds the card's internal structure.

  • Use your code editor's file sidebar to create feedback-card.js inside the project folder from earlier.
  • Build the custom element by adding this code to feedback-card.js:
class FeedbackCard extends HTMLElement {
  constructor() {
    super()

    // Create an isolated DOM boundary for the card
    const shadow = this.attachShadow({ mode: 'open' })

    // Render the card inside the shadow tree
    shadow.innerHTML = `
      <section class="feedback-card">
        <h2>Share your feedback</h2>
        <p>Tell us what you think about this page.</p>
        <button type="button">Send feedback</button>
      </section>
    `
  }
}

customElements.define('feedback-card', FeedbackCard)

What does this code do?

  • The FeedbackCard class extends HTMLElement so the browser can create it as an HTML element.
  • The attachShadow({ mode: 'open' }) call creates the shadow root that holds the card.
  • The open mode keeps the shadow root accessible to JavaScript for later inspection.
  • The shadow.innerHTML assignment renders the heading, message, and button inside the shadow tree.
  • The customElements.define('feedback-card', FeedbackCard) call connects the class to the feedback-card tag.
  • Save feedback-card.js.
  • Confirm that feedback-card.js appears beside index.html in your code editor's file sidebar.

Can't find the new file?

Check that you created feedback-card.js inside the same project folder as index.html.

Check the filename for missing letters or an extra file extension.

Help me check why my feedback-card.js file is missing from my project folder.

✔️ Awesome, I've got everything!

Great. Double-check that you saved feedback-card.js before connecting it to the page.

ⓧ I'd like to double check the full code

class FeedbackCard extends HTMLElement {
  constructor() {
    super()

    // Create an isolated DOM boundary for the card
    const shadow = this.attachShadow({ mode: 'open' })

    // Render the card inside the shadow tree
    shadow.innerHTML = `
      <section class="feedback-card">
        <h2>Share your feedback</h2>
        <p>Tell us what you think about this page.</p>
        <button type="button">Send feedback</button>
      </section>
    `
  }
}

customElements.define('feedback-card', FeedbackCard)
Connect the component to the page

The host page needs one custom tag for the card. It also needs one script tag so the browser can register that tag.

  • Switch back to index.html in your code editor.
  • Select the complete plain feedback-card block that contains its heading, message, and button.
  • Replace the selected block with this custom element:
<feedback-card></feedback-card>

Why use a custom tag?

The feedback-card element becomes the shadow host. The browser places the shadow tree from FeedbackCard behind this host.

  • Find the closing </body> tag near the bottom of index.html.
  • Load the component directly above that closing tag by adding this line:
<script src="feedback-card.js"></script>

What does this line do?

The script loads the class from feedback-card.js. The browser then upgrades every matching feedback-card element on the page.

Before you refresh, do you expect the page's broad selectors to restyle the heading and button inside the new shadow tree?

  • Save index.html.
  • Switch back to the browser from earlier.
  • Reload index.html.
  • Compare the page-owned button with the button inside the feedback card.

You'll see the page-owned button keep its aggressive global styling. The feedback-card button uses the browser's default appearance inside the shadow tree.

You'll also see the feedback heading lose the global h2 treatment. Its plain appearance proves that the selector stopped at the shadow boundary.

Card missing from the page?

Check that the script path is exactly feedback-card.js.

Check that feedback-card uses the same spelling in index.html and feedback-card.js.

Help me debug why my feedback-card custom element is not rendering.

That's the isolation working. Your host page can keep its aggressive rules without reshaping the widget.

Your card now has a real style boundary. Next, you will add its own styles inside that boundary while the page keeps its separate look.

Style the Protected Card

Your custom element now keeps the page's aggressive selectors away from its internal heading and button. The protected card still needs a component-owned design.

In this step, you will add scoped CSS inside the Shadow DOM. You will then compare both buttons to prove that each side keeps its own appearance.

In this step, get ready to:
  • Add scoped styles inside the card's shadow root.
  • Refresh the demo to see the protected card design.
  • Compare the card with page-owned elements to prove two-way style isolation.
Add styles inside the shadow root

Scoped CSS lives in a <style> block within the shadow root's rendered markup. Its selectors can style the card container plus each internal element.

  • Switch back to feedback-card.js in your code editor.
  • Locate the rendered markup assigned to the existing open shadow root.
  • Add scoped CSS for the card container, heading, message, and feedback button through a <style> block at the beginning of the shadow-root markup.
  • Save feedback-card.js.
  • Refresh the demo page in your browser.

You should see a designed feedback card with its own container, heading, message, and button styles. The page-owned button should keep its aggressive global styling.

You have now given the protected card its own visual system. Its appearance no longer depends on the host page's selectors.

How Do Scoped Styles Stay Contained?

  • The <style> block belongs to the shadow tree, so its selectors match elements inside the component.
  • The page's global h2 rule still controls headings in index.html.
  • The page's global button rule still controls the page-owned button.
  • The component's internal rules control the heading and feedback button inside <feedback-card>.
Compare both sides of the boundary

The page and the shadow tree now have separate selector scopes. A side-by-side comparison reveals whether that boundary works in both directions.

Before you check, do you expect the card's CSS to alter the page-owned button?

  • Reload the demo page in your browser.

You will see the page-owned button keep the page's aggressive global styling. You will see the card's heading and button keep the component's scoped styling.

Page selectors cannot select into the shadow tree. Shadow-tree selectors cannot select elements outside the component.

✔️ My card is styled independently

Your card has its own design while the page-owned button keeps the global page design. The shadow boundary is isolating selectors in both directions.

ⓧ I'd like to double check the full code

  • Confirm feedback-card.js still defines the FeedbackCard custom element.
  • Confirm the class still creates an open shadow root with attachShadow({ mode: 'open' }).
  • Confirm the shadow-root markup contains scoped CSS for the card container, heading, message, and feedback button.
  • Confirm the card structure remains inside the same shadow root as its scoped CSS.
  • Confirm index.html still contains the page-owned button plus the aggressive global heading and button rules.
  • Confirm index.html still contains one <feedback-card> element plus one script tag that loads feedback-card.js.

Card Styles Missing or Escaping?

If the card stays plain, confirm the <style> block sits inside the markup rendered into the shadow root. CSS placed outside that markup belongs to the page instead.

If the page-owned button changes, check that the component's button rule remains inside the shadow-root <style> block.

If the card disappears, check the edited markup for an unclosed <style> tag or missing CSS brace.

Ask for help diagnosing the boundary: Why are my feedback card styles missing or affecting the page-owned button?

Your feedback card now owns its appearance without disturbing the page around it. Next up, you will let the page pass title and message content into that protected design.

Add Slotted Card Content

Your card's styles now stay inside its Shadow DOM boundary. However, its title and message are still fixed inside feedback-card.js, so every copy of the card shows the same content.

Named slots give page content a labelled destination inside the card. The page controls the words. The shadow root controls where those words appear.

In this step, get ready to:
  • Replace the fixed card content with named slots.
  • Supply page-owned content to the feedback card.
  • Verify the slot mapping in the rendered card.
Replace fixed text with named slots

A slot acts as an insertion point in the shadow-root template. Its name tells the browser which page-owned child belongs there.

  • Switch back to feedback-card.js from earlier.
  • Find the hard-coded title text inside the shadow-root heading.
  • Replace only that text with a slot element named title.
  • Save feedback-card.js.
  • Refresh the page in your browser.

You'll see an empty heading position inside the styled card. The fixed message still appears below it.

  • Return to feedback-card.js from earlier.
  • Find the hard-coded message text inside the shadow-root message element.
  • Replace only that text with a slot element named message.
  • Save feedback-card.js.
  • Refresh the page in your browser.

You'll see the styled card shell. Only the feedback button still has content.

How does slot matching work?

Each slot's name identifies one destination inside the shadow root. The browser fills that destination with a page child carrying the same slot value.

The two insertion points are ready. Your card can now receive page-owned content.

Still seeing the fixed content?

Confirm that you replaced only the text inside the existing heading and message elements. Keep the surrounding card structure intact.

Check that the two slot names use lowercase title and message.

Why is my named slot not replacing the fixed card content?

Pass page content into the card

The host page owns each light DOM child, so page selectors can target that child. The shadow root keeps control of the card layout.

A matching slot name places each child into the correct position. The component's feedback button stays scoped inside the shadow root.

  • Choose your page-owned title as your feedback title.
  • Switch back to index.html from earlier.
  • Find the existing <feedback-card> element.
  • Add a child element for your feedback title with its slot attribute set to title.
  • Save index.html.
  • Refresh the page in your browser.

You'll see your page-provided title in the card's heading position. The message position remains empty.

Your first slot is connected. The title now comes from index.html.

  • Choose your page-owned message as your feedback message.
  • Return to index.html from earlier.
  • Add a child element for your feedback message with its slot attribute set to message.
  • Save index.html.

Before you refresh, which empty position do you expect the new message child to fill?

  • Refresh the page in your browser.

You'll see the page-provided title in the card's heading position. You'll see the page-provided message beneath it.

The page-owned button keeps its aggressive global styling. The card's feedback button keeps its scoped component styling.

That connection is working. Your host page now supplies the copy without taking over the card structure.

Content missing from the card?

Confirm that both content elements sit between the opening and closing <feedback-card> tags.

Match each slot value to the corresponding name in feedback-card.js. Slot names are case-sensitive.

Why is my slotted content missing from the feedback card?

Your page can now fill the card's protected layout with its own content. Next, you'll choose one styling hook that the page is allowed to override.

Expose and Restyle a Part

Your custom element now accepts page-owned content through slots. Its internal controls remain protected by the Shadow DOM boundary.

Some host pages need a controlled way to match a component to their visual identity. CSS shadow parts let you expose selected elements without opening the entire shadow tree to page styles.

In this step, get ready to:
  • Expose the internal feedback button as a named part.
  • Add one page-level ::part() override.
  • Confirm that only the exposed shadow element accepts the override.
Expose the feedback button

A part name becomes part of the component's public styling contract. The page can target that named element while every unexposed shadow element remains sealed.

What Does a Part Expose?

The part attribute labels one element inside the shadow root. The label gives the host page a narrow styling hook.

The part exposes presentation only. It does not move the button out of the shadow root or expose nearby elements.

  • Switch back to the feedback-card.js tab in your code editor.
  • Find the opening tag for the component's internal feedback button.
  • Add part="submit-button" inside that opening tag.
  • Save feedback-card.js.

You have opened one precise styling door. The rest of the component's shadow elements still have no page-facing part name.

Add the host-page override

The host page reaches an exposed element with the ::part() pseudo-element. The selector includes the custom element name plus the part name you just exposed.

  • Switch back to the index.html tab in your code editor.
  • Find the page's existing <style> block.
  • Add the following rule below the aggressive global button rule:
/* The host page may style only this exposed component part */
feedback-card::part(submit-button) {
  background: #7c3aed;
  border: 3px solid #f5d0fe;
  color: #ffffff;
}

What Does This CSS Do?

  • The feedback-card selector limits the rule to feedback card components.
  • The ::part(submit-button) selector reaches the one shadow element carrying the matching part name.
  • The color declarations make the allowed override easy to distinguish from the component's original button style.
  • Save index.html.

Before you refresh, which button do you expect the new part selector to change?

  • Refresh index.html in your browser.

You should see the feedback card's internal button turn purple with a pale border. The page-owned button keeps its aggressive global styling.

That visible difference proves the contract works. The page can override the exposed button while the component's other shadow-root elements remain protected.

Part Override Not Appearing?

Confirm that the internal button in feedback-card.js contains the exact attribute part="submit-button".

Check that feedback-card::part(submit-button) sits inside the page's <style> block. The part name must match in both files.

If the page-owned button also turns purple, confirm that the new declarations sit inside the part rule.

Ask for help checking the part name and selector.

Your component now exposes a deliberate styling hook without surrendering its isolation. Next up, you will inspect the shadow tree to see that boundary directly.

Inspect the Shadow Boundary

Your feedback card now keeps its internal styling separate while allowing the page to recolor its exposed button. That controlled exception is working in the browser.

In this step, you will use Browser DevTools to inspect the Shadow DOM boundary. You will trace the styles applied on each side.

In this step, get ready to:
  • Locate the open shadow root in Browser DevTools.
  • Compare the styles applied to page elements with the styles applied inside the component.
  • Confirm that the exposed button part is the page-controlled exception.
Reveal the open shadow tree

The <feedback-card> element is the custom element host. Its open shadow root contains the component's protected internal tree.

The document tree can feel crowded at first. Following the host one level at a time makes the boundary easier to spot.

  • Switch back to the completed demo page in your browser.
  • Use the page context menu to inspect the feedback card.
  • Locate <feedback-card> in the document tree.
  • Expand the <feedback-card> element.
  • Expand the #shadow-root (open) boundary.
  • Locate the element carrying slot="title" below the host.
  • Locate the element carrying slot="message" below the host.

You will see #shadow-root (open) wrapping the component's internal layout. The two slotted elements remain outside that boundary under <feedback-card>.

Read the Tree From Outside In

  • The <feedback-card> host belongs to the page document.
  • The #shadow-root (open) line marks the start of the component's internal tree.
  • The slotted title and message remain in the light DOM even though the slots display them inside the card.
  • The feedback button lives inside the shadow tree.

Can't See the Shadow Root?

  • Reload the completed demo page to run feedback-card.js again.
  • Confirm that you expanded <feedback-card> in the document tree.
  • Check that the card is still visible on the page.

Help me find the open shadow root in Browser DevTools.

Compare styles on both sides

Matched CSS rules reveal which tree owns each visual decision. Page selectors match page-owned elements while component selectors match elements inside the shadow root.

  • Select the page-owned button in the document tree.
  • Review the rules matching the page-owned button.

You will see the aggressive page button rule among the page-owned button's matched styles.

  • Select the internal feedback button beneath #shadow-root (open).
  • Review the rules matching the internal feedback button.

You will see the component's scoped button styles. The broad page button selector does not directly match the internal button.

  • Select the slotted title carrying slot="title".
  • Review the rules matching the slotted title.

You will see that the page's h2 rule can match the slotted title. Slotted content stays owned by the page.

Can't Find the Separate Rules?

  • Confirm that the internal feedback button sits below #shadow-root (open).
  • Confirm that the page-owned button sits outside <feedback-card>.
  • Select each element directly in the document tree before reviewing its matched styles.

Help me compare the page and shadow-root styles.

Verify the exposed part

A named part creates a controlled styling contract. The part="submit-button" attribute exposes only the internal feedback button to a page-level part selector.

Before you make the final comparison, predict whether the broad button selector or feedback-card::part(submit-button) reaches the internal button.

  • Select the internal feedback button beneath #shadow-root (open).
  • Locate feedback-card::part(submit-button) among its matched styles.

You will see feedback-card::part(submit-button) applying the permitted page override. The broad page button selector remains absent from the internal button's direct matches.

Why Does the Part Rule Appear?

The component author exposed the internal button by naming its part. The page can target that specific styling hook without gaining access to every element in the shadow tree.

  • Select an unexposed layout element inside the shadow tree.
  • Review the rules matching that unexposed element.

You will not see feedback-card::part(submit-button) among the unexposed element's matched styles. The override reaches only the named button part.

  • Select the page-owned button once more.

You will see the aggressive global button rule still styling the page-owned button. The page button and feedback button remain independently styled.

Part Rule Missing From the Button?

  • Confirm that the selected button is inside #shadow-root (open).
  • Confirm that the selected button exposes part="submit-button".
  • Confirm that the page still contains feedback-card::part(submit-button).

Help me trace the missing part override.

You have now seen the component boundary from the browser's point of view. Your widget protects its internal elements while exposing one deliberate styling hook.

Secret mission

Audit the Widget's Style Contract

A reusable widget needs a styling boundary another developer can verify. Stress-test blocked selectors against your card. Capture the page-owned slot path plus the explicitly exposed ::part path in a style contract.

Clean Up Your Resources

Clean Up Your Resources

Your project lives entirely in two local files. There are no cloud resources or ongoing costs.

Resources you used:

  • The reusable feedback-card.js component file.
  • The index.html demo page.

Keep everything running

No action needed. A single script tag can load your component on another page.

  • Keep feedback-card.js as your reusable Shadow DOM component.
  • Keep index.html as a test page for style isolation.

Pause - I'll come back to this later

Shut down the open browser tools to free up memory. Your local files remain available at no cost.

  • Save both local files in the code editor.
  • Close the DevTools panel from earlier.
  • Close the completed demo page in your browser.
  • Close the code editor from earlier.

Delete - I don't want to use this again

Deleting both local files permanently removes the finished component. It also removes the demo page.

Your browser and code editor remain untouched.

  • Copy feedback-card.js to another local folder if you may reuse the component.
  • Close the completed demo page in your browser.
  • Switch back to your code editor from earlier.
  • Delete feedback-card.js from the code editor's file sidebar.
  • Approve the deletion if your editor asks for confirmation.
  • Delete index.html from the code editor's file sidebar.
  • Confirm that neither file remains in the file sidebar.

Nice Work!

Nice Work!

You did it! Your feedback card now survives aggressive page styles without losing its own visual identity.

You've learned how to:

  • Packaged the feedback card as a reusable feedback-card Custom Element in feedback-card.js. Its open Shadow DOM boundary protects the internal heading and button from aggressive page rules.
  • Passed page-owned content through named slots. Added a controlled styling hook through part="submit-button". Targeted that hook from the page with ::part.
  • Used DevTools to inspect the visible #shadow-root boundary. Confirmed that page elements and shadow-tree elements receive separate styles.
  • Secret Mission: Completed an optional challenge that extended the isolated widget with another reusable capability.

Ready to quiz yourself?