Sidebar
Collapsible app navigation with nested items and sections. Primary product shell chrome.
Editorial
<!-- F12c editorial — non-derivable only. Review: Karen. -->
## Ejemplos
App shell chrome:
```tsx import { SidebarProvider, Sidebar, SidebarHeader, SidebarContent, SidebarFooter, SidebarItem, SidebarTrigger, } from '@/components/molecules/sidebar';
<SidebarProvider> <Sidebar side="left"> <SidebarHeader> <SidebarTrigger /> </SidebarHeader> <SidebarContent> <SidebarItem href="/home">Home</SidebarItem> </SidebarContent> <SidebarFooter>v1</SidebarFooter> </Sidebar> </SidebarProvider> ```
## Accesibilidad
- Must wrap with `SidebarProvider` (collapse state); `useSidebar()` outside throws. - Collapse is provider state, not a prop on `Sidebar` — toggle via `SidebarTrigger` / `toggle()`.
## Cuándo no usar
- Temporary filters or mobile sheets → `Drawer` / `Sheet`. - A single short link list in a page body → plain nav, not the full shell sidebar.
## Criterio de uso
- Usa Sidebar para navegacion persistente de una aplicacion, no para menus de sitio: su valor esta en mantener el contexto entre vistas. - El estado colapsado debe sobrevivir la navegacion; recordar la preferencia evita que el usuario la reajuste en cada pagina. - Agrupa por tarea del usuario, no por modulo tecnico, y limita la anidacion a un nivel.
## Gotchas
- Son 11 sub-componentes con un Provider: montar Content sin el Provider deja el colapso sin estado compartido. - `side` cambia la relacion espacial con el contenido, no solo el lado: en `right` el foco de lectura invierte y conviene revisar el orden de tabulacion.
Uso
import {
SidebarProvider, Sidebar, SidebarHeader, SidebarContent,
SidebarFooter, SidebarItem, SidebarTrigger,
} from '@/components/molecules/sidebar';
<SidebarProvider>
<Sidebar side="left">
<SidebarHeader>
<SidebarTrigger />
</SidebarHeader>
<SidebarContent>
<SidebarItem href="/home">Home</SidebarItem>
</SidebarContent>
<SidebarFooter>v1</SidebarFooter>
</Sidebar>
</SidebarProvider>Props
| Prop | Tipo | Default | Rango / opciones | What | How |
|---|---|---|---|---|---|
| side | select | left | `left`, `right` | Which edge of the layout the sidebar docks to. | left for app chrome; right for inspector panels. Default: left. |
Gotchas
- react
Must wrap with SidebarProvider (collapsed state). Compose SidebarHeader/Content/Footer + SidebarItem/Trigger; useSidebar() outside provider throws.
- layout
collapsed class is driven by provider state, not a Sidebar prop — toggle via SidebarTrigger or useSidebar().toggle.
Anatomía CSS
<div class="sidebar-collapsible"> <span class="sidebar-collapsible__chevron"></span> <span class="sidebar-collapsible__content"></span> <span class="sidebar-collapsible__inner"></span> <span class="sidebar-collapsible__label"></span> <span class="sidebar-collapsible__trigger"></span> </div>
| Clase | Propósito |
|---|---|
sidebar-collapsible | root |
sidebar-collapsible--open | modifier |
sidebar-collapsible__chevron | element |
sidebar-collapsible__content | element |
sidebar-collapsible__inner | element |
sidebar-collapsible__label | element |
sidebar-collapsible__trigger | element |
Tokens resueltos
Valores finales tras seguir la cadena de tokens. Derivados del source: si un token cambia, esta tabla cambia sola.
| Variante | Prop | Valor | Token |
|---|---|---|---|
| all | bg | none | (unparsed) |
| all | fg | #525252 | muted.foreground |
| all | border | none | (unparsed) |
| all | hover-bg | #f5f5f5 | muted |
| all | hover-fg | #0a0a0a | foreground |
| collapsed | bg | #fafafa | background |
| collapsed | fg | #0a0a0a | foreground |
| collapsed | hover-bg | #f5f5f5 | muted |
| collapsed | hover-fg | #525252 | placeholder |
| collapsed | border | #e5e5e5 | border |
Animaciones
| Propiedad | Duración | Easing |
|---|---|---|
width | 0.3s | cubic-bezier(0.19 |
1 | | |
0.22 | | |
1) | | |
background-color | 0.15s | cubic-bezier(0.19 |
1 | | |
0.22 | | |
1) | | |
color | 0.15s | cubic-bezier(0.19 |
1 | | |
0.22 | | |
1) | | |
scale | 0.45s | cubic-bezier(0.32 |
0.72 | | |
0 | | |
1) | | |
rotate | 0.5s | cubic-bezier(0.19 |
1 | | |
0.22 | | |
1) | | |
rotate | 0.6s | 0.05s cubic-bezier(0.19 |
1 | | |
0.22 | | |
1) | | |
background-color | 0.15s | ease |
color | 0.15s | ease |
box-shadow | 0.15s | ease |
background-color | 0.15s | ease |
color | 0.15s | ease |
transform | 0.2s | ease |
grid-template-rows | 0.25s | cubic-bezier(0.19 |
1 | | |
0.22 | | |
1) | | |
CSS mínimo funcional
Autocontenido: sin imports ni tokens. Para previews y prototipos — en producción se consume el CSS del DS (@atom-uikit/css/components.css</code>, <code>@atom-uikit/tokens/tokens.css).
/* -------------------------------------------------------------------------
Sidebar
Composable: Provider (state) + Container + Header/Content/Footer
Collapse: .sidebar--collapsed toggles layout via CSS
Labels: [data-sidebar-label] hidden in collapsed via display:none
Tooltips: [data-tooltip] from tooltip.css, overridden to right/light
Animations: scale on hover, icon rotation in collapsed
240px expanded, 48px collapsed. Semantic tokens for colors.
------------------------------------------------------------------------- */
/* ---- Container ---- */
.sidebar {
--sidebar-w: 240px;
--sidebar-w-collapsed: 48px;
display: flex;
flex-direction: column;
width: var(--sidebar-w);
height: 100%;
min-height: 100%;
background: #f5f5f5;
border-right: 1px solid #efefef;
flex-shrink: 0;
transition: width 0.3s cubic-bezier(0.19, 1, 0.22, 1);
}
.sidebar--collapsed { width: var(--sidebar-w-collapsed); }
/* ---- Sections ---- */
.sidebar__header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 12px;
flex-shrink: 0;
}
.sidebar__content {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.sidebar__footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
flex-shrink: 0;
margin-top: auto;
border-top: 1px solid #efefef;
}
.sidebar__divider {
border: none;
border-top: 1px solid #e5e5e5;
margin: 8px 0;
}
/* Collapsed sections */
.sidebar--collapsed .sidebar__header { flex-direction: column; gap: 8px; padding: 8px; align-items: center; }
.sidebar--collapsed .sidebar__content { overflow: visible; padding: 4px; }
.sidebar__footer .dropdown-menu { width: 100%; }
.sidebar__footer .dropdown-menu__trigger { width: 100%; }
.sidebar__footer .dropdown-menu__content { width: 100%; }
.sidebar--collapsed .sidebar__footer { padding: 8px; justify-content: center; }
/* ---- Labels: hide in collapsed ---- */
.sidebar--collapsed [data-sidebar-label] { display: none; }
/* ---- Group ---- */
.sidebar__group { display: flex; flex-direction: column; gap: 2px; }
.sidebar__group + .sidebar__group { margin-top: 24px; }
.sidebar__group-label {
font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
font-size: 10.24px;
font-weight: 500;
color: #525252;
padding: 4px 8px;
text-transform: uppercase;
letter-spacing: 0.04em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar--collapsed .sidebar__group-label { display: none; }
/* ---- Item ---- */
.sidebar__item {
display: flex;
align-items: center;
gap: 8px;
padding: 0 8px;
height: 36px;
border-radius: 6px;
font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
font-size: 12.8px;
font-weight: 500;
line-height: 1;
color: #0a0a0a;
text-decoration: none;
cursor: pointer;
user-select: none;
white-space: nowrap;
transition:
background-color 0.15s cubic-bezier(0.19, 1, 0.22, 1),
color 0.15s cubic-bezier(0.19, 1, 0.22, 1),
scale 0.45s cubic-bezier(0.32, 0.72, 0, 1);
}
.sidebar__item:hover { background: #f5f5f5; color: #525252; }
.sidebar__item:active:not(.sidebar__item--disabled) { scale: 0.955 0.925; }
.sidebar__item--active { background: #fff4ed; color: #a44200; }
.sidebar__item--disabled { color: #525252; cursor: not-allowed; pointer-events: none; }
/* Item parts */
.sidebar__item-icon { display: flex; align-items: center; justify-content: center; flex-shrink: 0; width: 18px; height: 18px; transition: rotate 0.5s cubic-bezier(0.19, 1, 0.22, 1); }
.sidebar__item-icon svg { width: 100%; height: 100%; }
.sidebar__item-label { flex: 1; overflow: hidden; text-overflow: ellipsis; }
.sidebar__item-badge { font-size: 10.24px; font-weight: 500; color: #525252; flex-shrink: 0; }
/* Collapsed items: icon-only, centered */
.sidebar--collapsed .sidebar__item { justify-content: center; padding: 0; width: 32px; height: 32px; margin: 0 auto; }
.sidebar--collapsed .sidebar__item-label,
.sidebar--collapsed .sidebar__item-badge { display: none; }
/* ---- Hover animations ---- */
@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
/* Expanded: subtle Button-style grow */
.sidebar:not(.sidebar--collapsed) .sidebar__item:hover:not(.sidebar__item--disabled):not(.sidebar__item--active) {
scale: 1.03 1.05;
}
/* Collapsed: IconButton-style scale + icon spin */
.sidebar--collapsed .sidebar__item:hover:not(.sidebar__item--disabled):not(.sidebar__item--active) {
scale: 1.095;
}
.sidebar--collapsed .sidebar__item:hover .sidebar__item-icon {
rotate: 360deg;
transition: rotate 0.6s 0.05s cubic-bezier(0.19, 1, 0.22, 1);
}
}
/* ---- Trigger ---- */
.sidebar__trigger {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
padding: 4px;
border: none;
background: #f5f5f5;
color: #525252;
cursor: pointer;
border-radius: 4px;
flex-shrink: 0;
transition: background-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
}
.sidebar__trigger:hover { background: #e5e5e5; color: #0a0a0a; }
.sidebar__trigger svg { width: 16px; height: 16px; }
.sidebar--collapsed .sidebar__trigger { background: #f5f5f5; color: #525252; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); }
.sidebar--collapsed .sidebar__trigger:hover { background: #f5f5f5; color: #525252; }
/* ---- Tooltips (collapsed only) ---- */
.sidebar:not(.sidebar--collapsed) [data-tooltip]::before,
.sidebar:not(.sidebar--collapsed) [data-tooltip]::after { display: none !important; }
.sidebar--collapsed [data-tooltip]::before {
bottom: auto !important; top: 50% !important;
left: calc(100% + 10px) !important; right: auto !important;
transform: translateY(-50%) translateX(-4px) !important;
background: #fafafa !important; color: #0a0a0a !important;
border: 1px solid #e5e5e5 !important;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.1) !important;
}
.sidebar--collapsed [data-tooltip]:hover::before { transform: translateY(-50%) translateX(0) !important; }
.sidebar--collapsed [data-tooltip]::after {
bottom: auto !important; top: 50% !important;
left: calc(100% + 7px) !important; right: auto !important;
transform: translateY(-50%) rotate(225deg) !important;
background: #fafafa !important;
border: 1px solid #e5e5e5 !important;
border-top: none !important; border-left: none !important;
}
/* ---- Collapsible ---- */
.sidebar-collapsible__trigger {
display: flex;
align-items: center;
gap: 8px;
padding: 0 8px;
height: 36px;
width: 100%;
border: none;
background: none;
border-radius: 6px;
font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
font-size: 12.8px;
font-weight: 500;
line-height: 1;
color: #0a0a0a;
cursor: pointer;
user-select: none;
text-align: left;
white-space: nowrap;
transition: background-color 0.15s ease, color 0.15s ease;
}
.sidebar-collapsible__trigger:hover { background: #f5f5f5; }
.sidebar-collapsible__label { flex: 1; overflow: hidden; text-overflow: ellipsis; }
.sidebar-collapsible__chevron { display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: #525252; transition: transform 0.2s ease; }
.sidebar-collapsible--open .sidebar-collapsible__chevron { transform: rotate(0deg); }
.sidebar-collapsible:not(.sidebar-collapsible--open) .sidebar-collapsible__chevron { transform: rotate(-90deg); }
/* Collapsible content: animated height */
.sidebar-collapsible__content { display: grid; grid-template-rows: 1fr; transition: grid-template-rows 0.25s cubic-bezier(0.19, 1, 0.22, 1); }
.sidebar-collapsible:not(.sidebar-collapsible--open) .sidebar-collapsible__content { grid-template-rows: 0fr; }
.sidebar-collapsible__inner { overflow: hidden; min-height: 0; display: flex; flex-direction: column; gap: 2px; }
.sidebar-collapsible__inner .sidebar__item { padding-left: 34px; }
/* Collapsed: collapsible becomes icon-only */
.sidebar--collapsed .sidebar-collapsible__trigger { justify-content: center; padding: 0; width: 32px; height: 32px; margin: 0 auto; }
.sidebar--collapsed .sidebar-collapsible__label,
.sidebar--collapsed .sidebar-collapsible__chevron { display: none; }
.sidebar--collapsed .sidebar-collapsible__content { display: none; }
Codigo fuente
import { type ReactNode } from 'react'; import { useSidebar } from './SidebarContext'; function cn(...classes: (string | false | undefined | null)[]) { return classes.filter(Boolean).join(' '); } export type SidebarProps = { side?: 'left' | 'right'; children: ReactNode; className?: string; }; export function Sidebar({ side = 'left', children, className }: SidebarProps) { const { collapsed } = useSidebar(); return ( <aside data-sidebar="" className={cn( 'sidebar', collapsed && 'sidebar--collapsed', side === 'right' && 'sidebar--right', className, )} > {children} </aside> ); }