Skip to content

Accessibility ​

This is the reason the project exists, not a pass made over it afterwards.

Many polar-alignment apps cost money, and most quietly assume a body that cooperates: that you can hold a phone steady, crouch behind an eyepiece, hit a small target in the dark, and read dim grey text on black. Plenty of astronomers can do none of those things. This app is free, and it is built for them first.

Input model ​

Assume the least capable input, not the most:

  • Nothing that only a gesture can do. The sky view can be dragged with a finger, tapped to centre, and its arrows held to keep moving — and every one of those has a button that reaches exactly the same places: the arrow pad goes everywhere the drag goes, through the same function and the same limits. The rule, in its author's words: gestures are fine "as long as a user has a button or easy accessible option … as long as no feature is available for only non-disabled users." No pinch, no two-finger anything, nothing needs two hands. The map takes the pointer only while a hand is steering it — in Auto Mode a finger on the map still scrolls the page, and the page always scrolls from anywhere that is not the map.
  • No chords. Nothing needs a modifier key held while another is pressed.
  • Big targets with big gaps. Minimum 3.4 rem, full-width buttons — and that includes every box you type in, not only the buttons. See below.
  • Targets never move. Values update in place and nothing reflows under the pointer, because re-acquiring a target that has shifted is expensive for someone using a mouthstick or a trackball.
  • Nothing is timed. No control disappears, collapses or advances on its own.

Every box you type in is a target too ​

The first prototype got this wrong, and the app's author caught it in review. It is worth writing down as it happened. "It's a little comical. The input for your date in the app that is easy for disabled people to use. The input box is absolutely tiny and very hard to select and even read."

Measured on a 375×812 phone at the default text size, the date field against the coordinate boxes two cards up:

Date and timeLatitude, Longitude, Altitude
Height21 px57.8 px
Font size13.3 px18.7 px
Inkblack on whitethe app's own

21 px is about a third of this app's own 3.4 rem floor and under the WCAG 2.2 AA minimum of 24 px. It was also white in Night Mode, where the entire promise is that nothing on screen is — and dark adaptation takes twenty minutes to build and one bright screen to lose.

Nothing was wrong with the field. Nothing was set on it. Every input rule in the stylesheet was written .manual input, scoped to the one card holding the coordinate boxes, so a field in the next card down got browser defaults.

The lesson is about the selector rather than the value. A rule scoped to a card means the next control added anywhere else is born unstyled — and it looks fine in review, because review happens on a desktop with a mouse. It is found by somebody trying to use it in a dark field. The rule now sits on input itself, a test refuses the card-scoped form, and the field is 58 px, 18.7 px type, and pure red on black at night. color-scheme: dark goes with it, because the picker's own calendar and spinners take their colours from there rather than from ours.

Every button says what pressing it does ​

One rule, no exceptions: a label is an imperative verb phrase naming the action. Not the state you are in, not the name of the thing, not a question.

Instead ofIt says
Dark ModeUse Night Mode
Constellations: onHide the constellations
Milky Way: onHide the Milky Way
Ground: onHide the ground
Sun (target)Sun — a value picker, selection carried by aria-pressed
Constellations (target)Constellations — walks each figure by name; aria-label says so
Only visible: onSkip what is down / Include what is down — the whole phrase, naming planets and constellations, is its accessible name
Wider view / Closer inShow more sky / Show less sky
FPS: onShow the drawing speed
Where is the ISS?Find the ISS

The app used to do both at once. The theme button read Dark Mode, meaning you are in Dark Mode. The compass button, one card below, read Turn on compass, meaning you are not in compass mode. Same shape, opposite sense — so neither label could be trusted without first working out which kind it was.

A button that has nowhere to go says so without losing your place. At the view's tightest and widest fields the zoom buttons that would cross the limit are marked aria-disabled and look spent, rather than disabled. A disabled button drops out of the tab order the moment it is disabled, so someone pressing Zoom out from the keyboard would reach the limit and find their focus thrown back to the top of the page. aria-disabled keeps the focus where it is, still tells a screen reader the button is unavailable, and a press at the limit simply does nothing. The Planets and Constellations buttons do the same when Skip what is down is on and nothing on their walk is above the horizon. A press on one then says so in the status line instead of moving the ring.

Why the action and not the state, here. A state label only works if you can also perceive which state you are in. Read aloud by a screen reader, "button, Dark Mode" says nothing about what happens next. At the largest text size in a dark field, the highlight that was carrying the state may be exactly the part that is hard to make out. An action label needs neither the highlight nor the context.

Five sizes, and two buttons that hold still ​

The ladder is five sizes: the normal one and four bigger. 1.0, 1.15, 1.30, 1.45, 1.60, and the button at either end looks spent. It ran 0.8 to 1.8 — six presses up from normal and two below it — and the top of that range is past the point where the app can lay itself out on a phone: "I think the button size [ -A ] [ +A ] should have a max of 5. Otherwise the buttons just get way too big."

Nothing goes below normal, by the same decision. A control whose whole job is to make the app readable has no business making it less readable, and every size it offers is a size the layout has to survive at every screen width. A setting saved by an older build is snapped onto this ladder on the way in, rather than stranding someone at a size the buttons can no longer reach.

The two sizing buttons never move and never change size. Everything on the page is sized in rem, and rem here is the setting — the root font size is 17 px times it — so these two were sized by the number they change. A press resized the button under the finger, the wrapping header re-flowed around it, and the next press landed on empty space: "The buttons to change the button size jump all over the place when enlarging."

For someone with limited mobility that is the worst target on the page, and it is the one you reach for when the screen is already hard to use. So the pair sits in a grid column of its own, anchored to the header's top-right corner, sized in px rather than rem. Measured in a browser at 375×812, walking the whole ladder: one box, 58×58, at every size. Before, five. The header's own inset is in px for the same reason — a growing inset walked the corner inwards by about 9 px across the range.

evo.ablecamera reached the same rule from the same report, and states it the same way: "one base locked size."

The one exception, named rather than left as drift. A segmented value picker — North/South, East/West — is labelled with the value, because the value is what you are choosing; the selection is carried by aria-pressed. Those four buttons are listed by id in test/labels.test.mjs, so the exception is a decision on the record rather than an inconsistency nobody got round to.

It is enforced, not remembered. test/labels.test.mjs reads index.html and app.js off disk, collects every button's label — including the ones swapped in at runtime, and following a const btn = $('id') alias — and fails the build on a state label, a question mark, or an opening word that is not on the list of verbs the app uses. A new button cannot quietly introduce a sixth convention.

Two jobs, two panes ​

The app started as a one-shot alignment tool: five numbered cards, top to bottom, done once at the start of a night. It is no longer only that. The live sky view, the constellation figures, the Milky Way band, the ISS and the Moon are all things people come back to all evening, and none of them is a step in a procedure.

Stacked in one scroll, the sky view was four cards down. Nobody scrolls past a polar-scope reticle to find the Big Dipper.

So there is a picker at the top with two sides:

  • Tonight's sky — the live view, the constellation and Milky Way toggles, the field-of-view buttons, the ISS, and the Moon.
  • Align my mount — which way true north is, every number the mount asks for, the reticle, and the star-hop chart, still numbered 1–2–3.

Where you are sits above both, because it is a prerequisite for each and a step in neither. Once answered it collapses to one line — 30.06° N, 95.21° W — with the full card one press behind Change. A first visit is now a single question and two buttons.

The picker is a tablist, not a pair of action buttons. role="tab" with aria-selected, left/right arrow keys scoped to the picker so they cannot collide with the arrow keys that pan the sky view, and the chosen pane takes focus. The labels name the two jobs rather than an action, which is the same exception the hemisphere picker takes: in a segmented picker the label is the value.

One crossing between them. Finding the pole is an alignment step, but the live view is the best tool for it, so step 3 carries Show me in the live sky view, which switches sides and aims at the pole. Nobody has to already know the view is over there.

Which side you were last on is remembered, so the app opens where you left it rather than where its author thought you should start.

The cards fold away ​

Each section is a card you can collapse by tapping anywhere along its heading — the whole heading row is the target, not a small triangle at one end. They are native <details> elements, so they are in the tab order, work from the keyboard, and are announced as expanded or collapsed without any script; the content is deliberately not part of the control, because a card you close by touching the thing you were reading is a trap. The sky map opens by default and everything else starts folded — the reason to collapse a wall of text is that a wall of text should not greet you — and each card remembers whether you left it open.

Planning a night that has not happened yet ​

Set a date and the whole app answers for that instant — where the stars, the Moon and the planets will be, and where the mount will need to point. One field, above both panes, next to where you are: the two questions every other number depends on.

It is deliberately not remembered. Everything else here persists — position, text size, theme, which pane you were on — but a stale planned date is the one setting whose wrong value is dangerous. Open the app in a field at midnight, having planned a trip a fortnight ago, and it would hand you a complete, confident set of mount numbers for the wrong night with nothing obviously wrong on screen. Every load starts at "right now".

The date box opens at the time it is now, and says so. It used to offer tonight at nine, with nothing to say it was a suggestion: at half past three in the afternoon it read 9:00 PM, under a label about the clock where you are standing, and looked like a wrong clock. One press of Use this date and time then drew the whole sky five and a half hours from where it is. Now it starts at the real time, and the line under it reads, for example, "It is 3:27 PM now. Change this to the date and time you will be out."

While a date is set, the app says so in three places — the bar turns and names the date, the mount numbers say "not for right now", and the sky view's own status line leads with the date it is drawing.

Two things refuse to answer for a future night, rather than answering wrongly:

  • The ISS is disabled and says why. Where the station is can only be asked for, not predicted; that needs orbit propagation from a fresh element set, which this app does not do.
  • The compass bearing is flagged past 2030, where the magnetic model stops being a model and starts being an extrapolation. True north, the star positions and the dial reading are unaffected — they never use it.

Time zones are the honest limitation. The date is read in the device's current zone, and the app cannot tell that you will be somewhere on a different clock. The field says so, in those words.

Auto Mode and Manual Mode ​

One control decides who is steering the sky view: Auto Mode follows the phone — turn or tilt it and the view turns with you — and Manual Mode shows the arrows and the buttons instead, for anyone who cannot hold a phone up and sweep it around.

It replaced two buttons whose names were a genuine trap. Follow the phone instead meant the view follows where the phone points; Follow the phone's rotation meant full screen follows whether the phone is turned sideways. Two unrelated settings, near-identical wording — a reader could not tell them apart, and asked. Now there is one switch, labelled with the action like every other button here, and Manual Mode is the only thing that shows the arrows: they cannot be hidden in the one mode that needs them.

Auto Mode is on from the moment the view opens, so the sky is already following the phone's heading and tilt by the time anyone looks — the app does not wait to be told. Its label follows the intent, not the achieved state: between opening and the phone's first orientation reading, the button already offers to switch to Manual, because Auto is already the mode. (iOS is the one exception the app cannot help from here: it requires a tap before it will hand over orientation at all, so on iOS the Use Auto Mode button — which is that tap — is what starts it.)

Filling the screen ​

Turn the phone sideways and the sky view fills the screen. Turn it back and it does not. No press required — turning a phone to look at something is already the gesture.

It follows a rotation, never the state at load. The first version filled the screen the instant the page opened whenever the window happened to be landscape — so every desktop, and any phone opened already sideways, went full screen on load. That was a bug: a rotation is a change, and the orientation at load is not one. Now opening the view records the orientation without acting on it, and only a genuine flip fills or empties. A desktop is permanently landscape and never rotates, so it never auto-fills at all — the control is a touch-device gesture, and there the button is the only way in.

The ⛶ icon in the bottom-right corner does it by hand — the corner and the symbol every video player uses. A manual press pins the choice; a later physical rotation is a strong enough statement of intent to resume the automatic behaviour.

It is a CSS overlay, not the Fullscreen API — and that is the whole reason it works. The API requires a user gesture, and an orientation change is not one any browser accepts, so a rotation-driven fullscreen request fails silently. Pressing the button is a gesture, so that path additionally asks for real fullscreen; when the request is refused, the overlay is already doing the job.

Zoom, pan, the targets and the mode switch stay reachable in full screen. The pane's controls sit below the canvas, and in full screen there is no below. Two large glyph buttons — + and −, 4 rem square at the normal text size, stacked bottom-right — carry the zoom; a directional cluster — ▲ ◀ ◎ ▶ ▼, the pad's own five actions in the pad's own arrangement — sits bottom-left, so each thumb has its own corner; and top-left is Track, a roll-up: one plate that opens the list of things to point at and closes it again, rolled up by default (the screen was "getting really crowded") and remembered. It is a disclosure in the WAI-ARIA sense — the button carries the section's name and the open/closed state rides on aria-expanded, with a chevron that turns for the eye — and when it is rolled up the list is hidden outright, so a screen reader is not walked through seven buttons that are not on the screen. The seventh is Skip what is down, the switch for the two walks, which rolls up with the targets it changes. The column stops above the arrow pad and the Az/Alt plate that sits on it, and scrolls within itself when it is taller than that — it never shrinks a button to fit. The Auto/Manual switch sits beneath it, outside the roll-up, so rolling the targets away never rolls away the way back. All of them delegate to the same controls that live below the canvas, carry the same accessible names, and are translucent plates with a light rim so they read against the sky rather than vanishing into it. A filled screen that could be zoomed but not moved was one of the real-phone reports; a control the same colour as the black canvas was another.

Where the view is pointing is written on the picture. A small plate shows the azimuth with its compass point and the altitude, in whole degrees, in both modes — bottom centre under the map, and in full screen directly above the arrow pad, aligned with it, so it reads as the pad's own gauge: press Up and watch Alt climb. (Bottom centre was tried in full screen too; on a 375 px phone it touched the Down button.) When the pad is hidden in Auto Mode the plate drops into the corner it leaves. In Auto Mode that is the phone's own heading and tilt with the declination applied, which is the pair of numbers a polar-alignment app is for and the one thing the picture had not said. It updates every frame, so it is deliberately not a live region — the mode hint under the map still announces the position when a hand moves it — and it is plain text a screen reader can read on request. A tap on the numbers stays on the plate, like a tap on the key, rather than falling through and moving the sky. (It is also the instrument: every drag bug so far was diagnosed by reading these two numbers out of a hidden hint.)

The canvas is resized to the box it is drawn into, rather than a fixed 720×480 stretched by CSS. That was soft on a phone and outright distorted the moment the aspect ratio changed — a circle drawn round the pole would have come out an oval, on the one view whose entire promise is that things are where they really are.

The caption is not deleted in full screen, it is moved out of sight and left for a screen reader, exactly as the reticle's description is. A full-screen view with no text alternative would be the one part of this app that is picture-only.

On its side, the controls go side by side. Everything above is stacked for a phone held upright. Turned on its side, the same phone is 390 px tall, and the stacks ran off it. At the default text size the key covered Zoom in, Hide the horizon and Hide the constellation art. One size up, the right-hand column climbed 59 px above the top edge, so Hide the constellation art could not be reached at all. One size further, the Track column sat on Look up. So in landscape the width is spent instead of the height:

  • the right-hand column goes two by two
  • the key steps beside it
  • the arrow pad, with its Az/Alt plate, moves beside the Track column instead of under it

No button gets smaller than its normal size. Checked the same way as everything else here, by measuring rather than looking. At 844 × 390, a rectangle-intersection sweep of every button, the key and the plate finds nothing overlapping and nothing off screen at the first three text sizes, where the old layout failed at every one. It also comes back clean upright and on a desktop.

In full screen the buttons grow with the text until the phone runs out of room, then stop. The arrow pad, the zoom column and the way out used to grow in step with the text. At the two largest sizes they kept growing after the phone had run out of space: on a 375 × 812 phone the arrow pad covered Zoom out and the key covered the Track column. Now these buttons have a size of their own. It follows the text until one of two things would happen, and then stops:

  • the pad and the zoom column would no longer fit side by side
  • the zoom column would no longer fit under the key

They are never smaller than at the normal text size. The words on the plates — the key, the Az/Alt numbers, the Track list — keep growing as before. The key stops at the Track column's edge, and its credit lines wrap. Measured with the same sweep, with all six rows of the key showing, a phone held upright is clear at every text size at 375 × 812, 390 × 844, 360 × 800 and 412 × 915. A phone on its side runs out of height first, so there the buttons stay at their normal size at the larger text sizes. For the same reason, on some phones they are a little smaller at the largest text size than at the middle one.

Nothing on the full screen covers anything else, at any text size. On short phones the key still came down over the right-hand buttons, and on a phone on its side it covered the arrow pad. So:

  • held upright too, the right-hand buttons go two by two, half as tall; where two across would not clear the arrow pad (a 320 px phone, or large text), they sit just above it instead
  • the key stops above them, and on the smallest phones at the largest text it scrolls within itself, like the Track list, rather than cover a button
  • on its side the key keeps to the right of the arrow pad, and when that gap gets too narrow it moves up over the right-hand buttons, where there is room
  • the full-screen Track labels wrap instead of losing their last letters, and "Constellations" breaks as "Constel-lations"

The buttons keep their size; the room goes to the key. Checked with the same sweep, on nine phones (five upright, four on their side) at all five text sizes, in the default mode and in WCAG Mode: nothing overlaps, nothing is off screen, no label is cut.

Seeing it ​

  • Five text sizes, the normal one and four bigger, from two permanent buttons in the header; the choice persists. See below.
  • Those two buttons never move and never change size. See below.
  • Night Mode: pure red on black, outlines only. See below — it is a measurable property, not a vibe.
  • The numbers are the interface. The reticle drawing and the sky chart are support, never the only source of anything. Each canvas carries a text alternative that states the same values in words, so the app is fully usable by someone who cannot see either graphic.
  • Brighter or dimmer stars. Make the stars brighter and Make the stars dimmer step the size of every star, from 0.6× to 2.8× the normal size, and the choice is remembered. They are in Visual Settings, and in full screen in the arrow pad's two bottom corners, which were empty, so adding them took no room. Stars are also sized for the screen's pixel density now: a phone used to draw them at half the size a computer did.
  • Paths and the ISS stay visible below the horizon. A path is fainter there than above it, but keeps 3:1 against the sky, and the ISS is drawn as a small station wherever it is, captioned when it is below the horizon.
  • Prefers-reduced-motion is honoured.

Night Mode, and why it is measured ​

Rod cells are effectively blind above about 620 nm. That is the whole basis of the century-old astronomy convention: deep red lets you read without bleaching rhodopsin, and dark adaptation takes 20–30 minutes to rebuild once it is lost. Green is the worst thing you can put on the screen — rods peak around 500–550 nm — so any green in a "red" theme is actively undoing the thing the theme exists for.

The first version of this app failed that test. Its night palette used #ff5544 and #ff4a3a, which read as red and are not: 0x55 of green and 0x44 of blue is a lot of light landing exactly where rods are most sensitive.

Night Mode is now pure red channel — every colour is #RR0000, so the only variable is intensity — and that is checkable rather than a matter of taste:

179 CSS colour properties audited  ->  0 leaking green or blue
sky chart canvas                   ->  0 green/blue pixels, 10,838 red
reticle canvas                     ->  0 green/blue pixels, 12,088 red

Nothing is filled. A filled button is a solid block of emitted light; the same button as an outline is a thin line of it. On a phone at arm's length in a dark field that is the difference between a lantern and a marker — and it is not only your own night vision at stake, it is everyone standing near you. So buttons, step badges, cards and figures all become transparent shapes with a red border. The only solid thing left is the black background, which emits nothing.

Two deliberate exceptions:

  • Stars stay filled. They are one to three pixels across; an outline at that size is invisible, and a point of light is what a star is. The intensity comes down instead.
  • All text is one red. The palette carried a dimmer secondary shade (#cc0000) for a while, on the reasoning that hierarchy is worth a second tone. In the field it read the other way: some text is darker than the rest, on a surface that is a single hue by design. So secondary text is now the same #ff0000 as primary — brighter is if anything more readable, so the original readability reason still holds — and only borders stay dimmer, because a border is not text.

The audit has to be run where the colours are, not where they are declared. A night rule can lose to a more specific day rule, and one did. In full screen the key sits on a plate whose rule outranked the Night Mode rule. So in full-screen Night Mode the key stayed near-white — rgb(242, 245, 250) text on a grey border — while the same key in the window was pure red. It was caught in a screenshot, not by the stylesheet audit. The check now reads the colours the browser actually computed: every visible element in full-screen Night Mode has its text, background, border and outline colour read, and none has any green or blue. The Night Mode picture on the install sheet has none in any of its 1.3 million pixels.

The green default layout is untouched and remains the default; Night Mode is a second theme, not a replacement.

And the docs open in the same darkness. The ? button in the header opens these pages, and when the app is in Night Mode it hands that over — the docs site reads it and paints itself red before the first frame, so reaching for help outdoors does not flash a white page at eyes that took twenty minutes to adapt. The theme travels both ways: leave the app in day mode and the docs open in day mode too.

The numbers, in the units the mount asks for ​

Hand controllers and setting circles want degrees and decimal minutes, not decimal degrees. Making someone convert 42.5078 into 42° 30.5′ by hand, in the dark, is exactly the avoidable work this app exists to remove. So the app publishes the whole set: altitude axis, azimuth (true and as a compass bearing), latitude and longitude in DM, elevation, UTC offset, and local sidereal time for an RA setting circle.

The latitude is also spelled out in words — "forty-two degrees, thirty point five minutes". That is not decoration. It is the one number that, set wrong, quietly ruins every exposure of the night, and it is read off a screen while your hands and attention are on a knob. 42 and 24 are one glance apart and transpose easily; forty-two does not. It also gives screen readers and the speech button something unambiguous to say, since 42° 30.5′ N is voiced very differently by different engines and some drop the units entirely.

The order to do it in ​

The app now carries the procedure itself, with your numbers already in the steps, because reconstructing a sequence from a page of figures is work.

The first step is level the tripod, which is the one most often skipped and the one everything else depends on: on an unlevel tripod the altitude and azimuth adjustments stop being independent, so moving one drags the other and you chase the pole around without converging.

Hearing it ​

"Read these out loud" speaks the altitude axis, the compass bearing and the reticle position through the browser's speech synthesis. The case this is for is real and specific: you are at the mount, in the dark, and the phone is not where your eyes are.

The words it says are also printed underneath it. Not a caption on audio — it is literally the same string, rendered from one source. That matters three ways: a deaf or hard-of-hearing user gets the sentence at all, where the button alone would give them nothing; a sighted user can check what will be read before committing to listening to it; and the two can never drift apart, because there is only one of them.

Writing it for the ear changed the wording. "Fourteen degrees on a compass, fourteen degrees west declination" sounds like a stutter — and for western declination those two numbers are always equal, so it is not a coincidence that can be avoided. It now says "…that is 14 degrees on a magnetic compass, because the needle here points 14 degrees west of true north", which turns an apparent repeat into the reason for itself.

Extracting it also caught a live bug. The spoken text said "Put Polaris" in both hemispheres while the screen correctly said "Put σ Oct" — so anyone relying on the audio in the southern hemisphere was sent after a star that never rises where they are standing. The briefing now names the right star, the right pole, and adds a warning the northern version does not need: that Sigma Octantis is magnitude 5.5 and the Southern Cross is the way to find the pole.

Haptic confirmation fires once when the live arrows reach Polaris, so "you're pointing at it" arrives without looking at the screen at all.

Pointing the phone at the sky ​

Hold the phone up like a camera and it guides both axes at once: turn left/right, and raise/lower. When both are right it says so, vibrates, and shows a star.

Before this the compass was the only live signal, so the app could say "turn left 20 degrees" while the altitude was a constant printed in the same sentence — "then look 46 degrees up" — with no idea whether you were anywhere near it. That reads like guidance and is not.

The tilt convention, which is the part that is easy to get backwards:

beta =   0   flat on a table, screen up
beta =  90   upright, screen facing you
beta = 180   flat, screen down

altitude the BACK of the phone is aimed at  =  beta - 90

So aiming at Polaris from 46°N wants beta near 136 — the phone tilted well back, screen toward your face, looking "through" it.

Three deliberate behaviours:

  • The target is the pole, not the pole star. In the north they are within half a degree so it makes no difference; in the south Sigma Octantis is magnitude 5.5 and there is nothing to see, so the pole itself is the only honest thing to aim at.
  • It corrects the bigger error first. Chasing both axes at once is how people oscillate and never settle.
  • A phone with no tilt sensor is told so. If the heading is right but tilt is unavailable, the app says the altitude is not being checked rather than reporting "on target" — that is how someone ends up confidently aimed at the ground.

The live tilt reading stays on screen whether or not it is on target. It doubles as an inclinometer you can hold against the mount's altitude scale, and it is the only honest way to find out what a given phone's beta actually means — like the compass reference question, that is not verified on real hardware and the number is shown so it can be.

The live sky view ​

Hold the phone up and the stars sit where they really are, the way Stellarium and its kin work — a rectilinear projection of the sky centred on wherever the phone is aimed, with the pole ringed and named.

Holding the phone up is the OPT-IN, not the default. That gesture — holding a phone up, steadily, and sweeping it around — is exactly the one the rest of this app exists to avoid needing, and plenty of people cannot do it at all. A sky view that only works that way is a sky view most of this app's intended users cannot open. It is also dead on a desktop, where there is no orientation sensor to follow.

So the view opens already aimed at the pole and is driven by a directional pad: Look up / down / left / right, a Find the pole button that recentres, and the arrow keys — one key at a time, no modifiers, which is the input this app is written for. Follow the phone instead is there for anyone who wants it, and pressing any pad button takes control back rather than fighting the sensor for it.

Three smaller choices follow from the same rule:

  • Zoom is two buttons, and a pinch as well. Two fingers on the sky zoom it wherever the sky owns the touch (full screen, or steering by hand), a trackpad pinch zooms it, and in full screen so does the mouse wheel. The + and − buttons stay, as the one-finger way to do the same: a two-finger gesture always needs one (WCAG 2.5.1), and nothing needs a pinch. Windowed in Auto Mode a pinch is still the browser's own page zoom, so it is never taken from anyone who enlarges the page that way.
  • When the pole is off-screen there is an arrow at the edge pointing at it. An empty sky and a sky pointed the wrong way look identical, and leaving someone to work out which is which is the failure mode.
  • Everything it shows still exists elsewhere — as numbers, as the arrows, and as the circumpolar chart. None of those changed.

The pad shows itself exactly when it is the only way to drive. It is five full-width buttons — most of a phone screen — and while the phone itself is steering they do nothing. So it is closed when the view is following an orientation sensor, and open when there is nothing to follow: every desktop, and any phone that declines the permission. That is not a default anyone has to discover, it is the state that matches the device. Move the view by hand opens it anyway, and that choice outranks the guess from then on.

And it is never disabled. While the phone is steering, a press on the pad hands control to the buttons — that is what a press means. An earlier version greyed the pad out during following, and on a dark screen at arm's length greyed reads as gone: the first real-phone report was "the controls to move the screen are not visible." Someone lying down who can lift a phone but cannot sweep it had a view pointed at the ceiling and four dead buttons under it.

There is no longer a button to open the sky view, because the view is the Tonight's sky pane. A press whose only job was to reveal the thing you had just navigated to was ceremony in front of the feature people come for.

Constellations and the Milky Way ​

Both are on by default, with a button each. The figures are how people recognise what they are looking at, and the band is what most of them are pointing a camera at in the first place — knowing where it runs, and where its bright core in Sagittarius sits, is the difference between framing a shot and hunting for one.

Thirty-two figures, the ones people actually point at, rather than all 88: a sky full of lines is unreadable exactly when you are trying to find one thing. Every drawing has its lines and its name. Until October 2026, six did not: Aries, Cancer, Libra, Capricornus, Aquarius and Pisces were drawn on the sky with no lines, no label and no place in the Constellations walk — "this artwork doesn't seem to have any corresponding constellations" — because the drawings and the stick figures had been chosen separately. All six now have both, and a test fails if a drawing ever ships without them.

They are written as Bayer letters and resolved against the shipped catalogue at build time, not as catalogue numbers. A line drawn to the wrong star still looks like a constellation — it is just a wrong one, and nothing in the app can tell. Resolving them means a mistake is a build failure with a star's name in it. That caught two on the first run. A letter means the star you can see: where a letter names a double, the line goes to its brighter half. It used to take the first-numbered one, which ran the Teapot's spout to a faint variable beside Alnasl, and one of Lyra's lines to a star too faint for the view to draw — a line ending at nothing.

The Milky Way is drawn from the galactic plane rotated into the sky, not traced by hand, so it cannot drift out of step with the stars drawn on top of it. The frame is built from the galactic pole and the galactic centre, and checked against five stars' published galactic latitudes — Deneb at 2.0°, Polaris at 26.5° — because a slightly wrong rotation shows up nowhere else. It is drawn as soft additive blobs rather than a filled shape: a hard-edged band reads as something drawn on the sky, and the real thing has no edge.

Why a rotation matrix rather than two angles ​

The arrows take heading for left/right and tilt for up/down, which is right for "which way do I turn". It falls apart for a sky view: tip the phone sideways and the sky must roll with it, and near the zenith the naive version gimbals and the view snaps around. Building the device's actual basis and projecting onto it costs a dozen lines and has neither problem.

One thing worth recording, because "gamma is roll" is the natural assumption and it is wrong: gamma rotates about the phone's top-to-bottom axis, so it always moves where the back points. A roll that leaves the aim fixed would be rotation about the view axis, which DeviceOrientation does not report. Held upright the phone's long axis is vertical, so gamma becomes a pure yaw — the ZXY convention's gimbal degeneracy, pinned by a test.

Stars behind you are dropped rather than projected. Without that test every star behind the phone is mirrored through the origin and drawn in front, which produces a plausible-looking sky that is upside down and back to front.

Not needing a compass at all ​

The strongest accessibility decision in the app is a factual one.

People assume they need to know where north is in order to find Polaris. It works the other way round: Polaris is within about 0.6° of the true pole, so pointing at it is pointing true north. Star-hop to it from the Big Dipper using the chart and the mount is aligned, with no compass, no declination and no magnetometer anywhere in the loop.

That matters practically as well as physically. A desktop browser has no magnetometer at all, plenty of phone compasses are badly calibrated, and holding a phone flat and steady to read one is itself a demand not everyone can meet. So the app offers three routes to north and only one of them involves a compass:

  1. Polaris itself — nothing required.
  2. A shadow at solar noon, computed to the minute — nothing required.
  3. A magnetic compass, with declination already applied.

Not making people type ​

Typing is expensive. Every field that can be filled some other way, is:

  • GPS fills position in one tap on a phone — latitude, longitude and altitude, no typing whatsoever.
  • Altitude can be looked up from the coordinates, so nobody has to go and find their own elevation. (It is a button rather than automatic because it is the only network request the app makes and it carries the user's coordinates off-device — see deploying.)
  • Position is remembered between visits, so the common case is zero input.
  • The boxes start from an example on your side of the equator. Before any position has been given, the hand-entry boxes are filled with Sydney when the phone is set to a southern time zone and with Kingwood, Texas otherwise, and the note under them says which. The time zone is read on the phone — no permission, no network — and the example is never used until you press Apply: a northern example in Sydney is the same trap as the dropped minus sign below.
  • Rejected entries keep their values. A bad latitude produces a message and leaves every field exactly as typed. Wiping a form someone paid for keystroke by keystroke is the worst thing an error handler can do.
  • The sign is a button, not a character. See below.

The minus sign was the worst input bug in the app ​

Latitude and longitude used to be typed as signed decimals — -33.8688. That is one character, easy to drop, and dropping it was silent and catastrophic: typing 33.8688 for Sydney produced a complete, confident, northern answer. Wrong pole star, wrong reticle circles, wrong chart, azimuth 180° out, and nothing on the page marking any of it as suspect.

Longitude was worse, because there is no hemisphere wording to contradict it. 95.2107 instead of -95.2107 moves you from Texas to central China, and the only thing that disagrees is the sidereal time — which is exactly the number nobody can sanity-check by eye.

So the sign is now two big buttons — North/South and East/West — and the field holds a plain magnitude. That removes a keystroke, which matters here, but the real gain is that the hemisphere becomes a visible state you can check rather than a punctuation mark you have to remember.

Three details that make it hold up:

  • A typed minus still works, and moves the button to match. Letting the button silently override an explicit -33.8688 would reintroduce the very bug the buttons exist to remove.
  • The field normalises to a magnitude afterwards, so the number and the button can never sit on screen contradicting each other. That invariant is asserted directly in test/coords.test.mjs.
  • GPS moves the buttons too, so opening the panel after locating shows the right side of the equator rather than a stale default.

Working where it is used ​

A dark field has no signal. The app is offline-first: everything, including the 9,096-star catalogue and the magnetic model, is on the device after the first load. No network call is required to produce any number it shows.

It installs to a home screen from the browser, so there is no app store account, no purchase, and no update anyone has to approve. It is the same app on every device — one set of files, not a version per platform:

  • Android: Chrome offers Install app. The install sheet shows pictures of the app, each with a label the sheet reads out, so you can see what you are installing first.
  • iPhone and iPad: open it in Safari, then Share → Add to Home Screen. iOS does not accept the app's SVG icon, so it gets a PNG of the same drawing; without it, the home screen showed an icon iOS made up.
  • Desktop: Chrome and Edge offer to install it from the address bar.

Installed, it opens in its own window with no browser bar, and works with no signal.

It turns with the phone. The installed app used to be locked to portrait. That fails WCAG 1.3.4 (Orientation) unless one orientation is essential, and here it is not: the sky view already corrects for how the screen is turned. The lock only ever affected the installed app, the version someone with a phone in a fixed landscape mount — on a wheelchair, say — would be using.

The site header is not an app control ​

On polaris.evomedia.net the app is preceded by the evomedia.net site header — the mark, the wordmark linking home, and the portfolio nav. It is injected by src/evomedia-chrome.js, mounts only on *.evomedia.net, and no-ops anywhere else, so nobody self-hosting this gets a header advertising someone else's business.

It is deliberately not sized with --tap. The 3.4 rem minimum exists because the app's controls are pressed outdoors, in the dark, with an unsteady hand or a mouthstick. Portfolio links are pressed once, indoors, by someone who has already decided to leave. Sizing them as if they were mount controls put three 58 px rows above the fold and spent 265 px of an 812 px phone screen before the app began. They are 2 rem instead — 34 px, comfortably past the WCAG 2.2 AA target-size floor of 24 px — and they still scale with the they do not grow with the text-size buttons, and the strip is one height at every setting: 83 px on a phone, 45 px on a desktop.

That last part is the second half of locking the sizing buttons. The strip sits above the app header, so growing it pushed the header down and the two buttons with it — 83, 139, 157, 175, 193 px across the ladder, about 18 px of travel a press. The first fix held the pair still inside a header that was itself being pushed, and that is not holding it still. The strip now takes its type from the unscaled base and sizes everything inside it in em, so its height does not move.

The trade is the right way round, and the argument was already written beside these links: they are portfolio links, pressed once, indoors, by someone who has decided to leave, and 34 px is already past the WCAG 2.2 AA target-size floor of 24 px. The app's own controls still scale. The one trap in the conversion is worth naming — the brand and the links each set their own smaller type, so 2em there resolves against that and silently shrank the targets to 30 px and 27 px. Both are pinned to the base instead.

The Evomedia mark is a white rounded square, and in Night Mode it was the one thing left on screen emitting white: #ffffff behind the 0.7 brightness filter is about #b3b3b3, with green and blue in it, landing where rods are most sensitive. It now gets the same treatment as every button — unfilled, pure red, said with a line instead of a block.

WCAG Mode ​

Under A− and A+ is a third button, Use WCAG Mode. It switches the app into a version built to meet WCAG 2.2 Level AA. Once it is on, the same button reads Exit WCAG Mode: it says what pressing does and never names the other mode, because calling it "Normal" would say the people who need this one are not. It is a separate option and is remembered between visits. Nothing about the default mode changes: every rule it brings applies only while it is on, and a test fails if one doesn't.

The audit behind it found the default mode already passing almost every A and AA criterion. In Dark Mode it even reaches AAA for text contrast (7:1) and control size (44 px). WCAG Mode closes what it could not promise:

CriterionWhat WCAG Mode does
2.4.11 Focus Not ObscuredA control that takes keyboard focus is scrolled clear of the pinned header, however the focus got there.
2.4.11, in full screenThe key's three dashed-line rows fold behind Show the Key, so the plate cannot cover the buttons under it. The photo credits stay, because the Milky Way's licence keeps its credit on the picture, and they stay at a fixed size so larger text cannot push them into the buttons.
1.4.4 and 1.4.10The full-screen Track labels wrap instead of losing their last letters. The default mode does this now too.
3.3.1 Error IdentificationA rejected latitude or longitude box is marked invalid, linked to the message that says why, and given focus.
1.4.1 and 1.4.3Links are underlined, and the photo credits are drawn at full strength, so they keep 5.25:1 in Night Mode instead of 3.93:1.
1.4.3 and 1.4.11, in Night ModeNight Mode lays a 30% black veil over the screen to protect dark adaptation, which takes pure red to about 2.9:1. WCAG Mode lifts the veil, so the night screen is pure red on black at 5.25:1.

With WCAG Mode on, Night Mode meets AA rather than AAA. Pure red on black tops out at 5.25:1, and anything brighter would spend the dark adaptation the mode exists to protect. Without WCAG Mode, the veil dims it further, to about 2.9:1, for the same reason.

How it was checked, on 2026-10-07:

  • axe-core 4.10 found no WCAG A or AA violations in WCAG Mode, in Dark Mode and in Night Mode, with every key row and credit on screen. axe does not see the night veil, which is a layer over the whole page, so the contrast with the veil was worked out by hand on 2026-10-11, and WCAG Mode now lifts it;
  • each A and AA criterion was reviewed by hand;
  • the page reflows at 320 px wide at the largest text size and under WCAG's text-spacing overrides;
  • the full-screen layout was measured on four screen sizes at all five text sizes.

What it does not yet cover. On the smallest phones, and on phones held sideways, at the three largest text sizes, the folded key's credits still touch a full-screen control. WCAG's own resize tests use browser zoom, where the layout is clear. Real screen-reader testing is still owed (see below).

Known gaps ​

  • Screen-reader testing so far is NVDA on Windows. The semantics are there — landmarks, live regions, labelled controls, text alternatives on both canvases — and NVDA reads them. That is not the same as having used it with TalkBack or VoiceOver on a phone, and should not be claimed as such until someone has.
  • No high-contrast mode distinct from night mode. Night mode is red-on-black for dark adaptation, which is not the same requirement as maximum contrast.
  • The compass reference question in accuracy is unresolved on real Android hardware.

Documentation hub for Evomedia.net LLC products.