diff --git a/resources/components/favourites/save.html b/resources/components/favourites/save.html
index 544dc83..8607252 100644
--- a/resources/components/favourites/save.html
+++ b/resources/components/favourites/save.html
@@ -1,4 +1,4 @@
-<form class="favourite-add-form" @submit="save">
+<form class="favourite-add-form" @submit.prevent="save">
     <div class="form-group">
         <label for="favourite_add_name">Nazwa</label>
         <div class="input-group">
diff --git a/resources/components/picker/stop.html b/resources/components/picker/stop.html
index 1f42643..b8d8997 100644
--- a/resources/components/picker/stop.html
+++ b/resources/components/picker/stop.html
@@ -35,8 +35,8 @@
     </fold>
 
     <keep-alive>
-        <popper reference="action-map" v-if="showMap" arrow class="popper--no-padding" style="width: 500px;" placement="right-start" v-hover:inMap>
+        <ui-dialog reference="action-map" v-if="showMap" arrow class="ui-popup--no-padding" style="width: 500px;" placement="right-start" v-hover:inMap>
             <stop-map :stop="stop" style="height: 300px"/>
-        </popper>
+        </ui-dialog>
     </keep-alive>
 </div>
diff --git a/resources/components/popper.html b/resources/components/popper.html
deleted file mode 100644
index 84c1c94..0000000
--- a/resources/components/popper.html
+++ /dev/null
@@ -1,4 +0,0 @@
-<div :class="[ 'popper', arrow && 'popper--arrow' ]" v-on="$listeners">
-    <div class="popper__arrow" ref="arrow" v-if="arrow"></div>
-    <slot />
-</div>
diff --git a/resources/components/tooltip.html b/resources/components/tooltip.html
index 65805b0..033eeb5 100644
--- a/resources/components/tooltip.html
+++ b/resources/components/tooltip.html
@@ -1,9 +1,9 @@
 <fragment>
     <portal to="popups">
         <transition name="tooltip">
-            <popper class="popper--tooltip" aria-hidden="true" arrow :reference="root" :placement="placement" v-if="show" :responsive="false">
+            <ui-dialog class="ui-popup--tooltip" aria-hidden="true" arrow :reference="root" :placement="placement" v-if="show" :responsive="false">
                 <slot />
-            </popper>
+            </ui-dialog>
         </transition>
     </portal>
     <span ref="root" class="sr-only"><slot /></span>
diff --git a/resources/components/ui/dialog.html b/resources/components/ui/dialog.html
new file mode 100644
index 0000000..7adf4cb
--- /dev/null
+++ b/resources/components/ui/dialog.html
@@ -0,0 +1,29 @@
+<div class="ui-backdrop" @click="handleBackdropClick" v-if="currentBehaviour === 'modal'">
+    <div class="ui-modal" v-bind="$attrs">
+        <div class="ui-modal__top-bar">
+            <div class="ui-modal__header">
+                <slot name="header">
+                    <div class="ui-modal__title">{{ title }}</div>
+                </slot>
+            </div>
+            <button class="btn btn-action ui-modal__close" @click.prevent="handleCloseClick">
+                <ui-icon icon="close"/>
+            </button>
+        </div>
+        <slot />
+        <div class="ui-modal__footer" v-if="hasFooter">
+            <slot name="footer" />
+        </div>
+    </div>
+</div>
+<div :class="[ 'ui-popup', arrow && 'ui-popup--arrow' ]" v-bind="$attrs" v-on="$listeners" v-else>
+    <div class="ui-popup__arrow" ref="arrow" v-if="arrow"></div>
+    <div class="ui-popup__header" v-if="hasHeader">
+        <slot name="header" />
+    </div>
+    <slot />
+    <div class="ui-popup__footer" v-if="hasFooter">
+        <slot name="footer" />
+    </div>
+</div>
+
diff --git a/resources/styles/_map.scss b/resources/styles/_map.scss
index ab177bf..89f34fc 100644
--- a/resources/styles/_map.scss
+++ b/resources/styles/_map.scss
@@ -1,5 +1,5 @@
 .map__label-box {
-  @extend .popper;
+  @extend .ui-popup;
 
   padding: .5rem;
   background: white;
diff --git a/resources/styles/main.scss b/resources/styles/main.scss
index 5304c5c..83d2198 100644
--- a/resources/styles/main.scss
+++ b/resources/styles/main.scss
@@ -75,12 +75,24 @@ $grid-gutter-width: $spacer * 2;
   }
 }
 
+@mixin position($position, $top: inherit, $right: inherit, $bottom: inherit, $left: inherit) {
+  $right:  if($right == inherit, $top, $right);
+  $bottom: if($bottom == inherit, $top, $bottom);
+  $left:   if($left == inherit, $right, $left);
+
+  position: $position;
+
+  top: $top;
+  right: $right;
+  left: $left;
+  bottom: $bottom;
+}
+
 @import "common";
 @import "stop";
 @import "departure";
 @import "line";
 @import "controls";
-@import "popper";
 @import "animations";
 @import "form";
 @import "favourites";
@@ -89,6 +101,8 @@ $grid-gutter-width: $spacer * 2;
 @import "map";
 
 @import "ui/switch";
+@import "ui/popup";
+@import "ui/modal";
 
 @import "page/provider-picker";
 
diff --git a/resources/styles/page/_provider-picker.scss b/resources/styles/page/_provider-picker.scss
index 75bc5bb..939910d 100644
--- a/resources/styles/page/_provider-picker.scss
+++ b/resources/styles/page/_provider-picker.scss
@@ -8,7 +8,7 @@
 }
 
 .provider-picker {
-  @extend .popper;
+  @extend .ui-popup;
   padding: 1rem;
   margin: 3rem;
 }
diff --git a/resources/styles/ui/_modal.scss b/resources/styles/ui/_modal.scss
new file mode 100644
index 0000000..2764a52
--- /dev/null
+++ b/resources/styles/ui/_modal.scss
@@ -0,0 +1,58 @@
+.ui-backdrop {
+  @include position(fixed, 0);
+  background: rgba(black, .75);
+  padding: $spacer;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  overflow-y: auto;
+  overscroll-behavior-y: contain;
+
+  &::after {
+    height: 1rem;
+    display: block;
+    content: "";
+  }
+}
+
+$dialog-margin: 1rem;
+$dialog-sizes: (
+        medium: 480px,
+        small: 320px,
+        large: 640px,
+)
+;
+
+.ui-modal {
+  padding: $dialog-margin;
+  background: white;
+  margin: auto;
+  box-shadow: rgba(black, .7) 0 1px 3px;
+  border-radius: 1px;
+
+  @each $size, $width in $dialog-sizes {
+    .ui-modal--#{$size} {
+      width: $width;
+    }
+  }
+}
+
+.ui-modal__close {
+  margin-right: -$dialog-margin;
+  padding: $dialog-margin $dialog-margin 0;
+  margin-top: -$dialog-margin;
+}
+
+.ui-modal__header {
+  flex: 1 1 auto;
+}
+
+.ui-modal__title {
+  font-weight: bold;
+  font-size: 0.875rem;
+}
+
+.ui-modal__top-bar {
+  display: flex;
+  margin-bottom: $dialog-margin * 0.75;
+}
diff --git a/resources/styles/_popper.scss b/resources/styles/ui/_popup.scss
similarity index 93%
rename from resources/styles/_popper.scss
rename to resources/styles/ui/_popup.scss
index 7b9ea5b..384d6c2 100644
--- a/resources/styles/_popper.scss
+++ b/resources/styles/ui/_popup.scss
@@ -54,7 +54,7 @@
 @mixin triangle-left($size, $color, $border: none)   { @include triangle(left, $size, $color, $border); }
 @mixin triangle-right($size, $color, $border: none)  { @include triangle(right, $size, $color, $border); }
 
-.popper {
+.ui-popup {
   $arrow-base: 8px;
   $arrow-color: white;
   $arrow-border: rgba(black, 0.2);
@@ -74,17 +74,17 @@
 
   border-radius: 2px;
 
-  .popper__arrow {
+  .ui-popup__arrow {
     position: absolute;
     width: 0;
     height: 0;
   }
 
-  &.popper--no-padding {
+  &.ui-popup--no-padding {
     padding: 0;
   }
 
-  .popper__heading {
+  .ui-popup__heading {
     font-size: $font-size-sm;
     font-weight: bold;
     margin-bottom: .5rem;
@@ -105,7 +105,7 @@
     &[x-placement*="#{$placement}"] {
       margin-#{map-get($opposite, $placement)}: $arrow-base;
 
-      .popper__arrow {
+      .ui-popup__arrow {
         #{map-get($opposite, $placement)}: 0;
         @include triangle(map-get($opposite, $placement), $arrow-base, $arrow-color, $arrow-border);
       }
@@ -119,11 +119,11 @@
     @include placement("bottom");
   }
 
-  &.popper--arrow {
+  &.ui-popup--arrow {
     @include arrows;
   }
 
-  &.popper--tooltip {
+  &.ui-popup--tooltip {
     background: $dark;
     color: white;
     padding: .5rem .75rem;
@@ -132,14 +132,14 @@
     min-width: 0;
     box-shadow: none;
 
-    &.popper--arrow {
+    &.ui-popup--arrow {
       $arrow-color: $dark;
       $arrow-border: none;
       $arrow-base: 6px;
 
       @include arrows;
 
-      .popper__arrow::before {
+      .ui-popup__arrow::before {
         border: none;
       }
     }
@@ -147,7 +147,7 @@
 }
 
 @include media-breakpoint-down('sm') {
-  .popper {
+  .ui-popup {
     margin-left: $spacer;
     margin-right: $spacer;
   }
diff --git a/resources/ts/app.ts b/resources/ts/app.ts
index 71804a7..a201127 100644
--- a/resources/ts/app.ts
+++ b/resources/ts/app.ts
@@ -31,10 +31,14 @@ Vue.use(VueMoment, { moment });
 declare module 'vue/types/vue' {
     interface Vue {
         $isTouch: boolean;
+        $hasSlot: (slot: string) => string;
     }
 }
 
 Vue.prototype.$isTouch = 'ontouchstart' in window || navigator.msMaxTouchPoints > 0;
+Vue.prototype.$hasSlot = function (this: Vue, slot: string): boolean {
+    return !!this.$slots[slot] || !!this.$scopedSlots[slot];
+}
 
 Component.registerHooks(['removed']);
 
diff --git a/resources/ts/components/ui/dialog.ts b/resources/ts/components/ui/dialog.ts
new file mode 100644
index 0000000..4ecd057
--- /dev/null
+++ b/resources/ts/components/ui/dialog.ts
@@ -0,0 +1,187 @@
+import Vue from "vue";
+import { Component, Prop, Watch } from "vue-property-decorator";
+import Popper, { Placement } from "popper.js";
+import { defaultBreakpoints } from "../../filters";
+
+/**
+ * How popup will be presented to user:
+ *  - "modal" - modal window
+ *  - "popup" - simple popup
+ */
+export type DialogBehaviour = "modal" | "popup";
+
+@Component({
+    template: require('../../../components/ui/dialog.html'),
+    inheritAttrs: false,
+})
+export default class UiDialog extends Vue {
+    @Prop({ type: String, default: "popup" })
+    private behaviour: DialogBehaviour;
+
+    @Prop({ type: String })
+    private mobileBehaviour: DialogBehaviour;
+
+    @Prop([String, HTMLElement])
+    public reference: string | HTMLElement;
+
+    @Prop(Object)
+    public refs: string;
+
+    @Prop({ type: String, default: "auto" })
+    public placement: Placement;
+
+    @Prop(Boolean)
+    public arrow: boolean;
+
+    @Prop({ type: Boolean, default: true })
+    public responsive: boolean;
+
+    @Prop(String)
+    public title: string;
+
+    private isMobile: boolean = false;
+
+    private _focusOutEvent;
+    private _resizeEvent;
+
+    private _popper;
+
+    get currentBehaviour(): DialogBehaviour {
+        if (!this.mobileBehaviour) {
+            return this.behaviour;
+        }
+
+        return this.isMobile ? this.mobileBehaviour : this.behaviour;
+    }
+
+    get hasFooter() {
+        return this.$hasSlot('footer')
+    }
+
+    get hasHeader() {
+        return this.$hasSlot('header')
+    }
+
+    private getReferenceElement() {
+        const isInWrapper = this.$parent.$options.name == 'portalTarget';
+
+        if (typeof this.reference === 'string') {
+            if (this.refs) {
+                return this.refs[this.reference];
+            }
+
+            if (isInWrapper) {
+                return this.$parent.$parent.$refs[this.reference];
+            }
+
+            return this.$parent.$refs[this.reference];
+        }
+
+        if (this.reference instanceof HTMLElement) {
+            return this.reference;
+        }
+
+        return isInWrapper ? this.$parent.$el : this.$el.parentElement;
+    }
+
+    focusOut(event: MouseEvent) {
+        if (this.$el.contains(event.target as Node)) {
+            return;
+        }
+
+        this.$emit('leave', event);
+    }
+
+    mounted() {
+        this.handleWindowResize();
+
+        if (this.behaviour === 'popup') {
+            this.initPopper();
+        }
+
+        window.addEventListener('resize', this._resizeEvent = this.handleWindowResize.bind(this));
+    }
+
+    private initPopper() {
+        const reference = this.getReferenceElement();
+
+        this._popper = new Popper(reference, this.$el, {
+            placement: this.placement,
+            modifiers: {
+                arrow: { enabled: this.arrow, element: this.$refs['arrow'] as Element },
+                responsive: {
+                    enabled: this.responsive,
+                    order: 890,
+                    fn(data) {
+                        if (window.innerWidth < 560) {
+                            data.instance.options.placement = 'top';
+                            data.styles.transform = `translate3d(0, ${ data.offsets.popper.top }px, 0)`;
+                            data.styles.right = '0';
+                            data.styles.left = '0';
+                            data.styles.width = 'auto';
+                            data.arrowStyles.left = `${ data.offsets.popper.left + data.offsets.arrow.left }px`;
+                        }
+
+                        return data;
+                    }
+                }
+            }
+        });
+
+        this.$nextTick(() => {
+            this._popper && this._popper.update();
+            document.addEventListener('click', this._focusOutEvent = this.focusOut.bind(this), { capture: true });
+        });
+    }
+
+    private removePopper() {
+        this._popper.destroy()
+        this._popper = null;
+    }
+
+    updated() {
+        if (this._popper) {
+            this._popper.update();
+        }
+    }
+
+    beforeDestroy() {
+        this._focusOutEvent && document.removeEventListener('click', this._focusOutEvent, { capture: true });
+    }
+
+    removed() {
+        if (this._popper) {
+            this.removePopper();
+        }
+    }
+
+    private handleBackdropClick(ev: Event) {
+        const target = ev.target as HTMLElement;
+
+        if (target.classList.contains("ui-backdrop")) {
+            this.$emit('leave');
+        }
+    }
+
+    private handleCloseClick() {
+        this.$emit('leave');
+        this.$emit('close');
+    }
+
+    private handleWindowResize() {
+        this.isMobile = screen.width < defaultBreakpoints.md;
+    }
+
+    @Watch('currentBehaviour')
+    private handleBehaviourChange(newBehaviour: DialogBehaviour, oldBehaviour: DialogBehaviour) {
+        if (oldBehaviour === 'popup') {
+            this.removePopper();
+        }
+
+        if (newBehaviour === 'popup') {
+            this.$nextTick(() => this.initPopper());
+        }
+    }
+}
+
+Vue.component("ui-dialog", UiDialog);
diff --git a/resources/ts/components/ui/icon.ts b/resources/ts/components/ui/icon.ts
index 11863a0..b25535a 100644
--- a/resources/ts/components/ui/icon.ts
+++ b/resources/ts/components/ui/icon.ts
@@ -7,7 +7,8 @@ import {
     faCheck,
     faCheckDouble,
     faChevronCircleUp,
-    faChevronDown, faChevronUp,
+    faChevronDown,
+    faChevronUp,
     faClock,
     faCog,
     faExclamationTriangle,
@@ -15,7 +16,8 @@ import {
     faInfoCircle,
     faMapMarkerAlt,
     faMoon,
-    faQuestionCircle, faQuestionSquare,
+    faQuestionCircle,
+    faQuestionSquare,
     faSearch,
     faSign,
     faStar,
@@ -23,7 +25,13 @@ import {
     faTimes,
     faTrashAlt
 } from "@fortawesome/pro-light-svg-icons";
-import { faClock as faClockBold, faCodeCommit, faMinus, faPlus, faSpinnerThird } from "@fortawesome/pro-regular-svg-icons";
+import {
+    faClock as faClockBold,
+    faCodeCommit,
+    faMinus,
+    faPlus,
+    faSpinnerThird
+} from "@fortawesome/pro-regular-svg-icons";
 import { faExclamationTriangle as faSolidExclamationTriangle, faWalking } from "@fortawesome/pro-solid-svg-icons";
 import { fac } from "../../icons";
 import { FontAwesomeIcon, FontAwesomeLayers, FontAwesomeLayersText } from "@fortawesome/vue-fontawesome";
@@ -88,6 +96,7 @@ const definitions: Dictionary<Icon> = {
         {icon: faClockBold},
         {icon: faSolidExclamationTriangle, transform: "shrink-5 down-4 right-6"}
     ]),
+    'close': simple(faTimes),
     ...lineTypeIcons,
     ...messageTypeIcons,
 };
diff --git a/resources/ts/components/ui/index.ts b/resources/ts/components/ui/index.ts
index 8814273..1909ea7 100644
--- a/resources/ts/components/ui/index.ts
+++ b/resources/ts/components/ui/index.ts
@@ -1,3 +1,4 @@
 export * from './switch';
 export * from './icon';
 export * from './numeric-input'
+export * from './dialog'
diff --git a/resources/ts/components/utils.ts b/resources/ts/components/utils.ts
index 9dc6837..464fc68 100644
--- a/resources/ts/components/utils.ts
+++ b/resources/ts/components/utils.ts
@@ -1,111 +1,6 @@
 import Vue from 'vue';
 import { Component, Prop, Watch } from "vue-property-decorator";
-import Popper, { Placement } from "popper.js";
-import vueRemovedHookMixin from "vue-removed-hook-mixin";
 
-@Component({
-    template: require("../../components/popper.html"),
-    mixins: [ vueRemovedHookMixin ]
-})
-export class PopperComponent extends Vue {
-    @Prop([ String, HTMLElement ])
-    public reference: string | HTMLElement;
-
-    @Prop(Object)
-    public refs: string;
-
-    @Prop({ type: String, default: "auto" })
-    public placement: Placement;
-
-    @Prop(Boolean)
-    public arrow: boolean;
-
-    @Prop({ type: Boolean, default: true })
-    public responsive: boolean;
-
-    private _event;
-    private _popper;
-
-    private getReferenceElement() {
-        const isInPortal = this.$parent.$options.name == 'portalTarget';
-
-        if (typeof this.reference === 'string') {
-            if (this.refs) {
-                return this.refs[this.reference];
-            }
-
-            if (isInPortal) {
-                return this.$parent.$parent.$refs[this.reference];
-            }
-
-            return this.$parent.$refs[this.reference];
-        }
-
-        if (this.reference instanceof HTMLElement) {
-            return this.reference;
-        }
-
-        return isInPortal ? this.$parent.$el : this.$el.parentElement;
-    }
-
-    focusOut(event: MouseEvent) {
-        if (this.$el.contains(event.target as Node)) {
-            return;
-        }
-
-        this.$emit('leave', event);
-    }
-
-    mounted() {
-        const reference = this.getReferenceElement();
-
-        this._popper = new Popper(reference, this.$el, {
-            placement: this.placement,
-            modifiers: {
-                arrow: { enabled: this.arrow, element: this.$refs['arrow'] as Element },
-                responsive: {
-                    enabled: this.responsive,
-                    order: 890,
-                    fn(data) {
-                        if (window.innerWidth < 560) {
-                            data.instance.options.placement = 'top';
-                            data.styles.transform = `translate3d(0, ${data.offsets.popper.top}px, 0)`;
-                            data.styles.right = '0';
-                            data.styles.left = '0';
-                            data.styles.width = 'auto';
-                            data.arrowStyles.left = `${data.offsets.popper.left + data.offsets.arrow.left}px`;
-                        }
-
-                        return data;
-                    }
-                }
-            }
-        });
-
-        this.$nextTick(() => {
-            this._popper.update();
-            document.addEventListener('click', this._event = this.focusOut.bind(this), { capture: true });
-        });
-    }
-
-    updated() {
-        this._popper.update();
-    }
-
-    @Watch('visible')
-    private onVisibilityUpdate() {
-        this._popper.update();
-        window.dispatchEvent(new Event('resize'));
-    }
-
-    beforeDestroy() {
-        this._event && document.removeEventListener('click', this._event, { capture: true });
-    }
-
-    removed() {
-        this._popper.destroy()
-    }
-}
 
 @Component({ template: require('../../components/fold.html') })
 export class FoldComponent extends Vue {
@@ -151,7 +46,6 @@ export class LazyComponent extends Vue {
     }
 }
 
-Vue.component('Popper', PopperComponent);
 Vue.component('Fold', FoldComponent);
 Vue.component('Lazy', LazyComponent);
 
diff --git a/resources/ts/filters.ts b/resources/ts/filters.ts
index 3a0e040..b3fd1e4 100644
--- a/resources/ts/filters.ts
+++ b/resources/ts/filters.ts
@@ -2,6 +2,14 @@ import { set, signed } from "./utils";
 import Vue from 'vue';
 import { condition } from "./decorators";
 
+export const defaultBreakpoints = {
+    'xs': 0,
+    'sm': 576,
+    'md': 768,
+    'lg': 1024,
+    'xl': 1200,
+}
+
 Vue.filter('signed', signed);
 
 Vue.directive('hover',  {
@@ -61,13 +69,7 @@ Vue.directive('autofocus', {
 
 Vue.directive('responsive', {
     inserted(el, binding) {
-        const breakpoints = typeof binding.value === 'object' ? binding.value : {
-            'xs': 0,
-            'sm': 576,
-            'md': 768,
-            'lg': 1024,
-            'xl': 1200,
-        };
+        const breakpoints = typeof binding.value === 'object' ? binding.value : defaultBreakpoints;
 
         const resize = binding['resize'] = () => {
             const width = el.scrollWidth;
diff --git a/templates/app.html.twig b/templates/app.html.twig
index a8143e6..214d70c 100644
--- a/templates/app.html.twig
+++ b/templates/app.html.twig
@@ -29,9 +29,9 @@
                         </button>
 
                         <portal to="popups">
-                            <popper reference="settings-messages" v-if="visibility.messages" arrow placement="left-start" @leave="visibility.messages = false">
+                            <ui-dialog reference="settings-messages" v-if="visibility.messages" arrow placement="left-start" @leave="visibility.messages = false">
                                 <settings-messages></settings-messages>
-                            </popper>
+                            </ui-dialog>
                         </portal>
                     </header>
                     <fold :visible="sections.messages">
@@ -54,9 +54,9 @@
                             <ui-icon icon="refresh" :spin="departures.state === 'fetching'" fixed-width></ui-icon>
                         </button>
                         <portal to="popups">
-                            <popper reference="settings-departures" v-if="visibility.departures" arrow placement="left-start" @leave="visibility.departures = false">
+                            <ui-dialog reference="settings-departures" v-if="visibility.departures" @leave="visibility.departures = false" arrow placement="left-start">
                                 <settings-departures></settings-departures>
-                            </popper>
+                            </ui-dialog>
                         </portal>
                     </header>
                     <departures :stops="stops" v-if="stops.length > 0"></departures>
@@ -105,9 +105,11 @@
                         </button>
                     </div>
 
-                    <popper reference="save" v-if="visibility.save" arrow tabindex="-1" @leave="visibility.save = false" placement="bottom-end">
-                        <favourites-adder @saved="visibility.save = false"/>
-                    </popper>
+                    <portal to="popups">
+                        <ui-dialog reference="settings-messages" v-if="visibility.messages" arrow placement="left-start" @leave="visibility.messages = false">
+                            <favourites-adder @saved="visibility.save = false"/>
+                        </ui-dialog>
+                    </portal>
                 </section>
                 <section class="section picker">
                     <header class="section__title flex">