๐Ÿ”ฅ Advanced CSS Hack: Dynamic CSS in Glide 3 - Badge Navigation via User Image URL Fragment

No Richtext needed โ€” everything runs on the avatar-URL fragment trick.

CSS-only approach โ€” UI reacts to hash fragments appended to the user avatar URL in the users table, displaying badge states accordingly. No extra HTML or Richtext elements are needed.

Reference threads (original method sources):

:small_blue_diamond: Richtext-based selector injection
:brain: CSS Hack: Dynamic CSS in Glide

:small_blue_diamond: Image-URL fragment listener
:link: CSS Hack: Dynamic CSS in Glide 2 via URL Fragment

This post focuses only on the merged CSS approach.


Badge will remain hidden when:

  1. Avatar URL contains no fragment tags
    โ†’ https://cdn.com/avatar.png

  2. Badge is explicitly zero OR empty
    โ†’ avatar.png#data-status=unread#data-badge=0
    โ†’ avatar.png#data-status=unread#data-badge=

  3. Any badge number but status is โ€œreadโ€
    โ†’ avatar.png#data-status=read#data-badge=7

Additionally:
When badge > 10, display becomes โ€œ10+โ€.


ScreenRecording2025-12-10

You only need to append #data-status=value#data-badge=value (or any other #tags) to the end of your avatar image URL.

CSS

/* Prep */
:is(.main-nav button, .sidebar-root li) {
  position: relative;
}
:is(.main-nav, .sidebar-root) {
  overflow: visible;
}

/* Mobile positions */
.has-tab-bar > div:last-child .main-nav button:nth-of-type(2)::after {
  top: -2px;
  left: calc(50% + 12px);
}

/* Desktop positions */
nav button:nth-of-type(2)::after,
.sidebar-root li:nth-of-type(2)::after {
  top: -6px;
  right: calc(100% - 20px );
}

/* Default assumption: badge exists โ†’ fallback 10+ */
#page-root:has(img[src*="#data-badge="])
  :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after {
  content: "10+";
  position: absolute;
  font-size: 12px;
  font-weight: bold;
  color: white;
  border-radius: 999px;
  min-width: 20px;
  height: 20px;
  line-height: 20px;
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 0 4px;
}

/* Hidden when badge is zero or blank */
#page-root:has(img[src$="#data-badge=0"], img[src$="#data-badge="])
  :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after {
  display: none;
}

/* Hidden when status=read */
#page-root:has(img[src*="#data-status=read"])
  :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after {
  display: none;
}

/* Status colors */
#page-root:has(img[src*="#data-status=unread"])
  :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after {
  background:  #0098cc;
}
#page-root:has(img[src*="#data-status=alert"])
  :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after {
  background: red;
}

/* Exact values override fallback */
#page-root:has(img[src$="#data-badge=1"])  :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after { content: "1"; }
#page-root:has(img[src$="#data-badge=2"])  :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after { content: "2"; }
#page-root:has(img[src$="#data-badge=3"])  :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after { content: "3"; }
#page-root:has(img[src$="#data-badge=4"])  :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after { content: "4"; }
#page-root:has(img[src$="#data-badge=5"])  :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after { content: "5"; }
#page-root:has(img[src$="#data-badge=6"])  :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after { content: "6"; }
#page-root:has(img[src$="#data-badge=7"])  :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after { content: "7"; }
#page-root:has(img[src$="#data-badge=8"])  :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after { content: "8"; }
#page-root:has(img[src$="#data-badge=9"])  :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after { content: "9"; }
#page-root:has(img[src$="#data-badge=10"]) :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after { content: "10"; }

9 Likes

Now THATโ€™S a hack!

In laymenโ€™s terms, because the user profile image is the only native glide element that shows up on every screen (in desktop mode anyway), you can append the profile image with the hashtag class and then later target it (or elements adjacent to it) using CSS. Neat!

My question, is how are you appending the profile image as the read count increases?

2 Likes

Both approaches are hacks, but they operate on different layers.

The rich text approach is data-driven because it can rely on computed columns; however, since Glide Classic, it must be placed on every screen and is structurally scoped under #main-root, not within the navigation (#nav-root).

The profile image URL approach leverages the only truly global element that consistently exists inside the navigation. That said, it is not natively reactiveโ€”since it only accepts basic columnsโ€”so it requires an external trigger or event outside of CSS (which I believe you could implement; this could even be an interesting addition to your latest video).

Additionally, URL fragments have inherent limitations in complex #tag scenarios, as CSS cannot reliably perform substring splitting or precise parsing of complex fragments. This limitation does not apply to rich text, where the markup (classes, IDs, or attributes) can be explicitly defined and fully controlled by data.

Ultimately, each method comes with its own trade-offs: the URL-based approach requires updating the counter on every relevant action, while the rich text approach mainly involves reusing the same rich text component across screens.

4 Likes