# Angle Slider Heading Compass

> A compass bearing control that shows the heading in degrees and as a cardinal point (N, NE, E and so on), snaps to 5 degrees and has quick buttons for the four directions.

A shadcn/ui block from the [@tealmac](https://ui.tealmac.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/tealmac/angle-slider-heading-compass).

- Page, with a live preview: https://shadcn.exchange/r/tealmac/angle-slider-heading-compass
- Screenshot: https://shadcn.exchange/shot/tealmac/angle-slider-heading-compass/light.png
- Registry item: https://ui.tealmac.com/r/angle-slider-heading-compass.json
- Listed: 2026-11-01, source last changed 2026-11-01

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/tealmac/angle-slider-heading-compass.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `button`, `@tealmac/angle-slider`

## Files

### `registry/patterns/angle-slider-heading-compass/angle-slider-heading-compass.tsx`

```tsx
"use client"

import * as React from "react"

import { AngleSlider } from "@/components/ui/angle-slider"
import { Button } from "@/components/ui/button"

export interface AngleSliderHeadingCompassProps {
  heading?: number
  onChange?: (degrees: number, cardinal: string) => void
}

const POINTS = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]

/** The eight-point compass name for a bearing in degrees. */
export function cardinal(degrees: number) {
  return POINTS[Math.round((((degrees % 360) + 360) % 360) / 45) % 8]
}

/**
 * Bearings are spoken as words as often as numbers, so the control prints both
 * ("135° SE"). The slider snaps to 5 degrees for a steadier hand; the four
 * buttons jump to N, E, S and W for the common case, and the live text is the
 * only thing announced, not every intermediate drag value.
 */
export function AngleSliderHeadingCompass({
  heading: initial = 0,
  onChange,
}: AngleSliderHeadingCompassProps) {
  const [heading, setHeading] = React.useState(initial)
  const set = (d: number) => {
    setHeading(d)
    onChange?.(d, cardinal(d))
  }

  return (
    <div className="bg-card flex w-fit flex-col items-center gap-3 rounded-xl border p-5">
      <AngleSlider
        label="Heading"
        value={heading}
        step={5}
        onValueChange={set}
      />
      <p className="text-lg font-semibold tabular-nums">
        {heading}° {cardinal(heading)}
      </p>
      <p role="status" className="sr-only">
        Heading {heading} degrees, {cardinal(heading)}
      </p>
      <div role="group" aria-label="Quick headings" className="flex gap-1.5">
        {[
          ["N", 0],
          ["E", 90],
          ["S", 180],
          ["W", 270],
        ].map(([label, deg]) => (
          <Button
            key={label}
            type="button"
            size="sm"
            variant={heading === deg ? "default" : "outline"}
            aria-pressed={heading === deg}
            onClick={() => set(deg as number)}
          >
            {label}
          </Button>
        ))}
      </div>
    </div>
  )
}
```
