Hi all,
I’m building a custom widget (Cell_Plug_Card_Statistik_expone_BoldHeader) for an outlet card with consumption stats. Clicking the “…” menu opens a detail popup that uses position: fixed to center itself over the whole page (backdrop + layer).
Problem: The popup does open, but it visually renders behind / between the other cards in the grid instead of appearing on top of everything — see the attached screenshot. You can see the “Wecker” and “Nuki Bridge” cards, as well as parts of the rollershutter cards, layered over my popup, even though I’m setting z-index: 9998 / 9999 and position: fixed.
My guess: a parent component in the grid (the f7-card with overflow: hidden, or the grid cell itself) creates its own stacking/containing context, which makes position: fixed resolve relative to that card instead of the viewport. That would explain why the popup only floats “locally” above its own card cell and gets visually covered by sibling cards in the grid instead of sitting globally on top.
Has anyone successfully implemented the pattern “build the expand-popup directly inside the widget” (instead of using f7-popup)? Specifically:
- Is there a reliable way to “teleport” an element to
document.bodyfrom inside a MainUI widget (a portal pattern), so thatposition: fixedis guaranteed to be relative to the viewport? - Or is
oh-popup-card/f7-popupthe only robust approach, and customposition: fixedlayers inside regular cards are fundamentally unreliable because of grid/card stacking contexts? - If anyone has a working workaround (e.g. an
oh-contextflag that renders a separate top-level widget) — what does that look like?
Full widget source code:
version: 1
widgets:
Cell_Plug_Card_Statistik_expone_BoldHeader:
tags:
- "2026"
- Wallplug
props:
parameters:
- description: Name der Steckdose
label: Header
name: header
required: false
type: TEXT
- context: item
label: Schalter Item
name: item_schalter
required: false
type: TEXT
- context: item
label: Aktueller Verbrauch (W)
name: item_power
required: false
type: TEXT
- context: item
label: Tagesverbrauch (kWh)
name: item_day
required: false
type: TEXT
- context: item
label: Jahresverbrauch (kWh)
name: item_year
required: false
type: TEXT
- context: item
label: Monatsverbrauch (kWh)
name: item_month
required: false
type: TEXT
- context: item
label: Gesamtverbrauch (kWh)
name: item_energy
required: false
type: TEXT
- context: item
label: Spannung (V)
name: item_voltage
required: false
type: TEXT
- context: item
label: Stromstärke (A)
name: item_current
required: false
type: TEXT
- context: item
label: Innentemperatur (°C)
name: item_temperature
required: false
type: TEXT
- context: item
label: WLAN-Signal
name: item_signal
required: false
type: TEXT
- context: item
label: Laufzeit
name: item_uptime
required: false
type: TEXT
- context: item
label: Heartbeat
name: item_heartbeat
required: false
type: TEXT
component: oh-context
config:
constants:
widgetID: =Number.parseInt(Math.random()*8912).toString(16).padStart(4, '0')
vars:
expanded: false
slots:
default:
- component: f7-block
config:
class: pc-wrap
style:
margin: 0
padding: 0
stylesheet: |
/* ---- base = narrow (< 12rem) ---- */
.pc-card { height: 11rem; }
.pc-headrow { justify-content: flex-end; padding: 0.4em 0.7em; }
.pc-title { display: none; color:white; font-size: 0.78em; font-weight: 700; line-height: 1.15; margin-top: 0.6em; margin-bottom: 0.15rem; padding: 0 0.0em; }
.pc-title-below { display: block; font-size: 0.78em; font-weight: 700; line-height: 1.15; margin-top: 0.0rem; margin-bottom: 0.15rem; padding: 0.3rem 0.7em; }
.pc-power { font-size: 1em; padding-left: 0.7rem;}
.pc-bigicon { display: none; }
.pc-menu { order: 2; align-self: flex-start; margin: 0.6rem 0.2em 0 0; }
.pc-content { padding-left: 0.7em; }
/* ---- medium (>= 12rem) ---- */
@container pc (min-width: 12rem) {
.pc-card { height: 9.375rem; }
.pc-headrow { justify-content: space-between; padding: 0.5rem; }
.pc-title { display: block; font-size: 1em; max-width: 60%; padding-top:0.1rem; padding-left: 0.3rem}
.pc-title-below { display: none; }
.pc-power { font-size: 1.1em; }
.pc-bigicon { display: flex; }
.pc-icon { transform: scale(1); transform-origin: center; }
.pc-menu { order: 2; align-self: flex-start; margin: 0.6rem 0 0 0; }
.pc-content { padding-left: 0.7em; }
}
/* ---- wide (>= 19rem) ---- */
@container pc (min-width: 19rem) {
.pc-card { height: 9.375rem; }
.pc-headrow { padding: 1em; }
.pc-title { display: block; font-size: 0.78em; font-weight: 700; line-height: 1.15; margin-top: 0.5rem; margin-bottom: 0.15rem; padding: 0 0.0em; }
.pc-title { font-size: 1.3em; max-width: 60%; }
.pc-power { font-size: 1.1em; }
.pc-bigicon { display: flex; }
.pc-icon { transform: scale(1); }
.pc-content { padding-left: 1em; }
}
/* ---- expand-as-popup ---- */
.pc-backdrop {
position: fixed !important;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100vw;
height: 100vh;
margin: 0;
background: rgba(0,0,0,0.45) !important;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease;
z-index: 9998;
border: none;
border-radius: 0 !important;
box-shadow: none !important;
}
.pc-backdrop.pc-backdrop-visible {
opacity: 1;
pointer-events: auto;
}
.pc-expand-layer {
position: fixed;
top: 50%;
left: 50%;
width: min(25rem, 92vw);
max-height: 85vh;
overflow-y: auto;
transform: translate(-50%, -50%);
opacity: 0;
pointer-events: none;
background: #f2f2f7;
border-radius: 1.25rem;
box-shadow: 0 1.5rem 3rem rgba(0,0,0,0.3);
box-sizing: border-box;
padding: 0;
z-index: 9999;
transition: opacity 0.25s ease;
}
.pc-expand-layer.pc-layer-visible {
opacity: 1;
pointer-events: auto;
}
.pc-expand-content { background: transparent; }
.pc-expand-inner { display: flex; flex-direction: column; }
slots:
default:
- component: f7-block
config:
class: pc-container
style:
container-name: pc
container-type: inline-size
margin: 0
padding: 0
width: 100%
slots:
default:
- component: f7-card
config:
class: =`pc-card card-${const.widgetID}`
style:
background-color: white
border: none
border-radius: var(--f7-card-expandable-border-radius)
box-shadow: 0 0.1875rem 0.75rem rgba(0,0,0,0.07)
margin: 0.3125rem
overflow: hidden
slots:
default:
- component: f7-card-content
config:
style:
box-sizing: border-box
height: 100%
padding: 0
slots:
default:
- component: f7-block
config:
style:
align-items: flex-start
box-sizing: border-box
display: flex
flex-direction: row
gap: 2%
height: 100%
justify-content: space-between
margin: 0
padding: 0
slots:
default:
- component: oh-button
config:
class: pc-menu
color: gray
iconF7: ellipsis_vertical
iconSize: 1.2em
action: variable
actionVariable: expanded
actionVariableValue: =true
style:
border: none
padding: 0
- component: f7-block
config:
style:
display: flex
flex: 1
flex-direction: column
justify-content: flex-start
margin: 0
min-width: 0
overflow: hidden
padding: 0
slots:
default:
- component: f7-block
config:
class: pc-headrow
style:
align-items: center
box-sizing: border-box
display: flex
margin-top: 0.0em
width: 100%
slots:
default:
- component: Label
config:
class: pc-title
style:
color: "#1a1a1a"
font-weight: 700
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
text: "=props.header ? props.header : 'Steckdose'"
- component: f7-block
config:
style:
align-items: center
display: flex
gap: 0.3em
margin-top: 0.5rem
padding: 0
slots:
default:
- component: oh-toggle
config:
item: =props.item_schalter
style:
transform: scale(0.8)
- component: Label
config:
class: pc-title-below
style:
color: "#1a1a1a"
font-weight: 700
line-height: 1.15
margin: 0
max-width: 100%
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
width: 100%
text: "=props.header ? props.header : 'Steckdose'"
- component: f7-block
config:
class: pc-content
style:
margin-top: 0.4rem
padding: 0
slots:
default:
- component: Label
config:
class: pc-power
style:
color: "=(items[props.item_schalter].state === 'ON') ? 'var(--f7-theme-color)' : '#666'"
font-weight: 600
text: =(items[props.item_power].numericState || '0') + ' W'
- component: Label
config:
style:
color: gray
font-size: 0.6em
letter-spacing: 0.03125rem
margin-top: -0.25em
padding-left: 0.6rem
text-transform: uppercase
text: Aktueller Verbrauch
- component: f7-row
config:
class: pc-content
style:
margin-top: 0.4rem
slots:
default:
- component: f7-col
slots:
default:
- component: Label
config:
style:
font-size: 0.9em
font-weight: 600
text: =(items[props.item_day].numericState || '0') + ' kWh'
- component: Label
config:
style:
color: gray
font-size: 0.55em
text-transform: uppercase
text: Tag
- component: f7-col
config:
visible: "=props.item_month ? true : false"
slots:
default:
- component: Label
config:
style:
font-size: 0.9em
font-weight: 600
text: =(items[props.item_month].numericState || '0') + ' kWh'
- component: Label
config:
style:
color: gray
font-size: 0.55em
text-transform: uppercase
text: Monat
- component: f7-col
slots:
default:
- component: Label
config:
style:
font-size: 0.9em
font-weight: 600
text: =(items[props.item_year].numericState || '0') + ' kWh'
- component: Label
config:
style:
color: gray
font-size: 0.55em
text-transform: uppercase
text: Jahr
- component: f7-block
config:
class: pc-bigicon
style:
align-items: center
align-self: center
flex-shrink: 0
justify-content: center
margin: 0
padding: 0
width: 25%
slots:
default:
- component: oh-icon
config:
class: pc-icon
icon: poweroutlet
state: =items[props.item_schalter].state
style:
filter: "=(items[props.item_schalter].state === 'ON') ? 'drop-shadow(0 0 0.3125rem rgba(0,0,0,0.1))' : 'grayscale(100%) opacity(0.6)'"
- component: oh-button
config:
class: "=`pc-backdrop` + (vars.expanded ? ' pc-backdrop-visible' : '')"
action: variable
actionVariable: expanded
actionVariableValue: =false
text: ""
style:
border-radius: 0
- component: f7-block
config:
class: "=`pc-expand-layer` + (vars.expanded ? ' pc-layer-visible' : '')"
style:
box-sizing: border-box
padding: 0
slots:
default:
- component: f7-block
config:
style:
height: 100%
slots:
default:
- component: f7-block
config:
class: pc-expand-inner
style:
--container-gap: 0.75rem
background: "#f2f2f7"
border-radius: 0
display: flex
flex-direction: column
height: auto
margin: 0
max-width: 25rem
min-height: auto
padding: 0.5rem 1rem 1rem 1rem
width: auto
slots:
default:
- component: Label
config:
style:
font-size: 1.1rem
font-weight: 800
margin: 0.5rem 0.25rem 0.75rem 0.25rem
text: "=props.header ? props.header : 'Steckdose'"
- component: f7-block
config:
style:
display: flex
flex-wrap: wrap
gap: var(--container-gap)
margin: 0
padding: 0
slots:
default:
- component: f7-block
config:
style:
align-items: center
background: "#ffffff"
border-radius: 1rem
box-sizing: border-box
display: flex
flex: 1 1 100%
justify-content: space-between
margin: 0
padding: 0.75rem 1rem
visible: "=props.item_schalter ? true : false"
slots:
default:
- component: Label
config:
style:
font-size: 0.8125rem
font-weight: 700
text: Schalter
- component: oh-toggle
config:
item: =props.item_schalter
style:
transform: scale(0.8)
- component: f7-block
config:
style:
align-items: center
background: "#ffffff"
border-radius: 1rem
box-sizing: border-box
display: flex
flex: 1 1 100%
flex-direction: column
justify-content: center
margin: 0
padding: 1rem
visible: "=props.item_power ? true : false"
slots:
default:
- component: Label
config:
style:
color: "=(items[props.item_schalter]?.state === 'ON') ? 'var(--f7-theme-color)' : '#1c1c1e'"
font-size: 1.6rem
font-weight: 800
text: =(items[props.item_power].numericState || '0') + ' W'
- component: Label
config:
style:
color: gray
font-size: 0.6rem
letter-spacing: 0.03125rem
text-transform: uppercase
text: Aktueller Verbrauch
- component: f7-block
config:
style:
align-items: center
background: "#ffffff"
border-radius: 1rem
box-sizing: border-box
display: flex
flex: 1 1 calc(50% - (var(--container-gap) / 2))
flex-direction: column
justify-content: center
margin: 0
min-width: 8.125rem
padding: 1rem
visible: "=props.item_day ? true : false"
slots:
default:
- component: Label
config:
style:
font-size: 1.1rem
font-weight: 700
text: =(items[props.item_day].numericState || '0') + ' kWh'
- component: Label
config:
style:
color: gray
font-size: 0.6rem
text-transform: uppercase
text: Tag
- component: f7-block
config:
style:
align-items: center
background: "#ffffff"
border-radius: 1rem
box-sizing: border-box
display: flex
flex: 1 1 calc(50% - (var(--container-gap) / 2))
flex-direction: column
justify-content: center
margin: 0
min-width: 8.125rem
padding: 1rem
visible: "=props.item_month ? true : false"
slots:
default:
- component: Label
config:
style:
font-size: 1.1rem
font-weight: 700
text: =(items[props.item_month].numericState || '0') + ' kWh'
- component: Label
config:
style:
color: gray
font-size: 0.6rem
text-transform: uppercase
text: Monat
- component: f7-block
config:
style:
align-items: center
background: "#ffffff"
border-radius: 1rem
box-sizing: border-box
display: flex
flex: 1 1 calc(50% - (var(--container-gap) / 2))
flex-direction: column
justify-content: center
margin: 0
min-width: 8.125rem
padding: 1rem
visible: "=props.item_year ? true : false"
slots:
default:
- component: Label
config:
style:
font-size: 1.1rem
font-weight: 700
text: =(items[props.item_year].numericState || '0') + ' kWh'
- component: Label
config:
style:
color: gray
font-size: 0.6rem
text-transform: uppercase
text: Jahr
- component: f7-block
config:
style:
background: "#ffffff"
border-radius: 1rem
box-sizing: border-box
display: flex
flex: 1 1 100%
flex-direction: column
gap: 0.5rem
margin: 0
padding: 1rem
visible: "=(props.item_energy || props.item_voltage || props.item_current || props.item_temperature || props.item_signal || props.item_uptime || props.item_heartbeat) ? true : false"
slots:
default:
- component: Label
config:
style:
color: gray
font-size: 0.6rem
font-weight: 700
letter-spacing: 0.03125rem
text-transform: uppercase
text: Details
- component: f7-block
config:
style:
align-items: center
display: flex
justify-content: space-between
margin: 0
padding: 0
visible: "=props.item_energy ? true : false"
slots:
default:
- component: Label
config:
style:
color: "#3c3c43"
font-size: 0.8125rem
text: Gesamtverbrauch
- component: Label
config:
style:
font-size: 0.8125rem
font-weight: 600
text: =(items[props.item_energy].displayState || items[props.item_energy].numericState || '–')
- component: f7-block
config:
style:
align-items: center
display: flex
justify-content: space-between
margin: 0
padding: 0
visible: "=props.item_voltage ? true : false"
slots:
default:
- component: Label
config:
style:
color: "#3c3c43"
font-size: 0.8125rem
text: Spannung
- component: Label
config:
style:
font-size: 0.8125rem
font-weight: 600
text: =(items[props.item_voltage].displayState || items[props.item_voltage].numericState || '–')
- component: f7-block
config:
style:
align-items: center
display: flex
justify-content: space-between
margin: 0
padding: 0
visible: "=props.item_current ? true : false"
slots:
default:
- component: Label
config:
style:
color: "#3c3c43"
font-size: 0.8125rem
text: Stromstärke
- component: Label
config:
style:
font-size: 0.8125rem
font-weight: 600
text: =(items[props.item_current].displayState || items[props.item_current].numericState || '–')
- component: f7-block
config:
style:
align-items: center
display: flex
justify-content: space-between
margin: 0
padding: 0
visible: "=props.item_temperature ? true : false"
slots:
default:
- component: Label
config:
style:
color: "#3c3c43"
font-size: 0.8125rem
text: Temperatur
- component: Label
config:
style:
font-size: 0.8125rem
font-weight: 600
text: =(items[props.item_temperature].displayState || items[props.item_temperature].numericState || '–')
- component: f7-block
config:
style:
align-items: center
display: flex
justify-content: space-between
margin: 0
padding: 0
visible: "=props.item_signal ? true : false"
slots:
default:
- component: Label
config:
style:
color: "#3c3c43"
font-size: 0.8125rem
text: WLAN-Signal
- component: Label
config:
style:
font-size: 0.8125rem
font-weight: 600
text: =(items[props.item_signal].displayState || items[props.item_signal].numericState || '–')
- component: f7-block
config:
style:
align-items: center
display: flex
justify-content: space-between
margin: 0
padding: 0
visible: "=props.item_uptime ? true : false"
slots:
default:
- component: Label
config:
style:
color: "#3c3c43"
font-size: 0.8125rem
text: Laufzeit
- component: Label
config:
style:
font-size: 0.8125rem
font-weight: 600
text: =(items[props.item_uptime].displayState || items[props.item_uptime].state || '–')
- component: f7-block
config:
style:
align-items: center
display: flex
justify-content: space-between
margin: 0
padding: 0
visible: "=props.item_heartbeat ? true : false"
slots:
default:
- component: Label
config:
style:
color: "#3c3c43"
font-size: 0.8125rem
text: Heartbeat
- component: Label
config:
style:
font-size: 0.8125rem
font-weight: 600
text: =(items[props.item_heartbeat].displayState || items[props.item_heartbeat].state || '–')
- component: oh-button
config:
action: variable
actionVariable: expanded
actionVariableValue: =false
style:
background: rgba(0, 122, 255, 0.1)
border: none
border-radius: 0.75rem
color: "#007AFF"
font-size: 0.875rem
font-weight: 600
height: 2.75rem
margin-top: 1rem
width: 100%
text: Close
Screenshot is attached. Thanks in advance for any pointers!
