# WireKit > Free, MIT-licensed UI component library for Laravel Livewire — 130+ components, full theming, GDPR-safe fonts. Every page listed in [llms.txt](https://docs.wirekit.app/llms.txt), in the same order. Each page opens with its title and the address of its markdown source. # App Rail Source: https://docs.wirekit.app/components/app-rail.md `` is the narrow, full-height column of application areas that sits outside your ordinary navigation. Each entry is a module — Insights, Billing, Settings — and selecting one decides what the column beside it contains. It is the second level of navigation that a single sidebar cannot express. A sidebar answers "where in this area am I"; the rail answers "which area am I in". ## Usage :::preview{title="Icon-only, with tooltips"} ::: `label` is a prop, not the slot, and it is not optional. In the default mode nothing of it is drawn, so that string is the link's only accessible name — leave it out and a screen reader announces "link", which is what makes most icon rails unusable without sight. Here it is `sr-only` rather than absent, and it also becomes the tooltip's text. ## Labeling Three ways for a module to name itself. | `labels` | Shape | When | | --- | --- | --- | | `tooltip` | Icon only, name on hover and focus | The narrowest column. Default | | `below` | A caption under the icon | Removes the hover dependency, which matters on touch, where hover does not exist | | `inline` | The name beside the icon | The rail *is* the navigation and there is no second column | :::preview{title="Captions under the icons"} ::: :::preview{title="Long names and a counter under the icons"} ::: Under the icon a name has the width of its entry and no more. One that does not fit wraps between its words, and a long compound word breaks at a syllable with a hyphen where the page's `lang` tells the browser how; a word the browser cannot hyphenate still wraps inside the entry. `truncate` on an item cuts its name with an ellipsis instead. The icon under which a name sits has its own size, `--size-wk-rail-icon-labeled`. It is the same as the narrow rail's `--size-wk-rail-icon` unless you set it, as the example does, so a larger icon in the labeled rail does not widen the narrow one. :::preview{title="Names beside the icons"} ::: ## Expanding to reveal the names `expandable` adds a toggle that widens the rail to inline labels and back. It composes with `labels` rather than replacing it: an expandable `labels="below"` rail shows captions when narrow and full names when wide. Pass `persist="key"` and the choice survives a reload through `localStorage`. :::preview{title="Expandable, with a persisted choice"} ::: The tooltip goes quiet the moment the label becomes visible. That is bound to the live state rather than decided when the page renders — a tooltip repeating a name already on screen gives the link two sources of the same accessible name, which a screen-reader user pays for twice. ### Driving it from elsewhere A trigger outside the rail cannot call its toggle directly: state is merged down the tree, so a button in a top bar is not inside the rail and never sees it. Dispatch a window event instead. The rail also announces its own state on arrival, so an outside trigger can paint the right `aria-expanded` before its first click rather than guessing. ```blade {{-- 1. Any control anywhere on the page can flip the rail. --}} ``` ### In the shell's drawer on a phone Below the shell's breakpoint the rail travels off-canvas, inside the shell's drawer. When the drawer holds the rail alone, the rail shows its names there, whether it is `expandable` or not. A tooltip needs a pointer that can hover, and on a touch screen a tap follows the link before any name could appear. A stored choice is left untouched and applies again at desktop width. Beside a module column in the shell's `sidebar` slot, the rail keeps its narrow form in the drawer, because the two columns together already fill a phone. A `labels="below"` rail keeps its names there too, and the module column gives way instead: the two sit side by side, and at 88px for the rail and 256px for the menu they would be wider than a 320px screen. Below the breakpoint the menu is at most the screen's width minus the rail's, so the drawer fits a 320px screen and nothing scrolls sideways. From 344px up both keep their width. ## The workspace at the top `` is the rail's own head: the mark that identifies the workspace, and — once the rail is wide enough to read them — its name and a quieter second line. Put it in the `brand` slot, inside a `` so it lines up with the heads of the columns beside it: :::preview{title="A workspace head that grows with the rail"} ::: The bar is about the **shell's top rule**: it gives the rail's head the same height and the same line as the heads of the columns beside it. Standing the mark on the same spine as the modules is not its job — `` carries the rail's own inset itself, so dropping it straight into the slot lines it up with the module glyphs just the same. It simply draws no rule. The name is drawn only in the wide rail; in the narrow one it is visually hidden, never absent. That matters more here than anywhere else in the component: a narrow rail's head is a circle with two letters in it, and without the name a screen-reader user has no way at all to know whose workspace they are in. An `expandable` rail counts as wide the moment it has finished expanding. The mark reads the same live state its modules read, so the name arrives together with theirs — and it waits for the column to stop moving rather than appearing at whatever width the animation is passing through. The `description` is dropped entirely when the rail is narrow rather than read out. A subline with no subject beside it — "Free plan", alone — is noise in a landmark summary, and the name above already carries the identity. For a workspace **switcher**, wrap it in a dropdown trigger rather than passing `href`: the control is then a button and announces itself as one. ### Two forms of one mark A rail that opens has two widths, and a brand usually has two forms: a signet that fits the narrow column, and a wordmark that is only legible in the wide one. Put the signet in the default slot and the wordmark in ``: ```blade {{-- 1. The rail's brand is the link home and carries the name. --}} {{-- 2. Each form is a brand with no link of its own, since it already sits in one. --}} ``` `size` sets each form's height and `logo-aspect` reserves its width before the file arrives; see [brand](/components/brand#inside-a-link) for why the forms must not carry a link of their own. The two swap with the rail's **live** state, not with the state it was rendered in — so a rail that starts narrow and is opened by the reader still gets the wordmark. Leave the slot out and nothing changes: the default slot is drawn at every width. ::: info `name` is what identifies the workspace to anyone not looking at it, and it stays in the accessibility tree at both widths. The two marks are presentation — give them an empty `alt` and let `name` do the naming. Labeling the images instead gives you two names for one thing, only one of them present at a time. ::: ## The workspace name beside the mark A rail that expands has room for a name once it is wide, and none at all while it is narrow. Mark anything in the `brand` slot that is a LABEL, and it appears only in the wide rail: ```blade {{-- 1. The mark is always shown; the name only when the rail is wide enough to hold it. --}} Acme Design ``` The brand row follows the rail's width on its own — centered while the column is an icon strip, and starting on the same vertical line as the modules once it is wide. Passing `align` to the bar is not required, and an explicit one still wins. ## Square app icons `--wk-rail-item-aspect` decides the shape of a module in the icon-only rail. It defaults to `auto` — a pill as tall as its content — and set to `1` every module becomes a square, which is what an app icon is everywhere else. ```css /* 1. Set it once, in your own app.css, and every icon-only rail follows. */ :root { --wk-rail-item-aspect: 1; } ``` Which one is right depends on what your rail stands for. A rail of product AREAS usually wants squares, because that is the shape people already read as "an application". A rail that is a list of destinations usually does not. It applies only where nothing shares the box with the glyph. A square with a word under it is not a square, so `labels="below"` and `labels="inline"` ignore it rather than stretching the row. :::preview{title="A pill and a square, side by side"} auto — a pill 1 — an app icon ::: The two rails above differ by that one declaration and nothing else. Hover either column to see what the ratio changes: the hit area, not just the glyph. ## Tone Four surfaces, each re-pointing the same set of `--color-wk-rail-*` roles. The rail's contents follow automatically: a toned column also re-points the generic neutral tokens for its own subtree, which is how ordinary components inside a dark rail come out legible without any of them knowing that tones exist. :::preview{title="The four tones"} ::: A rail with no tone of its own inherits the roles from a toned `` above it, so a whole chrome surface can be set in one place — and a `variant="flush"` sidebar beside it follows, because it paints nothing and the chrome is therefore its surface. `accent` is available **here only**. A shell and a sidebar stop at `inverse`, because their contents are ordinary components that paint a foreground and a hover surface from two tokens they cannot switch together — and this tone inverts. The rail's items can switch both, through roles built for it. That is also how the reference consoles are built: the colored surface is the rail, the chrome around it is neutral or dark. `accent` behaves differently from the other three on purpose: hover and the current module **invert** rather than tinting — the fill becomes the foreground color and the label becomes the accent. A colored surface has no safe room for a half-measure, and no tint percentage is right for every theme, so the tone borrows the contrast guarantee the accent pair already carries instead of needing one of its own. If you define your own tone, keep the same property: the roles that carry text should be plain aliases, not mixes. ## Shape and the current-module marker `variant="panel"` makes the rail a floating rounded strip with a gap around it, rather than a column that meets the shell's edges. `indicator="edge"` marks the current module with a bar on the rail's inline-end edge instead of filling its box — which reads better in a very narrow column, where a filled box dominates everything beside it. :::preview{title="A floating panel with an edge marker"} ::: The two markers are alternatives rather than a base with an addition — filling the box *and* drawing the bar reads as two selections. ### Inside Livewire's `@persist` A rail that Livewire keeps across `wire:navigate` visits with `@persist` is not rendered again, so the entry it marked as current would stay the page the reader left. Set `track-current` and the entry whose link matches the page takes the current state and `aria-current` after each visit. How the match works is described on the [sidebar page](/components/sidebar#inside-livewires-persist). ## Grouping `` clusters modules. Its heading is drawn only where the rail draws labels at all; in the icon-only column there is no room, and a truncated section title is worse than none. It stays the group's accessible name in every mode regardless, which is what lets a screen-reader user tell two clusters of icons apart. `separated` draws a rule above the group. Narrow rails usually cluster with a line rather than a heading, because a heading costs a row of height the column does not have. ## Counters `badge` renders digits where a label is visible and a dot where it is not. The digits have no room in a 3.5rem column, but an unread signal must not simply vanish where it matters most. The count stays part of the link's accessible name in both states rather than being hidden — a screen-reader user gets no icon cue at all and would otherwise lose the information outright. A dot says that something waits, and digits say how many, but neither says what. `badge-label` takes that sentence. A hover or a focus shows it in a tooltip in every mode: under the name in the icon-only rail, alone where the names are drawn. It is also the link's description, so a screen reader hears the name and then the sentence, and the bare count leaves the name. :::preview{title="A counter that says what it counts"} ::: When a module counts several kinds of waiting item, give `badge-label` a list, one sentence per item. The tooltip shows them as a list with a bullet before each, at most five, and then how many more there are; `badge-label-limit` sets the number. A sentence that wraps goes on under its own words, so each item stays apart from the next. One sentence stands alone, without a bullet. The link's description names every sentence, the ones past the limit included, so a screen reader hears all of them. :::preview{title="A counter with one line per waiting item"} ::: ## Theming | Token | Role | | --- | --- | | `--size-wk-rail` | Width in `tooltip` mode | | `--size-wk-rail-labeled` | Width in `below` mode | | `--size-wk-rail-icon` | The glyph a module is built around, and the base of `--size-wk-rail` | | `--size-wk-rail-icon-labeled` | The glyph in `below` mode. Defaults to `--size-wk-rail-icon` | | `--size-wk-rail-expanded` | Width in `inline` mode, and when expanded | | `--color-wk-rail-bg` | Surface | | `--color-wk-rail-text` | Foreground | | `--color-wk-rail-muted` | Resting module foreground | | `--color-wk-rail-hover-bg` | Hover surface | | `--color-wk-rail-active-bg` | Current-module fill, in `pill` mode | | `--color-wk-rail-active-text` | Current-module foreground on the column, and the `edge` marker | | `--color-wk-rail-hover-fg` | Foreground on a hover fill — the same color as the column's on a neutral surface, and deliberately not on a colored one | | `--color-wk-rail-active-fg` | Foreground on the current module's fill | | `--color-wk-rail-border` | Edges and separators | | `--color-wk-rail-ring` | Focus ring. Its own role because a ring tuned for the page can fall below the contrast floor on a dark rail | | `--color-wk-rail-badge` | The counter dot, and the pill it becomes in a wide rail. Its own role because the rail is the one surface whose background is a tone — on a dark column the page accent is near-black on near-black | | `--color-wk-rail-badge-fg` | The digits on that pill | | `--color-wk-rail-badge-on-fill` | The counter on a filled item: the current module where the pill marks it, and an item under the pointer. On the `accent` tone that fill is the counter's own color, so there it inverts with the label | | `--color-wk-rail-badge-fg-on-fill` | The digits on that counter in a wide rail | | `--radius-wk-shell-panel` | Corner radius in `variant="panel"` | | `--radius-wk-nav-item` | A module's own corner radius. A rounded rail re-points it so the two arcs stay concentric — an inner corner that is not (outer − the gap) makes the space between them pinch at 45° | | `--wk-rail-item-aspect` | `auto` (a pill) or `1` (a square app icon). Icon-only mode only | Override the roles in `:root` to reskin every tone's fallback, or under a tone's own selector to change one. See [Theming](/theming). ## Props ### `` | Prop | Type | Default | Description | | --- | --- | --- | --- | | `labels` | `'tooltip' \| 'below' \| 'inline'` | `'tooltip'` | How a module names itself | | `expandable` | `bool` | `false` | Adds a toggle that widens the rail to inline labels and back | | `expanded` | `bool\|null` | `null` | Initial state on a first visit, before storage answers. `null` means "no opinion" — the rail waits for the stored preference and seeds nothing. Passing `false` is DIFFERENT: it seeds a collapsed state into the cookie on the first render, so a reader who has never chosen gets that choice made for them | | `defaultExpanded` | `bool` | `false` | The state on a **first** visit, before the store has anything to say. Unlike `expanded` it does not turn the seeding off — a stored preference still wins. Both drivers answer it the same way | | `persist` | `string\|null` | `null` | Storage key. Null keeps the choice for the session only | | `persistDriver` | `string` | `'local'` | Where the choice is remembered: `local` or `cookie`. A cookie is the only store the server can read too, so the first render is already right — see below | | `tone` | `'default' \| 'muted' \| 'inverse' \| 'accent'` | `'default'` | The surface, via the `--color-wk-rail-*` roles | | `variant` | `'flush' \| 'panel'` | `'flush'` | Edge-to-edge chrome column, or a floating rounded strip | | `indicator` | `'pill' \| 'edge'` | `'pill'` | How the current module is marked | | `label` | `string` | `'Modules'` | Accessible name for the `