You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Modals are vertically centred, so their top edge sits in a different place for every dialog and moves whenever the content height changes. Proposal: cap the height and anchor the top at a fixed offset, so the top edge is always in the same place and only the bottom edge varies with content.
Concretely: max-h-[80dvh] with a 10dvh top offset on desktop, max-h-[90dvh] with 5dvh on mobile. Those pairs are self-consistent, a dialog at full height still has equal margin above and below, so a tall dialog looks centred and a short one simply stops earlier.
Why the current behaviour is a problem
Position comes from Radix/shadcn's default in frontend/src/components/ui/dialog.tsx:63:
Centring moves both edges whenever the height changes. In practice:
Postavke: switching tabs changes the body height, so the title, the tab row and the close button all jump under the pointer. Profil, Obavijesti, Preferencije and Sigurnost are all different heights.
Validation errors appearing or clearing resize the dialog.
The locations MultiSelect renders as an inline Collapsible below md (multi-select.tsx:47), so opening it grows the dialog and shifts the header upward.
Any async content settling after open does the same.
Top-anchored, none of these move anything the user is looking at. Only the bottom edge travels.
What other systems do
This is a real split, not a settled rule, and it is worth saying so plainly.
Top-anchored by default, centring is opt-in:
Bootstrap. "By default the Bootstrap modal window is aligned to the top of the page with some margin." Vertical centring is the opt-in .modal-dialog-centered modifier, and has been since v4. https://getbootstrap.com/docs/5.3/components/modal/
Ant Design. The centered prop defaults to false; the default presentation is a top offset, and centring is something you ask for. "You can use centered, style.top or other styles to set position of modal dialog." https://ant.design/components/modal
Those are two of the most widely deployed component libraries in existence, and both made top-anchoring the default.
Centred by default:
The native <dialog> element. The UA stylesheet centres it with inset-block-start: 0; inset-block-end: 0; margin: auto. Worth noting how: auto margins only centre while there is spare room, so a dialog taller than the viewport top-aligns automatically. Native <dialog> is really the hybrid, not pure centring.
Supporting argument from the <dialog> standards discussion: on whether a dialog should re-centre as its content grows, "having the dialog move up and down when stuff is added at the bottom would be quite weird". That is exactly the failure being described here, and the conclusion was that anchoring is preferable.
Three things top-anchoring fixes for free
Sub-pixel blur.translate(-50%, -50%) on an odd-height box resolves to a half pixel, and Chrome and WebKit render the half pixel, which softens text. A layout-positioned dialog never has the fractional offset. Long-standing and still current. https://www.alexbarker.me/post/blurry-html-with-transform-translate
The safe-area refusal at modal-shell.tsx:179. The comment there declines safe-area padding on the footer because "the dialog is centred with a transform, so padding grows it about its centre and pushes the bottom edge toward the home indicator rather than away from it." Top-anchored, padding grows downward and that objection disappears.
Keyboard behaviour. When dvh shrinks for the on-screen keyboard, a centred dialog slides its title upward as the box shortens. Top-anchored, the title stays put and only the body loses height, which is the behaviour the pinned-footer layout already assumes.
Implementation
Do it as a flex overlay rather than by fighting the transform:
Content: fixed top margin (mt-[10dvh], mt-[5dvh] below md), mb-auto, and drop top-[50%] translate-y-[-50%]
Radix's open animation is zoom-in-95, a scale, so it survives losing the translate. translate-x-[-50%] goes too, since the flex container handles horizontal centring.
This is an edit to ui/dialog.tsx, which AGENTS.md permits where the primitive is the natural home for the change. It is, since the positioning is the primitive's.
Trade-off
A short dialog anchored high leaves visible empty space below it, which some people read as stranded. Our confirm, donation and auth-status modals are the short ones.
The alternative is my-auto instead of a fixed top margin, which centres a short dialog and pins a tall one, and is exactly what native <dialog> does. But it reintroduces the inconsistency this issue exists to remove: the top edge would still land somewhere different per dialog.
Recommendation: take the fixed offset and the consistency. If the short dialogs look wrong in review, my-auto is a one-word fallback.
Checks
Every modal at phone and desktop width, particularly the four Postavke tabs, since inconsistent height across tabs is the clearest case.
Confirm, donation and auth-status, the short ones, and the ones that take the no-children branch where the header is the scroller (modal-shell.tsx:145).
With the keyboard open on a form modal.
The MultiSelect inline collapsible expanding below md.
docs/MOBILE-NAV.md documents the modal flex model at :944; it will need a line about positioning.
modal-shell.tsx:22TODO(responsive-drawer): if ModalShell eventually renders a vaul drawer below md, the mobile half of this becomes moot. Not a blocker; the desktop half stands either way.
What
Modals are vertically centred, so their top edge sits in a different place for every dialog and moves whenever the content height changes. Proposal: cap the height and anchor the top at a fixed offset, so the top edge is always in the same place and only the bottom edge varies with content.
Concretely:
max-h-[80dvh]with a10dvhtop offset on desktop,max-h-[90dvh]with5dvhon mobile. Those pairs are self-consistent, a dialog at full height still has equal margin above and below, so a tall dialog looks centred and a short one simply stops earlier.Why the current behaviour is a problem
Position comes from Radix/shadcn's default in
frontend/src/components/ui/dialog.tsx:63:Centring moves both edges whenever the height changes. In practice:
MultiSelectrenders as an inlineCollapsiblebelowmd(multi-select.tsx:47), so opening it grows the dialog and shifts the header upward.Top-anchored, none of these move anything the user is looking at. Only the bottom edge travels.
What other systems do
This is a real split, not a settled rule, and it is worth saying so plainly.
Top-anchored by default, centring is opt-in:
.modal-dialog-centeredmodifier, and has been since v4. https://getbootstrap.com/docs/5.3/components/modal/centeredprop defaults tofalse; the default presentation is a top offset, and centring is something you ask for. "You can use centered, style.top or other styles to set position of modal dialog." https://ant.design/components/modalThose are two of the most widely deployed component libraries in existence, and both made top-anchoring the default.
Centred by default:
<dialog>element. The UA stylesheet centres it withinset-block-start: 0; inset-block-end: 0; margin: auto. Worth noting how: auto margins only centre while there is spare room, so a dialog taller than the viewport top-aligns automatically. Native<dialog>is really the hybrid, not pure centring.Supporting argument from the
<dialog>standards discussion: on whether a dialog should re-centre as its content grows, "having the dialog move up and down when stuff is added at the bottom would be quite weird". That is exactly the failure being described here, and the conclusion was that anchoring is preferable.Three things top-anchoring fixes for free
translate(-50%, -50%)on an odd-height box resolves to a half pixel, and Chrome and WebKit render the half pixel, which softens text. A layout-positioned dialog never has the fractional offset. Long-standing and still current. https://www.alexbarker.me/post/blurry-html-with-transform-translatemodal-shell.tsx:179. The comment there declines safe-area padding on the footer because "the dialog is centred with a transform, so padding grows it about its centre and pushes the bottom edge toward the home indicator rather than away from it." Top-anchored, padding grows downward and that objection disappears.dvhshrinks for the on-screen keyboard, a centred dialog slides its title upward as the box shortens. Top-anchored, the title stays put and only the body loses height, which is the behaviour the pinned-footer layout already assumes.Implementation
Do it as a flex overlay rather than by fighting the transform:
fixed inset-0 flex justify-center overflow-y-automt-[10dvh],mt-[5dvh]belowmd),mb-auto, and droptop-[50%] translate-y-[-50%]Radix's open animation is
zoom-in-95, a scale, so it survives losing the translate.translate-x-[-50%]goes too, since the flex container handles horizontal centring.This is an edit to
ui/dialog.tsx, which AGENTS.md permits where the primitive is the natural home for the change. It is, since the positioning is the primitive's.Trade-off
A short dialog anchored high leaves visible empty space below it, which some people read as stranded. Our confirm, donation and auth-status modals are the short ones.
The alternative is
my-autoinstead of a fixed top margin, which centres a short dialog and pins a tall one, and is exactly what native<dialog>does. But it reintroduces the inconsistency this issue exists to remove: the top edge would still land somewhere different per dialog.Recommendation: take the fixed offset and the consistency. If the short dialogs look wrong in review,
my-autois a one-word fallback.Checks
childrenbranch where the header is the scroller (modal-shell.tsx:145).MultiSelectinline collapsible expanding belowmd.docs/MOBILE-NAV.mddocuments the modal flex model at:944; it will need a line about positioning.Related
modal-shell.tsx:22TODO(responsive-drawer): ifModalShelleventually renders a vaul drawer belowmd, the mobile half of this becomes moot. Not a blocker; the desktop half stands either way.Sources: Bootstrap Modal, Ant Design Modal, Blurry HTML with transform: translate(), CSS-Tricks on the dialog element, csswg-drafts #4645 dialog positioning.