đź§  CSS Hack: Dynamic CSS in Glide

If you’ve been waiting for this feature request “CSS Class Name” Field bound to data, it’s still unclear when it will arrive.

In the meantime, here’s a workaround that turns the Rich Text component into a CSS-controlled state bridge — a way to make your app’s CSS respond directly to data, without JavaScript or visibility logic.

This isn’t just a hack — it’s a new architectural concept born from Glide’s limitations.
Here, CSS serves not only as a styling layer but as a logical layer that interprets DOM state.

With this method, you can write HTML directly in a Glide table column to output class names or attributes (like data-badge, data-status, or data-theme).
You can even nest attributes to represent multiple conditions, then use the CSS pseudo-class :has() to make the interface respond automatically.

As an example, I’m continuing from: Notification Badge in New Glide


:one: HTML via Rich Text

<div class="flag" data-status="unread">
  <div data-badge="10+"></div>
</div>

The Rich Text isn’t visible to users — it acts as a data-driven trigger that your CSS can “listen” to.


:two: Dynamic CSS

nav > button {
  position: relative;
}

/* 🟣 Base badge — appears only when data-badge exists */
#page-root:has([data-badge]) nav > button:nth-child(2)::after {
  content: "";
  position: absolute;
  top: -6px;
  right: -6px;
  font-size: 12px;
  font-weight: bold;
  color: white;
  border-radius: 10px;
  min-width: 20px;
  width: fit-content;
  height: 20px;
  line-height: 20px;
  display: flex;
  justify-content: center;
  padding: 5px;
  align-items: center;
}

/* đźź  Unread */
#page-root:has([data-status="unread"]) nav > button:nth-child(2)::after {
  background: orange;
}

/* đź”´ Alert */
#page-root:has([data-status="alert"]) nav > button:nth-child(2)::after {
  background: red;
}

/* ⚪ Read — hide badge entirely */
#page-root:has([data-status="read"]) nav > button:nth-child(2)::after {
  display: none;
}

/* 🔢 Badge values 1–10+ */
#page-root:has([data-badge="1"])  nav > button:nth-child(2)::after { content: "1"; }
#page-root:has([data-badge="2"])  nav > button:nth-child(2)::after { content: "2"; }
#page-root:has([data-badge="3"])  nav > button:nth-child(2)::after { content: "3"; }
#page-root:has([data-badge="4"])  nav > button:nth-child(2)::after { content: "4"; }
#page-root:has([data-badge="5"])  nav > button:nth-child(2)::after { content: "5"; }
#page-root:has([data-badge="6"])  nav > button:nth-child(2)::after { content: "6"; }
#page-root:has([data-badge="7"])  nav > button:nth-child(2)::after { content: "7"; }
#page-root:has([data-badge="8"])  nav > button:nth-child(2)::after { content: "8"; }
#page-root:has([data-badge="9"])  nav > button:nth-child(2)::after { content: "9"; }
#page-root:has([data-badge="10+"]) nav > button:nth-child(2)::after { content: "10+"; }

/* ⚙️ ADVANCED LOGIC — Nested conditional control
   Combine multiple states: show animation if has badge but NOT read */
#page-root:has([data-badge]):not(:has([data-status="read"]))
  nav > button:nth-child(2)::after {
  animation: pulse 1s ease-in-out infinite;
}

@keyframes pulse {
  0%, 100% { transform: scale(1); opacity: 1; }
  50% { transform: scale(1.2); opacity: 0.6; }
}

ScreenRecording2025-10-23214950-ezgif.com-video-to-gif-converter


:dna: CSS-Controlled State Logic

Through this CSS-controlled state bridge, logic expands directly within your styles.
Glide data becomes the state layer, and CSS interprets that state dynamically.

#page-root:has(.parent .child) { … }
#page-root:has(.parent):not(:has(.child)) { … }

The DOM becomes your logic model — and CSS reacts to it, without any JavaScript or conditional visibility rules.


:light_bulb: CSS now carries logic.
In Glide, it no longer just styles the interface — it decides how the interface behaves, based on data output from Rich Text.

11 Likes

:star_struck:

4 Likes

Well, that’s clever! Reminds me of how we used Rich Text component in classic apps to style screens.

The only limitation is that you’d need that rich text component on every screen to ensure the CSS (your badges) persists as the user navigates around your app, ya?

You’re right — for now, that’s the maximum achievable approach for navigation menu cases, unless each navigation button could have a CSS class name bound to Glide data (as suggested here: Allow “CSS Class Name” Field to be bound to data).

For other components, a Rich Text element placed on the same screen works fine and can be combined with any component as long as they share the same screen context.

There’s also another, more efficient path I recently discovered — through component attributes.
For example, the title header of collection components such as Card, List, Table, Data Grid, Checklist, Calendar, Kanban, and Map has a title attribute that can be dynamically styled via CSS.
Each changing value in this title can be “heard” by CSS using the selector pattern:

.title-header [title="Item"] {
  color: red;
  font-weight: 700;
}


4 Likes

I think it’s time Glide considered putting you on their payroll. :clap: :clap: :clap:

1 Like

The next challenge will be creating logic directly through CSS — so that styles can dynamically react to data changes and conditions, not just static values. That’s where things start to get really interesting.

2 Likes

This is great! I have often done things similar, with a hidden button component and passed glide values through it there because it creates an aria-label=”glideValueHere” which can then be targeted with :has in the css.

Keen to see how you solve having dynamic values in the css!

2 Likes

Hi @Eitan that’s right — the aria-label attribute can indeed provide dynamic values based on data. In your CSS, you just need to target the component and define the style according to the label value you want.

#page-root:has([aria-label="active"]) .some-element {
  background: green;
}

ScreenRecording2025-10-28202612-ezgif.com-video-to-gif-converter

3 Likes

You inspired me to play around a bit. I built a way to custom style the Emphasis text of a title component within a custom collection based on the topic of that custom collection’s card.

It’s dynamically applying the CSS class on each card using the Rich Text component, but do I still have to manually define the styling in Settings for each class? See video below.

6 Likes

To clarify, when I say “dynamic CSS,” I mean styles that follow dynamic data, not dynamically generated selectors or injected code.

For example, as you’ve shown, you already have different background colors depending on the text emphasis (AI, WINS, TIPS). But in your video, you’re still repeating the entire CSS block for each variation — even though the only thing that changes is the background-color.

This can be simplified by using attributes instead of multiple classes.
You can define shared styling once:

.ccCard .isGrid:has([data-emphasis]) .metaText {  
  width: fit-content;  
  color: #fff;  
  padding: 5px 15px;  
  border-radius: 15px;  
}

And then only adjust the background-color for each data value:

.ccCard .isGrid:has([data-emphasis="AI"]) .metaText { background-color: black; }  
.ccCard .isGrid:has([data-emphasis="Wins"]) .metaText { background-color: #00b703; }  
.ccCard .isGrid:has([data-emphasis="Tips"]) .metaText { background-color: #00bfff; }  

This approach makes your CSS more data-driven and efficient.
However, each CSS property (like background-color) still needs to be defined explicitly, since a computer cannot guess additional colors unless you set them or create grouping logic.

The key idea is that you can build scalable logic using :has() and :not(:has()) — for example through nested HTML structures or parent wrappers — so your CSS is no longer static for just one condition.


:light_bulb: Extra Tip

The Rich Text component acts as a bridge that lets you inject structured HTML with data attributes or a combination of attributes and classes
—for example:


<div class="cardWrap" data-group="A">
  <div class="metaText" data-emphasis="AI"></div>
</div>

Since these attributes and classes can be nested, you can also create multi-level logic inside CSS, such as:


.cardWrap:has([data-emphasis="AI"]) .metaText:not(:has([data-variant])) {
  border: 2px solid #333;
}

When combined with :has() and :not(:has()), Rich Text effectively allows you to build conditional logic directly in CSS — which is the real meaning of “dynamic” in this context.

So, if you compare the Rich Text component in older Glide apps versus the new version, its role has shifted — in the new apps, Rich Text serves more as a bridge or helper to construct how CSS logic is applied , rather than directly applying code or selectors from Glide tables as it did in the old version.

4 Likes

Thanks @Himaladin, this was fantastic. We changed the CSS for a client who’s predominantly mobile-first (not top nav) but really appreciate this post :raising_hands:

/* Bottom tab bar buttons */
[aria-label=“Tab Navigation”] > button {
position: relative;
}

/* 🟣 Base badge — appears only when data-badge exists */
#page-root:has([data-badge])
[aria-label=“Tab Navigation”] > button:nth-child(4)::after {
content: “”;
position: absolute;
top: -6px;
right: 4px;
font-size: 12px;
font-weight: bold;
color: white;
border-radius: 10px;
min-width: 20px;
width: fit-content;
height: 20px;
line-height: 20px;
display: flex;
justify-content: center;
padding: 5px;
align-items: center;
}

/* đźź  Unread */
#page-root:has([data-status=“unread”])
[aria-label=“Tab Navigation”] > button:nth-child(4)::after {
background: orange;
}

/* đź”´ Alert */
#page-root:has([data-status=“alert”])
[aria-label=“Tab Navigation”] > button:nth-child(4)::after {
background: red;
}

/* ⚪ Read — hide badge entirely */
#page-root:has([data-status=“read”])
[aria-label=“Tab Navigation”] > button:nth-child(4)::after {
display: none;
}

/* 🔢 Badge values 1–10+ */
#page-root:has([data-badge=“1”])
[aria-label=“Tab Navigation”] > button:nth-child(4)::after { content: “1”; }
#page-root:has([data-badge=“2”])
[aria-label=“Tab Navigation”] > button:nth-child(4)::after { content: “2”; }
#page-root:has([data-badge=“3”])
[aria-label=“Tab Navigation”] > button:nth-child(4)::after { content: “3”; }
#page-root:has([data-badge=“4”])
[aria-label=“Tab Navigation”] > button:nth-child(4)::after { content: “4”; }
#page-root:has([data-badge=“5”])
[aria-label=“Tab Navigation”] > button:nth-child(4)::after { content: “5”; }
#page-root:has([data-badge=“6”])
[aria-label=“Tab Navigation”] > button:nth-child(4)::after { content: “6”; }
#page-root:has([data-badge=“7”])
[aria-label=“Tab Navigation”] > button:nth-child(4)::after { content: “7”; }
#page-root:has([data-badge=“8”])
[aria-label=“Tab Navigation”] > button:nth-child(4)::after { content: “8”; }
#page-root:has([data-badge=“9”])
[aria-label=“Tab Navigation”] > button:nth-child(4)::after { content: “9”; }
#page-root:has([data-badge=“10+”])
[aria-label=“Tab Navigation”] > button:nth-child(4)::after { content: “10+”; }
1 Like

Yes… that’s right, and thank you — I’m really glad to hear you managed to apply this method.

I’m sure you could combine both approaches for desktop and mobile by using the :is() pseudo-class if you’d like ( :artist_palette: CSS Hack: Role-Based App Themes ).

To make it easier for the community, here’s a universal badge system that works across all navigation layouts — whether you’re using a sidebar, top bar, or bottom tab bar.
This code does not apply to buttons inside the hamburger or More menus.

/* đź”· BADGE SYSTEM (Universal)  */

/* 🔸 Relative positioning for target elements */
:is(.main-nav button, .sidebar-root li) {
  position: relative;
}

/* 🔸 Ensure the badge bubble is not clipped */
:is(.main-nav, .sidebar-root) {
  overflow: visible;
}

/* 🟣 BASE BADGE  */
#page-root:has([data-badge])
  :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after {
  content: "";
  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 6px;
  box-sizing: border-box;
}

/* 🎯 POSITION RULES */

/* 🔹 Mobile (bottom tab bar) */
[aria-label="Tab Navigation"].main-nav button:nth-of-type(2)::after {
  top: -2px;
  right: 24px; /* top-right corner of the button */
}

/* 🔹 Desktop top bar and sidebar → top-left corner */
nav button:nth-of-type(2)::after,
.sidebar-root li:nth-of-type(2)::after {
  top: -6px;
  left: -6px; /* top-left corner of the button */
}

/* đźź  STATUS COLORS */

#page-root:has([data-status="unread"])
  :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after {
  background: orange;
}

#page-root:has([data-status="alert"])
  :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after {
  background: red;
}

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

/* 🔢 BADGE NUMBERS */

#page-root:has([data-badge="1"]) :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after { content: "1"; }
#page-root:has([data-badge="2"]) :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after { content: "2"; }
#page-root:has([data-badge="3"]) :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after { content: "3"; }
#page-root:has([data-badge="4"]) :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after { content: "4"; }
#page-root:has([data-badge="5"]) :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after { content: "5"; }
#page-root:has([data-badge="6"]) :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after { content: "6"; }
#page-root:has([data-badge="7"]) :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after { content: "7"; }
#page-root:has([data-badge="8"]) :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after { content: "8"; }
#page-root:has([data-badge="9"]) :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after { content: "9"; }
#page-root:has([data-badge="10+"]) :is(.main-nav button:nth-of-type(2), .sidebar-root li:nth-of-type(2))::after { content: "10+"; }
1 Like