# Density

> Set compact, default, or comfortable density for a whole section with one scope, and let nested sections override it.

Source: https://www.honestui.com/docs/components/density

```tsx
"use client"

import * as React from "react"
import { Ellipsis as EllipsisIcon } from "honestui/icons"

import { Button } from "@/components/honest-ui/ui/button"
import { DensityScope, type Density } from "@/components/honest-ui/ui/density"
import { Input } from "@/components/honest-ui/ui/input"
import {
  Menu,
  MenuItem,
  MenuPopup,
  MenuTrigger,
} from "@/components/honest-ui/ui/menu"
import {
  Select,
  SelectItem,
  SelectPopup,
  SelectTrigger,
  SelectValue,
} from "@/components/honest-ui/ui/select"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/honest-ui/ui/table"

const densities: Density[] = ["compact", "default", "comfortable"]

const statuses = [
  { label: "All statuses", value: "all" },
  { label: "Paid", value: "Paid" },
  { label: "Overdue", value: "Overdue" },
]

// Sample data for the demo.
const invoices = [
  { id: "INV-1042", customer: "Northwind Traders", status: "Paid", amount: "$1,280.00" },
  { id: "INV-1043", customer: "Contoso Ltd", status: "Overdue", amount: "$640.50" },
  { id: "INV-1044", customer: "Fabrikam Inc", status: "Paid", amount: "$2,115.75" },
]

export function DensityDemo() {
  const [density, setDensity] = React.useState<Density>("compact")
  const [status, setStatus] = React.useState("all")
  const visibleInvoices = invoices.filter(
    (invoice) => status === "all" || invoice.status === status,
  )

  return (
    <div className="grid w-full min-w-0 max-w-3xl gap-[var(--hui-space-7)]">
      <section
        aria-labelledby="density-demo-invoices"
        className="grid min-w-0 gap-[var(--hui-space-3)]"
      >
        <div className="flex flex-wrap items-center justify-between gap-[var(--hui-space-3)]">
          <p
            id="density-demo-invoices"
            className="[font-size:var(--hui-font-size-regular)] [font-weight:var(--hui-font-weight-medium)]"
          >
            Invoices
          </p>
          <div
            role="group"
            aria-label="Invoice table density"
            className="flex flex-wrap gap-[var(--hui-space-2)]"
          >
            {densities.map((value) => (
              <Button
                key={value}
                variant={density === value ? "secondary" : "ghost"}
                size="sm"
                aria-pressed={density === value}
                onClick={() => setDensity(value)}
              >
                {value[0].toUpperCase() + value.slice(1)}
              </Button>
            ))}
          </div>
        </div>

        <DensityScope
          density={density}
          data-testid="density-demo-table-scope"
          className="grid min-w-0 gap-[var(--hui-density-gap)]"
        >
          <div className="flex flex-wrap items-center gap-[var(--hui-density-gap)]">
            <Select items={statuses} value={status} onValueChange={(value) => setStatus(value ?? "all")}>
              <SelectTrigger aria-label="Filter by status" className="w-40">
                <SelectValue />
              </SelectTrigger>
              <SelectPopup>
                {statuses.map(({ label, value }) => (
                  <SelectItem key={value} value={value}>
                    {label}
                  </SelectItem>
                ))}
              </SelectPopup>
            </Select>
            <Button variant="secondary">Export</Button>
          </div>

          <Table>
            <TableHeader>
              <TableRow>
                <TableHead scope="col">Invoice</TableHead>
                <TableHead scope="col">Customer</TableHead>
                <TableHead scope="col">Status</TableHead>
                <TableHead scope="col" className="text-right">Amount</TableHead>
                <TableHead scope="col">
                  <span className="sr-only">Actions</span>
                </TableHead>
              </TableRow>
            </TableHeader>
            <TableBody>
              {visibleInvoices.map((invoice) => (
                <TableRow key={invoice.id}>
                  <TableCell>{invoice.id}</TableCell>
                  <TableCell>{invoice.customer}</TableCell>
                  <TableCell>{invoice.status}</TableCell>
                  <TableCell className="text-right">{invoice.amount}</TableCell>
                  <TableCell className="w-px">
                    <Menu>
                      <MenuTrigger
                        render={<Button variant="ghost" size="icon" />}
                        aria-label={`Actions for ${invoice.id}`}
                      >
                        <EllipsisIcon />
                      </MenuTrigger>
                      <MenuPopup align="end">
                        <MenuItem>View invoice</MenuItem>
                        <MenuItem>Download PDF</MenuItem>
                        <MenuItem>Send reminder</MenuItem>
                      </MenuPopup>
                    </Menu>
                  </TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </DensityScope>
      </section>

      <DensityScope density="comfortable" data-testid="density-demo-form-scope">
        <form
          aria-labelledby="density-demo-reminder"
          className="grid max-w-sm gap-[var(--hui-density-gap)]"
          onSubmit={(event) => event.preventDefault()}
        >
          <p
            id="density-demo-reminder"
            className="[font-size:var(--hui-font-size-regular)] [font-weight:var(--hui-font-weight-medium)]"
          >
            Reminder settings
          </p>
          <label
            htmlFor="density-demo-reply-to"
            className="[font-size:var(--hui-font-size-small)] [line-height:var(--hui-line-height-small)]"
          >
            Reply-to email
          </label>
          <Input id="density-demo-reply-to" type="email" autoComplete="email" />
          <Button type="submit">Save settings</Button>
        </form>
      </DensityScope>
    </div>
  )
}

```

## Overview [#overview]

A density scope sets the size of controls, menu items, table rows, and gaps for everything inside one element. You mark the section once instead of passing a size to every component in it.

There are three levels: `compact`, `default`, and `comfortable`. `default` matches the size every component has outside a scope.

## Anatomy [#anatomy]

Density has two parts. The `honestui/density.css` stylesheet defines the variables for each level on `[data-density]` and adds three Tailwind variants. `DensityScope` renders the element that carries `data-density` and shares the level through React context, which is how portaled popups and `DataGrid` receive it. `DensityProvider` shares a level without rendering an element, and `useDensity` reads it.

## Accessibility [#accessibility]

Density changes size only. Roles, names, focus order, focus rings, and font sizes are the same at every level.

### Touch behavior [#touch-behavior]

On devices whose primary pointer is coarse (`@media (pointer: coarse)`), every scope gives controls, menu items, and table rows a minimum height of 44px. Padding and gaps keep their level.

Compact rows are stacked edge to edge, so an invisible 44px hit area on a 28px control would overlap the row above and below. A real 44px box avoids that. The floor applies to all three levels so that compact is never larger than default on the same screen.

Two things to know:

* Outside a scope, components keep their existing touch behavior. Set a scope on your app root if you want the 44px floor everywhere.
* A control with an explicit size, such as `<Button size="sm">`, keeps that size at every density and on touch.

Change the floor by setting `--hui-density-touch-target` on `:root` or on a scope.

## Installation [#installation]

Density is optional. Components read each density variable with a fallback to their own size, so nothing changes until you load the stylesheet and set a scope.

Import the stylesheet after Tailwind CSS and the Honest UI tokens. It uses the [spacing tokens](/docs/theme/spacing).

```css
@import "tailwindcss";
@import "honestui/density.css";
```

Add `DensityScope` to your project. `Menu`, `Select`, `DataTable`, and `DataGrid` import it, so the CLI installs it with them.

<CliBlock commands="[&#x22;density&#x22;]" />

To add it by hand, copy this file:

### components/ui/density.tsx

```tsx
"use client"

import * as React from "react"

type Density = "compact" | "default" | "comfortable"

const DensityContext = React.createContext<Density | undefined>(undefined)

/**
 * The density of the nearest `DensityScope` or `DensityProvider`, or
 * `undefined` outside one. A bare `data-density` attribute is CSS-only and is
 * not visible here.
 */
function useDensity(): Density | undefined {
  return React.useContext(DensityContext)
}

/**
 * Shares a density with descendants without rendering an element. Use it when
 * the element that carries `data-density` is rendered elsewhere.
 */
function DensityProvider({
  density,
  children,
}: {
  density: Density
  children: React.ReactNode
}) {
  return (
    <DensityContext.Provider value={density}>{children}</DensityContext.Provider>
  )
}

/**
 * Sets the density for a section. Popups such as menus and select lists are
 * portaled out of the section's DOM subtree, so they read the density from
 * this component rather than from CSS inheritance.
 */
function DensityScope({
  density,
  children,
  ...props
}: React.ComponentPropsWithRef<"div"> & { density: Density }) {
  return (
    <div data-slot="density-scope" data-density={density} {...props}>
      <DensityProvider density={density}>{children}</DensityProvider>
    </div>
  )
}

export { DensityProvider, DensityScope, useDensity, type Density }

```

## Usage [#usage]

Use `DensityScope` for a section:

```tsx
import { DensityScope } from "@/components/ui/density"

export function InvoicesPanel() {
  return (
    <DensityScope density="compact">
      <InvoiceToolbar />
      <InvoiceTable />
    </DensityScope>
  )
}
```

`DensityScope` renders a `div` with `data-density` and accepts any `div` prop.

A bare attribute also works when the section has no popups, `DataTable`, or `DataGrid`:

```tsx
<section data-density="compact">…</section>
```

Menus and select lists are portaled to the end of the document, outside the section's DOM subtree, so CSS inheritance cannot reach them. They read the density from `DensityScope` instead. `DataTable` and `DataGrid` read it there too: they resolve their density in React, and `DataGrid` needs the row height in JavaScript for virtualization.

### Read the density in a component [#read-the-density-in-a-component]

`useDensity()` returns the density of the nearest `DensityScope`, or `undefined` outside one. It does not see a bare `data-density` attribute.

```tsx
import { useDensity } from "@/components/ui/density"

function RowAvatar() {
  const density = useDensity()
  return density === "compact" ? null : <Avatar />
}
```

Use `DensityProvider` to share a density without rendering an element, for example when your own component already renders the element that carries `data-density`.

## Examples [#examples]

### Nested scopes [#nested-scopes]

The nearest scope wins. A comfortable form inside a compact panel stays comfortable, and a sibling section without a scope keeps the component defaults.

```tsx
import { Button } from "@/components/honest-ui/ui/button"
import { DensityScope } from "@/components/honest-ui/ui/density"
import { Input } from "@/components/honest-ui/ui/input"

export function DensityNested() {
  return (
    <DensityScope
      density="compact"
      data-testid="density-nested-outer"
      className="grid w-full max-w-md gap-[var(--hui-density-gap)] rounded-[var(--hui-radius-3)] border-[0.5px] border-[var(--hui-color-border-base-primary)] p-[var(--hui-space-4)]"
    >
      <p className="[font-size:var(--hui-font-size-small)] [line-height:var(--hui-line-height-small)]">
        Compact panel
        <span className="hidden density-compact:inline"> (compact variant active)</span>
      </p>
      <div className="flex flex-wrap items-center gap-[var(--hui-density-gap)]">
        <Input aria-label="Search rows" placeholder="Search rows" className="w-44" />
        <Button variant="secondary">Filter</Button>
      </div>

      <DensityScope
        density="comfortable"
        data-testid="density-nested-inner"
        className="grid gap-[var(--hui-density-gap)] rounded-[var(--hui-radius-2)] bg-[var(--hui-color-background-neutral-primary)] p-[var(--hui-space-4)]"
      >
        <p className="[font-size:var(--hui-font-size-small)] [line-height:var(--hui-line-height-small)]">
          Comfortable override
          <span className="hidden density-compact:inline"> (compact variant active)</span>
        </p>
        <div className="flex flex-wrap items-center gap-[var(--hui-density-gap)]">
          <Input aria-label="Note" placeholder="Add a note" className="w-44" />
          <Button>Save note</Button>
        </div>
      </DensityScope>
    </DensityScope>
  )
}

```

## Components that follow density [#components-that-follow-density]

| Component   | What changes                                                              |
| ----------- | ------------------------------------------------------------------------- |
| `Button`    | Height and padding of the default size; width and height of `size="icon"` |
| `Input`     | Height of the default size                                                |
| `Select`    | Trigger height and padding of the default size; item padding              |
| `Menu`      | Item padding                                                              |
| `Table`     | Cell and header padding                                                   |
| `Toolbar`   | Gap and padding                                                           |
| `DataTable` | Row padding; `density` prop now also accepts `comfortable`                |
| `DataGrid`  | Row height (44px, 56px, or 64px)                                          |

`DataTable` and `DataGrid` use their `density` prop when it is set and inherit from `DensityScope` when it is not. Either way they become a scope for the buttons, inputs, and menus inside them.

## Variables [#variables]

Each level sets these variables on the scope element. Use them in your own components with a fallback, the same way the built-in components do.

<TokenTable
  caption="Honest UI density variables"
  tokens="[
  { name: &#x22;--hui-density-control-height&#x22;, value: &#x22;28px / 32px / 40px&#x22;, description: &#x22;Height of buttons, inputs, and select triggers. 44px in a scope on touch.&#x22; },
  { name: &#x22;--hui-density-control-padding-block&#x22;, value: &#x22;4px / 8px / 12px&#x22;, description: &#x22;Vertical padding of buttons and select triggers&#x22; },
  { name: &#x22;--hui-density-control-padding-inline&#x22;, value: &#x22;8px / 12px / 16px&#x22;, description: &#x22;Horizontal padding of buttons&#x22; },
  { name: &#x22;--hui-density-item-padding-block&#x22;, value: &#x22;4px / 8px / 12px&#x22;, description: &#x22;Vertical padding of menu and select items&#x22; },
  { name: &#x22;--hui-density-cell-padding-block&#x22;, value: &#x22;4px / 12px / 16px&#x22;, description: &#x22;Vertical padding of table cells&#x22; },
  { name: &#x22;--hui-density-cell-padding-inline&#x22;, value: &#x22;8px / 8px / 12px&#x22;, description: &#x22;Horizontal padding of table cells and headers&#x22; },
  { name: &#x22;--hui-density-header-padding-block&#x22;, value: &#x22;4px / 8px / 12px&#x22;, description: &#x22;Vertical padding of table headers&#x22; },
  { name: &#x22;--hui-density-gap&#x22;, value: &#x22;4px / 8px / 12px&#x22;, description: &#x22;Gap between related controls in the section&#x22; },
  { name: &#x22;--hui-density-group-gap&#x22;, value: &#x22;1px / 2px / 4px&#x22;, description: &#x22;Gap and padding inside a toolbar&#x22; },
  { name: &#x22;--hui-density-item-min-height&#x22;, value: &#x22;44px on touch&#x22;, description: &#x22;Minimum height of menu and select items. Set only in a scope on touch.&#x22; },
  { name: &#x22;--hui-density-row-min-height&#x22;, value: &#x22;44px on touch&#x22;, description: &#x22;Minimum height of table rows. Set only in a scope on touch.&#x22; },
  { name: &#x22;--hui-density-touch-target&#x22;, value: &#x22;44px&#x22;, description: &#x22;The touch floor used by the three variables above&#x22; },
]"
/>

Values are listed as compact / default / comfortable.

```tsx
<div className="grid gap-[var(--hui-density-gap,var(--hui-space-3))]">
  <button className="min-h-[var(--hui-density-control-height,var(--hui-space-9))]">
    Refresh
  </button>
</div>
```

## Tailwind variants [#tailwind-variants]

The stylesheet adds three Tailwind CSS v4 variants for changes a variable cannot express, such as hiding secondary text in compact sections:

```tsx
<p className="density-compact:hidden">Last synced 2 minutes ago</p>
<Avatar className="size-8 density-compact:size-6 density-comfortable:size-10" />
```

`density-compact:`, `density-default:`, and `density-comfortable:` match the scope element and the elements whose nearest scope has that level.

One limit: when a level is re-entered through a different level, such as compact inside comfortable inside compact, the variant does not match in the innermost scope. The variables have no such limit, so prefer them for sizing.

## API reference [#api-reference]

### DensityScope [#densityscope]

| Prop       | Type                                      | Default  | Description                   |
| ---------- | ----------------------------------------- | -------- | ----------------------------- |
| `density`  | `"compact" \| "default" \| "comfortable"` | Required | The density for this section. |
| `...props` | `React.ComponentProps<"div">`             | —        | Passed to the rendered `div`. |

### DensityProvider [#densityprovider]

| Prop      | Type                                      | Default  | Description                          |
| --------- | ----------------------------------------- | -------- | ------------------------------------ |
| `density` | `"compact" \| "default" \| "comfortable"` | Required | The density shared with descendants. |
