Back to blog
web componentsdesign systemsCSS

Inspecting web component props: a guide for design-system developers

You hover a <ui-card> in DevTools and the question hits: what props does this thing actually accept? The attributes panel shows you variant="elevated" and not much else, and now you’re three files deep in the component source trying to reconstruct its API.

Inspecting web component props feels like it should be a one-click operation. It usually isn’t. Here’s why standard browser tooling falls short, what you actually need to see, and how to make it a fast, repeatable part of your workflow.

Why DevTools doesn’t give you the full picture

Open any page that uses a design system built on custom elements. Inspect a component. What do you see in the Elements panel?

<ui-card variant="elevated" size="md" data-testid="featured-post">
  #shadow-root (open)
    ...
</ui-card>

You see the tag. You see the attributes that happen to be set on this instance. What you don’t see is the component’s full interface: the props it could accept but currently doesn’t, the ones sitting on their default value, the ones nobody got around to documenting.

That gap costs you when:

  • A designer hands you a spec and you need to know if a prop already exists before writing new code
  • You’re auditing a page to understand why a component looks wrong
  • You’re onboarding a new dev who has never seen your design system before

The attributes visible in the DOM are a partial snapshot of state, not the component contract.

Attributes vs. properties: the distinction that trips people up

Custom elements expose two parallel interfaces, and confusing them is where most of the pain comes from.

Attributes are what you see in the markup. They are always strings, you read them with getAttribute(), and they live in the DOM.

Properties are JavaScript values on the element object: booleans, numbers, arrays, full objects. They live in memory, and they don’t necessarily have a matching attribute. An attribute and a property can share a name and stay loosely linked (an element can mirror its disabled property to a disabled attribute), but that mirroring only happens if the component author writes the code to do it.

<!-- Attribute — visible in markup -->
<ui-button disabled>Submit</ui-button>
// Property — only visible in JS
document.querySelector('ui-button').loading = true;

Say a component accepts a loading property that toggles an internal spinner. If the author never reflects it to a loading attribute, setting it leaves no trace in the Elements panel. To find it you open the Console, grab the element, and start walking its prototype chain. Doable, but it’s the kind of friction that turns a 30-second question into a coffee break.

The DOM shows you what’s set. It doesn’t show you what’s possible.

The design-system developer’s real workflow

When you’re building or consuming a component library, what you actually need to answer quickly is:

  1. What tag is this element?
  2. What attributes/props does it currently have set?
  3. Which prefix does this component belong to (ui-, app-, ds-)?

That third point matters once multiple teams ship components into the same app. A <ui-card> from the core design system and an <app-card> from a product squad are unrelated components that happen to look alike in the DOM. Filtering by prefix, so you can see every ui- element on a page at once, is the difference between a design-system audit that takes minutes and one that takes an afternoon.

Inspecting a custom element step by step

The most reliable manual approach:

// Get the element
const el = document.querySelector('ui-card');

// See all reflected attributes
console.log(el.attributes);

// Inspect its own properties (not inherited)
console.log(Object.getOwnPropertyNames(Object.getPrototypeOf(el)));

This works, but it’s noisy. getOwnPropertyNames on the prototype pulls in whatever the class defines, and one level up you’re swimming in inherited HTMLElement methods. You spend more time filtering than reading.

A cleaner angle: ask the class itself what attributes it watches.

customElements.whenDefined('ui-card').then(() => {
  const ctor = customElements.get('ui-card');
  console.log(ctor.observedAttributes);
});

observedAttributes is the static array the component declares so it gets attributeChangedCallback calls when those attributes change. It’s the closest thing to a published API surface you can read without opening the source. Two catches: a component only lists the attributes it actively reacts to, so it’s not guaranteed to be the full set, and it tells you nothing about property-only APIs like that loading flag from earlier.

Why a prefix filter changes the audit game

Picture an adoption audit. You need to know which components on a product page already use the canonical ui- system and which are still on the legacy- versions that were supposed to be migrated last quarter. Done by hand, that’s reading every node in the DOM tree and keeping a tally in your head.

A component-prefix filter flips it around. Type ui- and you get every matching custom element on the page with its current attributes; type legacy- and you’ve got your migration backlog. It’s the kind of targeted inspection a general-purpose element panel was never designed for.

PxGuard’s Components tool is built around exactly this use case. It surfaces every custom element on the page with its tag name and active attributes, and lets you filter by component prefix so you can zero in on your design system’s components without wading through unrelated elements. It’s the “what is this thing and what does it have?” answer — directly in the browser, no Console gymnastics required.

If you author components, make them inspectable

The friction above is largely self-inflicted. A few habits make your components legible to anyone debugging them later, yourself included.

Reflect the props people will want to inspect or style. If a developer might reasonably CSS-select on variant or read it off in DevTools, mirror it to an attribute inside the setter. element.setAttribute('variant', value) is a couple of lines and it makes the prop visible everywhere.

List your attributes in observedAttributes even when you handle some of them lazily. The array doubles as documentation: tooling reads it, and so do the humans skimming your class.

Watch out for silent defaults. A size prop that defaults to "md" leaves no DOM trace until someone sets it explicitly, so the most common configuration is the one you can’t see. Document it, and reflect it if it’s worth surfacing.

And pick one prefix per team and stick to it. ui- for core, app- for product, mkt- for marketing. The consistency is what makes a page-level audit possible at all.

Further reading


Install PxGuard free →

See it on your own pages

PxGuard is a free Chrome extension. Inspect spacing, typography, and accessibility in seconds.

Install Free