Position an element relatively to another element from anywhere in the DOM


#1

Problem -

I need to position a popup modal / tooltip / dialog relative to another element, perhaps the button that I clicked to trigger the popup or relative to a block of text that’s relevant to the popup’s information. Something like this:

We want the positioned element relative to the target element at all times. Unfortunately, we really only have a couple of solutions and neither are ideal for this problem:

``1. Make the dialog a sibling/ancestor: As long as the dialog and target element are related, I can position: relative; a common parent and then position: absolute; the dialog. From there, I just need set the top, left, right, and bottom properties (or use transform: translate()) to set the desired position.

Problems with this solution:

  • Not at all DRY: If I have a list of items with buttons that need dialogs, I have to create as many dialogs (even if they say/accomplish the same thing) so that I can ensure they’re positioned correctly. I don’t want to add all those extra elements.
  • At the mercy of the parent: Parent have an overflow: hidden; set? Good game bro… this solution’s a no go. z-index not right? Too bad.

``2. Use JavaScript to calculate the position: Just grab the offset of the target element, position: absolute; the dialog and start setting styles.

Problems with this solution:

  • Violates SoC: I have to rely on JavaScript to stylistically position my dialog to the relevant element. Gross.
  • No Guarantees: Did the user resize the window? Guess I have to add an event handler to recalculate. Did the target element shift due to an animation or another event? Ugh, guess I can set some sort of callback(s) to deal with that. JavaScript turned off… oh well??

Suggested Solution -

We need to be able to visually associate one element (the connected) with another (the target), so why not extend the position property in CSS and allow us to pass in an element selector.

.connected {
    position: element(#Target);
    transform: translateY(-100%);
}

The syntax was borrowed from Mozilla’s element() function for the background-image property

When an element is elementally positioned, it’s X/Y page offset becomes identical to the target element’s. As the target shifts around the page due to various events, the connected element shifts with it. From there, it can be offset from the target by using top, left, right, bottom, transform: translate; or a combination of all 5. The connected element would act as if it was contained within the target element so right: 0, top: 0 would position the connected element in the top right corner of the connected element.

For all children of the connected element, position element() would act like position: relative.

With this solution, I can visually associate any element with any other, don’t need to duplicate elements, and I don’t need to rely on JavaScript for presentation.


Popup’s were just one use case, there are several others that I can list if we need them! I apologize for the wall text, please let me know if anything is unclear!


Native Overlays
#2

I like this idea - it also provides an elegant way to stack elements directly on top of each other. All but the first can just use position: element(#rootElem). Then you only need to move/animate the root element.


#3

Nice use case! Someone could have a lot of fun with animations/transitions with minimal code doing this!


#4

It would also be useful if you could position it relatively within that element. Imagine you want some kind of tooltip in the bottom-right of a larger element. Perhaps you could do this:

#toolTip {
    position: element(#someLargerElement);
    right: 0;
    bottom: 0;
}

This would be like position: fixed but relative to the given element’s area instead of the whole viewport. Alternatively your transform example can offset it.


#5

Lovin’ it! Is there any idea on whether or not you’ll be able to leverage the same, underlying functionality that the element rule has?

(https://developer.mozilla.org/en-US/docs/Web/CSS/element)


#6

Yea, I see this acting as if it were contained within the element (whether or not it could be). By leveraging the 4 directional properties and transform, I think we could cover many use cases. Another thing that would need to be ironed out is, what all should be relative to the #target element? If I specify width: 100%;, is that 100% of the connected element’s parent width or 100% of the #target element’s width? My thought would be the former, but there are definitely other things we wouldn’t want to inherit (like the target’s parent’s overflow). So maybe nothing at all and only the directional properties are impacted?


#7

My gut feeling is that implementers are going to reject this for the same we-can’t-have-things-that-subvert-the-DOM-hierarchy reasons as things like ::outside.

That said, if we are allowed to subvert the DOM like this, I’d like this to have a syntax like position: outside-element(#example) (where you can specify either top or bottom but not both) and/or a position: absolute before-element(#example) and position: absolute after-element(#example) (which would use the same rules for positioning the element as a pseudo-element).


#8

Nah, abspos already subverts that a good bit, so it’s not nearly as much of a stretch as ::outside. We might need to apply some restriction like “the target element must be in the same or a descendant containing block”, but that’s generally easy to work with.

I do plan to pursue this in the Position spec.


#9

In the case where restrictions would need to apply, I’m just trying to wrap my head around this statement:

Unrelated: would NOT work

Since <dialog> and #target are not related, the position does not apply.

Siblings: This would work

Since <dialog> and #target are siblings it works as expected

Distant Relatives: would NOT work??

Since <dialog> and #target are not direct relatives, the position does not apply.

Would these statements be accurate? If there are performance implications I completely understand, but the ideal would be for that all three of these situations work.


#10

Assuming that what I said was accurate (that we could do element-based positioning if the target was in the same or a descendant containing block), then all three of your examples would work. Here’s an example that wouldn’t work:

<body>
  <div id='target'>...</div>
  <div style="position: relative;">
    <dialog style="position: element(#target);">...</dialog>
  </div>
</body>

This would break because the dialog’s containing block is generated by its parent, and the target is not a descendant of that element.


#11

Perfect, got it. Wasn’t nearly as restricting as I was thinking. Thanks for clearing that up.


#12

Really glad this got brought up, and I like your solution too. It addresses a problem I think most of us have faced many times. My only gripe is that it makes you either use inline styles or tie yourself to DOM ids in the CSS to be able to use it, which puts some limits on how dynamic it can be. That’s a hard thing to get around, though. One way to decouple it a bit and maintain SoC could be to allow something like:

<body>
  <div id="target"></div>
  <dialog class="attached" reference="target"></dialog>
  <style>
    .attached {
      position: reference;
    }
  </style>
</body>

Just spitballing here to get the idea across. Could also use the attr() function with a custom attribute or something like that, potentially.


#13

I’m all for syntax reform (I just pulled a usable example from an existing feature), but I’m not sure I’m on board with your suggestion. I’ve always felt that there’s a little bit of an illusion when it comes to full SoC with our front-end code. Our JS and CSS files will always need to be somewhat aware of the DOM structure (either through tags, attributes or classes/ids) in order to operate on it.

Your example relies on a reference attribute and an attached class to tie your two elements together. At first blush that may seem decoupled, but ultimately, to me, it feels less maintainable and verbose for the end user. I have to set two values to accomplish the same thing I would be able to do with the former syntax in one value.

If we were trying to convey semantic meaning behind this, I think utilizing an attribute would be a good idea. We could treat it like the for attribute and have it be a part of UA stylesheets or something. But I see this as purely presentational, so I think a pure CSS solution is ideal here.


#14

What about changing element() to be something like first-sibling() or first-ancestor() (or I guess first-descendant(), though I thought lookahead CSS rules were a no-go), where it refers to the first element (along a search path relative to the element, within the rules Tab stated) matching a given CSS selector (not just ID selectors)?

I imagine such an ancestor search could have perf problems, but there’s probably some way to sidestep that, eg. by using values with more constrained search paths (like maybe previous for the sibling element immediately before this one).

Also, for that reason, I think the selector should be a second word of the property, with the first defining the relation to the selected element like relative-to (with other possible values, as I mentioned above, like before or after).


#15

Based on what I’ve seen over at Bootstrap, supporting (or aiding implementation of) robust full-featured positioning of tooltip-ish elements is a tad more complicated, although this is certainly a step in the right direction.

Some complications / use-cases to consider:

(A) Say the tooltip is above the element. If the user scrolls down so that the element is at the very top of the screen, my tooltip is now partially/totally outside the viewport and not visible. I want my tooltip to “flip” to be below the element instead. Strawman syntax just to give some examples of what I’m talking about:

  • relative-placement: top; = tooltip always goes above element, regardless of whether that puts it outside the viewport
  • relative-placement: auto(top, right, bottom); = tooltip goes above element, unless that would put it outside of the viewport, otherwise the tooltip goes on the right side of the element, otherwise the tooltip goes below the element; if all of those placements are outside the viewport, then go above since top is first in the list. Authors are advised to include all available directions in their auto preference list.

(B) Say the element is a <span> with multiple rendering boxes cut across two lines (example visual: http://imgur.com/ta68YjM.png). Where does the tooltip get positioned? Most users feel that placing it over the empty space between the two boxes feels pretty weird, although that’s the easiest behavior to opt for.

© You probably want display:none to propagate to any attached elements too?


#16

It might be good to ping the Tether folks and ask them what CSS feature(s) would make their lives easier. (Or to at least mine their docs for inspiration.)


#17

So I’m generally in favor of giving the end-user as much flexibility as possible when it comes to features because they end up doing things the implementors could never imagine with them… and that’s great! Allowing any CSS selector would give a lot more flexibility but it would also open it up to some issues and I don’t think the tradeoffs are worth it.

By sticking with Id’s, we can select one element as fast as possible and we have built in guarantees that the element is unique (or else the document is invalid). It also gives us a much simpler syntax than the one you’re proposing. Finally, if a user runs into issues using the property, there’s a single point of failure to look at… that element exists in the DOM or it does not.

Using your proposed syntax, there’s no guarantee we’ll find only one element and each of these queries could/would be noticeably slower than using Id’s, even if we’re only using the first element returned by the selection.

All that to say I’m favoring simplicity and speed over flexibility in this case.


#18

Yes, but a rule that only works once on the page is useless for anything but fixed UI elements and dialog boxes. Assigning the rule to a unique element means it’s completely incapable of working for any sort of repeatable component outside of Shadow DOM.

This argument could be applied to anything that uses CSS selectors. It’s not a valid point.

Since I’ve never actually touched UA code for CSS engines, I leave tractable perf concerns to people like Tab who have some applicable experience in this domain.


#19

Thanks for your response!

I’m pretty sure I’m misunderstanding what it is exactly that you’re trying to accomplish with your syntax (over the original) so if you could provide an example that’d be great.

As I read it, your solution ultimately only targets one element as well. If you factor JavaScript into the equation anything is possible with either solution. I can trivially change classes and Id’s on any element thereby changing the rule to point to the element I need. If you’re not factoring JavaScript into the equation, then the DOM isn’t changing and either solution will target just the one element.

The one exception to this (in your case) would be the ability to use pseudo classes, which would be a definite plus!

Except that selectors don’t typically reside in property values. I think the additional clarity afforded to me by an Id here would be beneficial.

I haven’t touched UA code either, but I’m not asking you to take my word for it. Just run some perf tests in your own browser using dev tools. I imagine the scoping of your functional syntax would help mitigate some of the issues, but you should be able to see get a general idea of how the selectors perform.

To add to that, I feel like I worded my initial response poorly so let me try to better explain my point. I’m not trying to say that only Id’s will perform adequately. I’m just suggesting that the open-ended nature of your syntax invites performance considerations. By restricting the selectors to just Id’s, we know that it’ll work, because we have viable examples using the same syntax in existing browsers.

I guess my big question for you is: Why would I want all the extra stuff in your syntax, when at the end of the day all I’m trying to target is one element? Id’s only return one element and that’s what I want… why make it more complicated?


#20

HTML:

<div class="card">
  <img src="user-f00.png" class="avatar"><div class="unread">16</div>
</div>
<div class="card">
  <img src="user-ba2.png" class="avatar"><div class="unread">23</div>
</div>

CSS:

.unread {
  position: relative-to first-sibling(.avatar);
  top: 2px;
  left: 2px;
}