diff --git a/library/Icinga/Web/Navigation/DropdownItem.php b/library/Icinga/Web/Navigation/DropdownItem.php
new file mode 100644
index 000000000..98f153500
--- /dev/null
+++ b/library/Icinga/Web/Navigation/DropdownItem.php
@@ -0,0 +1,21 @@
+children->setLayout(Navigation::LAYOUT_DROPDOWN);
+ }
+}
+
diff --git a/library/Icinga/Web/Navigation/Navigation.php b/library/Icinga/Web/Navigation/Navigation.php
new file mode 100644
index 000000000..3ec0fd7de
--- /dev/null
+++ b/library/Icinga/Web/Navigation/Navigation.php
@@ -0,0 +1,235 @@
+
+ * setLayout(Navigation::LAYOUT_TABS);
+ * $home = new NavigationItem();
+ * $home
+ * ->setIcon('home')
+ * ->setLabel('Home');
+ * ->setUrl('/home');
+ * $logout = new NavigationItem();
+ * $logout
+ * ->setIcon('logout')
+ * ->setLabel('Logout')
+ * ->setUrl('/logout');
+ * $dropdown = new DropdownItem();
+ * $dropdown
+ * ->setIcon('user')
+ * ->setLabel('Preferences');
+ * $preferences = new NavigationItem();
+ * $preferences
+ * ->setIcon('preferences');
+ * ->setLabel('preferences')
+ * ->setUrl('/preferences');
+ * $dropdown->addChild($preferences);
+ * $navigation
+ * ->addItem($home)
+ * ->addItem($logout);
+ * ->addItem($dropdown);
+ * echo $navigation
+ * ->getRenderer()
+ * ->setCssClass('example-nav')
+ * ->render();
+ */
+class Navigation implements ArrayAccess, Countable, IteratorAggregate
+{
+ /**
+ * Flag for dropdown layout
+ *
+ * @var int
+ */
+ const LAYOUT_DROPDOWN = 1;
+
+ /**
+ * Flag for tabs layout
+ *
+ * @var int
+ */
+ const LAYOUT_TABS = 2;
+
+ /**
+ * Navigation items
+ *
+ * @var NavigationItem[]
+ */
+ protected $items = array();
+
+ /**
+ * Navigation layout
+ *
+ * @var int
+ */
+ protected $layout;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetExists($offset)
+ {
+ return isset($this->items[$offset]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetGet($offset)
+ {
+ return isset($this->items[$offset]) ? $this->items[$offset] : null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetSet($offset, $value)
+ {
+ $this->items[$offset] = $value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetUnset($offset)
+ {
+ unset($this->items[$offset]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function count()
+ {
+ return count($this->items);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIterator()
+ {
+ return new ArrayIterator($this->items);
+ }
+
+ /**
+ * Ad a navigation item
+ *
+ * @param NavigationItem|array $item The item to append
+ *
+ * @return $this
+ * @throws InvalidArgumentException If the item argument is invalid
+ */
+ public function addItem(NavigationItem $item)
+ {
+ if (! $item instanceof NavigationItem) {
+ if (! is_array($item)) {
+ throw new InvalidArgumentException(
+ 'Argument item must be either an array or an instance of NavigationItem'
+ );
+ }
+ $item = new NavigationItem($item);
+ }
+ $this->items[] = $item;
+ return $this;
+ }
+
+ /**
+ * Get the item with the given ID
+ *
+ * @param mixed $id
+ * @param mixed $default
+ *
+ * @return NavigationItem|mixed
+ */
+ public function getItem($id, $default = null)
+ {
+ if (isset($this->items[$id])) {
+ return $this->items[$id];
+ }
+ return $default;
+ }
+
+ /**
+ * Get the items
+ *
+ * @return array
+ */
+ public function getItems()
+ {
+ return $this->items;
+ }
+
+ /**
+ * Get whether the navigation has items
+ *
+ * @return bool
+ */
+ public function hasItems()
+ {
+ return ! empty($this->items);
+ }
+
+ /**
+ * Get whether the navigation is empty
+ *
+ * @return bool
+ */
+ public function isEmpty()
+ {
+ return empty($this->items);
+ }
+
+ /**
+ * Get the layout
+ *
+ * @return int
+ */
+ public function getLayout()
+ {
+ return $this->layout;
+ }
+
+ /**
+ * Set the layout
+ *
+ * @param int $layout
+ *
+ * @return $this
+ */
+ public function setLayout($layout)
+ {
+ $this->layout = (int) $layout;
+ return $this;
+ }
+
+ /**
+ * Get the navigation renderer
+ *
+ * @return RecursiveNavigationRenderer
+ */
+ public function getRenderer()
+ {
+ return new RecursiveNavigationRenderer($this);
+ }
+}
+
diff --git a/library/Icinga/Web/Navigation/NavigationItem.php b/library/Icinga/Web/Navigation/NavigationItem.php
new file mode 100644
index 000000000..4bc0936cc
--- /dev/null
+++ b/library/Icinga/Web/Navigation/NavigationItem.php
@@ -0,0 +1,483 @@
+setProperties($properties);
+ }
+ $this->children = new Navigation();
+ $this->init();
+ }
+
+ /**
+ * Initialize the navigation item
+ */
+ public function init()
+ {
+
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function count()
+ {
+ return $this->children->count();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIterator()
+ {
+ return $this->children;
+ }
+
+ /**
+ * Get whether the item is active
+ *
+ * @return bool
+ */
+ public function getActive()
+ {
+ return $this->active;
+ }
+
+ /**
+ * Set whether the item is active
+ *
+ * Bubbles active state.
+ *
+ * @param bool $active
+ *
+ * @return $this
+ */
+ public function setActive($active = true)
+ {
+ $this->active = (bool) $active;
+ $parent = $this;
+ while (($parent = $parent->parent) !== null) {
+ $parent->setActive($active);
+ }
+ return $this;
+ }
+
+ /**
+ * Get an attribute's value of the item's element
+ *
+ * @param string $name Name of the attribute
+ * @param mixed $default Default value
+ *
+ * @return mixed
+ */
+ public function getAttribute($name, $default = null)
+ {
+ $name = (string) $name;
+ if (array_key_exists($name, $this->attributes)) {
+ return $this->attributes[$name];
+ }
+ return $default;
+ }
+
+ /**
+ * Set an attribute of the item's element
+ *
+ * @param string $name Name of the attribute
+ * @param mixed $value Value of the attribute
+ *
+ * @return $this
+ */
+ public function setAttribute($name, $value)
+ {
+ $name = (string) $name;
+ $this->attributes[$name] = $value;
+ return $this;
+ }
+
+ /**
+ * Get the item's attributes
+ *
+ * @return array
+ */
+ public function getAttributes()
+ {
+ return $this->attributes;
+ }
+
+ /**
+ * Set the item's attributes
+ *
+ * @param array $attributes
+ *
+ * @return $this
+ */
+ public function setAttributes(array $attributes)
+ {
+ foreach ($attributes as $name => $value) {
+ $this->setAttribute($name, $value);
+ }
+ return $this;
+ }
+
+ /**
+ * Add a child item to this item
+ *
+ * Bubbles active state.
+ *
+ * @param NavigationItem|array $child The child to add
+ *
+ * @return $this
+ * @throws InvalidArgumentException If the child argument is invalid
+ */
+ public function addChild($child)
+ {
+ if (! $child instanceof NavigationItem) {
+ if (! is_array($child)) {
+ throw new InvalidArgumentException(
+ 'Argument child must be either an array or an instance of NavigationItem'
+ );
+ }
+ $child = new NavigationItem($child);
+ }
+ $child->parent = $this;
+ $this->children->addItem($child);
+ if ($child->getActive()) {
+ $this->setActive();
+ }
+ return $this;
+ }
+
+ /**
+ * Get the item's children
+ *
+ * @return Navigation
+ */
+ public function getChildren()
+ {
+ return $this->children;
+ }
+
+ /**
+ * Get whether the item has children
+ *
+ * @return bool
+ */
+ public function hasChildren()
+ {
+ return ! $this->children->isEmpty();
+ }
+
+ /**
+ * Set children
+ *
+ * @param Navigation $children
+ *
+ * @return $this
+ */
+ public function setChildren(Navigation $children)
+ {
+ $this->children = $children;
+ return $this;
+ }
+
+ /**
+ * Get the icon
+ *
+ * @return string|null
+ */
+ public function getIcon()
+ {
+ return $this->icon;
+ }
+
+ /**
+ * Set the icon
+ *
+ * @param string $icon
+ *
+ * @return $this
+ */
+ public function setIcon($icon)
+ {
+ $this->icon = (string) $icon;
+ return $this;
+ }
+
+ /**
+ * Get the item's ID
+ *
+ * @return mixed
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * Set the item's ID
+ *
+ * @param mixed $id ID of the item
+ *
+ * @return $this
+ */
+ public function setId($id)
+ {
+ $this->id = $id;
+ return $this;
+ }
+
+ /**
+ * Get the label
+ *
+ * @return string|null
+ */
+ public function getLabel()
+ {
+ return $this->label;
+ }
+
+ /**
+ * Set the label
+ *
+ * @param string $label
+ *
+ * @return $this
+ */
+ public function setLabel($label)
+ {
+ $this->label = (string) $label;
+ return $this;
+ }
+
+ /**
+ * Get the URL
+ *
+ * @return Url|null
+ */
+ public function getUrl()
+ {
+ if ($this->url !== null && ! $this->url instanceof Url) {
+ $this->url = Url::fromPath((string) $this->url);
+ }
+ return $this->url;
+ }
+
+ /**
+ * Set the URL
+ *
+ * @param Url|string $url
+ *
+ * @return $this
+ */
+ public function setUrl($url)
+ {
+ $this->url = $url;
+ return $this;
+ }
+
+ /**
+ * Get the URL parameters
+ *
+ * @return array
+ */
+ public function getUrlParameters()
+ {
+ return $this->urlParameters;
+ }
+
+ /**
+ * Set the URL parameters
+ *
+ * @param array $urlParameters
+ *
+ * @return $this
+ */
+ public function setUrlParameters(array $urlParameters)
+ {
+ $this->urlParameters = $urlParameters;
+ return $this;
+ }
+
+ /**
+ * Get the view
+ *
+ * @return View
+ */
+ public function getView()
+ {
+ if ($this->view === null) {
+ $this->view = Icinga::app()->getViewRenderer()->view;
+ }
+ return $this->view;
+ }
+
+ /**
+ * Set the view
+ *
+ * @param View $view
+ *
+ * @return $this
+ */
+ public function setView(View $view)
+ {
+ $this->view = $view;
+ return $this;
+ }
+
+ /**
+ * Set properties for the item
+ *
+ * @param array $properties
+ *
+ * @return $this
+ */
+ public function setProperties(array $properties = array())
+ {
+ foreach ($properties as $name => $value) {
+ $setter = 'set' . ucfirst($name);
+ if (method_exists($this, $setter)) {
+ $this->$setter($value);
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * Render the navigation item to HTML
+ *
+ * @return string
+ */
+ public function render()
+ {
+ $view = $this->getView();
+ $label = $view->escape($this->getLabel());
+ if (null !== $icon = $this->getIcon()) {
+ $label = $view->icon($icon) . $label;
+ }
+ if (null !== $url = $this->getUrl()) {
+ $content = sprintf(
+ '%s',
+ $view->propertiesToString($this->getAttributes()),
+ $view->url($url, $this->getUrlParameters()),
+ $label
+ );
+ } else {
+ $content = sprintf(
+ '<%1$s%2$s>%3$s%1$s>',
+ static::LINK_ALTERNATIVE,
+ $view->propertiesToString($this->getAttributes()),
+ $label
+ );
+ }
+ return $content;
+ }
+
+ /**
+ * Render the navigation item to HTML
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->render();
+ }
+}
diff --git a/library/Icinga/Web/Navigation/NavigationRenderer.php b/library/Icinga/Web/Navigation/NavigationRenderer.php
new file mode 100644
index 000000000..60cfcf900
--- /dev/null
+++ b/library/Icinga/Web/Navigation/NavigationRenderer.php
@@ -0,0 +1,340 @@
+iterator = $navigation->getIterator();
+ $this->navigation = $navigation;
+ $this->flags = $flags;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getChildren()
+ {
+ return new static($this->current()->getChildren(), $this->flags & static::NAV_DISABLE);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasChildren()
+ {
+ return $this->current()->hasChildren();
+ }
+
+ /**
+ * {@inheritdoc}
+ * @return \Icinga\Web\Navigation\NavigationItem
+ */
+ public function current()
+ {
+ return $this->iterator->current();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function key()
+ {
+ return $this->iterator->key();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function next()
+ {
+ $this->iterator->next();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rewind()
+ {
+ $this->iterator->rewind();
+ if (! (bool) ($this->flags & static::NAV_DISABLE) && ! $this->inIteration) {
+ $this->content[] = $this->beginMarkup();
+ $this->inIteration = true;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function valid()
+ {
+ $valid = $this->iterator->valid();
+ if (! (bool) ($this->flags & static::NAV_DISABLE) && ! $valid && $this->inIteration) {
+ $this->content[] = $this->endMarkup();
+ $this->inIteration = false;
+ }
+ return $valid;
+ }
+
+ /**
+ * Begin navigation markup
+ *
+ * @return string
+ */
+ public function beginMarkup()
+ {
+ $content = array();
+ if ($this->flags & static::NAV_MAJOR) {
+ $el = 'nav';
+ } else {
+ $el = 'div';
+ }
+ if (($elCssClass = $this->getCssClass()) !== null) {
+ $elCss = ' class="' . $elCssClass . '"';
+ } else {
+ $elCss = '';
+ }
+ $content[] = sprintf(
+ '<%s%s role="navigation">',
+ $el,
+ $elCss
+ );
+ $content[] = sprintf(
+ '