---
title: useDeviceKind
description: Which machine the stylesheet is currently drawing, as a value. Null until mounted, on purpose.
source: https://adamjurek.com/components/use-device-kind
---

## Home


`Device` renders every machine and lets media queries choose. This is for the
code that has to *say* which one won.

<!-- ::start:showcase demo="use-device-kind" height="260" -->
<!-- ::end:showcase -->

```tsx
const kind = useDeviceKind(); // "phone" | "tablet" | "laptop" | null
```

## Null is the answer, not a gap

It returns `null` until the first effect runs, and never guesses.

A default of `"laptop"` would be a claim the server cannot support. Every
caller would then have one render where the value is confidently wrong, and the
callers here are things like *"tell the model which machine the reader is
using"* - where confidently wrong is worse than absent by a wide margin.

Being `null` costs nothing in practice, because everything that needs this
needs it after a click.

> [!CAUTION] Do not use this to pick what to render
> That is the mistake this whole design exists to avoid. A tree chosen from
> this hook renders nothing on the server and the wrong thing on the first
> client frame. If the decision is visual, it belongs in the stylesheet -
> `data-device`, or a media query from `devices.md`.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/use-device-kind.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/use-device-kind.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | layout · Scenes |
| Files | `use-device-kind.ts`, `device-kinds.ts` |
| Dependencies | None |
| Tags | responsive, media-query, ssr, no-deps |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { useDeviceKind } from "@sushindustries/ui";

export function DeviceReadout() {
	const kind = useDeviceKind(); // "phone" | "tablet" | "laptop" | null

	return <p>{kind ?? "not mounted yet"}</p>;
}
```

## What you should see

"not mounted yet" for the first render - including the one the server sent -
then the real machine name as soon as the effect runs and a media query
matches. Resize the window across a breakpoint and the value updates without
a reload.

## If nothing happens

The value stays `null` forever only when `window.matchMedia` is unavailable,
which in practice means a non-browser environment. In a normal browser the
usual mistake is expecting a value on the very first render - it is `null`
there by contract, on the server and on the client, so code that reads it
before the first effect has to handle that case rather than treat it as a
bug.


## Guides


## It listens rather than measuring

```ts
window.matchMedia(`(min-width: ${device.from}px)`);
```

One `MediaQueryList` per machine, and the widest match wins - which is exactly
how the cascade resolves the same queries in `devices.css`.

Reading `innerWidth` would be one line and would disagree with the stylesheet
the moment a scrollbar exists: a media query measures the viewport *including*
the scrollbar, and `innerWidth` does not. That is a 15px window where the
number says one machine and the screen shows another, and it is the kind of
disagreement nobody finds by reading either file.

Listeners are removed on unmount, and none are attached at all when an override
is passed.

## An override short-circuits it

```tsx
useDeviceKind(settings.device === "auto" ? undefined : settings.device);
```

Passed a kind, it returns that kind and attaches nothing. This matches what the
stylesheet does with `data-device`, so a settings panel that writes the
attribute and reads this hook cannot end up with the two disagreeing.

## What it is generated from

Nothing here holds a number. `DEVICES` comes from
`packages/atoms/devices.md`, which is also what the media queries are compiled
from, so this hook is correct by construction rather than by somebody
remembering to update it twice.

| Export | What it is |
| --- | --- |
| `DEVICES` | every machine, narrowest first, with its `from`, `width` and `aspect` |
| `DEVICE_KINDS` | just the names, for a menu |
| `deviceKindFor(width)` | the same decision without touching the DOM |
| `deviceQuery(kind)` | the media query text, for anything doing its own matching |


## API


<!-- generated:api -->

## Signature

```ts
useDeviceKind(override?: DeviceKind): DeviceKind | null
```

The machine the window's width currently selects, or `null` before mount.

<!-- /generated:api -->

## Notes

Passing `override` is not the same as ignoring the return value - with an
override, no `matchMedia` listener is attached at all, so a component that
switches between "auto" and a forced device by toggling `override` on and off
also switches between listening and not listening, with no leftover
listener from the other mode.

There is no third state for "the environment cannot tell": `null` covers
both "not mounted yet" and "no `matchMedia` available", because both cases
call for the same fallback behaviour - use the narrowest machine's numbers,
never assume laptop.


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="use-device-kind" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

The desktop shelf on this site's home page needs a column count as a number,
not as CSS, because snapping a dropped icon to a cell is arithmetic:

```tsx
import { DEVICES, useDeviceKind } from "@sushindustries/ui";

function deviceColumns(kind: ReturnType<typeof useDeviceKind>): number {
	const found = DEVICES.find((device) => device.kind === kind);
	return found ? found.columns : DEVICES[0].columns;
}

export function IconGrid() {
	const columns = deviceColumns(useDeviceKind());
	return <div style={{ "--columns": columns } as React.CSSProperties}>...</div>;
}
```

`kind` is `null` on the very first call, and `DEVICES[0]` - the narrowest
machine - is the fallback: three columns fits everywhere, where guessing the
laptop's seven would pack icons into a grid too narrow for them on the first
paint of every phone that loads the page.

## What this example is not

This is not the pattern for deciding what to render. It only ever feeds a
number into layout math that already runs after mount; the grid itself is
present in the DOM from the server render regardless of `kind`.
