Custom Styling Shopify Web Components: A Developer's Guide to Mastering Embedded App Navigation
The Custom Styling Conundrum with Shopify Web Components
Hey there, fellow Shopify developers and store owners! At Shopping Cart Mover, we often see developers tackling complex challenges when building bespoke solutions for the Shopify ecosystem. One such challenge, recently highlighted in the Shopify Community, revolves around a common frustration for anyone building embedded apps for the Shopify admin: trying to apply custom styling to Shopify's new web components, specifically and , and finding your styles just wouldn't stick. Let's dive into what we learned and how you can navigate this challenge to build truly custom and performant embedded apps.
Our community member, dvendy, kicked off the thread with a familiar scenario. They were building an embedded Shopify admin app using a React Router/Hydrogen-style setup and wanted to use the new and components for internal navigation. Sounds straightforward, right? The navigation itself worked perfectly, but dvendy was trying to fully custom-style these components with Tailwind CSS.
The problem? These components applied their own built-in interaction styles — think hover, pressed, and focus states with a subtle gray overlay and default link styling. And no matter what dvendy tried — text-decoration: none, background: transparent !important, targeting the components directly, even ::part() selectors — those default styles just wouldn't budge. The default hover/pressed gray state kept overriding their custom button and card designs. dvendy's goal was clear: use these components only for their navigation behavior, making them visually transparent containers for their custom Tailwind UI.
Why Your Custom Styles Aren't Working (The Shadow DOM Secret)
This is where the community, particularly anmolkumar, provided some crucial insights. The core reason dvendy's overrides weren't working lies in how these Shopify web components are built. They're constructed using Shadow DOM, a web standard that allows component-specific markup, styles, and behaviors to be encapsulated and hidden from the rest of the document. Think of it as a separate, isolated DOM tree attached to an element, preventing external CSS from bleeding in and internal CSS from leaking out.
Shopify's Intent: Polaris and Encapsulation
Shopify's Polaris design system is at the heart of the admin experience, providing a consistent and intuitive UI. The and components are built on these Polaris Web Components and render inside Shadow DOM precisely to enforce this consistency. Their hover, pressed, and focus states are intentionally encapsulated. Shopify expects them to be styled via Polaris design tokens, not entirely replaced with custom Tailwind UI or other external CSS frameworks.
The Technical Barriers to Overriding
When you try to apply your custom CSS or Tailwind utilities to these components, you encounter several barriers:
- Shadow DOM Blocks Normal CSS: The styles you write in your app's global stylesheet or even scoped CSS simply cannot penetrate the Shadow DOM boundary. It's designed to protect the component's internal styling.
::part()Limitations: While::part()selectors are designed to allow some external styling of elements within a Shadow DOM, they only work if the component explicitly exposes those parts. Many Polaris components, by design, do not expose all their internal elements for full theme-replacement, especially for interaction states.- Internal Interaction States: The hover, pressed, and focus states are often handled internally by the component's JavaScript and encapsulated CSS, making them difficult to override externally.
In essence, Shopify's intent is clear: Use Polaris comp Polaris styling. Want full custom UI = use your own components + navigation. So, at the moment, there's no official way to completely disable their built-in interaction styles if you choose to use these specific Polaris web components.
Recommended Approaches for Custom UI and Navigation
Given these constraints, what's the best path forward for developers aiming for a fully custom UI within their embedded Shopify admin apps?
1. For Internal App Navigation: Embrace React Router (or your chosen routing library)
If you're building a single-page application (SPA) within the Shopify admin, you should rely on your client-side routing library (like React Router for React apps) for internal navigation. This ensures a seamless user experience and, crucially, maintains your app's authentication session.
Incorrect Approach (loses auth session):
Go to Page 2
Using for internal routes will cause the embedded app to reload the entire iframe, often leading to a lost authentication session and a broken user flow.
Correct Approach (React Router):
Use React Router's component or its programmatic navigation API:
import { Link, useNavigate } from 'react-router-dom';
// Using Link component
Go to Page 2
// Programmatic navigation
const navigate = useNavigate();
const goToPage2 = () => {
navigate('/page2');
};
This approach handles navigation within your app's iframe without reloading the entire page, preserving the user's session.
2. For External Shopify Admin Navigation: Use target="_top" with shopify://admin/
The target="_top" attribute does have a place, but it should be used specifically when you intend to redirect the user to a native Shopify Admin page outside of your embedded app's iframe. This is typically done using the shopify://admin/ protocol.
View Shopify Products
Go to Account Settings
This will navigate the top-level Shopify Admin window to the specified page, breaking out of your app's iframe.
3. Achieving 100% Visual Control: Build Your Own Components
If your goal is complete visual control and custom styling with frameworks like Tailwind CSS, the most effective strategy is to not use or for styling purposes at all.
Instead:
- Wrap your own styled component: Create a standard HTML element (like a
or) or a custom React component. Apply all your Tailwind CSS classes and custom styles to this component.- Trigger navigation programmatically: Attach an
onClickhandler to your custom component that uses your client-side router's programmatic navigation (e.g., React Router'snavigate()function) or theshopify.app.navigation.navigate()API if you're using App Bridge.// Example with React and Tailwind CSS import { useNavigate } from 'react-router-dom'; function CustomStyledLink({ to, children }) { const navigate = useNavigate(); const handleClick = () => { navigate(to); }; return ( ); } // Usage:Go to My Custom Page This approach gives you full control over the component's appearance while leveraging the correct navigation behavior for embedded apps.
Key Takeaways for Embedded App Developers
- Understand Shopify's Design Philosophy: Polaris components are designed for consistency within the Shopify admin. If you use them, expect Polaris styling.
- Respect Shadow DOM: Recognize that Shadow DOM encapsulates styles, making direct CSS overrides difficult or impossible for certain Polaris components.
- Separate Concerns: Distinguish between navigation behavior and visual styling. Use your client-side router for internal navigation behavior, and custom HTML/CSS for visual styling.
- Use
target="_top"Judiciously: Reserve it for navigating to native Shopify Admin pages using theshopify://admin/protocol. - Prioritize User Experience: Correct navigation (especially maintaining auth sessions) is paramount for a smooth user experience in embedded apps.
Building powerful embedded apps for Shopify requires a deep understanding of its architecture and best practices. While the initial desire to fully customize every component is natural, knowing when to leverage existing tools and when to build your own is key to efficient and successful development. If you're looking to integrate complex features or migrate existing solutions into the Shopify ecosystem, our experts at Shopping Cart Mover are here to help you navigate these development nuances and ensure a seamless e-commerce experience.
- Trigger navigation programmatically: Attach an