Ivan Perisic scored the winner with three minutes left after Croatia keeper Danijel Subasic had saved a penalty from Spain captain Sergio Ramos with 20 minutes remaining.
Croatia’s Nikola Kalinic had cancelled out an early goal from Spain striker Alvaro Morata in the first half.
Spain meet Group E winners Italy in a heavyweight last-16 tie on Monday in Paris — a rematch of the 2012 final — while Croatia will play one of the third-placed teams in Lens on Saturday.
Kalinic’s goal just before the break was the first goal conceded by Spain in 948 minutes of European Championship matches since October 2014.
Croatia’s excellence and commitment in battling back from behind condemned Spain, going for a third straight title, to their first defeat in a European Championship finals since they were beaten 1-0 by Portugal in the 2004 group stages.
The deserved winners overcame the disappointment of their second group match against the Czech Republic by starting confidently, seeking to set the tone with an intense press.
However, it was the Spanish who struck decisively in the seventh minute with a goal that encapsulated the technical prowess, the movement and the vision that sets them apart from the rest of this summer’s contenders.
David Silva drove inside before sending a delicious reverse pass out to Cesc Fabregas, whose diagonal run across the box went unnoticed by everyone else.
His cross was met by Morata, arriving at the back post, and on the day that his move back to Real Madrid from Juventus was announced, the striker touched home for his third of the tournament, moving level with top scorer Gareth Bale.
Spain, unchanged and enjoying the cohesion that familiarity breeds, could have doubled their lead when Nolito directed Silva’s searching cross just wide, but Croatia held tight and steadily began to rediscover their early menace.
Ivan Rakitic sent an ambitious chip onto the bar with the goalkeeper stranded while Kalinic, picked ahead of goalless Mario Mandzukic, offered mobility and a tenacity that had previously been missing.
With Spain eyeing a second, it was Croatia who struck, as Perisic’s cross found Kalinic, stealing in ahead of Ramos, who flicked the ball past De Gea with the outside of his foot.
It was hugely entertaining fare, thankfully unaffected by the crowd troubles that have accompanied Croatia’s other matches this summer.
They were then left fuming at an unjust penalty in the 72nd minute when Silva, chasing a delicious lofted ball from Andres Iniesta, fell under imperceptible pressure from Sime Vrsaljko.
Captain Ramos sent a hesitant spot kick down the centre of the goal and Subasic, advancing well off his line before the shot, was able to bat it away as justice was served.
Late goals have been a theme of this tournament, and while Spain struck late to down the Czech Republic in their opener, here they were on the receiving end.
A Spanish foray forward was halted abruptly and Marko Pjaca surged on the counter before laying the ball off to Perisic, whose low shot, aided by the merest deflection, evaded De Gea at his near post as Croatia were able to celebrate a famous win.